board.rb 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. module ChessData
  2. # Pieces are structures, made from:
  3. # - piece: is a string "P", "p", "N", "n", etc
  4. # - square: is square definition, either a symbol :e4 or string "E4"
  5. PieceDefn = Struct.new(:piece, :square)
  6. # Holds information about a chess position, including:
  7. # - location of all pieces
  8. # - options for castling king or queen side
  9. # - halfmove and fullmove counts
  10. # - possible enpassant target
  11. #
  12. class Board
  13. # The next player to move, "w" or "b".
  14. attr_accessor :to_move
  15. # True if white king-side castling is valid
  16. attr_accessor :white_king_side_castling
  17. # True if white queen-side castling is valid
  18. attr_accessor :white_queen_side_castling
  19. # True if black king-side castling is valid
  20. attr_accessor :black_king_side_castling
  21. # True if black queen-side castling is valid
  22. attr_accessor :black_queen_side_castling
  23. # If enpassant is possible, holds the target square, or "-"
  24. attr_accessor :enpassant_target
  25. # Counts the number of half moves
  26. attr_accessor :halfmove_clock
  27. # Counts the number of full moves
  28. attr_accessor :fullmove_number
  29. # Creates an instance of an empty chess board
  30. def initialize
  31. @board = []
  32. 8.times do
  33. @board << [nil] * 8
  34. end
  35. @to_move = "w"
  36. @white_king_side_castling = false
  37. @white_queen_side_castling = false
  38. @black_king_side_castling = false
  39. @black_queen_side_castling = false
  40. @enpassant_target = "-"
  41. @halfmove_clock = 0
  42. @fullmove_number = 1
  43. end
  44. # Makes a full copy of this board instance
  45. def clone
  46. copy = Board.new
  47. 8.times do |row|
  48. 8.times do |col|
  49. copy.set row, col, @board[row][col]
  50. end
  51. end
  52. copy.to_move = @to_move
  53. copy.white_king_side_castling = @white_king_side_castling
  54. copy.white_queen_side_castling = @white_queen_side_castling
  55. copy.black_king_side_castling = @black_king_side_castling
  56. copy.black_queen_side_castling = @black_queen_side_castling
  57. copy.enpassant_target = @enpassant_target
  58. copy.halfmove_clock = @halfmove_clock
  59. copy.fullmove_number = @fullmove_number
  60. return copy
  61. end
  62. # Provide a way of looking up items based on usual chess
  63. # notation, i.e. :e4 or "E4".
  64. # Raises an ArgumentError if square is not a valid chessboard position.
  65. # @param [String, Symbol] square is location to find
  66. # @return [String] chess on the given square
  67. def [](square)
  68. col, row = Board.square_to_coords square
  69. return @board[row][col]
  70. end
  71. # Change the piece on a given square.
  72. # @param [String, Symbol] square is location to change
  73. # @param [String] piece
  74. # @return [String] chess on the given square
  75. def []=(square, piece)
  76. col, row = Board.square_to_coords square
  77. @board[row][col] = piece
  78. end
  79. # Compare two boards for equality
  80. def == board
  81. return false unless @to_move == board.to_move &&
  82. @white_king_side_castling == board.white_king_side_castling &&
  83. @white_queen_side_castling == board.white_queen_side_castling &&
  84. @black_king_side_castling == board.black_king_side_castling &&
  85. @black_queen_side_castling == board.black_queen_side_castling &&
  86. @enpassant_target == board.enpassant_target &&
  87. @halfmove_clock == board.halfmove_clock &&
  88. @fullmove_number == board.fullmove_number
  89. 8.times do |i|
  90. 8.times do |j|
  91. square = Board.coords_to_square i, j
  92. return false unless self[square] == board[square]
  93. end
  94. end
  95. return true
  96. end
  97. # Return the location of given piece on board.
  98. # Identifier can be a letter or number, and if present the piece location must contain it
  99. def locations_of piece, identifier=""
  100. identifier = identifier.upcase
  101. result = []
  102. 8.times do |row|
  103. 8.times do |col|
  104. if @board[row][col] == piece
  105. square = Board.coords_to_square col, row
  106. if identifier.empty? || square.include?(identifier)
  107. result << square
  108. end
  109. end
  110. end
  111. end
  112. return result
  113. end
  114. # Count the number of occurrences of the given piece on the board.
  115. def count piece
  116. @board.flatten.count piece
  117. end
  118. # Creates a simple 2D board representation, suitable for printing to a terminal.
  119. def to_s
  120. result = ""
  121. 8.times do |i|
  122. 8.times do |j|
  123. square = Board.coords_to_square j, i
  124. piece = self[square]
  125. piece = "." if piece.nil?
  126. result += piece
  127. end
  128. result += "\n"
  129. end
  130. return result
  131. end
  132. # Check if the white king is in check.
  133. def white_king_in_check?
  134. white_king = locations_of("K").first
  135. black_pieces.any? do |defn|
  136. Moves.can_reach self, defn.piece, defn.square, white_king
  137. end
  138. end
  139. # Check if the black king is in check.
  140. def black_king_in_check?
  141. black_king = locations_of("k").first
  142. white_pieces.any? do |defn|
  143. Moves.can_reach self, defn.piece, defn.square, black_king
  144. end
  145. end
  146. # Creates a chessboard from a FEN description.
  147. # The FEN description may be a single string, representing a board
  148. # or a full six-field description.
  149. # Raises an ArgumentError if fen is not a valid FEN description.
  150. #
  151. # @param [String] fen a board definition in FEN format
  152. # @return [Board] an instance of board matching the FEN description
  153. def Board.from_fen fen
  154. fields = fen.split " "
  155. unless fields.length == 1 || fields.length == 6
  156. raise ArgumentError, "Invalid FEN description"
  157. end
  158. # create and populate a new instance of ChessBoard
  159. board = Board.new
  160. board.send(:setup_board_from_fen, fields[0])
  161. if fields.length == 6
  162. board.to_move = fields[1].downcase
  163. board.white_king_side_castling = fields[2].include? "K"
  164. board.white_queen_side_castling = fields[2].include? "Q"
  165. board.black_king_side_castling = fields[2].include? "k"
  166. board.black_queen_side_castling = fields[2].include? "q"
  167. board.enpassant_target = fields[3]
  168. board.halfmove_clock = fields[4].to_i
  169. board.fullmove_number = fields[5].to_i
  170. end
  171. return board
  172. end
  173. # Creates a board instance representing the start position.
  174. def Board.start_position
  175. Board.from_fen \
  176. "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
  177. end
  178. # Converts array coordinates into a square representation.
  179. #
  180. # > ChessBoard::Board.coords_to_square 0, 7 => "A8"
  181. # > ChessBoard::Board.coords_to_square 7, 0 => "H1"
  182. # > ChessBoard::Board.coords_to_square 4, 4 => "E5"
  183. #
  184. # The conversion is cached, for speed.
  185. #
  186. def Board.coords_to_square col, row
  187. unless defined? @coords_store
  188. @coords_store = []
  189. 8.times do
  190. @coords_store << [nil] * 8
  191. end
  192. 8.times do |cl|
  193. 8.times do |rw|
  194. @coords_store[cl][rw] = Board.square_from_coords(cl, rw)
  195. end
  196. end
  197. end
  198. return @coords_store[col][row]
  199. end
  200. # Converts a square represention into array coordinates.
  201. #
  202. # > ChessData::Board.square_to_coords "e4" => [4, 4]
  203. # > ChessData::Board.square_to_coords "a8" => [0, 7]
  204. # > ChessData::Board.square_to_coords "h1" => [7, 0]
  205. #
  206. # The conversion is cached, for speed.
  207. #
  208. def Board.square_to_coords square
  209. unless defined? @square_hash
  210. @square_hash = {}
  211. 8.times do |col|
  212. 8.times do |row|
  213. @square_hash[Board.square_from_coords(col, row)] = [col, row]
  214. end
  215. end
  216. end
  217. square = square.to_s.upcase # convert symbols to strings, ensure upper case
  218. unless @square_hash.has_key? square
  219. raise ArgumentError, "Invalid board notation -|#{square}|-"
  220. end
  221. return @square_hash[square]
  222. end
  223. # Provides a fast method to set value of board at given row/col index values
  224. # -- used to optimise clone
  225. def set row, col, value
  226. @board[row][col] = value
  227. end
  228. private
  229. # Converts a square represention into array coordinates.
  230. #
  231. # > ChessData::Board.square_from_coords "e4" => [4, 4]
  232. # > ChessData::Board.square_from_coords "a1" => [0, 7]
  233. # > ChessData::Board.square_from_coords "h8" => [7, 0]
  234. #
  235. def Board.square_from_coords col, row
  236. first = (65+col).chr
  237. second = (49+(7-row)).chr
  238. return "#{first}#{second}"
  239. end
  240. # Setup the current board
  241. def setup_board_from_fen fen
  242. rows = fen.split "/"
  243. unless rows.length == 8
  244. raise ArgumentError, "Invalid FEN description"
  245. end
  246. 8.times do |row|
  247. col = 0
  248. rows[row].chars.each do |i|
  249. case i
  250. when "K", "k", "Q", "q", "R", "r", "N", "n", "B", "b", "P", "p"
  251. @board[row][col] = i
  252. col += 1
  253. when /[1-8]/
  254. col += i.to_i
  255. else
  256. raise ArgumentError, "Invalid character in FEN description"
  257. end
  258. end
  259. end
  260. end
  261. # Returns the location of all white pieces and pawns.
  262. def white_pieces
  263. find_pieces "KQRBNP"
  264. end
  265. # Returns the location of all black pieces and pawns.
  266. def black_pieces
  267. find_pieces "kqrbnp"
  268. end
  269. # Returns piece+position for all pieces on the board which are in the given
  270. # list of pieces.
  271. def find_pieces pieces
  272. result = []
  273. 8.times do |row|
  274. 8.times do |col|
  275. unless @board[row][col].nil?
  276. if pieces.include? @board[row][col]
  277. result << PieceDefn.new(@board[row][col], Board.coords_to_square(col, row))
  278. end
  279. end
  280. end
  281. end
  282. return result
  283. end
  284. end
  285. end