class JustShogi::GameState

Game State

Represents a game of Shogi in progress.

Attributes

current_player_number[R]
errors[R]
hands[R]
squares[R]

Public Class Methods

default() click to toggle source

Instantiates a new GameState object in the starting position.

@return [GameState]

# File lib/just_shogi/game_state.rb, line 48
def self.default
  new(
    current_player_number: 1,
    squares: [
      { id: '91', x: 0, y: 0, piece: { id: 1, player_number: 2, type: 'kyousha' } },
      { id: '81', x: 1, y: 0, piece: { id: 2, player_number: 2, type: 'keima' } },
      { id: '71', x: 2, y: 0, piece: { id: 3, player_number: 2, type: 'ginshou' } },
      { id: '61', x: 3, y: 0, piece: { id: 4, player_number: 2, type: 'kinshou' } },
      { id: '51', x: 4, y: 0, piece: { id: 5, player_number: 2, type: 'oushou' } },
      { id: '41', x: 5, y: 0, piece: { id: 6, player_number: 2, type: 'kinshou' } },
      { id: '31', x: 6, y: 0, piece: { id: 7, player_number: 2, type: 'ginshou' } },
      { id: '21', x: 7, y: 0, piece: { id: 8, player_number: 2, type: 'keima' } },
      { id: '11', x: 8, y: 0, piece: { id: 9, player_number: 2, type: 'kyousha' } },

      { id: '92', x: 0, y: 1, piece: nil },
      { id: '82', x: 1, y: 1, piece: { id: 10, player_number: 2, type: 'hisha' } },
      { id: '72', x: 2, y: 1, piece: nil },
      { id: '62', x: 3, y: 1, piece: nil },
      { id: '52', x: 4, y: 1, piece: nil },
      { id: '42', x: 5, y: 1, piece: nil },
      { id: '32', x: 6, y: 1, piece: nil },
      { id: '22', x: 7, y: 1, piece: { id: 11, player_number: 2, type: 'kakugyou' } },
      { id: '12', x: 8, y: 1, piece: nil },

      { id: '93', x: 0, y: 2, piece: { id: 12, player_number: 2, type: 'fuhyou' } },
      { id: '83', x: 1, y: 2, piece: { id: 13, player_number: 2, type: 'fuhyou' } },
      { id: '73', x: 2, y: 2, piece: { id: 14, player_number: 2, type: 'fuhyou' } },
      { id: '63', x: 3, y: 2, piece: { id: 15, player_number: 2, type: 'fuhyou' } },
      { id: '53', x: 4, y: 2, piece: { id: 16, player_number: 2, type: 'fuhyou' } },
      { id: '43', x: 5, y: 2, piece: { id: 17, player_number: 2, type: 'fuhyou' } },
      { id: '33', x: 6, y: 2, piece: { id: 18, player_number: 2, type: 'fuhyou' } },
      { id: '23', x: 7, y: 2, piece: { id: 19, player_number: 2, type: 'fuhyou' } },
      { id: '13', x: 8, y: 2, piece: { id: 20, player_number: 2, type: 'fuhyou' } },

      { id: '94', x: 0, y: 3, piece: nil },
      { id: '84', x: 1, y: 3, piece: nil },
      { id: '74', x: 2, y: 3, piece: nil },
      { id: '64', x: 3, y: 3, piece: nil },
      { id: '54', x: 4, y: 3, piece: nil },
      { id: '44', x: 5, y: 3, piece: nil },
      { id: '34', x: 6, y: 3, piece: nil },
      { id: '24', x: 7, y: 3, piece: nil },
      { id: '14', x: 8, y: 3, piece: nil },

      { id: '95', x: 0, y: 4, piece: nil },
      { id: '85', x: 1, y: 4, piece: nil },
      { id: '75', x: 2, y: 4, piece: nil },
      { id: '65', x: 3, y: 4, piece: nil },
      { id: '55', x: 4, y: 4, piece: nil },
      { id: '45', x: 5, y: 4, piece: nil },
      { id: '35', x: 6, y: 4, piece: nil },
      { id: '25', x: 7, y: 4, piece: nil },
      { id: '15', x: 8, y: 4, piece: nil },

      { id: '96', x: 0, y: 5, piece: nil },
      { id: '86', x: 1, y: 5, piece: nil },
      { id: '76', x: 2, y: 5, piece: nil },
      { id: '66', x: 3, y: 5, piece: nil },
      { id: '56', x: 4, y: 5, piece: nil },
      { id: '46', x: 5, y: 5, piece: nil },
      { id: '36', x: 6, y: 5, piece: nil },
      { id: '26', x: 7, y: 5, piece: nil },
      { id: '16', x: 8, y: 5, piece: nil },

      { id: '97', x: 0, y: 6, piece: { id: 21, player_number: 1, type: 'fuhyou' } },
      { id: '87', x: 1, y: 6, piece: { id: 22, player_number: 1, type: 'fuhyou' } },
      { id: '77', x: 2, y: 6, piece: { id: 23, player_number: 1, type: 'fuhyou' } },
      { id: '67', x: 3, y: 6, piece: { id: 24, player_number: 1, type: 'fuhyou' } },
      { id: '57', x: 4, y: 6, piece: { id: 25, player_number: 1, type: 'fuhyou' } },
      { id: '47', x: 5, y: 6, piece: { id: 26, player_number: 1, type: 'fuhyou' } },
      { id: '37', x: 6, y: 6, piece: { id: 27, player_number: 1, type: 'fuhyou' } },
      { id: '27', x: 7, y: 6, piece: { id: 28, player_number: 1, type: 'fuhyou' } },
      { id: '17', x: 8, y: 6, piece: { id: 29, player_number: 1, type: 'fuhyou' } },

      { id: '98', x: 0, y: 7, piece: nil },
      { id: '88', x: 1, y: 7, piece: { id: 30, player_number: 1, type: 'kakugyou' } },
      { id: '78', x: 2, y: 7, piece: nil },
      { id: '68', x: 3, y: 7, piece: nil },
      { id: '58', x: 4, y: 7, piece: nil },
      { id: '48', x: 5, y: 7, piece: nil },
      { id: '38', x: 6, y: 7, piece: nil },
      { id: '28', x: 7, y: 7, piece: { id: 31, player_number: 1, type: 'hisha' } },
      { id: '18', x: 8, y: 7, piece: nil },

      { id: '99', x: 0, y: 8, piece: { id: 32, player_number: 1, type: 'kyousha' } },
      { id: '89', x: 1, y: 8, piece: { id: 33, player_number: 1, type: 'keima' } },
      { id: '79', x: 2, y: 8, piece: { id: 34, player_number: 1, type: 'ginshou' } },
      { id: '69', x: 3, y: 8, piece: { id: 35, player_number: 1, type: 'kinshou' } },
      { id: '59', x: 4, y: 8, piece: { id: 36, player_number: 1, type: 'gyokushou' } },
      { id: '49', x: 5, y: 8, piece: { id: 37, player_number: 1, type: 'kinshou' } },
      { id: '39', x: 6, y: 8, piece: { id: 38, player_number: 1, type: 'ginshou' } },
      { id: '29', x: 7, y: 8, piece: { id: 39, player_number: 1, type: 'keima' } },
      { id: '19', x: 8, y: 8, piece: { id: 40, player_number: 1, type: 'kyousha' } }
    ],
    hands: [
      { player_number: 1, pieces: [] },
      { player_number: 2, pieces: [] },
    ]
  )
