# Chess engine in Ruby

I've decided to write a Chess engine in Ruby. The first step (before any move generation, AI, search trees, etc.) is to get a solid board representation so the computer and I can effectively communicate the game state to one another. The board representation I am using here is of the 64-bitstring variety. (I implemented one using 0x88 board rep'n too, but that isn't here. From what I understand, having a several representations on hand in memory will be most useful to solve various number-crunching issues that will appear along the way.)

#!/usr/bin/env ruby
#################################################
=begin
A game of chess can be played in the terminal!
User(s) must know the rules because the program knows none.
Moves must be entered as "initial_square-terminal_square",
where the squares must be given in algebraic chess notation
and any whitespace is ok in that entered string.
The program checks if initial square contains a piece of your color
and if the termnal square does not.
There are no other restrictions on piece movement.
Game runs in an infinite loop, so quit with Ctrl-C.
=end
#################################################
class Integer
# out: String = standard chessboard coordinates (e.g., "D4")
# in: Fixnum = coordinates on the 8x16 board in memory
def to_square
if (0..63).to_a.include?(self)
then
square = String.new
square << ((self.to_i % 8) + "A".ord).chr
square << ((self.to_i/8).floor + 1).to_s
end
return square
end

# out: move "1" from init to term position and change init position to "0"
# in: two cells
def move(initial_cell,terminal_cell)
return self - 2**initial_cell + 2**terminal_cell
end

# out: change "1" from given cell position to "0"
# in: cell
def remove(cell)
return self - 2**cell
end

# convert 64bitstring to array of cells
def to_cells
cells_array = Array.new
0.upto(63) do |i|
cells_array << i if 2**i & self > 0
end
return cells_array
end
end
#################################################
class String
# out: String = standard chessboard coordinates (e.g., "A3", "D4")
# in: Fixnum = virtual cell location (e.g., 0,19,51,... )
def to_cell
# check if in standard form
return nil if self.length!= 2
rank = self[0].upcase
file = self[1]
cell = file.to_i*8 + (rank.ord - 65) - 8
return cell
end
end
#################################################
class Game
# setup the game. its attributes are the pieces in game, access white pawns with "<game_name>.whitePawns"
attr_accessor :whitePawns, :whiteKnights, :whiteBishops, :whiteRooks, :whiteQueens, :whiteKing,
:blackPawns, :blackKnights, :blackBishops, :blackRooks, :blackQueens, :blackKing,
:whitePieces, :blackPieces,
:whiteCastled, :blackCastled,
:whitesMove

# initialize game, i.e., define initial piece locations
def initialize
# assign white pieces' cells
@whitePawns =   0b0000000000000000000000000000000000000000000000001111111100000000
@whiteKnights = 0b0000000000000000000000000000000000000000000000000000000001000010
@whiteBishops = 0b0000000000000000000000000000000000000000000000000000000000100100
@whiteRooks =   0b0000000000000000000000000000000000000000000000000000000010000001
@whiteQueens =  0b0000000000000000000000000000000000000000000000000000000000010000
@whiteKing =    0b0000000000000000000000000000000000000000000000000000000000001000
# assign black pieces' cells
@blackPawns =   0b0000000011111111000000000000000000000000000000000000000000000000
@blackKnights = 0b0100001000000000000000000000000000000000000000000000000000000000
@blackBishops = 0b0010010000000000000000000000000000000000000000000000000000000000
@blackRooks =   0b1000000100000000000000000000000000000000000000000000000000000000
@blackQueens =  0b0001000000000000000000000000000000000000000000000000000000000000
@blackKing =    0b0000100000000000000000000000000000000000000000000000000000000000
# game control flags
@whitesMove = true
@whiteCastled = false
@blackCastles = false
end

