chess_data 1.0.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,190 @@
1
+ module ChessData
2
+
3
+ # Represents a chess game, as read in from a PGN file.
4
+ #
5
+ # Header information in a pgn is stored in a hash table, and method_missing
6
+ # used to provide an accessor-like mechanism for storing/retrieving header
7
+ # information. For example:
8
+ #
9
+ # db = Database.new
10
+ # db.add_games_from "test/data/fischer.pgn"
11
+ # game = db[0]
12
+ # puts game.event
13
+ # puts game.white
14
+ # puts game.black
15
+ #
16
+ # Each of the 'event', 'white', 'black' values are retrieved from the
17
+ # PGN header.
18
+ #
19
+ # New key values can be created and assigned to, to construct a header for
20
+ # a game. For example:
21
+ #
22
+ # db = Database.new
23
+ # game = Game.new
24
+ # game.white = "Peter"
25
+ # game.black = "Paul"
26
+ # game.result = "1-0"
27
+ # game << "e4"
28
+ # db << game
29
+ # db.to_file "mygames.pgn"
30
+ #
31
+ # And the pgn file will contain:
32
+ #
33
+ # [Result "1-0"]
34
+ # [White "Peter"]
35
+ # [Black "Paul"]
36
+ #
37
+ # 1. e4 1-0
38
+ #
39
+ #
40
+ class Game
41
+ # Stores the sequence of moves in the game.
42
+ attr_accessor :moves
43
+
44
+ def initialize
45
+ @header = {}
46
+ @moves = []
47
+ end
48
+
49
+ # method_missing is used for accessing key-value terms in the header.
50
+ # * Any unknown method call is checked if it is the key of a header
51
+ # item and, if so, the value for that key is returned.
52
+ # * If the unknown method has an '=' sign in it, a new key is
53
+ # created and assigned a value, which must be an argument to method.
54
+ def method_missing iname, *args
55
+ name = iname.to_s
56
+ if @header.has_key? name
57
+ @header[name]
58
+ elsif name.include? "=" # assign, so create new key
59
+ key = name.delete "="
60
+ @header[key] = args[0]
61
+ else
62
+ puts "Unknown key '#{name}' for header #{@header}"
63
+ super
64
+ end
65
+ end
66
+
67
+ # Append given _move_ to list of moves.
68
+ # Given move can be a valid move type or a string.
69
+ #
70
+ # An InvalidMoveError is raised if the move is not valid.
71
+ #
72
+ def << move
73
+ if move.respond_to? :make_move
74
+ @moves << move
75
+ elsif move.kind_of? String
76
+ @moves << Moves.new_move(move)
77
+ else
78
+ raise InvalidMoveError.new("Invalid type of move: #{move}")
79
+ end
80
+ end
81
+
82
+ # Return the number of half-moves in the game.
83
+ def half_moves
84
+ @moves.size
85
+ end
86
+
87
+ # Return the start position for the game.
88
+ # PGN games may provide a start-position, if they do not begin from the start position.
89
+ def start_position
90
+ if @header.has_key? "fen"
91
+ ChessData::Board.from_fen @header["fen"]
92
+ else
93
+ ChessData::Board.start_position
94
+ end
95
+ end
96
+
97
+ # Step through the game from start position, one half-move at a time.
98
+ # Yields to a block the current board position and the next move.
99
+ # Yields final board position and result at end of game.
100
+ def play_game
101
+ board = start_position
102
+ @moves.each do |move|
103
+ yield board, move
104
+ board = move.make_move board
105
+ end
106
+ yield board, result
107
+ end
108
+
109
+ # Test if game meets the position definition given in the block
110
+ # using Game#play_game to step through the game.
111
+ def search &block
112
+ defn = ChessData::PositionDefinition.new(&block)
113
+ play_game do |board|
114
+ return true if defn.check board
115
+ end
116
+ return false
117
+ end
118
+
119
+ # Write game in PGN format to given IO stream.
120
+ # This method is usually called from Database#to_file
121
+ # but can also be called directly.
122
+ #
123
+ def to_pgn stream
124
+ @header.keys.each do |key|
125
+ stream.puts "[#{key.capitalize} \"#{@header[key]}\"]"
126
+ end
127
+ stream.puts # blank separating line
128
+ move_str = ""
129
+ move_number = 1
130
+ @moves.each_slice(2) do |full_move|
131
+ move_str += "#{move_number}. #{full_move[0]} #{full_move[1]} "
132
+ move_number += 1
133
+ end
134
+ move_str += result
135
+ stream.puts WordWrap.ww(move_str, 80)
136
+ end
137
+
138
+ # Regular expression used to match a PGN header line.
139
+ MatchHeader = /\[(\w+) \"(.*)\"\]/
140
+
141
+ # Reads a single game from a given IO stream.
142
+ # Returns nil if failed to read a game or its moves.
143
+ def Game.from_pgn stream
144
+ game = Game.new
145
+ moves = []
146
+ # ignore blank lines
147
+ begin
148
+ line = stream.gets
149
+ return nil if line.nil? # failed to read game/empty file
150
+ end while line.strip.empty?
151
+ # read the header
152
+ while MatchHeader =~ line
153
+ game.send "#{$1.downcase}=", $2
154
+ line = stream.gets.strip
155
+ end
156
+ # ignore blank lines
157
+ begin
158
+ line = stream.gets
159
+ return nil if line.nil? # failed to read moves for game
160
+ end while line.strip.empty?
161
+ # read the moves
162
+ begin
163
+ next if line.start_with? "%"
164
+ semi_index = line.index ";" # look for ; comment start
165
+ line = line[0...semi_index] if semi_index # and strip it
166
+ moves << line.strip
167
+ line = stream.gets
168
+ end until line.nil? || line.strip.empty? || MatchHeader =~ line
169
+ # return an event line if it immediately follows the moves
170
+ # so can be read for next game
171
+ stream.ungetc line if MatchHeader =~ line
172
+ # parse the moves and add to game
173
+ move_str = moves.join(" ")
174
+ if /{.*}/.match(move_str) # remove { } comments
175
+ move_str = $` + " " + $'
176
+ end
177
+ move_str.split(" ").each do |token|
178
+ case token
179
+ when "1-0", "0-1", "1/2-1/2", "*" then game.result = token
180
+ when /^\d+/ then ; # ignore the move number
181
+ when /^$\d+/ then ; # ignore NAG
182
+ when Moves::LegalMove then game << Moves.new_move(token)
183
+ end
184
+ end
185
+
186
+ return game
187
+ end
188
+ end
189
+ end
190
+
@@ -0,0 +1,512 @@
1
+
2
+ module ChessData
3
+
4
+ # Used to indicate an error in making a move.
5
+ class InvalidMoveError < RuntimeError
6
+ end
7
+
8
+ # Moves is a collection of regular expressions and methods to recognise
9
+ # how moves are written in PGN files, and to make the moves on a given
10
+ # board.
11
+ #
12
+ # As the moves are obtained from PGN files, they are assumed to be correct.
13
+ #
14
+ module Moves
15
+
16
+ # :nodoc:
17
+ # Regular expressions to match each of the move types.
18
+ Square = /[a-h][1-8]/
19
+ Piece = /[KQRBN]/
20
+ MatchKingsideCastles = /^O-O\+?\Z/
21
+ MatchQueensideCastles = /^O-O-O\+?\Z/
22
+ MatchPieceMove = /^(#{Piece})([a-h]?|[1-8]?)x?(#{Square})\+?\Z/
23
+ MatchPawnCapture = /^([a-h])x(#{Square})\+?\Z/
24
+ MatchPromotionPawnMove = /^([a-h][18])=([QqRrBbNn])\+?\Z/
25
+ MatchSimplePawnMove = /^(#{Square})\+?\Z/
26
+ MatchPromotionPawnCapture = /^([a-h])x([a-h][18])=([QqRrBbNn])\+?\Z/
27
+
28
+ # Combined regular expression, to match a legal move.
29
+ LegalMove = /#{MatchKingsideCastles}|#{MatchQueensideCastles}|#{MatchPieceMove}|#{MatchPawnCapture}|#{MatchPromotionPawnMove}|#{MatchSimplePawnMove}|#{MatchPromotionPawnCapture}/
30
+ # :doc:
31
+
32
+ # Returns an instance of the appropriate move type.
33
+ # string:: a move read from a PGN file.
34
+ def Moves.new_move string
35
+ case string
36
+ when MatchKingsideCastles then KingsideCastles.new
37
+ when MatchQueensideCastles then QueensideCastles.new
38
+ when MatchPieceMove then PieceMove.new string
39
+ when MatchPawnCapture then PawnCapture.new string
40
+ when MatchPromotionPawnMove then PromotionPawnMove.new string
41
+ when MatchSimplePawnMove then SimplePawnMove.new string
42
+ when MatchPromotionPawnCapture then PromotionPawnCapture.new string
43
+ else raise InvalidMoveError.new("Invalid move: #{string}")
44
+ end
45
+ end
46
+
47
+ # Return true if given _piece_ can move from _start_ square to _finish_ on given _board_.
48
+ def Moves.can_reach board, piece, start, finish
49
+ start = start.upcase
50
+ finish = finish.upcase
51
+ case piece
52
+ when "K", "k" then Moves.king_can_reach start, finish
53
+ when "Q", "q" then Moves.queen_can_reach board, start, finish
54
+ when "R", "r" then Moves.rook_can_reach board, start, finish
55
+ when "B", "b" then Moves.bishop_can_reach board, start,finish
56
+ when "N", "n" then Moves.knight_can_reach start, finish
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ # Return true if moving the giving piece from start to finish
63
+ # will leave the moving side's king in check.
64
+ def Moves.king_left_in_check board, piece, start, finish
65
+ test_board = board.clone
66
+ test_board[start] = nil
67
+ test_board[finish] = piece
68
+
69
+ if board.to_move == "w"
70
+ test_board.white_king_in_check?
71
+ else
72
+ test_board.black_king_in_check?
73
+ end
74
+ end
75
+
76
+ def Moves.king_can_reach start, finish
77
+ Moves.step_h(start, finish) <= 1 && Moves.step_v(start, finish) <= 1
78
+ end
79
+
80
+ def Moves.queen_can_reach board, start,finish
81
+ Moves.rook_can_reach(board, start, finish) ||
82
+ Moves.bishop_can_reach(board, start, finish)
83
+ end
84
+
85
+ def Moves.rook_can_reach board, start, finish
86
+ start_col, start_row = Board.square_to_coords start
87
+ end_col, end_row = Board.square_to_coords finish
88
+ if start_col == end_col # moving along column
89
+ row_1 = [start_row, end_row].min + 1
90
+ row_2 = [start_row, end_row].max - 1
91
+ row_1.upto(row_2) do |row|
92
+ return false unless board[Board.coords_to_square(start_col, row)] == nil
93
+ end
94
+ elsif start_row == end_row # moving along row
95
+ col_1 = [start_col, end_col].min + 1
96
+ col_2 = [start_col, end_col].max - 1
97
+ col_1.upto(col_2) do |col|
98
+ return false unless board[Board.coords_to_square(col, start_row)] == nil
99
+ end
100
+ else
101
+ return false
102
+ end
103
+ return true
104
+ end
105
+
106
+ def Moves.bishop_can_reach board, start,finish
107
+ return false unless Moves.step_h(start,finish) == Moves.step_v(start, finish)
108
+ start_col, start_row = Board.square_to_coords start
109
+ end_col, end_row = Board.square_to_coords finish
110
+ dirn_h = (end_row - start_row) / (end_row - start_row).abs
111
+ dirn_v = (end_col - start_col) / (end_col - start_col).abs
112
+ 1.upto(Moves.step_h(start,finish)-1) do |i|
113
+ square = Board.coords_to_square(start_col+(i*dirn_v),
114
+ start_row+(i*dirn_h))
115
+ unless board[square] == nil
116
+ return false
117
+ end
118
+ end
119
+ return true
120
+ end
121
+
122
+ def Moves.knight_can_reach start, finish
123
+ h = Moves.step_h start, finish
124
+ v = Moves.step_v start, finish
125
+ return (h == 2 && v == 1) || (h == 1 && v == 2)
126
+ end
127
+
128
+ # Return size of horizontal gap between start and finish
129
+ def Moves.step_h start, finish
130
+ (start.bytes[0] - finish.bytes[0]).abs
131
+ end
132
+
133
+ # Return size of vertical gap between start and finish
134
+ def Moves.step_v start, finish
135
+ (start.bytes[1] - finish.bytes[1]).abs
136
+ end
137
+
138
+ # Methods to support king-side castling move.
139
+ class KingsideCastles
140
+ def to_s
141
+ "O-O"
142
+ end
143
+
144
+ # Depending on the colour to move, will either castle king-side for white or black.
145
+ # Returns a new instance of the board.
146
+ def make_move board
147
+ if board.to_move == "w"
148
+ white_castles board
149
+ else
150
+ black_castles board
151
+ end
152
+ end
153
+
154
+ private
155
+ def white_castles board
156
+ raise InvalidMoveError.new("white O-O") unless board["E1"] == "K" &&
157
+ board["F1"] == nil && board["G1"] == nil &&
158
+ board["H1"] == "R" && board.white_king_side_castling
159
+
160
+ revised_board = board.clone
161
+ revised_board["E1"] = nil
162
+ revised_board["F1"] = "R"
163
+ revised_board["G1"] = "K"
164
+ revised_board["H1"] = nil
165
+
166
+ revised_board.to_move = "b"
167
+ revised_board.enpassant_target = "-"
168
+ revised_board.halfmove_clock += 1
169
+ revised_board.white_king_side_castling = false
170
+ revised_board.white_queen_side_castling = false
171
+
172
+ return revised_board
173
+ end
174
+
175
+ def black_castles board
176
+ raise InvalidMoveError.new("black O-O") unless board["E8"] == "k" &&
177
+ board["F8"] == nil && board["G8"] == nil &&
178
+ board["H8"] == "r" && board.black_king_side_castling
179
+
180
+ revised_board = board.clone
181
+ revised_board["E8"] = nil
182
+ revised_board["F8"] = "r"
183
+ revised_board["G8"] = "k"
184
+ revised_board["H8"] = nil
185
+
186
+ revised_board.to_move = "w"
187
+ revised_board.enpassant_target = "-"
188
+ revised_board.halfmove_clock += 1
189
+ revised_board.fullmove_number += 1
190
+ revised_board.black_king_side_castling = false
191
+ revised_board.black_queen_side_castling = false
192
+
193
+ return revised_board
194
+ end
195
+ end
196
+
197
+ # Methods to support queen-side castling move.
198
+ class QueensideCastles
199
+ def to_s
200
+ "O-O-O"
201
+ end
202
+
203
+ # Depending on the colour to move, will either castle queen-side for white or black.
204
+ # Returns a new instance of the board.
205
+ def make_move board
206
+ if board.to_move == "w"
207
+ white_castles board
208
+ else
209
+ black_castles board
210
+ end
211
+ end
212
+
213
+ private
214
+ def white_castles board
215
+ raise InvalidMoveError.new("white O-O-O") unless board["E1"] == "K" &&
216
+ board["D1"] == nil && board["C1"] == nil &&
217
+ board["B1"] == nil && board["A1"] == "R" &&
218
+ board.white_queen_side_castling
219
+
220
+ revised_board = board.clone
221
+ revised_board["E1"] = nil
222
+ revised_board["D1"] = "R"
223
+ revised_board["C1"] = "K"
224
+ revised_board["B1"] = nil
225
+ revised_board["A1"] = nil
226
+
227
+ revised_board.to_move = "b"
228
+ revised_board.enpassant_target = "-"
229
+ revised_board.halfmove_clock += 1
230
+ revised_board.white_king_side_castling = false
231
+ revised_board.white_queen_side_castling = false
232
+
233
+ return revised_board
234
+ end
235
+
236
+ def black_castles board
237
+ raise InvalidMoveError.new("black O-O") unless board["E8"] == "k" &&
238
+ board["D8"] == nil && board["C8"] == nil &&
239
+ board["B8"] == nil && board["A8"] == "r" &&
240
+ board.black_queen_side_castling
241
+
242
+ revised_board = board.clone
243
+ revised_board["E8"] = nil
244
+ revised_board["D8"] = "r"
245
+ revised_board["C8"] = "k"
246
+ revised_board["B8"] = nil
247
+ revised_board["A8"] = nil
248
+
249
+ revised_board.to_move = "w"
250
+ revised_board.enpassant_target = "-"
251
+ revised_board.halfmove_clock += 1
252
+ revised_board.fullmove_number += 1
253
+ revised_board.black_king_side_castling = false
254
+ revised_board.black_queen_side_castling = false
255
+
256
+ return revised_board
257
+ end
258
+ end
259
+
260
+ # Methods to support a simple pawn move, moving directly forward.
261
+ class SimplePawnMove
262
+ def initialize move
263
+ @move_string = move
264
+ move =~ MatchSimplePawnMove
265
+ @destination = $1
266
+ end
267
+
268
+ def to_s
269
+ @move_string
270
+ end
271
+
272
+ # Returns a new instance of the board after move is made.
273
+ def make_move board
274
+ if board.to_move == "w"
275
+ white_move board
276
+ else
277
+ black_move board
278
+ end
279
+ end
280
+
281
+ private
282
+ def white_move board
283
+ revised_board = board.clone
284
+
285
+ if single_step board
286
+ revised_board[@destination] = "P"
287
+ revised_board[previous_square(board.to_move)] = nil
288
+ revised_board.enpassant_target = "-"
289
+ elsif initial_step board
290
+ revised_board[@destination] = "P"
291
+ revised_board[initial_square(board.to_move)] = nil
292
+ revised_board.enpassant_target = previous_square(board.to_move)
293
+ else
294
+ raise InvalidMoveError.new "white #{@move_string}"
295
+ end
296
+
297
+ revised_board.to_move = "b"
298
+ revised_board.halfmove_clock = 0
299
+
300
+ return revised_board
301
+ end
302
+
303
+ def black_move board
304
+ revised_board = board.clone
305
+
306
+ if single_step board
307
+ revised_board[@destination] = "p"
308
+ revised_board[previous_square(board.to_move)] = nil
309
+ revised_board.enpassant_target = "-"
310
+ elsif initial_step board
311
+ revised_board[@destination] = "p"
312
+ revised_board[initial_square(board.to_move)] = nil
313
+ revised_board.enpassant_target = previous_square(board.to_move)
314
+ else
315
+ raise InvalidMoveError.new "black #{@move_string}"
316
+ end
317
+
318
+ revised_board.to_move = "w"
319
+ revised_board.halfmove_clock = 0
320
+ revised_board.fullmove_number += 1
321
+
322
+ return revised_board
323
+ end
324
+
325
+ def single_step board
326
+ if board.to_move == "w"
327
+ pawn = "P"
328
+ else
329
+ pawn = "p"
330
+ end
331
+ board[@destination] == nil &&
332
+ board[previous_square(board.to_move)] == pawn
333
+ end
334
+
335
+ def initial_step board
336
+ if board.to_move == "w"
337
+ pawn = "P"
338
+ rank = 4
339
+ else
340
+ pawn = "p"
341
+ rank = 5
342
+ end
343
+ board[@destination] == nil && @destination[1].to_i == rank &&
344
+ board[initial_square(board.to_move)] == pawn
345
+ end
346
+
347
+ def previous_square colour
348
+ if colour == "w"
349
+ offset = -1
350
+ else
351
+ offset = +1
352
+ end
353
+ "#{@destination[0]}#{@destination[1].to_i+offset}"
354
+ end
355
+
356
+ def initial_square colour
357
+ if colour == "w"
358
+ initial_rank = 2
359
+ else
360
+ initial_rank = 7
361
+ end
362
+ "#{@destination[0]}#{initial_rank}"
363
+ end
364
+ end
365
+
366
+ # Methods to support a pawn move leading to promotion.
367
+ class PromotionPawnMove < SimplePawnMove
368
+ def initialize string
369
+ @move_string = string
370
+ string =~ MatchPromotionPawnMove
371
+ string.split("=")
372
+ @destination = $1
373
+ @piece = $2
374
+ end
375
+
376
+ # Returns a new instance of the board after move is made.
377
+ def make_move board
378
+ @piece.downcase! if board.to_move == "b"
379
+ revised_board = super board
380
+ revised_board[@destination] = @piece
381
+ return revised_board
382
+ end
383
+ end
384
+
385
+ # Methods to support a pawn move which makes a capture.
386
+ class PawnCapture
387
+ def initialize move
388
+ @move_string = move
389
+ move =~ MatchPawnCapture
390
+ @source = $1
391
+ @destination = $2
392
+ end
393
+
394
+ def to_s
395
+ @move_string
396
+ end
397
+
398
+ # Returns a new instance of the board after move is made.
399
+ def make_move board
400
+ origin = find_origin board.to_move
401
+
402
+ revised_board = board.clone
403
+ if @destination == board.enpassant_target
404
+ revised_board["#{@destination[0]}#{origin[1]}"] = nil
405
+ end
406
+ revised_board[origin] = nil
407
+ revised_board[@destination] = board[origin]
408
+ revised_board.enpassant_target = "-"
409
+ revised_board.halfmove_clock = 0
410
+ if board.to_move == "w"
411
+ revised_board.to_move = "b"
412
+ else
413
+ revised_board.to_move = "w"
414
+ revised_board.fullmove_number += 1
415
+ end
416
+
417
+ return revised_board
418
+ end
419
+
420
+ private
421
+ # For a pawn capture, find the originating row and create origin square
422
+ def find_origin to_move
423
+ row = @destination[1].to_i
424
+ if to_move == "w"
425
+ row -= 1
426
+ else
427
+ row += 1
428
+ end
429
+
430
+ return "#{@source}#{row}"
431
+ end
432
+ end
433
+
434
+ # Methods to support a pawn move which is both a capture and a promotion.
435
+ class PromotionPawnCapture < PawnCapture
436
+ def initialize move
437
+ @move_string = move
438
+ move =~ MatchPromotionPawnCapture
439
+ @source = $1
440
+ @destination = $2
441
+ @piece = $3
442
+ end
443
+
444
+ def to_s
445
+ @move_string
446
+ end
447
+
448
+ # Returns a new instance of the board after move is made.
449
+ def make_move board
450
+ @piece.downcase! if board.to_move == "b"
451
+ revised_board = super board
452
+ revised_board[@destination] = @piece
453
+ return revised_board
454
+ end
455
+ end
456
+
457
+ # Methods to support a piece move.
458
+ class PieceMove
459
+ def initialize move
460
+ @move_string = move
461
+ move =~ MatchPieceMove
462
+ @piece = $1
463
+ @identifier = $2
464
+ @destination = $3
465
+ @is_capture = move.include? "x"
466
+ end
467
+
468
+ def to_s
469
+ @move_string
470
+ end
471
+
472
+ # Returns a new instance of the board after move is made.
473
+ def make_move board
474
+ @piece.downcase! if board.to_move == "b"
475
+ # for given piece type, locate those pieces on board which can reach destination
476
+ origin = board.locations_of(@piece, @identifier).select do |loc|
477
+ Moves.can_reach(board, @piece, loc, @destination)
478
+ end
479
+ # filter out ambiguities raised by king being left in check
480
+ if origin.length > 1
481
+ origin = origin.delete_if do |loc|
482
+ Moves.king_left_in_check(board, @piece, loc, @destination)
483
+ end
484
+ end
485
+ # there should only be one unique piece at this point
486
+ # raise an InvalidMoveError if not
487
+ unless origin.length == 1 && board[origin.first] == @piece
488
+ raise InvalidMoveError, "Not a unique/valid choice for #{@piece} to #{@destination}"
489
+ end
490
+ # setup a revised board with the move completed
491
+ revised_board = board.clone
492
+ revised_board[origin.first] = nil
493
+ revised_board[@destination] = @piece
494
+ revised_board.to_move = case board.to_move
495
+ when "w" then "b"
496
+ when "b" then "w"
497
+ end
498
+ revised_board.enpassant_target = "-"
499
+ if @is_capture
500
+ revised_board.halfmove_clock = 0
501
+ else
502
+ revised_board.halfmove_clock += 1
503
+ end
504
+ revised_board.fullmove_number += 1 if board.to_move == "b"
505
+
506
+ return revised_board
507
+ end
508
+ end
509
+ end
510
+
511
+ end
512
+