11
\$\begingroup\$

I wrote a chess game in Ruby using object-oriented principles.

One of the challenges was deciding which particular methods/actions belonged to a particular class, as there were some that felt as if they could go in any class.

General rationale for OOP choices:

  • Pieces should be as dumb as possible. They should return their available moves regardless of the current state of the board/game (I tried to ensure they didn't hold much information).
  • Board should be made up of Square objects which have Pieces on them (or not). Board should have a general idea of what moves are available and what moves are not, based on the state of the board. It should also keep a History of past moves.
  • Player should generally know about his/her own pieces and they should be the ones that know what a piece can and cannot do.
  • Game should control the flow of the game (whose turn it is, what move that player wants to make, whether or not that move is a valid choice, etc.) Game also checks for stalemate, three-fold repetition, fifty-move rule, insufficient material, check, and checkmate.
  • The game can also be saved in YAML and saved games can be loaded from the YAML file.

Chess

require 'colored'
require './lib/player'
require './lib/board'
require './lib/history'
require './lib/square'
require './lib/game'
require './lib/piece'
require './lib/pawn'
require './lib/rook'
require './lib/knight'
require './lib/bishop'
require './lib/queen'
require './lib/king'
require 'yaml'

def play_again?
  puts "Play again? (yes or no)".green
  answer = gets.chomp.downcase
  return answer == "yes"
end

loop do 
  Game.new.play_game
  unless play_again?
    puts "Goodbye"
    break
  end
end

Player

class Player
  attr_accessor :color, :pieces, :captured_pieces
  def initialize(color)
    @color = color
    @captured_pieces = []
    @pieces = [Pawn.new(color),
               Pawn.new(color),
               Pawn.new(color),
               Pawn.new(color),
               Pawn.new(color),
               Pawn.new(color),
               Pawn.new(color),
               Pawn.new(color),
               Rook.new(color),
               Rook.new(color),
               Knight.new(color),
               Knight.new(color),
               Bishop.new(color),
               Bishop.new(color),
               Queen.new(color),
               King.new(color)
              ]
  end

  def valid_move?(from_square, to_square, piece)
    if piece.class == Pawn && (to_square.x == from_square.x) && to_square.piece_on_square.nil?
      piece.get_valid_moves(from_square, to_square)
    elsif piece.class == Pawn && (to_square.x == from_square.x) && !to_square.piece_on_square.nil?
      false
    elsif piece.class == Pawn && (to_square.x != from_square.x) && !to_square.piece_on_square.nil? && (to_square.piece_on_square.color != piece.color)
      piece.get_valid_captures(from_square, to_square)
    elsif piece.class == Pawn && (to_square.x != from_square.x) && (to_square.piece_on_square.nil? || to_square.piece_on_square.color == piece.color)
      false
    else
     piece.class.get_valid_moves(from_square, to_square)
    end
  end

  def en_passant_move?(from_square, to_square, piece)
    piece.class == Pawn ? piece.get_en_passant_moves(from_square, to_square) : false
  end

  def promote_pawn(square, piece)
    square.piece_on_square = Object.const_get(piece).new(color, square.coordinates)
    @pieces << square.piece_on_square
  end

  def choose_player_piece(type)
    @pieces.find {|i| i.class == type && i.position == nil}
  end

  def king
    @pieces.find {|i| i.class == King}
  end

  def short_side_rook
    self.color == "white" ? @pieces.find {|i| i.position == "h1"} : @pieces.find {|i| i.position == "h8"}
  end

  def long_side_rook
    self.color == "white" ? @pieces.find {|i| i.position == "a1"} : @pieces.find {|i| i.position == "a8"}
  end

  def bishop_and_king_only?
    @pieces.all? {|i| i.class == King || i.class == Bishop}
  end

  def knight_and_king_only?
    @pieces.all? {|i| i.class == King || i.class == Knight}
  end

  def bishop_origin
    @pieces.find {|i| i.class == Bishop}.origin
  end

  def set_position(piece, to_square)
    piece.position = to_square.coordinates
  end

  def pieces_on_initial_square?
    if self.long_side_rook.on_initial_square && self.king.on_initial_square
      true
    elsif self.short_side_rook.on_initial_square && self.king.on_initial_square
      true
    else  
      false
    end
  end
end

Board

class Board
  attr_accessor :square_hash, :history, :last_move
  Letters = ("a".."h").to_a
  Numbers = (1..8).to_a
  Letters_hash = {1=>"a", 2=>"b", 3=>"c", 4=>"d", 5=>"e", 6=>"f", 7=>"g", 8=>"h"}
  def initialize
    @history = History.new
    @square_hash = Hash.new
    assign_coordinate_names
    @white_background = false
  end

  def deep_copy(i)
    Marshal.load(Marshal.dump(i))
  end

  def assign_coordinate_names
    Letters.each_with_index do |letter,index|
      Numbers.each do |n|
        @square_hash["#{letter}#{n}"] = Square.new(index+1,n,"#{letter}#{n}")
      end
    end
  end

  def to_s
    board_string = "\t  a  b  c  d  e  f  g  h  \n\t"
    Numbers.each_with_index do |number, index|
      board_string += "#{Numbers[7 - index]}"
      Letters.each do |letter|
        if !@square_hash["#{letter}#{9 - number}"].piece_on_square.nil?
          board_string += color_background(" #{@square_hash["#{letter}#{9 - number}"].piece_on_square.unicode} ")
        else 
          board_string += color_background("   ")
        end
        @white_background = !@white_background
      end
      @white_background = !@white_background
      board_string += " #{Numbers[7 - index]}\n\t"
    end
    board_string += "  a  b  c  d  e  f  g  h  \n"
    board_string
  end

  def color_background(string)
    @white_background ? string = string.on_black : string = string.on_white
    string
  end

  def simplified_board
    @simplified_board = {}
    @square_hash.each do |k,v|
      v.piece_on_square.nil? ? @simplified_board[k] = nil : @simplified_board[k] = v.piece_type.to_s
    end
    @simplified_board
  end

  def store_board
    @history.snapshot.push(simplified_board)
  end 

  def store_move(from_square, to_square)
    @history.last_move = {}
    @history.last_move["#{from_square.piece_type}"] = [from_square, to_square]
  end 

  def place_piece(from_square, to_square)
    to_square.piece_on_square = from_square.piece_on_square
    from_square.piece_on_square = nil
  end

  def square_free?(square, board_hash=@square_hash)
    board_hash[square].piece_on_square.nil?
  end

  def same_color_on_square?(square, player_color, board_hash=@square_hash)
    !square_free?(square, board_hash) && board_hash[square].piece_on_square.color == player_color ? true : false
  end

  def diagonal_up_right?(from_square, to_square)
    (from_square.x < to_square.x) && (from_square.y < to_square.y) ? true : false
  end

  def diagonal_down_right?(from_square, to_square)
    (from_square.x < to_square.x) && (from_square.y > to_square.y) ? true : false
  end

  def diagonal_up_left?(from_square, to_square)
    (from_square.x > to_square.x) && (from_square.y < to_square.y) ? true : false
  end

  def diagonal_down_left?(from_square, to_square)
    (from_square.x > to_square.x) && (from_square.y > to_square.y) ? true : false
  end

  def horizontal_right?(from_square, to_square)
    from_square.x < to_square.x ? true : false
  end

  def horizontal_left?(from_square, to_square)
    from_square.x > to_square.x ? true : false
  end

  def up?(from_square, to_square)
    from_square.y < to_square.y ? true : false
  end

  def down?(from_square, to_square)
    from_square.y > to_square.y ? true : false
  end

  def pawn_promotion?
    @square_hash.any? do |_,v|
      (v.y == 8 && v.piece_type == Pawn) || (v.y == 1 && v.piece_type == Pawn)
    end
  end

  def pawn_advance_two_squares?
    if (@history.last_move.key? "Pawn") && @history.last_move["Pawn"][0].y == 7 && @history.last_move["Pawn"][1].y == 5
      true
    elsif (@history.last_move.key? "Pawn") && @history.last_move["Pawn"][0].y == 2 && @history.last_move["Pawn"][1].y == 4
      true
    else
      false
    end
  end

  def valid_en_passant?(from_square, to_square, piece)
    piece.class == Pawn && pawn_advance_two_squares? && adjacent_to_piece?(to_square, piece) ? true : false
  end

  def adjacent_to_piece?(to_square, piece)
    if piece.color == "white" && (@history.last_move["Pawn"][1].y == to_square.y - 1)
      true
    elsif piece.color == "black" && (@history.last_move["Pawn"][1].y == to_square.y + 1)
      true
    else
      false
    end
  end

  def valid_castle?(castle_side, player_color)
    if castle_side == "short" && player_color == "white" && square_free?("f1") && square_free?("g1")
      true
    elsif castle_side == "short" && player_color == "black" && square_free?("f8") && square_free?("g8")
      true
    elsif castle_side == "long" && player_color == "white" && square_free?("b1") && square_free?("c1") && square_free?("d1")
      true
    elsif castle_side == "long" && player_color == "black" && square_free?("b8") && square_free?("c8") && square_free?("d8")
      true
    else 
      false
    end
  end

  def castle(castle_side, player)
    if castle_side == "short" && player.color == "white"
      @square_hash["g1"].piece_on_square = player.king
      @square_hash["f1"].piece_on_square = player.short_side_rook
      @square_hash["e1"].piece_on_square = nil
      @square_hash["h1"].piece_on_square = nil
    elsif castle_side == "short" && player.color == "black"
      @square_hash["g8"].piece_on_square = player.king
      @square_hash["f8"].piece_on_square = player.short_side_rook
      @square_hash["e8"].piece_on_square = nil
      @square_hash["h8"].piece_on_square = nil
    elsif castle_side == "long" && player.color == "white"
      @square_hash["c1"].piece_on_square = player.king
      @square_hash["d1"].piece_on_square = player.long_side_rook
      @square_hash["e1"].piece_on_square = nil
      @square_hash["a1"].piece_on_square = nil
    elsif castle_side == "long" && player.color == "black"
      @square_hash["c8"].piece_on_square = player.king
      @square_hash["d8"].piece_on_square = player.long_side_rook
      @square_hash["e8"].piece_on_square = nil
      @square_hash["a8"].piece_on_square = nil
    end
  end

  def path_clear?(from_square, to_square, player_color, board_hash=@square_hash)
    if from_square.piece_type == Knight && (square_free?(to_square.coordinates, board_hash) || !same_color_on_square?(to_square.coordinates, player_color, board_hash))
      true
    elsif from_square.piece_type == Knight && same_color_on_square?(to_square.coordinates, player_color, board_hash)
      false
    elsif diagonal_up_right?(from_square, to_square)
       (to_square.x - from_square.x).times do |i| 
        i += 1
        if square_free?("#{Letters_hash[from_square.x + i]}#{from_square.y + i}", board_hash)
          true
        elsif (from_square.x + i  == to_square.x) && (from_square.y + i  == to_square.y) && !same_color_on_square?(to_square.coordinates, player_color, board_hash)
          true 
        else 
          break false
        end
      end
    elsif diagonal_down_right?(from_square, to_square)
      (to_square.x - from_square.x).times do |i| 
        i += 1
        if square_free?("#{Letters_hash[from_square.x + i]}#{from_square.y - i}", board_hash)
          true
        elsif (from_square.x + i  == to_square.x) && (from_square.y - i  == to_square.y) && !same_color_on_square?(to_square.coordinates, player_color, board_hash)
          true
        else 
          break false
        end
      end
    elsif diagonal_down_left?(from_square, to_square)
      (from_square.x - to_square.x).times do |i|
        i += 1
        if square_free?("#{Letters_hash[from_square.x - i]}#{from_square.y - i}", board_hash)
          true
        elsif (from_square.x - i == to_square.x) && (from_square.y - i == to_square.y) && !same_color_on_square?(to_square.coordinates, player_color, board_hash)
          true
        else
          break false
        end
      end
    elsif diagonal_up_left?(from_square, to_square)
      (from_square.x - to_square.x).times do |i|
        i += 1
        if square_free?("#{Letters_hash[from_square.x - i]}#{from_square.y + i}", board_hash)
          true
        elsif (from_square.x - i == to_square.x) && (from_square.y + i == to_square.y) && !same_color_on_square?(to_square.coordinates, player_color, board_hash)
          true
        else  
          break false
        end
      end
    elsif horizontal_left?(from_square, to_square)
      (from_square.x - to_square.x).times do |i| 
        i += 1
        if square_free?("#{Letters_hash[from_square.x - i]}#{from_square.y}", board_hash)
          true
        elsif from_square.x - i == to_square.x && !same_color_on_square?(to_square.coordinates, player_color, board_hash)
          true
        else 
          break false
        end
      end
    elsif horizontal_right?(from_square, to_square)
      (to_square.x - from_square.x).times do |i|
        i += 1
        if square_free?("#{Letters_hash[from_square.x + i]}#{from_square.y}", board_hash)
          true
        elsif from_square.x + i == to_square.x && !same_color_on_square?(to_square.coordinates, player_color, board_hash)
          true
        else 
          break false
        end
      end
    elsif down?(from_square, to_square) 
      (from_square.y - to_square.y).times do |i|
        i += 1
        if square_free?("#{Letters_hash[from_square.x]}#{from_square.y - i}", board_hash)
          true
        elsif from_square.y - i == to_square.y && !same_color_on_square?(to_square.coordinates, player_color, board_hash)
          true
        else 
          break false
        end
      end
    elsif up?(from_square, to_square)
       (to_square.y - from_square.y).times do |i|
        i += 1
        if square_free?("#{Letters_hash[from_square.x]}#{from_square.y + i}", board_hash)
          true
        elsif from_square.y + i == to_square.y && !same_color_on_square?(to_square.coordinates, player_color, board_hash)
          true
        else 
          break false
        end
      end
    else 
      puts "Error"
      false
    end
  end
end

History

class History
  attr_accessor :snapshot, :last_move
  def initialize
    @snapshot = []
    @last_move = {}
  end
end

Square

class Square
  attr_accessor :piece_on_square, :x, :y, :coordinates
  def initialize(piece_on_square=nil, x, y, coordinates)
    @piece_on_square = piece_on_square
    @x = x
    @y = y
    @coordinates = coordinates
  end

  def piece_type
    !self.piece_on_square.nil? ? self.piece_on_square.class : nil
  end
end

Game

class Game
  attr_accessor :board
  def initialize
    @player1 = Player.new("white")
    @player2 = Player.new("black")
    @board = Board.new
    @current_turn = 1
    set_opening_positions
    refresh_mock_hash
  end

  def refresh_mock_hash
    @mock_hash = @board.deep_copy(@board.square_hash)
  end

  def set_opening_positions
    @board.square_hash.each do |_,value|
      case value.y
      when 2
        value.piece_on_square = @player1.choose_player_piece(Pawn)
        @player1.choose_player_piece(Pawn).position = value.coordinates
      when 7
        value.piece_on_square = @player2.choose_player_piece(Pawn)
        @player2.choose_player_piece(Pawn).position = value.coordinates
      end

      case value.coordinates
      when "a1", "h1"
        value.piece_on_square = @player1.choose_player_piece(Rook)
        @player1.choose_player_piece(Rook).position = value.coordinates
      when "b1", "g1"
        value.piece_on_square = @player1.choose_player_piece(Knight)
        @player1.choose_player_piece(Knight).position = value.coordinates
      when "c1", "f1"
        value.piece_on_square = @player1.choose_player_piece(Bishop)
        @player1.choose_player_piece(Bishop).origin = value.coordinates
        @player1.choose_player_piece(Bishop).position = value.coordinates
      when "d1" 
        value.piece_on_square = @player1.choose_player_piece(Queen)
        @player1.choose_player_piece(Queen).position = value.coordinates
      when "e1"
        value.piece_on_square = @player1.choose_player_piece(King)
        @player1.choose_player_piece(King).position = value.coordinates
      when "a8", "h8"
        value.piece_on_square = @player2.choose_player_piece(Rook)
        @player2.choose_player_piece(Rook).position = value.coordinates
      when "b8", "g8"
        value.piece_on_square = @player2.choose_player_piece(Knight)
        @player2.choose_player_piece(Knight).position = value.coordinates
      when "c8", "f8"
        value.piece_on_square = @player2.choose_player_piece(Bishop)
        @player2.choose_player_piece(Bishop).origin = value.coordinates
        @player2.choose_player_piece(Bishop).position = value.coordinates
      when "d8"
        value.piece_on_square = @player2.choose_player_piece(Queen)
        @player2.choose_player_piece(Queen).position = value.coordinates
      when "e8"
        value.piece_on_square = @player2.choose_player_piece(King)
        @player2.choose_player_piece(King).position = value.coordinates
      end
    end
  end

  def play_game
    load_game
    while !checkmate? && !draw?
      puts @board
      move(current_player)
      refresh_mock_hash
      @board.store_board
    end
    print_game_result
  end

  def load_game
    puts "Would you like to load the last game you saved? (yes or no)"
    response = gets.chomp
    load_or_play(response)
  end

  def load_or_play(response)
    if response == "yes"
      output = File.new('game_state.yaml', 'r')
      data = YAML.load(output.read)
      @player1 = data[0]
      @player2 = data[1]
      @board = data[2]
      @current_turn = data[3]
      @mock_hash = data[4]
      output.close
    end
  end

  def exit_game
    abort("Goodbye")
  end

  def capture_piece(to_square)
    current_player.captured_pieces << to_square.piece_on_square
  end

  def capture_en_passant(opponent_pawn_square)
    capture_piece(opponent_pawn_square)
    opponent_pawn_square.piece_on_square = nil    
  end

  def remove_from_player_pieces(to_square)
    opponent.pieces.delete_if {|i| i.position == to_square.coordinates}
  end

  def square_under_attack?(square)
    @mock_hash.any? do |k,v|
      !v.piece_on_square.nil? && v.piece_on_square.color == opponent.color && move_ok?(opponent, @mock_hash[k], @mock_hash[square], v.piece_on_square, @mock_hash) 
    end
  end

  def castle_through_attack?(player_color, castle_side)
    if player_color == "white" && castle_side == "short" && !square_under_attack?("e1") && !square_under_attack?("f1") && !square_under_attack?("g1")
      false
    elsif player_color == "white" && castle_side == "long" && !square_under_attack?("e1") && !square_under_attack?("d1") && !square_under_attack?("c1")
      false  
    elsif player_color == "black" && castle_side == "short" && !square_under_attack?("e8") && !square_under_attack?("f8") && !square_under_attack?("g8")
      false
    elsif player_color == "black" && castle_side == "long" && !square_under_attack?("e8") && !square_under_attack?("d8") && !square_under_attack?("c8")
      false
    else
      true
    end
  end

  def mock_king_position
    @mock_hash.find {|_,v| v.piece_type == King && v.piece_on_square.color == current_player.color}[0]
  end

  def mock_move(from_square, to_square)
    @board.place_piece(from_square, to_square) 
  end

  def move_ok?(player, from_square, to_square, piece, board=@board.square_hash)
    if player == current_player
      return player.valid_move?(from_square, to_square, piece) && @board.path_clear?(from_square, to_square, piece.color, board) && !square_under_attack?(mock_king_position)
    elsif player == opponent
      return opponent.valid_move?(from_square, to_square, piece) && @board.path_clear?(from_square, to_square, piece.color, board)
    end
  end

  def castle_ok?(player, castle_side)
    return player.pieces_on_initial_square? && !castle_through_attack?(player.color, castle_side)
  end

  def move(player) 
    puts "Type 'save' to save your game
          \nIf you would like to 'castle', please type castle
          \nWhich piece would you like to move '#{player.color} player'? (please choose a square ex: c2)"
    choice = gets.chomp.downcase
    if choice == "save"
      data = [@player1, @player2, @board, @current_turn, @mock_hash]
      output = File.new('game_state.yaml', 'w')
      output.puts YAML.dump(data)
      output.close
      exit_game
    elsif choice != "castle" && @board.square_hash[choice].nil?
      puts "Error. Please choose again".red
    elsif choice == "castle"
      puts "Would you like to castle short (on the kingside) or long (on the queenside)
            \nplease type 'short' or 'long'".cyan  
      castle_side = gets.chomp.downcase
      if castle_side == "short" && @board.valid_castle?(castle_side, player.color) && castle_ok?(player, castle_side) 
        @board.castle(castle_side, player)
        adjust_instance_methods(player.king)
        adjust_instance_methods(player.short_side_rook)
        player.set_position(player.king, new_short_king_position)
        player.set_position(player.short_side_rook, new_short_rook_position)
        @current_turn += 1 
      elsif castle_side == "long" && @board.valid_castle?(castle_side, player.color) && castle_ok?(player, castle_side) 
        @board.castle(castle_side, player)
        adjust_instance_methods(player.king)
        adjust_instance_methods(player.long_side_rook)
        player.set_position(player.king, new_long_king_position)
        player.set_position(player.long_side_rook, new_long_rook_position)
        @current_turn += 1
      else
        puts "Unable to castle".red
      end
    elsif @board.same_color_on_square?(choice, player.color)
      piece = @board.square_hash[choice].piece_on_square
      puts "To where would you like to move that #{piece.class}?".green
      new_square = gets.chomp.downcase
      mock_move(@mock_hash[choice], @mock_hash[new_square]) unless @board.square_hash[new_square].nil?
      @mock_hash[new_square].piece_on_square.position = new_square unless @board.square_hash[new_square].nil?
      from_square = @board.square_hash[choice]
      to_square = @board.square_hash[new_square]
      if @board.square_hash[new_square].nil?
        puts "Error. Please choose again".red
      elsif !@board.square_free?(new_square) && move_ok?(player, from_square, to_square, piece) 
        capture_piece(to_square)
        @board.store_move(from_square, to_square)
        remove_from_player_pieces(to_square)
        adjust_instance_methods(piece)
        @board.place_piece(from_square, to_square)
        player.set_position(piece, to_square)
        @current_turn += 1   
      elsif @board.square_free?(new_square) && move_ok?(player, from_square, to_square, piece) 
        @board.store_move(from_square, to_square)
        adjust_instance_methods(piece)
        @board.place_piece(from_square, to_square)
        player.set_position(piece, to_square)
        @current_turn += 1
      elsif @current_turn > 1 && player.en_passant_move?(from_square, to_square, piece) && @board.square_free?(new_square) && @board.valid_en_passant?(from_square, to_square, piece) && !square_under_attack?(mock_king_position)
        capture_en_passant(@board.history.last_move["Pawn"][1])
        remove_from_player_pieces(@board.history.last_move["Pawn"][1])
        @board.store_move(from_square, to_square)
        @board.place_piece(from_square, to_square)
        player.set_position(piece, to_square)
        @current_turn += 1
      else
        puts "Invalid move, please choose again".red
        refresh_mock_hash
      end
      if @board.pawn_promotion?
        puts "Your pawn is eligible for promotion
              \nTo what piece would you like to promote that pawn (Knight, Bishop, Rook, Queen)".cyan
        new_piece = gets.chomp.capitalize
        player.promote_pawn(to_square, new_piece)
      end
    elsif @board.square_free?(choice) || !@board.same_color_on_square?(choice, player.color)
      puts "You do not have a piece there, please choose again".red
    end
  end

  def new_short_king_position
    @current_turn.even? ? @board.square_hash["g8"] : @board.square_hash["g1"]
  end

  def new_short_rook_position
    @current_turn.even? ? @board.square_hash["f8"] : @board.square_hash["f1"]
  end

  def new_long_king_position
    @current_turn.even? ? @board.square_hash["c8"] : @board.square_hash["c1"]
  end

  def new_long_rook_position
    @current_turn.even? ? @board.square_hash["d8"] : @board.square_hash["d1"]
  end

  def adjust_instance_methods(piece)
    if piece.class == Pawn || piece.class == Rook || piece.class == King
      piece.on_initial_square = false
    end
  end

  def current_player
    @current_turn.even? ? @player2 : @player1
  end

  def opponent
    @current_turn.even? ? @player1 : @player2
  end

  def print_game_result
    if checkmate? 
      puts @board
      puts "Checkmate by #{opponent.color} player".green
      puts "Game Over".cyan
    elsif draw?
      puts @board
      puts "This game is a draw".yellow
    end
  end

  def draw?
    if threefold_repetition? 
      true
    elsif stalemate?
      true
    elsif fifty_moves?
      true
    elsif insufficient_material?
      true
    else
      false
    end
  end

  def checkmate?
    !move_available? && square_under_attack?(mock_king_position) ? true : false
  end

  def stalemate?
    !move_available? && !square_under_attack?(mock_king_position) ? true : false
  end

  def move_available?
    current_player.pieces.each do |i|
      @mock_hash.each do |k,v|
        next if @mock_hash[i.position] == @mock_hash[k] || k == mock_king_position
        mock_move(@mock_hash[i.position], @mock_hash[k]) 
        @available_move = false
        if move_ok?(current_player, @board.square_hash[i.position], @board.square_hash[k], i) 
          refresh_mock_hash
          @available_move = true
          break @available_move
        else
          refresh_mock_hash
        end
      end
      break if @available_move 
    end
    @available_move
  end

  def no_pawns?
    return current_player.pieces.none? {|i| i.class == Pawn} && opponent.pieces.none? {|i| i.class == Pawn}
  end

  def only_kings?
    return current_player.pieces.all? {|i| i.class == King} && opponent.pieces.all? {|i| i.class == King}
  end

  def only_king_and_knight_or_bishop? 
    if current_player.pieces.all? {|i| i.class == King} && opponent.pieces.length == 2 && opponent.knight_and_king_only?
      true   
    elsif current_player.pieces.length == 2 && current_player.knight_and_king_only? && opponent.pieces.all? {|i| i.class == King}
      true
    elsif current_player.pieces.all? {|i| i.class == King} && opponent.pieces.length == 2 && opponent.bishop_and_king_only?
      true
    elsif current_player.pieces.length == 2 && current_player.bishop_and_king_only? && opponent.pieces.all? {|i| i.class == King}
      true
    else
      false
    end
  end

  def bishops_same_color? 
    if current_player.bishop_origin == "c1" && opponent.bishop_origin == "f8"
      true
    elsif current_player.bishop_origin == "f8" && opponent.bishop_origin == "c1"
      true
    elsif current_player.bishop_origin == "f1" && opponent.bishop_origin == "c8"
      true
    elsif current_player.bishop_origin == "c8" && opponent.bishop_origin == "f1"
      true
    else
      false
    end
  end

  def bishops_kings? 
    current_player.pieces.length == 2 && current_player.bishop_and_king_only? && opponent.pieces.length == 2 && opponent.bishop_and_king_only? ? true : false
  end

  def insufficient_material? 
    (no_pawns? && only_kings?) || (no_pawns? && only_king_and_knight_or_bishop?) || (bishops_kings? && bishops_same_color?) ? true : false
  end

  def fifty_moves?
    snapshot_array = @board.history.snapshot
    snapshot_array.length > 50 && snapshot_array.last.values.count(nil) == snapshot_array[-50].values.count(nil) && (snapshot_array.last.reject {|_,v| v != "Pawn"} == snapshot_array[-50].reject {|_,v| v != "Pawn"}) ? true : false
  end

  def threefold_repetition?
    snapshot_array = @board.history.snapshot
    snapshot_array.detect {|i| snapshot_array.count(i) > 3} && snapshot_array.each_with_index.none? {|x,index| x == snapshot_array[index + 1]} ? true : false
  end
end

Piece

class Piece
  attr_accessor :color, :unicode, :position
  def initialize(color, position=nil)
    @color = color
    @position = position
  end
end

Pawn

class Pawn < Piece
  attr_accessor :on_initial_square, :color
  def initialize(color)
    super(color)
    @on_initial_square = true
    case @color
    when "black"
      @unicode = "\u2659"
    when "white"
      @unicode = "\u265F"
    end
  end

  def get_valid_moves(from_square, to_square)
    potentials = []
    if @on_initial_square && @color == "white"
      potentials.push(
        [from_square.x, from_square.y + 1],
        [from_square.x, from_square.y + 2] 
        )
    elsif @on_initial_square && @color == "black"
      potentials = []
      potentials.push(
        [from_square.x, from_square.y - 1],
        [from_square.x, from_square.y - 2] 
        )
    elsif !@on_initial_square && @color == "white"
      potentials = []
      potentials.push(
        [from_square.x, from_square.y + 1]
        )
    elsif !@on_initial_square && @color == "black"
      potentials = []
      potentials.push(
        [from_square.x, from_square.y - 1]
        )
    end

    valid_children = potentials.select do |i|
      i[0].between?(0,8) &&
      i[1].between?(0,8)
    end
    valid_children.include? [to_square.x, to_square.y]   
  end

  def get_valid_captures(from_square, to_square)
    potentials = []
    if @color == "white"
      potentials.push(
        [from_square.x + 1, from_square.y + 1],
        [from_square.x - 1, from_square.y + 1]
        )
    elsif @color == "black"
      potentials.push(
        [from_square.x - 1, from_square.y - 1],
        [from_square.x + 1, from_square.y - 1]
        )
    end

    valid_children = potentials.select do |i|
      i[0].between?(0,8) &&
      i[1].between?(0,8)
    end
    valid_children.include? [to_square.x, to_square.y]
  end

  def get_en_passant_moves(from_square, to_square)
    potentials = []
    if @color == "white"
      potentials.push(
        [from_square.x + 1, 6],
        [from_square.x - 1, 6]
        )
    elsif @color == "black"
      potentials.push(
        [from_square.x - 1, 3],
        [from_square.x + 1, 3]
        )
    end

    valid_children = potentials.select do |i|
      i[0].between?(0,8) &&
      i[1].between?(0,8)
    end
    valid_children.include? [to_square.x, to_square.y]
  end
end

Rook

class Rook < Piece
  attr_accessor :on_initial_square
  def initialize(color, position=nil)
    super(color)
    @position = position
    @on_initial_square = true
    case @color 
    when "black"
      @unicode = "\u2656"
    when "white"
      @unicode = "\u265C"
    end
  end

  def self.get_valid_moves(from_square, to_square)
    potentials = []
    potentials.push(
      [from_square.x + 1, from_square.y],
      [from_square.x + 2, from_square.y],
      [from_square.x + 3, from_square.y],
      [from_square.x + 4, from_square.y],
      [from_square.x + 5, from_square.y],
      [from_square.x + 6, from_square.y],
      [from_square.x + 7, from_square.y],
      [from_square.x, from_square.y + 1],
      [from_square.x, from_square.y + 2],
      [from_square.x, from_square.y + 3],
      [from_square.x, from_square.y + 4],
      [from_square.x, from_square.y + 5],
      [from_square.x, from_square.y + 6],
      [from_square.x, from_square.y + 7],
      [from_square.x - 1, from_square.y],
      [from_square.x - 2, from_square.y],
      [from_square.x - 3, from_square.y],
      [from_square.x - 4, from_square.y],
      [from_square.x - 5, from_square.y],
      [from_square.x - 6, from_square.y],
      [from_square.x - 7, from_square.y],
      [from_square.x, from_square.y - 1],
      [from_square.x, from_square.y - 2],
      [from_square.x, from_square.y - 3],
      [from_square.x, from_square.y - 4],
      [from_square.x, from_square.y - 5],
      [from_square.x, from_square.y - 6],
      [from_square.x, from_square.y - 7]
      )

    valid_children = potentials.select do |i|
      i[0].between?(0,8) &&
      i[1].between?(0,8)
    end
    valid_children.include? [to_square.x, to_square.y]
  end
end

Knight

class Knight < Piece
  def initialize(color, position=nil)
    super(color)
    @position = position
    case @color 
    when "black"
      @unicode = "\u2658"
    when "white"
      @unicode = "\u265E"
    end
  end

  def self.get_valid_moves(from_square, to_square)
    potentials = []
    potentials.push(
      [from_square.x + 2, from_square.y + 1],
      [from_square.x + 2, from_square.y - 1],
      [from_square.x + 1, from_square.y + 2],
      [from_square.x + 1, from_square.y - 2],
      [from_square.x - 2, from_square.y + 1],
      [from_square.x - 2, from_square.y - 1], 
      [from_square.x - 1, from_square.y + 2], 
      [from_square.x - 1, from_square.y - 2]
      )

    valid_children = potentials.select do |i|
      i[0].between?(0,8) &&
      i[1].between?(0,8)
    end
    valid_children.include? [to_square.x, to_square.y]
  end
end

Bishop

class Bishop < Piece
  attr_accessor :origin
  def initialize(color, position=nil, origin=nil)
    super(color)
    @position = position
    @origin = origin
    case @color
    when "black"
      @unicode = "\u2657"
    when "white"
      @unicode = "\u265D"
    end
  end

  def self.get_valid_moves(from_square, to_square)
    potentials = []
    potentials.push(
      [from_square.x + 1, from_square.y + 1],
      [from_square.x + 2, from_square.y + 2],
      [from_square.x + 3, from_square.y + 3],
      [from_square.x + 4, from_square.y + 4],
      [from_square.x + 5, from_square.y + 5],
      [from_square.x + 6, from_square.y + 6],
      [from_square.x + 7, from_square.y + 7],
      [from_square.x - 1, from_square.y + 1],
      [from_square.x - 2, from_square.y + 2],
      [from_square.x - 3, from_square.y + 3],
      [from_square.x - 4, from_square.y + 4],
      [from_square.x - 5, from_square.y + 5],
      [from_square.x - 6, from_square.y + 6],
      [from_square.x - 7, from_square.y + 7],
      [from_square.x + 1, from_square.y - 1],
      [from_square.x + 2, from_square.y - 2],
      [from_square.x + 3, from_square.y - 3],
      [from_square.x + 4, from_square.y - 4],
      [from_square.x + 5, from_square.y - 5],
      [from_square.x + 6, from_square.y - 6],
      [from_square.x + 7, from_square.y - 7],
      [from_square.x - 1, from_square.y - 1],
      [from_square.x - 2, from_square.y - 2],
      [from_square.x - 3, from_square.y - 3],
      [from_square.x - 4, from_square.y - 4],
      [from_square.x - 5, from_square.y - 5],
      [from_square.x - 6, from_square.y - 6],
      [from_square.x - 7, from_square.y - 7]
      )

    valid_children = potentials.select do |i|
      i[0].between?(0,8) &&
      i[1].between?(0,8)
    end
    valid_children.include? [to_square.x, to_square.y]
  end
end

Queen

class Queen < Piece
  def initialize(color, position=nil)
    super(color)
    @position = position
    case @color
    when "black"
      @unicode = "\u2655"
    when "white"
      @unicode = "\u265B"
    end
  end

  def self.get_valid_moves(from_square, to_square)
    potentials = []
    potentials.push(
      [from_square.x + 1, from_square.y],
      [from_square.x + 2, from_square.y],
      [from_square.x + 3, from_square.y],
      [from_square.x + 4, from_square.y],
      [from_square.x + 5, from_square.y],
      [from_square.x + 6, from_square.y],
      [from_square.x + 7, from_square.y],
      [from_square.x, from_square.y + 1],
      [from_square.x, from_square.y + 2],
      [from_square.x, from_square.y + 3],
      [from_square.x, from_square.y + 4],
      [from_square.x, from_square.y + 5],
      [from_square.x, from_square.y + 6],
      [from_square.x, from_square.y + 7],
      [from_square.x - 1, from_square.y],
      [from_square.x - 2, from_square.y],
      [from_square.x - 3, from_square.y],
      [from_square.x - 4, from_square.y],
      [from_square.x - 5, from_square.y],
      [from_square.x - 6, from_square.y],
      [from_square.x - 7, from_square.y],
      [from_square.x, from_square.y - 1],
      [from_square.x, from_square.y - 2],
      [from_square.x, from_square.y - 3],
      [from_square.x, from_square.y - 4],
      [from_square.x, from_square.y - 5],
      [from_square.x, from_square.y - 6],
      [from_square.x, from_square.y - 7],
      [from_square.x + 1, from_square.y + 1],
      [from_square.x + 2, from_square.y + 2],
      [from_square.x + 3, from_square.y + 3],
      [from_square.x + 4, from_square.y + 4],
      [from_square.x + 5, from_square.y + 5],
      [from_square.x + 6, from_square.y + 6],
      [from_square.x + 7, from_square.y + 7],
      [from_square.x - 1, from_square.y + 1],
      [from_square.x - 2, from_square.y + 2],
      [from_square.x - 3, from_square.y + 3],
      [from_square.x - 4, from_square.y + 4],
      [from_square.x - 5, from_square.y + 5],
      [from_square.x - 6, from_square.y + 6],
      [from_square.x - 7, from_square.y + 7],
      [from_square.x + 1, from_square.y - 1],
      [from_square.x + 2, from_square.y - 2],
      [from_square.x + 3, from_square.y - 3],
      [from_square.x + 4, from_square.y - 4],
      [from_square.x + 5, from_square.y - 5],
      [from_square.x + 6, from_square.y - 6],
      [from_square.x + 7, from_square.y - 7],
      [from_square.x - 1, from_square.y - 1],
      [from_square.x - 2, from_square.y - 2],
      [from_square.x - 3, from_square.y - 3],
      [from_square.x - 4, from_square.y - 4],
      [from_square.x - 5, from_square.y - 5],
      [from_square.x - 6, from_square.y - 6],
      [from_square.x - 7, from_square.y - 7]
      )

    valid_children = potentials.select do |i|
      i[0].between?(0,8) &&
      i[1].between?(0,8)
    end
    valid_children.include? [to_square.x, to_square.y]
  end
end

King

class King < Piece
  attr_accessor :on_initial_square, :color, :valid_children
  def initialize(color)
    super(color)
    @on_initial_square = true
    case @color
    when "black"
      @unicode = "\u2654"
    when "white"
      @unicode = "\u265A"
    end

  end

  def self.get_valid_moves(from_square, to_square)
    potentials = []
    potentials.push(
      [from_square.x, from_square.y + 1],
      [from_square.x, from_square.y - 1],
      [from_square.x + 1, from_square.y],
      [from_square.x - 1, from_square.y],
      [from_square.x + 1, from_square.y + 1],
      [from_square.x - 1, from_square.y - 1],
      [from_square.x + 1, from_square.y - 1],
      [from_square.x - 1, from_square.y + 1]
      )

    valid_children = potentials.select do |i|
      i[0].between?(0,8) &&
      i[1].between?(0,8)
    end
    valid_children.include? [to_square.x, to_square.y]
  end
end
\$\endgroup\$
  • 2
    \$\begingroup\$ Don't have time for a full answer now, but one thing sticks out right away: the repetition in defining allowed moves one by one, as you have. Instead, create helper methods like horizontal_move?, vertical_move?, diagonal_move?, and so on. They could take args to determing the number of squares, too. This will not only make the code much shorter, but also much more readable. Eg, you can define a queen's move as "anything horizontal, vertical, or diagonal," which is exactly how we naturally think about it. \$\endgroup\$ – Jonah Jan 17 '16 at 18:14
  • 2
    \$\begingroup\$ You should make a git for this, so we can download it. \$\endgroup\$ – Bam Jan 18 '16 at 20:46
  • \$\begingroup\$ "Pieces should be as dumb as possible." I think this decision has interfered with your decomposition, and Board, Game, and Player have, as a result, become "utility drawers" for Piece behaviour. Also, there seems to be a glaring lack of a Move object in the current model. \$\endgroup\$ – Drenmi Jan 19 '16 at 7:27
  • 1
    \$\begingroup\$ (wrt Piece) "They should return their available moves regardless of the current state of the board/game" Why? \$\endgroup\$ – Nic Hartley Apr 27 '16 at 5:10
  • \$\begingroup\$ Also, please link the gems you require in the question. It makes reviewing simpler :) \$\endgroup\$ – Nic Hartley Apr 27 '16 at 5:12
1
\$\begingroup\$

Biggest syntax suggestion
There's a lot of repetition and naming issues that makes the code hard to read. For example, Player#valid_move? could instead be (ignoring correctness of logic):

def valid_move?(from_square, to_square, piece)
  is_pawn = piece.is_a?(Pawn) # not sure why of all piece types, pawns are specifically being singled out here
  same_x = to_square.x == from_square.x # not sure why only x is being checked
  dest_occupied = !!to_square.piece_on_square
  land_on_enemy_piece = dest_occupied && to_square.piece_on_square.color == piece.color # give a name to this "concept"

  if is_pawn && !same_x && land_on_enemy_piece
    piece.get_valid_captures(from_square, to_square)
  elsif !is_pawn || (same_x && !dest_occupied)
    piece.get_valid_moves(from_square, to_square)
  else
    false
  end
end

Biggest OOP design suggestion

Pieces should be as dumb as possible

Pieces ought to define how they move, but they shouldn't be able to Piece.get_valid_moves. Determining valid moves requires a few things:

  • How a piece generally moves
  • The piece's position on the board
  • State of the board/where other pieces are
  • Whether pieces in their path are allies or enemies

If Pieces can determine valid moves, they'd need to "know" nearly everything on the board. This defeats the purpose of OO Encapsulation! If pieces are "dumb" and well-encapsulated, then Piece is a lower level abstraction and Board and Player depends on Piece, not the other way around.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.