chess_data 1.0.6

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 97031a873d57187ce167a3fa0eb86b0055c2350c53debe92c0cec99e875df18f
4
+ data.tar.gz: 1f40251c1880e067836eb37c04f7514618f7252f0b5ccf6565ce4a8cf9dd8153
5
+ SHA512:
6
+ metadata.gz: 81ffd070d6b823f0e9d0d19fc22d12f64dd8a916f33e6522f72ed2b882621c3637d97f5f42c6033e1cfedb0046270316cd3be177766aa53023fef3315f6abe6f
7
+ data.tar.gz: ca62c4e5700267ff063b3d5065a9f08370c5fff204248a99f60b2238b30bdfbfec2b45506adf9b2ade549011e455dfe3152521bdad0b58569c42707f797a905a
data/LICENSE.rdoc ADDED
@@ -0,0 +1,22 @@
1
+ = MIT License
2
+
3
+ Copyright (c) 2019-23, Peter Lane
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
data/README.rdoc ADDED
@@ -0,0 +1,115 @@
1
+ = chess_data
2
+
3
+ Install from {RubyGems}[https://rubygems.org/gems/chess_data/]:
4
+
5
+ > gem install chess_data
6
+
7
+ source:: https://notabug.org/peterlane/chess_data/
8
+
9
+ == Description
10
+
11
+ For searching/filtering datasets of chess games.
12
+
13
+ Features
14
+
15
+ * Read and save collections of PGN files
16
+ * Filter games looking for games containing positions with certain combinations
17
+ of pieces
18
+ * Save collections of PGN games or individual positions after filtering.
19
+ * PGN lexer follows specification in https://www.chessclub.com/help/PGN-spec
20
+ * note that the lexer recognises but ignores comments and NAGs, so they will
21
+ not appear in any resaved games
22
+
23
+ == Position Definitions
24
+
25
+ Games are stored in an instance of +ChessData+::+Database+, and can be filtered using
26
+ the +search+ method. The search method takes a block defining the numbers of pieces of
27
+ each type and returns a new database containing just those games which match the
28
+ definition. For example, the following filters for 5-4 rook endings:
29
+
30
+ rook_endings_database = database.search do
31
+ exactly 1, "R", "r"
32
+ exactly 5, "P"
33
+ exactly 4, "p"
34
+ end
35
+
36
+ Filters include:
37
+
38
+ * 'exactly n, *pieces'
39
+ * 'at_least n, *pieces'
40
+ * 'at_most n, *pieces'
41
+
42
+ == Example: Extracting all 5-4 R+P endings
43
+
44
+ # Read database of games and query
45
+
46
+ require 'chess_data'
47
+
48
+ # create a database
49
+ database = ChessData::Database.new
50
+
51
+ # read in pgn files provided on command line and add them to the database
52
+ ARGV.each do |file|
53
+ puts "Reading from #{file}"
54
+ database.add_games_from file
55
+ end
56
+
57
+ # extract those games which at some point reach given position definition
58
+ selected = database.search do
59
+ exactly 1, "R", "r"
60
+ exactly 5, "P"
61
+ exactly 4, "p"
62
+ end
63
+
64
+ # report and save result
65
+ puts "Found #{selected.size} games"
66
+ selected.to_file "selected.pgn"
67
+
68
+ puts "Selected #{selected.size} out of #{database.size}"
69
+
70
+ == Example: Study a game move-by-move
71
+
72
+ The ChessData::Game#play_game method takes a block, which is passed the current
73
+ board and next move after each half move. The following example prints out the
74
+ board position and information to create a trace of the game:
75
+
76
+ $game.play_game do |board, next_move|
77
+ puts board.to_s
78
+ puts
79
+ puts "Move #{board.fullmove_number}: #{if board.to_move == "w" then "" else "... " end}#{next_move}"
80
+ end
81
+
82
+ Sample output:
83
+
84
+ Move 32: Rxd7+
85
+ ........
86
+ ..kR...p
87
+ ....pb.Q
88
+ ........
89
+ ..P..Pq.
90
+ .Pn.....
91
+ ......P.
92
+ ......BK
93
+
94
+ Move 32: ... Kxd7
95
+ ........
96
+ ...k...p
97
+ ....pb.Q
98
+ ........
99
+ ..P..Pq.
100
+ .Pn.....
101
+ ......P.
102
+ ......BK
103
+
104
+ Move 33: Qxf6
105
+ ........
106
+ ...k...p
107
+ ....pQ..
108
+ ........
109
+ ..P..Pq.
110
+ .Pn.....
111
+ ......P.
112
+ ......BK
113
+
114
+ Move 33: ... 1/2-1/2
115
+
@@ -0,0 +1,324 @@
1
+
2
+ module ChessData
3
+
4
+ # Pieces are structures, made from:
5
+ # - piece: is a string "P", "p", "N", "n", etc
6
+ # - square: is square definition, either a symbol :e4 or string "E4"
7
+ PieceDefn = Struct.new(:piece, :square)
8
+
9
+ # Holds information about a chess position, including:
10
+ # - location of all pieces
11
+ # - options for castling king or queen side
12
+ # - halfmove and fullmove counts
13
+ # - possible enpassant target
14
+ #
15
+ class Board
16
+ # The next player to move, "w" or "b".
17
+ attr_accessor :to_move
18
+ # True if white king-side castling is valid
19
+ attr_accessor :white_king_side_castling
20
+ # True if white queen-side castling is valid
21
+ attr_accessor :white_queen_side_castling
22
+ # True if black king-side castling is valid
23
+ attr_accessor :black_king_side_castling
24
+ # True if black queen-side castling is valid
25
+ attr_accessor :black_queen_side_castling
26
+ # If enpassant is possible, holds the target square, or "-"
27
+ attr_accessor :enpassant_target
28
+ # Counts the number of half moves
29
+ attr_accessor :halfmove_clock
30
+ # Counts the number of full moves
31
+ attr_accessor :fullmove_number
32
+
33
+ # Creates an instance of an empty chess board
34
+ def initialize
35
+ @board = []
36
+ 8.times do
37
+ @board << [nil] * 8
38
+ end
39
+ @to_move = "w"
40
+ @white_king_side_castling = false
41
+ @white_queen_side_castling = false
42
+ @black_king_side_castling = false
43
+ @black_queen_side_castling = false
44
+ @enpassant_target = "-"
45
+ @halfmove_clock = 0
46
+ @fullmove_number = 1
47
+ end
48
+
49
+ # Makes a full copy of this board instance
50
+ def clone
51
+ copy = Board.new
52
+
53
+ 8.times do |row|
54
+ 8.times do |col|
55
+ copy.set row, col, @board[row][col]
56
+ end
57
+ end
58
+ copy.to_move = @to_move
59
+ copy.white_king_side_castling = @white_king_side_castling
60
+ copy.white_queen_side_castling = @white_queen_side_castling
61
+ copy.black_king_side_castling = @black_king_side_castling
62
+ copy.black_queen_side_castling = @black_queen_side_castling
63
+ copy.enpassant_target = @enpassant_target
64
+ copy.halfmove_clock = @halfmove_clock
65
+ copy.fullmove_number = @fullmove_number
66
+
67
+ return copy
68
+ end
69
+
70
+ # Provide a way of looking up items based on usual chess
71
+ # notation, i.e. :e4 or "E4".
72
+ # Raises an ArgumentError if square is not a valid chessboard position.
73
+ # @param [String, Symbol] square is location to find
74
+ # @return [String] chess on the given square
75
+ def [](square)
76
+ col, row = Board.square_to_coords square
77
+ return @board[row][col]
78
+ end
79
+
80
+ # Change the piece on a given square.
81
+ # @param [String, Symbol] square is location to change
82
+ # @param [String] piece
83
+ # @return [String] chess on the given square
84
+ def []=(square, piece)
85
+ col, row = Board.square_to_coords square
86
+ @board[row][col] = piece
87
+ end
88
+
89
+ # Compare two boards for equality
90
+ def == board
91
+ return false unless @to_move == board.to_move &&
92
+ @white_king_side_castling == board.white_king_side_castling &&
93
+ @white_queen_side_castling == board.white_queen_side_castling &&
94
+ @black_king_side_castling == board.black_king_side_castling &&
95
+ @black_queen_side_castling == board.black_queen_side_castling &&
96
+ @enpassant_target == board.enpassant_target &&
97
+ @halfmove_clock == board.halfmove_clock &&
98
+ @fullmove_number == board.fullmove_number
99
+
100
+ 8.times do |i|
101
+ 8.times do |j|
102
+ square = Board.coords_to_square i, j
103
+ return false unless self[square] == board[square]
104
+ end
105
+ end
106
+
107
+ return true
108
+ end
109
+
110
+ # Return the location of given piece on board.
111
+ # Identifier can be a letter or number, and if present the piece location must contain it
112
+ def locations_of piece, identifier=""
113
+ identifier = identifier.upcase
114
+ result = []
115
+
116
+ 8.times do |row|
117
+ 8.times do |col|
118
+ if @board[row][col] == piece
119
+ square = Board.coords_to_square col, row
120
+ if identifier.empty? || square.include?(identifier)
121
+ result << square
122
+ end
123
+ end
124
+ end
125
+ end
126
+
127
+ return result
128
+ end
129
+
130
+ # Count the number of occurrences of the given piece on the board.
131
+ def count piece
132
+ @board.flatten.count piece
133
+ end
134
+
135
+ # Creates a simple 2D board representation, suitable for printing to a terminal.
136
+ def to_s
137
+ result = ""
138
+
139
+ 8.times do |i|
140
+ 8.times do |j|
141
+ square = Board.coords_to_square j, i
142
+ piece = self[square]
143
+ piece = "." if piece.nil?
144
+ result += piece
145
+ end
146
+ result += "\n"
147
+ end
148
+
149
+ return result
150
+ end
151
+
152
+ # Check if the white king is in check.
153
+ def white_king_in_check?
154
+ white_king = locations_of("K").first
155
+ black_pieces.any? do |defn|
156
+ Moves.can_reach self, defn.piece, defn.square, white_king
157
+ end
158
+ end
159
+
160
+ # Check if the black king is in check.
161
+ def black_king_in_check?
162
+ black_king = locations_of("k").first
163
+ white_pieces.any? do |defn|
164
+ Moves.can_reach self, defn.piece, defn.square, black_king
165
+ end
166
+ end
167
+
168
+ # Creates a chessboard from a FEN description.
169
+ # The FEN description may be a single string, representing a board
170
+ # or a full six-field description.
171
+ # Raises an ArgumentError if fen is not a valid FEN description.
172
+ #
173
+ # @param [String] fen a board definition in FEN format
174
+ # @return [Board] an instance of board matching the FEN description
175
+ def Board.from_fen fen
176
+ fields = fen.split " "
177
+ unless fields.length == 1 || fields.length == 6
178
+ raise ArgumentError, "Invalid FEN description"
179
+ end
180
+ # create and populate a new instance of ChessBoard
181
+ board = Board.new
182
+ board.send(:setup_board_from_fen, fields[0])
183
+ if fields.length == 6
184
+ board.to_move = fields[1].downcase
185
+ board.white_king_side_castling = fields[2].include? "K"
186
+ board.white_queen_side_castling = fields[2].include? "Q"
187
+ board.black_king_side_castling = fields[2].include? "k"
188
+ board.black_queen_side_castling = fields[2].include? "q"
189
+ board.enpassant_target = fields[3]
190
+ board.halfmove_clock = fields[4].to_i
191
+ board.fullmove_number = fields[5].to_i
192
+ end
193
+
194
+ return board
195
+ end
196
+
197
+ # Creates a board instance representing the start position.
198
+ def Board.start_position
199
+ Board.from_fen \
200
+ "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
201
+ end
202
+
203
+
204
+ # Converts array coordinates into a square representation.
205
+ #
206
+ # > ChessBoard::Board.coords_to_square 0, 7 => "A8"
207
+ # > ChessBoard::Board.coords_to_square 7, 0 => "H1"
208
+ # > ChessBoard::Board.coords_to_square 4, 4 => "E5"
209
+ #
210
+ # The conversion is cached, for speed.
211
+ #
212
+ def Board.coords_to_square col, row
213
+ unless defined? @coords_store
214
+ @coords_store = []
215
+ 8.times do
216
+ @coords_store << [nil] * 8
217
+ end
218
+ 8.times do |cl|
219
+ 8.times do |rw|
220
+ @coords_store[cl][rw] = Board.square_from_coords(cl, rw)
221
+ end
222
+ end
223
+ end
224
+
225
+ return @coords_store[col][row]
226
+ end
227
+
228
+ # Converts a square represention into array coordinates.
229
+ #
230
+ # > ChessData::Board.square_to_coords "e4" => [4, 4]
231
+ # > ChessData::Board.square_to_coords "a8" => [0, 7]
232
+ # > ChessData::Board.square_to_coords "h1" => [7, 0]
233
+ #
234
+ # The conversion is cached, for speed.
235
+ #
236
+ def Board.square_to_coords square
237
+ unless defined? @square_hash
238
+ @square_hash = {}
239
+ 8.times do |col|
240
+ 8.times do |row|
241
+ @square_hash[Board.square_from_coords(col, row)] = [col, row]
242
+ end
243
+ end
244
+ end
245
+
246
+ square = square.to_s.upcase # convert symbols to strings, ensure upper case
247
+ unless @square_hash.has_key? square
248
+ raise ArgumentError, "Invalid board notation -|#{square}|-"
249
+ end
250
+
251
+ return @square_hash[square]
252
+ end
253
+
254
+ # Provides a fast method to set value of board at given row/col index values
255
+ # -- used to optimise clone
256
+ def set row, col, value
257
+ @board[row][col] = value
258
+ end
259
+
260
+ private
261
+
262
+ # Converts a square represention into array coordinates.
263
+ #
264
+ # > ChessData::Board.square_from_coords "e4" => [4, 4]
265
+ # > ChessData::Board.square_from_coords "a1" => [0, 7]
266
+ # > ChessData::Board.square_from_coords "h8" => [7, 0]
267
+ #
268
+ def Board.square_from_coords col, row
269
+ first = (65+col).chr
270
+ second = (49+(7-row)).chr
271
+ return "#{first}#{second}"
272
+ end
273
+
274
+ # Setup the current board
275
+ def setup_board_from_fen fen
276
+ rows = fen.split "/"
277
+ unless rows.length == 8
278
+ raise ArgumentError, "Invalid FEN description"
279
+ end
280
+ 8.times do |row|
281
+ col = 0
282
+ rows[row].chars.each do |i|
283
+ case i
284
+ when "K", "k", "Q", "q", "R", "r", "N", "n", "B", "b", "P", "p"
285
+ @board[row][col] = i
286
+ col += 1
287
+ when /[1-8]/
288
+ col += i.to_i
289
+ else
290
+ raise ArgumentError, "Invalid character in FEN description"
291
+ end
292
+ end
293
+ end
294
+ end
295
+
296
+ # Returns the location of all white pieces and pawns.
297
+ def white_pieces
298
+ find_pieces "KQRBNP"
299
+ end
300
+
301
+ # Returns the location of all black pieces and pawns.
302
+ def black_pieces
303
+ find_pieces "kqrbnp"
304
+ end
305
+
306
+ # Returns piece+position for all pieces on the board which are in the given
307
+ # list of pieces.
308
+ def find_pieces pieces
309
+ result = []
310
+
311
+ 8.times do |row|
312
+ 8.times do |col|
313
+ unless @board[row][col].nil?
314
+ if pieces.include? @board[row][col]
315
+ result << PieceDefn.new(@board[row][col], Board.coords_to_square(col, row))
316
+ end
317
+ end
318
+ end
319
+ end
320
+
321
+ return result
322
+ end
323
+ end
324
+ end
@@ -0,0 +1,83 @@
1
+
2
+ module ChessData
3
+
4
+ # Class to store a set of chess games
5
+ #
6
+ # The usual way to create a database is directly from a filename.
7
+ # If your pgn file is called 'my_games.pgn', create a database using
8
+ #
9
+ # Database.from_file 'my_games.pgn'
10
+ #
11
+ class Database
12
+ include Enumerable
13
+
14
+ def initialize
15
+ @games = []
16
+ end
17
+
18
+ # Adds a given _game_ to the database.
19
+ def << game
20
+ @games << game
21
+ end
22
+
23
+ # Returns the game at the given _index_ value in the database.
24
+ def [] index
25
+ @games[index]
26
+ end
27
+
28
+ # Returns the number of games in the database.
29
+ def size
30
+ @games.size
31
+ end
32
+
33
+ # Returns a new database based on search criteria given in the block.
34
+ # Each game is scanned to see if it meets the criteria. Also see Game#search.
35
+ def search &block
36
+ db = Database.new
37
+
38
+ @games.each do |game|
39
+ db << game if game.search(&block)
40
+ end
41
+
42
+ return db
43
+ end
44
+
45
+ # Add games from a specified filename to database.
46
+ # filename:: the name of a pgn file from which to load games
47
+ # encoding:: defaults to "iso-8859-1" (which works for TWIC files).
48
+ def add_games_from filename, encoding: "iso-8859-1"
49
+ File.open filename, mode: "r", encoding: encoding do |file|
50
+ begin
51
+ begin
52
+ game = Game.from_pgn file
53
+ @games << game unless game.nil?
54
+ rescue ArgumentError
55
+ puts "Error in #{filename}, previous game #{@games.last.white} vs #{@games.last.black}"
56
+ return
57
+ end
58
+ end until file.eof?
59
+ end
60
+ end
61
+
62
+ # Save the database in PGN format to given _filename_.
63
+ # If filename exists, the games are _appended_ to the file.
64
+ def to_file filename
65
+ File.open(filename, "a") do |f|
66
+ @games.each do |game|
67
+ game.to_pgn f
68
+ f.puts
69
+ end
70
+ end
71
+ end
72
+
73
+ # Loads and returns a new database based on given filename.
74
+ # filename:: the name of a pgn file from which to load games
75
+ # encoding:: defaults to "iso-8859-1" (which works for TWIC files).
76
+ def Database.from_file filename, encoding: "iso-8859-1"
77
+ database = Database.new
78
+ database.add_games_from filename, encoding: encoding
79
+ return database
80
+ end
81
+ end
82
+ end
83
+