class PGN::MoveCalculator
{PGN::MoveCalculator} is responsible for computing all of the ways that a specific move changes the current position. This includes which squares on the board need to be updated, new castling restrictions, the en passant square and whether to update fullmove and halfmove counters.
@!attribute board
@return [PGN::Board] the current board
@!attribute move
@return [PGN::Move] the current move
@!attribute origin
@return [String, nil] the origin square in SAN
Constants
- CASTLING
The squares to update for each possible castling move.
- DIRECTIONS
Specifies the movement of pieces who are allowed to move in a given direction until they reach an obstacle or the end of the board.
- MOVES
Specifies the movement of pieces that have a limited set of moves they are allowed to make.
- PAWN_MOVES
Specifies possible pawn movements. It may seem backwards since it is used to compute the origin square and not the destination.
Attributes
Public Class Methods
@param board [PGN::Board] the current board @param move [PGN::Move] the current move
# File lib/pgn/move_calculator.rb, line 90 def initialize(board, move) self.board = board self.move = move self.origin = compute_origin end
Public Instance Methods
@return [Array<String>] which castling moves are no longer available
# File lib/pgn/move_calculator.rb, line 107 def castling_restrictions restrict = [] # when a king or rook is moved case self.move.piece when 'K' restrict += ['K', 'Q'] when 'k' restrict += ['k', 'q'] when 'R' restrict << {'a1' => 'Q', 'h1' => 'K'}[self.origin] when 'r' restrict << {'a8' => 'q', 'h8' => 'k'}[self.origin] end # when castling occurs restrict += ['K', 'Q'] if ['K', 'Q'].include? move.castle restrict += ['k', 'q'] if ['k', 'q'].include? move.castle # when a rook is taken restrict << 'Q' if self.move.destination == 'a1' restrict << 'q' if self.move.destination == 'a8' restrict << 'K' if self.move.destination == 'h1' restrict << 'k' if self.move.destination == 'h8' restrict.compact.uniq end
@return [String, nil] the en passant square if applicable
# File lib/pgn/move_calculator.rb, line 149 def en_passant_square return nil if move.castle if self.move.pawn? && (self.origin[1].to_i - self.move.destination[1].to_i).abs == 2 self.move.white? ? self.origin[0] + '3' : self.origin[0] + '6' end end
@return [Boolean] whether to increment the fullmove counter
# File lib/pgn/move_calculator.rb, line 143 def increment_fullmove? self.move.black? end
@return [Boolean] whether to increment the halfmove clock
# File lib/pgn/move_calculator.rb, line 137 def increment_halfmove? !(self.move.capture || self.move.pawn?) end
@return [PGN::Board] the board after the move is made
# File lib/pgn/move_calculator.rb, line 98 def result_board new_board = self.board.dup new_board.change!(changes) new_board end
Private Instance Methods
# File lib/pgn/move_calculator.rb, line 161 def changes changes = {} changes.merge!(CASTLING[self.move.castle]) if self.move.castle changes.merge!( self.origin => nil, self.move.destination => self.move.piece, en_passant_capture => nil, ) if self.move.promotion changes[self.move.destination] = self.move.promotion end changes.reject! {|key, _| key.nil? or key.empty? } changes end
Using the current position and move, figure out where the piece came from.
# File lib/pgn/move_calculator.rb, line 181 def compute_origin return nil if move.castle possibilities = case move.piece when /[brq]/i then direction_origins when /[kn]/i then move_origins when /p/i then pawn_origins end if possibilities.length > 1 possibilities = disambiguate(possibilities) end self.board.position_for(possibilities.first) end
# File lib/pgn/move_calculator.rb, line 335 def destination_coords self.board.coordinates_for(self.move.destination) end
From the destination square, move in each direction stopping if we reach the end of the board. If we encounter a piece, add it to the list of origin possibilities if it is the moving piece, or else check the next direction.
# File lib/pgn/move_calculator.rb, line 202 def direction_origins directions = DIRECTIONS[move.piece.downcase] possibilities = [] directions.each do |dir| piece, square = first_piece(destination_coords, dir) possibilities << square if piece == self.move.piece end possibilities end
# File lib/pgn/move_calculator.rb, line 250 def disambiguate(possibilities) possibilities = disambiguate_san(possibilities) possibilities = disambiguate_pawns(possibilities) if possibilities.length > 1 possibilities = disambiguate_discovered_check(possibilities) if possibilities.length > 1 possibilities end
A piece can't move if it would result in a discovered check.
# File lib/pgn/move_calculator.rb, line 276 def disambiguate_discovered_check(possibilities) DIRECTIONS.each do |attacking_piece, directions| attacking_piece = attacking_piece.upcase if self.move.black? directions.each do |dir| piece, square = first_piece(king_position, dir) next unless piece == self.move.piece && possibilities.include?(square) piece, _ = first_piece(square, dir) possibilities.reject! {|p| p == square } if piece == attacking_piece end end possibilities end
A pawn can't move two spaces if there is a pawn in front of it.
# File lib/pgn/move_calculator.rb, line 268 def disambiguate_pawns(possibilities) self.move.piece.match(/p/i) && !self.move.capture ? possibilities.reject {|p| self.board.position_for(p).match(/2|7/) } : possibilities end
Try to disambiguate based on the standard algebraic notation.
# File lib/pgn/move_calculator.rb, line 260 def disambiguate_san(possibilities) move.disambiguation ? possibilities.select {|p| self.board.position_for(p).match(move.disambiguation) } : possibilities end
If the move is a capture and there is no piece on the destination square, it must be an en passant capture.
# File lib/pgn/move_calculator.rb, line 308 def en_passant_capture return nil if self.move.castle if !self.board.at(self.move.destination) && self.move.capture self.move.destination[0] + self.origin[1] end end
# File lib/pgn/move_calculator.rb, line 292 def first_piece(from, direction) file, rank = from i, j = direction piece = nil while valid_square?(file += i, rank += j) break if piece = self.board.at(file, rank) end [piece, [file, rank]] end
# File lib/pgn/move_calculator.rb, line 316 def king_position king = self.move.white? ? 'K' : 'k' coords = nil 0.upto(7) do |file| 0.upto(7) do |rank| if self.board.at(file, rank) == king coords = [file, rank] end end end coords end
From the destination square, make each move. If it is a valid square and matches the moving piece, add it to the list of origin possibilities.
# File lib/pgn/move_calculator.rb, line 218 def move_origins(moves = nil) moves ||= MOVES[move.piece.downcase] possibilities = [] file, rank = destination_coords moves.each do |i, j| f = file + i r = rank + j if valid_square?(f, r) && self.board.at(f, r) == move.piece possibilities << [f, r] end end possibilities end
Computes the possbile pawn origins based on the destination square and whether or not the move is a capture.
# File lib/pgn/move_calculator.rb, line 238 def pawn_origins _, rank = destination_coords double_rank = (rank == 3 && self.move.white?) || (rank == 4 && self.move.black?) pawn_moves = PAWN_MOVES[self.move.piece] moves = self.move.capture ? pawn_moves[:capture] : pawn_moves[:normal] moves += pawn_moves[:double] if double_rank move_origins(moves) end
# File lib/pgn/move_calculator.rb, line 331 def valid_square?(file, rank) (0..7) === file && (0..7) === rank end