class ChessGame

A chess game that contains the game board, rules, validation of moves, and computing check/checkmate.

Constants

BLACK_SQAURE
BLANK_SQUARE
BOARD_TO_COORDINATES
COORDINATES_TO_BOARD
WHITE_SQUARE

Public Class Methods

new(white_player='human', black_player='human') click to toggle source

Creates instance variables and sets up the board for the start of the game.

# File lib/chess_game.rb, line 28
def initialize(white_player='human', black_player='human')
  @board = setup_board
  @current_player_color = 'white'
  @king_locs = { white: [0, 4], black: [7, 4] }
  @move_history = []
  @game_winner = nil
  @check_in_play = false
  @castle_king = false
  @players = { 'white' => white_player, 'black' => black_player }
end

Public Instance Methods

available_moves() click to toggle source

Returns an hash of moves that will allow the current player to be out of check. If the list is empty, nil is returned instead.

@return [Hash <Array, Array> or nil] Array of valid moves unless it's empty.

# File lib/chess_game.rb, line 503
def available_moves
  player_pieces = board_pieces_by_color[@current_player_color == 'white' ? 0 : 1]
  valid_moves = {}
  player_pieces.each do |piece|
    valid_moves[piece.position] = []
    legal_moves(piece).each do |move|
      valid_moves[piece.position] << forecast_move(piece.position, move)
    rescue StandardError
      next
    end
  end

  valid_moves.filter! { |_key, val| !val.empty? }

  valid_moves.empty? ? nil : valid_moves
end
board_pieces_by_color() click to toggle source

Returns an array with 2 arrays, the first of which is all the white pieces and the second is all the black pieces.

@return [Array<Array<ChessPieces>>] Array of arrays for white and black pieces.

# File lib/chess_game.rb, line 536
def board_pieces_by_color
  pieces = @board.flatten.filter { |space| space.is_a? ChessPiece }
  white_pieces = pieces.filter { |piece| piece.color == 'white' }
  black_pieces = pieces.filter { |piece| piece.color == 'black' }
  [white_pieces, black_pieces]
end
can_castle?(from, to) click to toggle source

Returns whether or not a king can make the castle move or not

@param [Array<Integer>] from An integer array of length 2 denoting the position of the piece to move. @param [Array<Integer>] to An integer array of length 2 denoting the position to move the piece at from to. @return [Boolean] whether the king can make the castle move or not.

# File lib/chess_game.rb, line 351
def can_castle?(from, to)
  return false unless from[0] == to[0] && to[1].between?(from[1] - 2, from[1] + 2)

  tcol = to[1]
  fcol = from[1]
  row = from[0]
  case fcol - tcol
  when 2 # Moving left, check all the left spaces are empty
    @board[row][0].is_a?(Rook) && @board[row][1] == BLANK_SQUARE && @board[row][2] == BLANK_SQUARE && @board[row][3] == BLANK_SQUARE
  when -2 # Moving right
    @board[row][7].is_a?(Rook) && @board[row][6] == BLANK_SQUARE && @board[row][5] == BLANK_SQUARE
  else
    false
  end
end
check_check() click to toggle source

Checks whether a king is currently in check. If it is, it returns the piece that puts it in check and nil otherwise.

@return [ChessPiece or nil] The piece that puts a king in check and nil otherwise.

# File lib/chess_game.rb, line 484
def check_check
  white_pieces, black_pieces = board_pieces_by_color
  # If black king is in check
  white_pieces.each do |piece|
    return piece if legal_moves(piece).include? @king_locs[:black]
  end
  # If white king is in check
  black_pieces.each do |piece|
    return piece if legal_moves(piece).include? @king_locs[:white]
  end

  nil
end
check_game_over() click to toggle source

Determines whether the game ends with a checkmate or stalemate.

@return [String] “Checkmate” for a game over, “Stalemate” for a draw, and “continue” otherwise.

