Take the 2-minute tour ×
Code Review Stack Exchange is a question and answer site for peer programmer code reviews. It's 100% free, no registration required.

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.)

Please offer any advice on the beginnings of this Chess engine.

#!/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.
No saving, loading, undoing, etc.
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 ########################
#################################################
share|improve this question
add comment

2 Answers 2

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).

share|improve this answer
    
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
add comment

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.

share|improve this answer
add comment

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

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