end
new(current_player_number: , squares: [], hands: []) click to toggle source
# File lib/just_shogi/game_state.rb, line 23
def initialize(current_player_number: , squares: [], hands: [])
  @current_player_number = current_player_number
  @squares = if squares.is_a?(SquareSet)
               squares
             else
               SquareSet.new(squares: squares)
             end
  @hands = if hands.is_a?(Array)
            if hands.all? { |hand| hand.is_a?(Hand) }
              hands
            elsif hands.all? { |hand| hand.is_a?(Hash) }
              hands.map { |hand| Hand.new(**hand) }
            else
              raise ArgumentError, "hands must all be the same class"
            end
           else
             raise ArgumentError, "hands must be an array"
           end
end

Public Instance Methods

as_json() click to toggle source

serializes the game state as ahash

@return [Hash]

# File lib/just_shogi/game_state.rb, line 152
def as_json
  {
    current_player_number: current_player_number,
    squares: squares.as_json,
    hands: hands.map(&:as_json)
  }
end
clone() click to toggle source

deep clone of the game state

@return [GameState]

# File lib/just_shogi/game_state.rb, line 163
def clone
  self.class.new(**as_json)
end
drop(player_number, piece_id, square_id) click to toggle source
# File lib/just_shogi/game_state.rb, line 220
def drop(player_number, piece_id, square_id)
  @errors = []
  
  piece = hand_for_player(player_number).find_piece_by_id(piece_id)
  square = squares.find_by_id(square_id)

  if current_player_number != player_number
    @errors.push JustShogi::NotPlayersTurnError.new
  elsif piece.nil? 
    @errors.push JustShogi::PieceNotFoundError.new
  elsif square.nil?
    @errors.push JustShogi::OffBoardError.new
  elsif square.occupied?
    @errors.push JustShogi::SquareOccupiedError.new
  elsif !piece.has_legal_moves_from_y(square.y)
    @errors.push JustShogi::NoLegalMovesError.new
  elsif squares.where(x: square.x).occupied_by_piece(JustShogi::Fuhyou).occupied_by_player(player_number).any?
    @errors.push JustShogi::TwoFuhyouInFileError.new
  else
    duplicate = self.clone
    duplicate.perform_complete_drop(player_number, piece_id, square_id)

    if duplicate.in_check?(opposing_player_number)
      @errors.push JustShogi::DroppedIntoCheckError.new
    else
      perform_complete_drop(player_number, piece_id, square_id)
    end
  end

  @errors.empty?
