Wednesday, November 5, 2014

More on Chess

I've been working on my chess game (see previous post) for a while by now. Primarily because once I had the main logic in, I did quite a bit of refactoring of the design over and over. While it can be improved further, my current design is as follows.

There are nine classes: Game, Board, Piece, and 6 Piece subclasses (Pawn, King etc). There is also a module called Converter that assists in converting x and y coordinates of the Board into 1D array, and vice versa.

The Piece class has the majority of methods that subclasses inherit from. Its basic structure is as follows:

class Piece
    include Converter
    attr_accessor :id, :x, :y

    def initialize(color=:white,x=0,y=0)
    end

    def move_to(x,y)
    end

    # determines what cells a piece can theoretically go to
    # in parent class, this method checks the board boundaries 
    def can_piece_move?(to_x, to_y)
    end

    # determines whether a piece can move to a cell (e.g., if it is empty).
    # Also "captures" an enemy piece
    # if the destination cell is occupied by one.
    def valid_move?(board, to_x, to_y)
    end
end

All Piece subclasses inherit these methods and override them as needed. Thus, each Piece subclass naturally implements its own version of can_piece_move?(to_x, to_y). All Piece subclasses except for Pawn use the method valid_move?(board, to_x, to_y) as defined in the parent class. However, Pawn has its own implementation of the valid_move? method because Pawn can only move in one direction vertically and can only capture in one direction diagonally.

Additionally, the Pawn class implements a special method that allows to count how many times the Pawn was moved (as a reminder, a Pawn can be moved 1 or 2 steps at a first move, but only 1 step afterwards). The King class has a special method as well - this method loops through surrounding cells and returns an array of cells where the King can move to. This method is used in determining whether the King is in check.

The Board class is shown below. Instead of placing Pieces directly into the Board matrix, I have chosen to create a collection of pieces, where each Piece has an id calculated on the basis of x and y coordinates of the Board. All positions are thus calculated on the basis of this id. Accordingly, it is easy to find a Piece in a collection by its id or to remove a Piece from the collection once it's been captured.

class Board
    include Converter
    attr_accessor :board, :piece_collection, :destination

    def initialize
        @board = Array.new (Converter.rows) { |i| Array.new(Converter.cols) { |j| nil } }
        @piece_collection = []
        @king_collection = []
    end
   
    # places pieces into corresponding collections
    def setup_pieces
    end

    def remove_piece_from_collection(piece)
    end

    def find_piece_in_collection(pid)
    end
   
    # determines whether the king has any moves left
    def legal_moves_left?(king)
    end

    # determines if the king is in immediate danger
    def threatening_piece(king)
    end
   
    # determines if the path is free/blocked 
    def is_it_blocked?(from_x, from_y, to_x, to_y)
    end

    def display
    end
end

Finally, the Game class determines the sequence of events and checks for a winner. The sequence of events is quite straightforward: a player inputs an id of the Piece she wants to move, and an id of the location where to move the Piece to. If the move is valid, the Game updates x and y coordinates of the involved Pieces and calls a method to determine whether there is a winner. The winner method looks as follows:

def determine_winners(board)
    board.king_collection.each do |king|
       board.destination = king
       threat = board.threatening_piece(king)
       legal_moves = board.legal_moves_left?(king)

       if legal_moves == true && !threat.nil?
          puts "WINNING: #{threat.full_description} Check!"
          return true
       elsif legal_moves == false && !threat.nil?
          puts "WINNING: #{threat.full_description} Check & Mate!"
          @game = false
          return true
       elsif legal_moves == false && threat.nil?
          puts "WINNING: #{player_turn.upcase} StaleMate!"
          @game = false
          return true
       end
    end
    false
end

The full code is available at my Github. I still wonder how it is possible to improve the design. It appears that the Piece and Board are heavily dependent on each other and the same arguments (board, to_x, to_y) are constantly being passed around. As they said in BerkeleyX SaaS Engineering course, if a group of arguments keeps traveling around, they may have to be extracted into a separate class. I guess as I learn more about OO design (and Sandi Metz's book "Practical Object Oriented Design in Ruby" has been already a great help), I will find myself re-writing the game again.

No comments :

Post a Comment