# File lib/chess_game.rb, line 371
def check_game_over
  immovable = available_moves.nil?
  # Checkmate condition
  return 'Checkmate' if @check_in_play && immovable

  # Stalemate condition
  return 'Stalemate' if !@check_in_play && immovable

  # Neither conidtions met
  'continue'
end
convert_algebraic_coordinates(coord_arr) click to toggle source

Converts coordinates into algebraic notation space names. Inverse method of convert_coordinates.

@param [Array<Integer>] coord_arr An array of integers of length 2 corresponding to the row and column of the space on +@board+.

@return [String] The string corresponding to the row and column name of the space of the board using algebraic notation.

# File lib/chess_game.rb, line 164
def convert_algebraic_coordinates(coord_arr)
  "#{COORDINATES_TO_BOARD[coord_arr[1]]}#{coord_arr[0] + 1}"
end
convert_coordinates(coord_str) click to toggle source

Converts algebraic notation space names to coordinates corresponding with @board.

@param [String] coord_str A string of length 2 that has encoded within it the column and row of a space on the chess board. @return [Array<Integer>] An array of integers of length 2 denoting the row and column positions corresponding to @board.

For example, passing in 'a4' would return [0, 3].

# File lib/chess_game.rb, line 153
def convert_coordinates(coord_str)
  [(coord_str[1].to_i - 1), BOARD_TO_COORDINATES[coord_str[0]]]
end
get_en_passant_moves(pawn) click to toggle source

Returns an array of the valid En Passant moves for a given pawn

@param [Pawn] pawn The pawn to find the En Passant moves of. @return [Array<Array<Integer>>] Array of legal move spaces that the pawn can move to where En Passant applies.

# File lib/chess_game.rb, line 462
def get_en_passant_moves(pawn)
  # Check spaces beside the pawn for En Passant
  pawn_row, pawn_col = pawn.position
  r_piece_loc = [pawn_row + pawn.direction, pawn_col + 1]
  r_piece = @board[pawn_row][pawn_col + 1]

  l_piece_loc = [pawn_row + pawn.direction, pawn_col - 1]
  l_piece = @board[pawn_row][pawn_col - 1]
  en_passant_moves = []

  en_passant_moves << r_piece_loc if (r_piece.is_a? Pawn) && pawn.can_capture?(r_piece)

  en_passant_moves << l_piece_loc if (l_piece.is_a? Pawn) && pawn.can_capture?(l_piece)

  en_passant_moves
end
make_move(from, to) click to toggle source

Moves a piece at a given location to another location if the move is valid. Raises an InvalidMoveError otherwise.

@param [Array<Integer>] from An integer array of length 2 denoting the position of the piece to move. @param [Array<Integer>] to An integer array of length 2 denoting the position to move the piece at from to.

# File lib/chess_game.rb, line 236
def make_move(from, to)
  # Raises an execption if the move isn't valid
  validate_move(from, to)

  frow, fcol = from
  piece = @board[frow][fcol]
  trow, tcol = to
  other_space = @board[trow][tcol]

  # Keep track of move_counts for Pawns and Kings
  case piece
  when King
    reset_move_count = piece.moved?
  when Pawn
    reset_move_count = piece.move_count
  end

  # Capture a piece if the pawn performed En Passant
  @board[frow][tcol] = BLANK_SQUARE if piece.is_a?(Pawn) && get_en_passant_moves(piece).include?(to)

  # Move the piece on the board
  if piece.is_a?(Pawn)
    @board[trow][tcol] = piece.move(to, @players[@current_player_color])
  else
    @board[trow][tcol] = piece.move(to)
  end
  
  @board[frow][fcol] = BLANK_SQUARE

  # If a piece is a king, do special checks
  if piece.is_a?(King)
    @king_locs[@current_player_color.to_sym] = to

    # Check if the king is being castled
    if @castle_king
      # Reset the flag for the other player's king
      @castle_king = false
      if tcol == fcol - 2
        @board[trow][tcol + 1] = @board[trow][0].move([trow, tcol + 1])
        @board[trow][0] = BLANK_SQUARE
      else
        @board[trow][tcol - 1] = @board[trow][7].move([trow, tcol - 1])
        @board[trow][7] = BLANK_SQUARE
      end
    end
  end

  # Check for check
  check_piece = check_check

  @check_in_play = !check_check.nil?

  unless check_piece.nil?

    # Move keeps/makes their own king in check.
    if check_piece.color != @current_player_color || check_piece.is_a?(King)
      # Undo the move
      @board[frow][fcol] = piece.move(from)
      @board[trow][tcol] = other_space
      # Undo movement for tracking ones
      case piece
      when King
        piece.moved = reset_move_count
      when Pawn
        piece.move_count -= 2
      end
      raise(InvalidMoveError, "Moving that #{piece.class} leaves your king in check!")
    else
      puts 'Check!'
      puts 'Type "help" to get a list of moves to get out of check!'
    end
  end

  # Change whose turn it is
  change_turn
