chess_data 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.rdoc +22 -0
- data/README.rdoc +115 -0
- data/lib/chess_data/board.rb +324 -0
- data/lib/chess_data/database.rb +83 -0
- data/lib/chess_data/game.rb +190 -0
- data/lib/chess_data/moves.rb +512 -0
- data/lib/chess_data/position-definition.rb +106 -0
- data/lib/chess_data.rb +7 -0
- metadata +80 -0
@@ -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
|
+
|