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.