123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513 |
- module ChessData
-
- # Used to indicate an error in making a move.
- class InvalidMoveError < RuntimeError
- end
- # Moves is a collection of regular expressions and methods to recognise
- # how moves are written in PGN files, and to make the moves on a given
- # board.
- #
- # As the moves are obtained from PGN files, they are assumed to be correct.
- #
- module Moves
- # :nodoc:
- # Regular expressions to match each of the move types.
- Square = /[a-h][1-8]/
- Piece = /[KQRBN]/
- MatchKingsideCastles = /^O-O\+?\Z/
- MatchQueensideCastles = /^O-O-O\+?\Z/
- MatchPieceMove = /^(#{Piece})([a-h]?|[1-8]?)x?(#{Square})\+?\Z/
- MatchPawnCapture = /^([a-h])x(#{Square})\+?\Z/
- MatchPromotionPawnMove = /^([a-h][18])=([QqRrBbNn])\+?\Z/
- MatchSimplePawnMove = /^(#{Square})\+?\Z/
- MatchPromotionPawnCapture = /^([a-h])x([a-h][18])=([QqRrBbNn])\+?\Z/
- # Combined regular expression, to match a legal move.
- LegalMove = /#{MatchKingsideCastles}|#{MatchQueensideCastles}|#{MatchPieceMove}|#{MatchPawnCapture}|#{MatchPromotionPawnMove}|#{MatchSimplePawnMove}|#{MatchPromotionPawnCapture}/
- # :doc:
- # Returns an instance of the appropriate move type.
- # string:: a move read from a PGN file.
- def Moves.new_move string
- case string
- when MatchKingsideCastles then KingsideCastles.new
- when MatchQueensideCastles then QueensideCastles.new
- when MatchPieceMove then PieceMove.new string
- when MatchPawnCapture then PawnCapture.new string
- when MatchPromotionPawnMove then PromotionPawnMove.new string
- when MatchSimplePawnMove then SimplePawnMove.new string
- when MatchPromotionPawnCapture then PromotionPawnCapture.new string
- else raise InvalidMoveError.new("Invalid move: #{string}")
- end
- end
- # Return true if given _piece_ can move from _start_ square to _finish_ on given _board_.
- def Moves.can_reach board, piece, start, finish
- start = start.upcase
- finish = finish.upcase
- case piece
- when "K", "k" then Moves.king_can_reach start, finish
- when "Q", "q" then Moves.queen_can_reach board, start, finish
- when "R", "r" then Moves.rook_can_reach board, start, finish
- when "B", "b" then Moves.bishop_can_reach board, start,finish
- when "N", "n" then Moves.knight_can_reach start, finish
- end
- end
- private
- # Return true if moving the giving piece from start to finish
- # will leave the moving side's king in check.
- def Moves.king_left_in_check board, piece, start, finish
- test_board = board.clone
- test_board[start] = nil
- test_board[finish] = piece
- if board.to_move == "w"
- test_board.white_king_in_check?
- else
- test_board.black_king_in_check?
- end
- end
- def Moves.king_can_reach start, finish
- Moves.step_h(start, finish) <= 1 && Moves.step_v(start, finish) <= 1
- end
- def Moves.queen_can_reach board, start,finish
- Moves.rook_can_reach(board, start, finish) ||
- Moves.bishop_can_reach(board, start, finish)
- end
- def Moves.rook_can_reach board, start, finish
- start_col, start_row = Board.square_to_coords start
- end_col, end_row = Board.square_to_coords finish
- if start_col == end_col # moving along column
- row_1 = [start_row, end_row].min + 1
- row_2 = [start_row, end_row].max - 1
- row_1.upto(row_2) do |row|
- return false unless board[Board.coords_to_square(start_col, row)] == nil
- end
- elsif start_row == end_row # moving along row
- col_1 = [start_col, end_col].min + 1
- col_2 = [start_col, end_col].max - 1
- col_1.upto(col_2) do |col|
- return false unless board[Board.coords_to_square(col, start_row)] == nil
- end
- else
- return false
- end
- return true
- end
- def Moves.bishop_can_reach board, start,finish
- return false unless Moves.step_h(start,finish) == Moves.step_v(start, finish)
- start_col, start_row = Board.square_to_coords start
- end_col, end_row = Board.square_to_coords finish
- dirn_h = (end_row - start_row) / (end_row - start_row).abs
- dirn_v = (end_col - start_col) / (end_col - start_col).abs
- 1.upto(Moves.step_h(start,finish)-1) do |i|
- square = Board.coords_to_square(start_col+(i*dirn_v),
- start_row+(i*dirn_h))
- unless board[square] == nil
- return false
- end
- end
- return true
- end
- def Moves.knight_can_reach start, finish
- h = Moves.step_h start, finish
- v = Moves.step_v start, finish
- return (h == 2 && v == 1) || (h == 1 && v == 2)
- end
- # Return size of horizontal gap between start and finish
- def Moves.step_h start, finish
- (start.bytes[0] - finish.bytes[0]).abs
- end
- # Return size of vertical gap between start and finish
- def Moves.step_v start, finish
- (start.bytes[1] - finish.bytes[1]).abs
- end
- # Methods to support king-side castling move.
- class KingsideCastles
- def to_s
- "O-O"
- end
- # Depending on the colour to move, will either castle king-side for white or black.
- # Returns a new instance of the board.
- def make_move board
- if board.to_move == "w"
- white_castles board
- else
- black_castles board
- end
- end
-
- private
- def white_castles board
- raise InvalidMoveError.new("white O-O") unless board["E1"] == "K" &&
- board["F1"] == nil && board["G1"] == nil &&
- board["H1"] == "R" && board.white_king_side_castling
- revised_board = board.clone
- revised_board["E1"] = nil
- revised_board["F1"] = "R"
- revised_board["G1"] = "K"
- revised_board["H1"] = nil
- revised_board.to_move = "b"
- revised_board.enpassant_target = "-"
- revised_board.halfmove_clock += 1
- revised_board.white_king_side_castling = false
- revised_board.white_queen_side_castling = false
- return revised_board
- end
- def black_castles board
- raise InvalidMoveError.new("black O-O") unless board["E8"] == "k" &&
- board["F8"] == nil && board["G8"] == nil &&
- board["H8"] == "r" && board.black_king_side_castling
- revised_board = board.clone
- revised_board["E8"] = nil
- revised_board["F8"] = "r"
- revised_board["G8"] = "k"
- revised_board["H8"] = nil
- revised_board.to_move = "w"
- revised_board.enpassant_target = "-"
- revised_board.halfmove_clock += 1
- revised_board.fullmove_number += 1
- revised_board.black_king_side_castling = false
- revised_board.black_queen_side_castling = false
- return revised_board
- end
- end
- # Methods to support queen-side castling move.
- class QueensideCastles
- def to_s
- "O-O-O"
- end
- # Depending on the colour to move, will either castle queen-side for white or black.
- # Returns a new instance of the board.
- def make_move board
- if board.to_move == "w"
- white_castles board
- else
- black_castles board
- end
- end
- private
- def white_castles board
- raise InvalidMoveError.new("white O-O-O") unless board["E1"] == "K" &&
- board["D1"] == nil && board["C1"] == nil &&
- board["B1"] == nil && board["A1"] == "R" &&
- board.white_queen_side_castling
- revised_board = board.clone
- revised_board["E1"] = nil
- revised_board["D1"] = "R"
- revised_board["C1"] = "K"
- revised_board["B1"] = nil
- revised_board["A1"] = nil
- revised_board.to_move = "b"
- revised_board.enpassant_target = "-"
- revised_board.halfmove_clock += 1
- revised_board.white_king_side_castling = false
- revised_board.white_queen_side_castling = false
- return revised_board
- end
- def black_castles board
- raise InvalidMoveError.new("black O-O") unless board["E8"] == "k" &&
- board["D8"] == nil && board["C8"] == nil &&
- board["B8"] == nil && board["A8"] == "r" &&
- board.black_queen_side_castling
- revised_board = board.clone
- revised_board["E8"] = nil
- revised_board["D8"] = "r"
- revised_board["C8"] = "k"
- revised_board["B8"] = nil
- revised_board["A8"] = nil
- revised_board.to_move = "w"
- revised_board.enpassant_target = "-"
- revised_board.halfmove_clock += 1
- revised_board.fullmove_number += 1
- revised_board.black_king_side_castling = false
- revised_board.black_queen_side_castling = false
- return revised_board
- end
- end
- # Methods to support a simple pawn move, moving directly forward.
- class SimplePawnMove
- def initialize move
- @move_string = move
- move =~ MatchSimplePawnMove
- @destination = $1
- end
- def to_s
- @move_string
- end
- # Returns a new instance of the board after move is made.
- def make_move board
- if board.to_move == "w"
- white_move board
- else
- black_move board
- end
- end
- private
- def white_move board
- revised_board = board.clone
- if single_step board
- revised_board[@destination] = "P"
- revised_board[previous_square(board.to_move)] = nil
- revised_board.enpassant_target = "-"
- elsif initial_step board
- revised_board[@destination] = "P"
- revised_board[initial_square(board.to_move)] = nil
- revised_board.enpassant_target = previous_square(board.to_move)
- else
- raise InvalidMoveError.new "white #{@move_string}"
- end
- revised_board.to_move = "b"
- revised_board.halfmove_clock = 0
- return revised_board
- end
- def black_move board
- revised_board = board.clone
- if single_step board
- revised_board[@destination] = "p"
- revised_board[previous_square(board.to_move)] = nil
- revised_board.enpassant_target = "-"
- elsif initial_step board
- revised_board[@destination] = "p"
- revised_board[initial_square(board.to_move)] = nil
- revised_board.enpassant_target = previous_square(board.to_move)
- else
- raise InvalidMoveError.new "black #{@move_string}"
- end
- revised_board.to_move = "w"
- revised_board.halfmove_clock = 0
- revised_board.fullmove_number += 1
- return revised_board
- end
- def single_step board
- if board.to_move == "w"
- pawn = "P"
- else
- pawn = "p"
- end
- board[@destination] == nil &&
- board[previous_square(board.to_move)] == pawn
- end
- def initial_step board
- if board.to_move == "w"
- pawn = "P"
- rank = 4
- else
- pawn = "p"
- rank = 5
- end
- board[@destination] == nil && @destination[1].to_i == rank &&
- board[initial_square(board.to_move)] == pawn
- end
- def previous_square colour
- if colour == "w"
- offset = -1
- else
- offset = +1
- end
- "#{@destination[0]}#{@destination[1].to_i+offset}"
- end
- def initial_square colour
- if colour == "w"
- initial_rank = 2
- else
- initial_rank = 7
- end
- "#{@destination[0]}#{initial_rank}"
- end
- end
- # Methods to support a pawn move leading to promotion.
- class PromotionPawnMove < SimplePawnMove
- def initialize string
- @move_string = string
- string =~ MatchPromotionPawnMove
- string.split("=")
- @destination = $1
- @piece = $2
- end
- # Returns a new instance of the board after move is made.
- def make_move board
- @piece.downcase! if board.to_move == "b"
- revised_board = super board
- revised_board[@destination] = @piece
- return revised_board
- end
- end
- # Methods to support a pawn move which makes a capture.
- class PawnCapture
- def initialize move
- @move_string = move
- move =~ MatchPawnCapture
- @source = $1
- @destination = $2
- end
- def to_s
- @move_string
- end
- # Returns a new instance of the board after move is made.
- def make_move board
- origin = find_origin board.to_move
- revised_board = board.clone
- if @destination == board.enpassant_target
- revised_board["#{@destination[0]}#{origin[1]}"] = nil
- end
- revised_board[origin] = nil
- revised_board[@destination] = board[origin]
- revised_board.enpassant_target = "-"
- revised_board.halfmove_clock = 0
- if board.to_move == "w"
- revised_board.to_move = "b"
- else
- revised_board.to_move = "w"
- revised_board.fullmove_number += 1
- end
- return revised_board
- end
- private
- # For a pawn capture, find the originating row and create origin square
- def find_origin to_move
- row = @destination[1].to_i
- if to_move == "w"
- row -= 1
- else
- row += 1
- end
- return "#{@source}#{row}"
- end
- end
- # Methods to support a pawn move which is both a capture and a promotion.
- class PromotionPawnCapture < PawnCapture
- def initialize move
- @move_string = move
- move =~ MatchPromotionPawnCapture
- @source = $1
- @destination = $2
- @piece = $3
- end
- def to_s
- @move_string
- end
- # Returns a new instance of the board after move is made.
- def make_move board
- @piece.downcase! if board.to_move == "b"
- revised_board = super board
- revised_board[@destination] = @piece
- return revised_board
- end
- end
- # Methods to support a piece move.
- class PieceMove
- def initialize move
- @move_string = move
- move =~ MatchPieceMove
- @piece = $1
- @identifier = $2
- @destination = $3
- @is_capture = move.include? "x"
- end
- def to_s
- @move_string
- end
- # Returns a new instance of the board after move is made.
- def make_move board
- @piece.downcase! if board.to_move == "b"
- # for given piece type, locate those pieces on board which can reach destination
- origin = board.locations_of(@piece, @identifier).select do |loc|
- Moves.can_reach(board, @piece, loc, @destination)
- end
- # filter out ambiguities raised by king being left in check
- if origin.length > 1
- origin = origin.delete_if do |loc|
- Moves.king_left_in_check(board, @piece, loc, @destination)
- end
- end
- # there should only be one unique piece at this point
- # raise an InvalidMoveError if not
- unless origin.length == 1 && board[origin.first] == @piece
- raise InvalidMoveError, "Not a unique/valid choice for #{@piece} to #{@destination}"
- end
- # setup a revised board with the move completed
- revised_board = board.clone
- revised_board[origin.first] = nil
- revised_board[@destination] = @piece
- revised_board.to_move = case board.to_move
- when "w" then "b"
- when "b" then "w"
- end
- revised_board.enpassant_target = "-"
- if @is_capture
- revised_board.halfmove_clock = 0
- else
- revised_board.halfmove_clock += 1
- end
- revised_board.fullmove_number += 1 if board.to_move == "b"
- return revised_board
- end
- end
- end
- end
|