def board
@board = Array.new
0.upto(63) do |i|
@board[i] = nil
end
0.upto(63) do |i|
bit_compare = 2**i
@board[i] = "P" if @whitePawns    & bit_compare != 0
@board[i] = "N" if @whiteKnights  & bit_compare != 0
@board[i] = "B" if @whiteBishops  & bit_compare != 0
@board[i] = "R" if @whiteRooks    & bit_compare != 0
@board[i] = "Q" if @whiteQueens   & bit_compare != 0
@board[i] = "K" if @whiteKing     & bit_compare != 0
@board[i] = "p" if @blackPawns    & bit_compare != 0
@board[i] = "n" if @blackKnights  & bit_compare != 0
@board[i] = "b" if @blackBishops  & bit_compare != 0
@board[i] = "r" if @blackRooks    & bit_compare != 0
@board[i] = "q" if @blackQueens   & bit_compare != 0
@board[i] = "k" if @blackKing     & bit_compare != 0
end
return @board
end

# change: piece bitstrings according to move
# in: String, String = squares to move from and to
def make_move(initial_cell,terminal_cell)
# find and alter captured piece's (if any) bitstring
instance_variables.select{ |var| var =~ /Pawns|Knights|Bishops|Rooks|Queens|King/ }.each do |var|
if opponentsPieces & 2**terminal_cell > 0
then
instance_variables.select{ |var2| var2 =~ /Pawns|Knights|Bishops|Rooks|Queens|King/ }.each do |opp_var|
if 2**terminal_cell & instance_variable_get(opp_var) > 0
instance_variable_set(opp_var,instance_variable_get(opp_var).remove(terminal_cell))
end
end
end
end
# find and alter moving piece's bitstring
instance_variables.select{ |var| var =~ /Pawns|Knights|Bishops|Rooks|Queens|King/ }.each do |var|
if 2**initial_cell & instance_variable_get(var) > 0
instance_variable_set(var,instance_variable_get(var).move(initial_cell,terminal_cell))
end
end
end

# out; bitstring of white piece locations
def whitePieces
return @whitePawns | @whiteKnights | @whiteBishops | @whiteRooks | @whiteQueens | @whiteKing
end

# out; bitstring of black piece locations
def blackPieces
return @blackPawns | @blackKnights | @blackBishops | @blackRooks | @blackQueens | @blackKing
end

# out: bitstring of pieces belonging to the moving color
def moversPieces
case @whitesMove
when true then return whitePieces
when false then return blackPieces
end
end

# out: bitstring of pieces belonging to not the moving color
def opponentsPieces
case @whitesMove
when true then return blackPieces
when false then return whitePieces
end
end

# heyy, can i move a piece to this terminal cell?
# out: Boolean = true if mover's play terminates in a cell not containing a friendly piece
# in: Fixnum = terminal cell in 0..63
def legal_move?(terminal_cell)
return 2**terminal_cell & movers_pieces == 0
end

def save_state

end

# output current state or game board to terminal
def display
system('clear')
puts
# show board with pieces
print "\t\tA\tB\tC\tD\tE\tF\tG\tH\n\n"
print "\t    +", " ----- +"*8,"\n\n"
8.downto(1) do |rank|
print "\t#{rank}   |\t"
'A'.upto('H') do |file|
if board["#{file}#{rank}".to_cell] then piece = board["#{file}#{rank}".to_cell]
else piece = " "
end
print "#{piece}   |\t"
end
print "#{rank}\n\n\t    +", " ----- +"*8,"\n\n"
end
print "\t\tA\tB\tC\tD\tE\tF\tG\tH"
puts "\n\n"
# show occupancy
print " White occupancy: "
puts whitePieces.to_cells.map{ |cell| cell.to_square}.join(", ")
print " Black occupancy: "
puts blackPieces.to_cells.map{ |cell| cell.to_square}.join(", ")
puts
# show whose move it is
case @whitesMove
when true
puts " WHITE to move."
when false
puts " BLACK to move."
end
puts
end