end
parse_move(input) click to toggle source

Parses a move input string to determine the type of input given. The different suppored moves are:

  1. A string of length 4 indicating the start space and the end space.

  • e.g 'a2a4' would return [[0, 1], [0, 3]]

  1. A string of length 8 in the form of “[start] to [end]”

  • e.g 'a2 to a4' would return [[0, 1], [0, 3]]

TODO : Add more supported moves

@param [String] input The raw player input

@return [Array<Array<Integer>>] An array of arrays of length 2 representing from and to to pass into make_move.

# File lib/chess_game.rb, line 132
def parse_move(input)
  case input.size
  when 4
    from = convert_coordinates(input[0..1])
    to = convert_coordinates(input[2..3])
    [from, to]
  when 8
    from = convert_coordinates(input[0..1])
    to = convert_coordinates(input[-2..-1])
    [from, to]
  end
end
play() click to toggle source

Runs the main game loop for a chess game, which is comprised of several stages:

  1. Parse player input

  2. Attempt to make a move

  3. Check for check/checkmate

  4. Repeat until game over or saved

@return [String] @game_winner The winner of the game if there is one.

# File lib/chess_game.rb, line 48
def play
  puts self
  while @game_winner.nil?
    puts "It's #{@current_player_color}'s turn."

    case @players[@current_player_color]
    when 'human'
      input_command = player_input
    when 'random'
      input_command = random_move
    else
      input_command = random_move
    end

    if input_command.nil?
      puts 'Please input a legal move. Type "help" for an example.'
      next
    elsif input_command == 'save'
      break
    end

    case input_command.size
    when 2
      from, to = input_command
    end

    begin
      make_move(from, to)
      @move_history << "#{convert_algebraic_coordinates(from)}#{convert_algebraic_coordinates(to)}"
      puts self
    rescue InvalidMoveError => e
      puts 'Invalid move!'
      puts e.message
      next
    rescue StandardError => e
      puts e.message
    end

    # Check whether the game is over or not
    case check_game_over
    when 'Checkmate'
      @game_winner = @current_player_color == 'white' ? 'black' : 'white'
      puts 'Checkmate!'
    when 'Stalemate'
      @game_winner = 'Stalemate'
      puts 'It\'s a draw!'
    else
      @game_winner = board_pieces_by_color.map(&:size) == [1, 1] ? 'stalemate' : nil
    end
  end

  @game_winner
end
player_input() click to toggle source

Accepts player input and either returns the components needed to input a move or saves the game.

@return [Array<Array<Integer>>] An array of arrays of length 2 representing from and to to pass into make_move.

# File lib/chess_game.rb, line 107
def player_input
  input = gets.chomp
  input = input&.downcase
  case player_input_type(input)
  when 'move'
    parse_move(input)
  when 'help'
    print_help_menu
  when 'save'
    save_game
  end