end
in_check?(player_number) click to toggle source
# File lib/just_shogi/game_state.rb, line 298
def in_check?(player_number)
  ou_square = squares.find_ou_for_player(player_number)
  threatened_by = squares.threatened_by(opposing_player_number(player_number), self)
  threatened_by.include?(ou_square)
end
in_checkmate?(player_number) click to toggle source
# File lib/just_shogi/game_state.rb, line 304
def in_checkmate?(player_number)
  (in_check?(player_number) || non_ou_pieces_cannot_move?(player_number)) && ou_cannot_move?(player_number)
end
move(player_number, from_id, to_id, promote = false) click to toggle source

Moves a piece owned by the player, from one square, to another.

It has an option to promote the moving piece. It moves the piece and returns true if the move is valid and it's the player's turn. It returns false otherwise.

Example:

# Moves a piece from a square to perform a move
game_state.move(1, '77', '78')

@param [Fixnum] player_number

the player number, 1 or 2

@param [String] from_id

the id of the from square

@param [String] to_id

the id of the to square

@param [boolean] promote

@return [Boolean]

# File lib/just_shogi/game_state.rb, line 189
def move(player_number, from_id, to_id, promote = false)
  @errors = []

  from = squares.find_by_id(from_id)
  to = squares.find_by_id(to_id)

  if current_player_number != player_number
    @errors.push JustShogi::NotPlayersTurnError.new
  elsif from.unoccupied?
    @errors.push JustShogi::NoPieceError.new
  elsif to.nil? 
    @errors.push JustShogi::OffBoardError.new
  elsif promote && !promotable?(player_number, from, to) 
    @errors.push JustShogi::InvalidPromotionError.new
  elsif from.piece.can_move?(from, to, self) 
    
    duplicate = self.clone
    duplicate.perform_complete_move(player_number, from_id, to_id, promote)

    if duplicate.in_check?(current_player_number)
      @errors.push JustShogi::MovedIntoCheckError.new
    else
      perform_complete_move(player_number, from_id, to_id, promote)
    end
  else
    @errors.push JustShogi::InvalidMoveError.new
  end

  @errors.empty?
