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 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
+