moves.rb 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513
  1. module ChessData
  2. # Used to indicate an error in making a move.
  3. class InvalidMoveError < RuntimeError
  4. end
  5. # Moves is a collection of regular expressions and methods to recognise
  6. # how moves are written in PGN files, and to make the moves on a given
  7. # board.
  8. #
  9. # As the moves are obtained from PGN files, they are assumed to be correct.
  10. #
  11. module Moves
  12. # :nodoc:
  13. # Regular expressions to match each of the move types.
  14. Square = /[a-h][1-8]/
  15. Piece = /[KQRBN]/
  16. MatchKingsideCastles = /^O-O\+?\Z/
  17. MatchQueensideCastles = /^O-O-O\+?\Z/
  18. MatchPieceMove = /^(#{Piece})([a-h]?|[1-8]?)x?(#{Square})\+?\Z/
  19. MatchPawnCapture = /^([a-h])x(#{Square})\+?\Z/
  20. MatchPromotionPawnMove = /^([a-h][18])=([QqRrBbNn])\+?\Z/
  21. MatchSimplePawnMove = /^(#{Square})\+?\Z/
  22. MatchPromotionPawnCapture = /^([a-h])x([a-h][18])=([QqRrBbNn])\+?\Z/
  23. # Combined regular expression, to match a legal move.
  24. LegalMove = /#{MatchKingsideCastles}|#{MatchQueensideCastles}|#{MatchPieceMove}|#{MatchPawnCapture}|#{MatchPromotionPawnMove}|#{MatchSimplePawnMove}|#{MatchPromotionPawnCapture}/
  25. # :doc:
  26. # Returns an instance of the appropriate move type.
  27. # string:: a move read from a PGN file.
  28. def Moves.new_move string
  29. case string
  30. when MatchKingsideCastles then KingsideCastles.new
  31. when MatchQueensideCastles then QueensideCastles.new
  32. when MatchPieceMove then PieceMove.new string
  33. when MatchPawnCapture then PawnCapture.new string
  34. when MatchPromotionPawnMove then PromotionPawnMove.new string
  35. when MatchSimplePawnMove then SimplePawnMove.new string
  36. when MatchPromotionPawnCapture then PromotionPawnCapture.new string
  37. else raise InvalidMoveError.new("Invalid move: #{string}")
  38. end
  39. end
  40. # Return true if given _piece_ can move from _start_ square to _finish_ on given _board_.
  41. def Moves.can_reach board, piece, start, finish
  42. start = start.upcase
  43. finish = finish.upcase
  44. case piece
  45. when "K", "k" then Moves.king_can_reach start, finish
  46. when "Q", "q" then Moves.queen_can_reach board, start, finish
  47. when "R", "r" then Moves.rook_can_reach board, start, finish
  48. when "B", "b" then Moves.bishop_can_reach board, start,finish
  49. when "N", "n" then Moves.knight_can_reach start, finish
  50. end
  51. end
  52. private
  53. # Return true if moving the giving piece from start to finish
  54. # will leave the moving side's king in check.
  55. def Moves.king_left_in_check board, piece, start, finish
  56. test_board = board.clone
  57. test_board[start] = nil
  58. test_board[finish] = piece
  59. if board.to_move == "w"
  60. test_board.white_king_in_check?
  61. else
  62. test_board.black_king_in_check?
  63. end
  64. end
  65. def Moves.king_can_reach start, finish
  66. Moves.step_h(start, finish) <= 1 && Moves.step_v(start, finish) <= 1
  67. end
  68. def Moves.queen_can_reach board, start,finish
  69. Moves.rook_can_reach(board, start, finish) ||
  70. Moves.bishop_can_reach(board, start, finish)
  71. end
  72. def Moves.rook_can_reach board, start, finish
  73. start_col, start_row = Board.square_to_coords start
  74. end_col, end_row = Board.square_to_coords finish
  75. if start_col == end_col # moving along column
  76. row_1 = [start_row, end_row].min + 1
  77. row_2 = [start_row, end_row].max - 1
  78. row_1.upto(row_2) do |row|
  79. return false unless board[Board.coords_to_square(start_col, row)] == nil
  80. end
  81. elsif start_row == end_row # moving along row
  82. col_1 = [start_col, end_col].min + 1
  83. col_2 = [start_col, end_col].max - 1
  84. col_1.upto(col_2) do |col|
  85. return false unless board[Board.coords_to_square(col, start_row)] == nil
  86. end
  87. else
  88. return false
  89. end
  90. return true
  91. end
  92. def Moves.bishop_can_reach board, start,finish
  93. return false unless Moves.step_h(start,finish) == Moves.step_v(start, finish)
  94. start_col, start_row = Board.square_to_coords start
  95. end_col, end_row = Board.square_to_coords finish
  96. dirn_h = (end_row - start_row) / (end_row - start_row).abs
  97. dirn_v = (end_col - start_col) / (end_col - start_col).abs
  98. 1.upto(Moves.step_h(start,finish)-1) do |i|
  99. square = Board.coords_to_square(start_col+(i*dirn_v),
  100. start_row+(i*dirn_h))
  101. unless board[square] == nil
  102. return false
  103. end
  104. end
  105. return true
  106. end
  107. def Moves.knight_can_reach start, finish
  108. h = Moves.step_h start, finish
  109. v = Moves.step_v start, finish
  110. return (h == 2 && v == 1) || (h == 1 && v == 2)
  111. end
  112. # Return size of horizontal gap between start and finish
  113. def Moves.step_h start, finish
  114. (start.bytes[0] - finish.bytes[0]).abs
  115. end
  116. # Return size of vertical gap between start and finish
  117. def Moves.step_v start, finish
  118. (start.bytes[1] - finish.bytes[1]).abs
  119. end
  120. # Methods to support king-side castling move.
  121. class KingsideCastles
  122. def to_s
  123. "O-O"
  124. end
  125. # Depending on the colour to move, will either castle king-side for white or black.
  126. # Returns a new instance of the board.
  127. def make_move board
  128. if board.to_move == "w"
  129. white_castles board
  130. else
  131. black_castles board
  132. end
  133. end
  134. private
  135. def white_castles board
  136. raise InvalidMoveError.new("white O-O") unless board["E1"] == "K" &&
  137. board["F1"] == nil && board["G1"] == nil &&
  138. board["H1"] == "R" && board.white_king_side_castling
  139. revised_board = board.clone
  140. revised_board["E1"] = nil
  141. revised_board["F1"] = "R"
  142. revised_board["G1"] = "K"
  143. revised_board["H1"] = nil
  144. revised_board.to_move = "b"
  145. revised_board.enpassant_target = "-"
  146. revised_board.halfmove_clock += 1
  147. revised_board.white_king_side_castling = false
  148. revised_board.white_queen_side_castling = false
  149. return revised_board
  150. end
  151. def black_castles board
  152. raise InvalidMoveError.new("black O-O") unless board["E8"] == "k" &&
  153. board["F8"] == nil && board["G8"] == nil &&
  154. board["H8"] == "r" && board.black_king_side_castling
  155. revised_board = board.clone
  156. revised_board["E8"] = nil
  157. revised_board["F8"] = "r"
  158. revised_board["G8"] = "k"
  159. revised_board["H8"] = nil
  160. revised_board.to_move = "w"
  161. revised_board.enpassant_target = "-"
  162. revised_board.halfmove_clock += 1
  163. revised_board.fullmove_number += 1
  164. revised_board.black_king_side_castling = false
  165. revised_board.black_queen_side_castling = false
  166. return revised_board
  167. end
  168. end
  169. # Methods to support queen-side castling move.
  170. class QueensideCastles
  171. def to_s
  172. "O-O-O"
  173. end
  174. # Depending on the colour to move, will either castle queen-side for white or black.
  175. # Returns a new instance of the board.
  176. def make_move board
  177. if board.to_move == "w"
  178. white_castles board
  179. else
  180. black_castles board
  181. end
  182. end
  183. private
  184. def white_castles board
  185. raise InvalidMoveError.new("white O-O-O") unless board["E1"] == "K" &&
  186. board["D1"] == nil && board["C1"] == nil &&
  187. board["B1"] == nil && board["A1"] == "R" &&
  188. board.white_queen_side_castling
  189. revised_board = board.clone
  190. revised_board["E1"] = nil
  191. revised_board["D1"] = "R"
  192. revised_board["C1"] = "K"
  193. revised_board["B1"] = nil
  194. revised_board["A1"] = nil
  195. revised_board.to_move = "b"
  196. revised_board.enpassant_target = "-"
  197. revised_board.halfmove_clock += 1
  198. revised_board.white_king_side_castling = false
  199. revised_board.white_queen_side_castling = false
  200. return revised_board
  201. end
  202. def black_castles board
  203. raise InvalidMoveError.new("black O-O") unless board["E8"] == "k" &&
  204. board["D8"] == nil && board["C8"] == nil &&
  205. board["B8"] == nil && board["A8"] == "r" &&
  206. board.black_queen_side_castling
  207. revised_board = board.clone
  208. revised_board["E8"] = nil
  209. revised_board["D8"] = "r"
  210. revised_board["C8"] = "k"
  211. revised_board["B8"] = nil
  212. revised_board["A8"] = nil
  213. revised_board.to_move = "w"
  214. revised_board.enpassant_target = "-"
  215. revised_board.halfmove_clock += 1
  216. revised_board.fullmove_number += 1
  217. revised_board.black_king_side_castling = false
  218. revised_board.black_queen_side_castling = false
  219. return revised_board
  220. end
  221. end
  222. # Methods to support a simple pawn move, moving directly forward.
  223. class SimplePawnMove
  224. def initialize move
  225. @move_string = move
  226. move =~ MatchSimplePawnMove
  227. @destination = $1
  228. end
  229. def to_s
  230. @move_string
  231. end
  232. # Returns a new instance of the board after move is made.
  233. def make_move board
  234. if board.to_move == "w"
  235. white_move board
  236. else
  237. black_move board
  238. end
  239. end
  240. private
  241. def white_move board
  242. revised_board = board.clone
  243. if single_step board
  244. revised_board[@destination] = "P"
  245. revised_board[previous_square(board.to_move)] = nil
  246. revised_board.enpassant_target = "-"
  247. elsif initial_step board
  248. revised_board[@destination] = "P"
  249. revised_board[initial_square(board.to_move)] = nil
  250. revised_board.enpassant_target = previous_square(board.to_move)
  251. else
  252. raise InvalidMoveError.new "white #{@move_string}"
  253. end
  254. revised_board.to_move = "b"
  255. revised_board.halfmove_clock = 0
  256. return revised_board
  257. end
  258. def black_move board
  259. revised_board = board.clone
  260. if single_step board
  261. revised_board[@destination] = "p"
  262. revised_board[previous_square(board.to_move)] = nil
  263. revised_board.enpassant_target = "-"
  264. elsif initial_step board
  265. revised_board[@destination] = "p"
  266. revised_board[initial_square(board.to_move)] = nil
  267. revised_board.enpassant_target = previous_square(board.to_move)
  268. else
  269. raise InvalidMoveError.new "black #{@move_string}"
  270. end
  271. revised_board.to_move = "w"
  272. revised_board.halfmove_clock = 0
  273. revised_board.fullmove_number += 1
  274. return revised_board
  275. end
  276. def single_step board
  277. if board.to_move == "w"
  278. pawn = "P"
  279. else
  280. pawn = "p"
  281. end
  282. board[@destination] == nil &&
  283. board[previous_square(board.to_move)] == pawn
  284. end
  285. def initial_step board
  286. if board.to_move == "w"
  287. pawn = "P"
  288. rank = 4
  289. else
  290. pawn = "p"
  291. rank = 5
  292. end
  293. board[@destination] == nil && @destination[1].to_i == rank &&
  294. board[initial_square(board.to_move)] == pawn
  295. end
  296. def previous_square colour
  297. if colour == "w"
  298. offset = -1
  299. else
  300. offset = +1
  301. end
  302. "#{@destination[0]}#{@destination[1].to_i+offset}"
  303. end
  304. def initial_square colour
  305. if colour == "w"
  306. initial_rank = 2
  307. else
  308. initial_rank = 7
  309. end
  310. "#{@destination[0]}#{initial_rank}"
  311. end
  312. end
  313. # Methods to support a pawn move leading to promotion.
  314. class PromotionPawnMove < SimplePawnMove
  315. def initialize string
  316. @move_string = string
  317. string =~ MatchPromotionPawnMove
  318. string.split("=")
  319. @destination = $1
  320. @piece = $2
  321. end
  322. # Returns a new instance of the board after move is made.
  323. def make_move board
  324. @piece.downcase! if board.to_move == "b"
  325. revised_board = super board
  326. revised_board[@destination] = @piece
  327. return revised_board
  328. end
  329. end
  330. # Methods to support a pawn move which makes a capture.
  331. class PawnCapture
  332. def initialize move
  333. @move_string = move
  334. move =~ MatchPawnCapture
  335. @source = $1
  336. @destination = $2
  337. end
  338. def to_s
  339. @move_string
  340. end
  341. # Returns a new instance of the board after move is made.
  342. def make_move board
  343. origin = find_origin board.to_move
  344. revised_board = board.clone
  345. if @destination == board.enpassant_target
  346. revised_board["#{@destination[0]}#{origin[1]}"] = nil
  347. end
  348. revised_board[origin] = nil
  349. revised_board[@destination] = board[origin]
  350. revised_board.enpassant_target = "-"
  351. revised_board.halfmove_clock = 0
  352. if board.to_move == "w"
  353. revised_board.to_move = "b"
  354. else
  355. revised_board.to_move = "w"
  356. revised_board.fullmove_number += 1
  357. end
  358. return revised_board
  359. end
  360. private
  361. # For a pawn capture, find the originating row and create origin square
  362. def find_origin to_move
  363. row = @destination[1].to_i
  364. if to_move == "w"
  365. row -= 1
  366. else
  367. row += 1
  368. end
  369. return "#{@source}#{row}"
  370. end
  371. end
  372. # Methods to support a pawn move which is both a capture and a promotion.
  373. class PromotionPawnCapture < PawnCapture
  374. def initialize move
  375. @move_string = move
  376. move =~ MatchPromotionPawnCapture
  377. @source = $1
  378. @destination = $2
  379. @piece = $3
  380. end
  381. def to_s
  382. @move_string
  383. end
  384. # Returns a new instance of the board after move is made.
  385. def make_move board
  386. @piece.downcase! if board.to_move == "b"
  387. revised_board = super board
  388. revised_board[@destination] = @piece
  389. return revised_board
  390. end
  391. end
  392. # Methods to support a piece move.
  393. class PieceMove
  394. def initialize move
  395. @move_string = move
  396. move =~ MatchPieceMove
  397. @piece = $1
  398. @identifier = $2
  399. @destination = $3
  400. @is_capture = move.include? "x"
  401. end
  402. def to_s
  403. @move_string
  404. end
  405. # Returns a new instance of the board after move is made.
  406. def make_move board
  407. @piece.downcase! if board.to_move == "b"
  408. # for given piece type, locate those pieces on board which can reach destination
  409. origin = board.locations_of(@piece, @identifier).select do |loc|
  410. Moves.can_reach(board, @piece, loc, @destination)
  411. end
  412. # filter out ambiguities raised by king being left in check
  413. if origin.length > 1
  414. origin = origin.delete_if do |loc|
  415. Moves.king_left_in_check(board, @piece, loc, @destination)
  416. end
  417. end
  418. # there should only be one unique piece at this point
  419. # raise an InvalidMoveError if not
  420. unless origin.length == 1 && board[origin.first] == @piece
  421. raise InvalidMoveError, "Not a unique/valid choice for #{@piece} to #{@destination}"
  422. end
  423. # setup a revised board with the move completed
  424. revised_board = board.clone
  425. revised_board[origin.first] = nil
  426. revised_board[@destination] = @piece
  427. revised_board.to_move = case board.to_move
  428. when "w" then "b"
  429. when "b" then "w"
  430. end
  431. revised_board.enpassant_target = "-"
  432. if @is_capture
  433. revised_board.halfmove_clock = 0
  434. else
  435. revised_board.halfmove_clock += 1
  436. end
  437. revised_board.fullmove_number += 1 if board.to_move == "b"
  438. return revised_board
  439. end
  440. end
  441. end
  442. end