end
player_input_type(input) click to toggle source

Returns the type of player input, whether they input a game-ending command or a move command.

@param [String] input String of the player input to parse and determine the type of input. @return [String] The type of player input; 'save', 'help', or 'move'.

# File lib/chess_game.rb, line 174
def player_input_type(input)
  %w[save quit exit end].each { |end_word| return 'save' if input.include?(end_word) }

  %w[help tutorial ?].each { |help_word| return 'help' if input.include?(help_word) }

  'move'
end
print_help_menu() click to toggle source

Prints the help menu complete with tutorial and move explanations

@return [String] 'help_menu' after printing the help menu.

print_out_of_check_moves() click to toggle source

Prints the moves that the current player can make to get out of check.

random_move() click to toggle source

Chooses a random move from the list of available moves.

@return [Array<Array<Integer>>] [from, to]

# File lib/chess_game.rb, line 524
def random_move
  move_hash = available_moves
  from = move_hash.keys.sample
  to = move_hash[from].sample
  [from, to]
end
save_game() click to toggle source

Saves the game by dumping the object into a YAML file.

# File lib/chess_game.rb, line 184
def save_game
  puts 'Please input a name for the save file.'
  file_name = gets.chomp
  while File.exist?("saves/#{file_name}")
    puts "saves/#{file_name} already exists; please choose a different name."
    file_name = gets.chomp
  end
  save_file = File.new("saves/#{file_name}", 'w')
  save_file.write(YAML.dump(self))
  save_file.close

  'save'
end
to_s() click to toggle source

Returns a string of a board with pieces and appropriately shaded spaces.

@return [String] The chess board.

# File lib/chess_game.rb, line 548
def to_s
  string_stream = StringIO.new
  history_offset = @move_history.size
  8.downto(1) do |row|
    string_stream << " #{row} "
    @board[row - 1].each_with_index do |col, col_num|
      bg_color = (row - 1) % 2 == col_num % 2 ? BLACK_SQAURE : WHITE_SQUARE
      string_stream << col.to_s.colorize(bg_color)
    end
    string_stream << "\t"
    move_index = 8 - row + (history_offset - 8)
    move_index = 8 - row if (history_offset - 8).negative?
    string_stream << "#{move_index + 1}. #{@move_history[move_index]}"
    string_stream << "\n"
  end

  string_stream << '   a b c d e f g h '

  string_stream.string
end
validate_move(from, to) click to toggle source

Determines whether or not the move is a valid move. If the move is valid, it returns that space on the board. Raises an InvalidMoveError with a specific message if the move is invalid.

@param [Array<Integer>] from An integer array of length 2 denoting the position of the piece to move. @param [Array<Integer>] to An integer array of length 2 denoting the position to move the piece at from to.

# File lib/chess_game.rb, line 320
def validate_move(from, to)
  frow, fcol = from
  piece = @board[frow][fcol]
  raise(InvalidMoveError, "No piece at #{convert_algebraic_coordinates(from)}") unless piece.is_a? ChessPiece

  unless piece.color == @current_player_color
    raise(InvalidMoveError,
          "You cannot move opponent's piece at #{convert_algebraic_coordinates(from)}")
  end

  possible_moves = legal_moves(piece)
  unless possible_moves.include?(to)
    # Check if it's trying to castle the king
    if piece.is_a?(King) && !piece.moved? && can_castle?(from, to)
      @castle_king = true
    else
      raise(InvalidMoveError,
            "Your #{piece.class} cannot move from #{convert_algebraic_coordinates(from)} to #{convert_algebraic_coordinates(to)}.")
    end
  end

  trow, tcol = to
  @board[trow][tcol]
end

Protected Instance Methods

change_turn() click to toggle source

Changes whose turn it is by switching between 'black' and 'white'.

@return [String] @current_player_color The current player color whose turn it is.

# File lib/chess_game.rb, line 635
def change_turn
  @current_player_color = @current_player_color == 'white' ? 'black' : 'white'