def play
until false do
# show board
display
# request move
initial_cell, terminal_cell = nil
until !initial_cell.nil? & !terminal_cell.nil? do
print " enter move : "
# get move in D2-D4 format; break apart into array by "-" and remove any whitespace in each piece
user_input = gets.strip.upcase.delete(' ')
# if string entered is something like "A4-C5" or " a4  -C5  " etc
if user_input =~ /[A-H][1-8]-[A-H][1-8]/
user_move = user_input.split("-").map { |cell| cell.strip }
# if initial square contains one of your pieces   & terminal square does not
if ((2**user_move[0].to_cell & moversPieces) > 0) & ((2**user_move[1].to_cell & ~moversPieces) > 0)
then
initial_cell, terminal_cell = user_move[0].to_cell, user_move[1].to_cell
end
end
end
make_move(initial_cell,terminal_cell)
@whitesMove = !@whitesMove
end
end
end
#################################################
#################################################
#################################################
game = Game.new
game.play
#################################################
#################### fin ########################
#################################################


I would separate logic from presentation and I would write unit tests from begin on.

I would split display in two methods:

• build_board is creating a String or an Array
• display writes the result of build_board

Advantage: You can write unit tests:

• Check the result of build_board
• make a move
• check again the result of build_board.

I made a modified version of play:

def play

user_input = nil
while true do
# show board
display
# request move
initial_cell, terminal_cell = nil
until ! initial_cell.nil? & !terminal_cell.nil? do
print " enter move : "
# get move in D2-D4 format; break apart into array by "-" and remove any whitespace in each piece
user_input = gets.strip.upcase.delete(' ')
#Check user input
case user_input
# if string entered is something like "A4-C5" or " a4  -C5  " etc
when /[A-H][1-8]-[A-H][1-8]/
user_move = user_input.split("-").map { |cell| cell.strip }
# if initial square contains one of your pieces   & terminal square does not
if ((2**user_move[0].to_cell & moversPieces) > 0) & ((2**user_move[1].to_cell & ~moversPieces) > 0)
initial_cell, terminal_cell = user_move[0].to_cell, user_move[1].to_cell
end
when 'SAVE' #store
puts "Store not implemented yet"
next
when 'END' #stop the game
puts "Thanks for playing"
exit
else
puts 'Invalid move. Please use ...' ##Add short description of usage ###
next
end #case user_input
end
case make_move(initial_cell,terminal_cell)
when true
@whitesMove = !@whitesMove
when :empty_field
puts "Start field was empty - please redo"
else
puts "Move not done - please redo"
end
end
end


This play offers the possibility to add more commands (I prepared END and SAVE).

Very important: Give a response if there is a problem

make_move should return a result. trueif it is ok, in other cases provide an error code. So the player can get a response and you have the possibility to test the function in your unittest (make an invalid move and check if the error code is correct).

• great, thanks for the advice. i have begun separating logic from presentation. i am realizing now that this has always been something i've struggled with. question: i am not familiar with using :empty_field, when would that last case statement evaluate to :empty_field? – mwatson Apr 13 '12 at 15:34
• Never ;) It was just a idea, what you could do. Imagine you call make_move and the method detect, that you start from an empty field, then you could return :empty_field. When you detect, that the figure has the wrong color, you may return :wrong_color... Depending on the return code, you can inform the player, why the move is invalid. – knut Apr 13 '12 at 17:54
• ohh, i see what you're saying now. good idea, thank you. – mwatson Apr 13 '12 at 18:07

I don't think using bit arithmetic will give you any performance advantages in ruby in the long run (still 1<<i will be faster than 2**i though). Just compare

0b0000000000000000000000000000000000000000000000001111111100000000.class


with

0b1000000100000000000000000000000000000000000000000000000000000000.class


While the former is a Fixnum, the latter is a Bignum. So in the end Ruby can't use primitive types for these arithmetics anyway and has to do all kinds of conversions in the background. It's probably best to store all pieces in a simple 64 field Array.