end
perform_complete_drop(player_number, piece_id, square_id) click to toggle source
# File lib/just_shogi/game_state.rb, line 286
def perform_complete_drop(player_number, piece_id, square_id)
  hand = hand_for_player(player_number)
  square = squares.find_by_id(square_id)

  @last_change = { type: 'drop', data: { player_number: player_number, piece: piece_id, square: square_id } }

  piece = hand.pop_piece(piece_id)
  square.piece = piece

  pass_turn
end
perform_complete_move(player_number, from_id, to_id, promote = false) click to toggle source

Moves a piece owned by the player, from one square, to another, with the option to promote.

It moves the piece and returns true if the move is valid and it's the player's turn. It returns false otherwise. It promotes if possible and specified.

# File lib/just_shogi/game_state.rb, line 272
def perform_complete_move(player_number, from_id, to_id, promote = false)
  from = squares.find_by_id(from_id)
  to = squares.find_by_id(to_id)

  captured = to.occupied? ? to : nil

  @last_change = { type: 'move', data: { player_number: player_number, from: from_id, to: to_id } }

  perform_move(player_number, from, to, captured)

  promote_piece(to) if promote
  pass_turn
end
winner() click to toggle source

The player number of the winner. It returns nil if there is no winner.

@return [Fixnum,NilClass]

# File lib/just_shogi/game_state.rb, line 255
def winner
  case
  when in_checkmate?(1)
    2
  when in_checkmate?(2)
    1
  else
    nil
  end
end

Private Instance Methods

hand_for_player(player_number) click to toggle source
# File lib/just_shogi/game_state.rb, line 340
def hand_for_player(player_number)
  hands.find { |h| h.player_number == player_number }
end
non_ou_pieces_cannot_move?(player_number) click to toggle source
# File lib/just_shogi/game_state.rb, line 310
def non_ou_pieces_cannot_move?(player_number)
  squares.occupied_by_player(player_number).excluding_piece(JustShogi::OuBase).all? { |s| s.piece.destinations(s, self).empty? }
end
opposing_player_number(player_number = nil) click to toggle source
# File lib/just_shogi/game_state.rb, line 328
def opposing_player_number(player_number = nil)
  if player_number
    other_player_number(player_number)
  else
    other_player_number(@current_player_number) 
  end
end
other_player_number(player_number) click to toggle source
# File lib/just_shogi/game_state.rb, line 336
def other_player_number(player_number)
  (player_number == 1) ? 2 : 1 
end
ou_cannot_move?(player_number) click to toggle source
# File lib/just_shogi/game_state.rb, line 314
def ou_cannot_move?(player_number)
  ou_square = squares.find_ou_for_player(player_number)
  destinations = ou_square.piece.destinations(ou_square, self)
  destinations.all? do |destination|
    duplicate = self.clone
    duplicate.perform_complete_move(player_number, ou_square.id, destination.id)
    duplicate.in_check?(player_number)
  end
end
pass_turn() click to toggle source
# File lib/just_shogi/game_state.rb, line 324
def pass_turn
  @current_player_number = opposing_player_number
end
perform_move(player_number, from, to, captured) click to toggle source
# File lib/just_shogi/game_state.rb, line 344
def perform_move(player_number, from, to, captured)
  if captured
    hand_for_player(player_number).push_piece(to.piece)
    captured.piece = nil
  end
  to.piece = from.piece
  from.piece = nil
end
promotable?(player_number, from, to) click to toggle source
# File lib/just_shogi/game_state.rb, line 353
def promotable?(player_number, from, to)
  PromotionFactory.new(from.piece).promotable? && to.promotion_zone(player_number)
end
promote_piece(square) click to toggle source
# File lib/just_shogi/game_state.rb, line 357
def promote_piece(square)
  new_piece = PromotionFactory.new(square.piece).promote
  square.piece = new_piece
end