14
\$\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, [email protected]_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 [email protected]_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) || [email protected]_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
Vogel612
25.5k7 gold badges59 silver badges141 bronze badges
asked Jan 16, 2016 at 20:01
\$\endgroup\$
8
  • 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\$ Commented Jan 17, 2016 at 18:14
  • 2
    \$\begingroup\$ You should make a git for this, so we can download it. \$\endgroup\$ Commented Jan 18, 2016 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\$ Commented Jan 19, 2016 at 7:27
  • 1
    \$\begingroup\$ (wrt Piece) "They should return their available moves regardless of the current state of the board/game" Why? \$\endgroup\$ Commented Apr 27, 2016 at 5:10
  • \$\begingroup\$ Also, please link the gems you require in the question. It makes reviewing simpler :) \$\endgroup\$ Commented Apr 27, 2016 at 5:12

1 Answer 1

3
\$\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.

answered Mar 3, 2019 at 8:12
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.