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

board[RW]
move[RW]
origin[RW]

Public Class Methods

new(board, move) click to toggle source

@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

castling_restrictions() click to toggle source

@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
en_passant_square() click to toggle source

@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
increment_fullmove?() click to toggle source

@return [Boolean] whether to increment the fullmove counter

# File lib/pgn/move_calculator.rb, line 143
def increment_fullmove?
  self.move.black?
end
increment_halfmove?() click to toggle source

@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
result_board() click to toggle source

@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

changes() click to toggle source
# 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
compute_origin() click to toggle source

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
destination_coords() click to toggle source
# File lib/pgn/move_calculator.rb, line 335
def destination_coords
  self.board.coordinates_for(self.move.destination)
end
direction_origins() click to toggle source

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
disambiguate(possibilities) click to toggle source
# 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
disambiguate_discovered_check(possibilities) click to toggle source

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
disambiguate_pawns(possibilities) click to toggle source

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
disambiguate_san(possibilities) click to toggle source

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
en_passant_capture() click to toggle source

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
first_piece(from, direction) click to toggle source
# 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
king_position() click to toggle source
# 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
move_origins(moves = nil) click to toggle source

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
pawn_origins() click to toggle source

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
valid_square?(file, rank) click to toggle source
# File lib/pgn/move_calculator.rb, line 331
def valid_square?(file, rank)
  (0..7) === file && (0..7) === rank
end