123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325 |
- module ChessData
- # Pieces are structures, made from:
- # - piece: is a string "P", "p", "N", "n", etc
- # - square: is square definition, either a symbol :e4 or string "E4"
- PieceDefn = Struct.new(:piece, :square)
- # Holds information about a chess position, including:
- # - location of all pieces
- # - options for castling king or queen side
- # - halfmove and fullmove counts
- # - possible enpassant target
- #
- class Board
- # The next player to move, "w" or "b".
- attr_accessor :to_move
- # True if white king-side castling is valid
- attr_accessor :white_king_side_castling
- # True if white queen-side castling is valid
- attr_accessor :white_queen_side_castling
- # True if black king-side castling is valid
- attr_accessor :black_king_side_castling
- # True if black queen-side castling is valid
- attr_accessor :black_queen_side_castling
- # If enpassant is possible, holds the target square, or "-"
- attr_accessor :enpassant_target
- # Counts the number of half moves
- attr_accessor :halfmove_clock
- # Counts the number of full moves
- attr_accessor :fullmove_number
- # Creates an instance of an empty chess board
- def initialize
- @board = []
- 8.times do
- @board << [nil] * 8
- end
- @to_move = "w"
- @white_king_side_castling = false
- @white_queen_side_castling = false
- @black_king_side_castling = false
- @black_queen_side_castling = false
- @enpassant_target = "-"
- @halfmove_clock = 0
- @fullmove_number = 1
- end
- # Makes a full copy of this board instance
- def clone
- copy = Board.new
- 8.times do |row|
- 8.times do |col|
- copy.set row, col, @board[row][col]
- end
- end
- copy.to_move = @to_move
- copy.white_king_side_castling = @white_king_side_castling
- copy.white_queen_side_castling = @white_queen_side_castling
- copy.black_king_side_castling = @black_king_side_castling
- copy.black_queen_side_castling = @black_queen_side_castling
- copy.enpassant_target = @enpassant_target
- copy.halfmove_clock = @halfmove_clock
- copy.fullmove_number = @fullmove_number
- return copy
- end
- # Provide a way of looking up items based on usual chess
- # notation, i.e. :e4 or "E4".
- # Raises an ArgumentError if square is not a valid chessboard position.
- # @param [String, Symbol] square is location to find
- # @return [String] chess on the given square
- def [](square)
- col, row = Board.square_to_coords square
- return @board[row][col]
- end
- # Change the piece on a given square.
- # @param [String, Symbol] square is location to change
- # @param [String] piece
- # @return [String] chess on the given square
- def []=(square, piece)
- col, row = Board.square_to_coords square
- @board[row][col] = piece
- end
- # Compare two boards for equality
- def == board
- return false unless @to_move == board.to_move &&
- @white_king_side_castling == board.white_king_side_castling &&
- @white_queen_side_castling == board.white_queen_side_castling &&
- @black_king_side_castling == board.black_king_side_castling &&
- @black_queen_side_castling == board.black_queen_side_castling &&
- @enpassant_target == board.enpassant_target &&
- @halfmove_clock == board.halfmove_clock &&
- @fullmove_number == board.fullmove_number
- 8.times do |i|
- 8.times do |j|
- square = Board.coords_to_square i, j
- return false unless self[square] == board[square]
- end
- end
- return true
- end
- # Return the location of given piece on board.
- # Identifier can be a letter or number, and if present the piece location must contain it
- def locations_of piece, identifier=""
- identifier = identifier.upcase
- result = []
- 8.times do |row|
- 8.times do |col|
- if @board[row][col] == piece
- square = Board.coords_to_square col, row
- if identifier.empty? || square.include?(identifier)
- result << square
- end
- end
- end
- end
- return result
- end
- # Count the number of occurrences of the given piece on the board.
- def count piece
- @board.flatten.count piece
- end
- # Creates a simple 2D board representation, suitable for printing to a terminal.
- def to_s
- result = ""
- 8.times do |i|
- 8.times do |j|
- square = Board.coords_to_square j, i
- piece = self[square]
- piece = "." if piece.nil?
- result += piece
- end
- result += "\n"
- end
- return result
- end
- # Check if the white king is in check.
- def white_king_in_check?
- white_king = locations_of("K").first
- black_pieces.any? do |defn|
- Moves.can_reach self, defn.piece, defn.square, white_king
- end
- end
- # Check if the black king is in check.
- def black_king_in_check?
- black_king = locations_of("k").first
- white_pieces.any? do |defn|
- Moves.can_reach self, defn.piece, defn.square, black_king
- end
- end
- # Creates a chessboard from a FEN description.
- # The FEN description may be a single string, representing a board
- # or a full six-field description.
- # Raises an ArgumentError if fen is not a valid FEN description.
- #
- # @param [String] fen a board definition in FEN format
- # @return [Board] an instance of board matching the FEN description
- def Board.from_fen fen
- fields = fen.split " "
- unless fields.length == 1 || fields.length == 6
- raise ArgumentError, "Invalid FEN description"
- end
- # create and populate a new instance of ChessBoard
- board = Board.new
- board.send(:setup_board_from_fen, fields[0])
- if fields.length == 6
- board.to_move = fields[1].downcase
- board.white_king_side_castling = fields[2].include? "K"
- board.white_queen_side_castling = fields[2].include? "Q"
- board.black_king_side_castling = fields[2].include? "k"
- board.black_queen_side_castling = fields[2].include? "q"
- board.enpassant_target = fields[3]
- board.halfmove_clock = fields[4].to_i
- board.fullmove_number = fields[5].to_i
- end
- return board
- end
- # Creates a board instance representing the start position.
- def Board.start_position
- Board.from_fen \
- "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
- end
- # Converts array coordinates into a square representation.
- #
- # > ChessBoard::Board.coords_to_square 0, 7 => "A8"
- # > ChessBoard::Board.coords_to_square 7, 0 => "H1"
- # > ChessBoard::Board.coords_to_square 4, 4 => "E5"
- #
- # The conversion is cached, for speed.
- #
- def Board.coords_to_square col, row
- unless defined? @coords_store
- @coords_store = []
- 8.times do
- @coords_store << [nil] * 8
- end
- 8.times do |cl|
- 8.times do |rw|
- @coords_store[cl][rw] = Board.square_from_coords(cl, rw)
- end
- end
- end
- return @coords_store[col][row]
- end
- # Converts a square represention into array coordinates.
- #
- # > ChessData::Board.square_to_coords "e4" => [4, 4]
- # > ChessData::Board.square_to_coords "a8" => [0, 7]
- # > ChessData::Board.square_to_coords "h1" => [7, 0]
- #
- # The conversion is cached, for speed.
- #
- def Board.square_to_coords square
- unless defined? @square_hash
- @square_hash = {}
- 8.times do |col|
- 8.times do |row|
- @square_hash[Board.square_from_coords(col, row)] = [col, row]
- end
- end
- end
- square = square.to_s.upcase # convert symbols to strings, ensure upper case
- unless @square_hash.has_key? square
- raise ArgumentError, "Invalid board notation -|#{square}|-"
- end
- return @square_hash[square]
- end
- # Provides a fast method to set value of board at given row/col index values
- # -- used to optimise clone
- def set row, col, value
- @board[row][col] = value
- end
- private
- # Converts a square represention into array coordinates.
- #
- # > ChessData::Board.square_from_coords "e4" => [4, 4]
- # > ChessData::Board.square_from_coords "a1" => [0, 7]
- # > ChessData::Board.square_from_coords "h8" => [7, 0]
- #
- def Board.square_from_coords col, row
- first = (65+col).chr
- second = (49+(7-row)).chr
- return "#{first}#{second}"
- end
- # Setup the current board
- def setup_board_from_fen fen
- rows = fen.split "/"
- unless rows.length == 8
- raise ArgumentError, "Invalid FEN description"
- end
- 8.times do |row|
- col = 0
- rows[row].chars.each do |i|
- case i
- when "K", "k", "Q", "q", "R", "r", "N", "n", "B", "b", "P", "p"
- @board[row][col] = i
- col += 1
- when /[1-8]/
- col += i.to_i
- else
- raise ArgumentError, "Invalid character in FEN description"
- end
- end
- end
- end
- # Returns the location of all white pieces and pawns.
- def white_pieces
- find_pieces "KQRBNP"
- end
- # Returns the location of all black pieces and pawns.
- def black_pieces
- find_pieces "kqrbnp"
- end
- # Returns piece+position for all pieces on the board which are in the given
- # list of pieces.
- def find_pieces pieces
- result = []
- 8.times do |row|
- 8.times do |col|
- unless @board[row][col].nil?
- if pieces.include? @board[row][col]
- result << PieceDefn.new(@board[row][col], Board.coords_to_square(col, row))
- end
- end
- end
- end
- return result
- end
- end
- end
|