end
forecast_move(from, to) click to toggle source

Moves a piece at a given location to another location, checks if the the move leaves the current player's king in check, returns the piece to its original spot, and returns the move if valid.

@param [Array<Integer>] from An integer array of length 2 denoting the position of the piece to move. @param [Array<Integer>] to An integer array of length 2 denoting the position to move the piece at from to.

# File lib/chess_game.rb, line 646
def forecast_move(from, to)
  # Raises an execption if the move isn't valid
  validate_move(from, to)

  frow, fcol = from
  piece = @board[frow][fcol]
  trow, tcol = to
  other_space = @board[trow][tcol]

  # Keep track of move_counts for Kings
  has_moved = piece.moved? if piece.is_a?(King)

  # Move the piece on the board
  if piece.is_a?(Pawn)
    @board[trow][tcol] = piece.move(to, @players[@current_player_color])
  else
    @board[trow][tcol] = piece.move(to)
  end
  @board[frow][fcol] = BLANK_SQUARE

  @king_locs[@current_player_color.to_sym] = to if piece.is_a?(King)

  # Check for check
  check_piece = check_check

  # Undo the move
  @king_locs[@current_player_color.to_sym] = from if piece.is_a?(King)
  @board[frow][fcol] = piece.move(from)
  @board[trow][tcol] = other_space

  # Undo movement for tracking ones
  case piece
  when King
    piece.moved = has_moved
  when Pawn
    piece.move_count -= 2
  end

  unless check_piece.nil? || (check_piece.color == @current_player_color && !check_piece.is_a?(King))
    # If check_piece is a King, then both kings are in check and it's an illegal
    # move.
    raise(InvalidMoveError, "Moving that #{piece.class} leaves your king in check!")
  end

  to
end
place_back_row(color) click to toggle source

Returns an array of back row pieces of the appropriate color on the appropriate row based on color.

@param [String] color A string denoting the color of pieces to place. @return [Array<ChessPiece>] The row of chess pieces in the standard setup.

# File lib/chess_game.rb, line 619
def place_back_row(color)
  back_row = color == 'white' ? 0 : 7
  [Rook.new(color, [back_row, 0]),
   Knight.new(color, [back_row, 1]),
   Bishop.new(color, [back_row, 2]),
   Queen.new(color, [back_row, 3]),
   King.new(color, [back_row, 4]),
   Bishop.new(color, [back_row, 5]),
   Knight.new(color, [back_row, 6]),
   Rook.new(color, [back_row, 7])]
end
place_pawns(color) click to toggle source

Returns an array of pawns of the appropriate color on the appropriate row based on color.

@param [String] color A string denoting the color of pawns to place. @return [Array<Pawn>] Row of pawns to place on the board.

# File lib/chess_game.rb, line 606
def place_pawns(color)
  pawn_row = color == 'white' ? 1 : 6
  pawns = []
  8.times { |col| pawns << Pawn.new(color, [pawn_row, col]) }
  pawns
end
place_pieces(color) click to toggle source

Returns the 2 arrays of the chess pieces in the proper places based od their color.

@param [String] color A string denoting the color of pieces to place. @return [Array<Array<ChessPiece>>] The two rows of chess pieces for the standard setup of the chess board.

# File lib/chess_game.rb, line 590
def place_pieces(color)
  pawns = place_pawns(color)
  back_row_pieces = place_back_row(color)
  if color == 'white'
    [back_row_pieces, pawns]
  else
    [pawns, back_row_pieces]
  end
end
setup_board() click to toggle source

Returns an array that sets the board up for the start of the game.

@return [Array<Array>] The chess board represented as an array.

# File lib/chess_game.rb, line 576
def setup_board
  rows = place_pieces('white')
  rows += Array.new(4) { Array.new(8, BLANK_SQUARE) }
  rows += place_pieces('black')

  rows
end