pgn2fen 0.9.0

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 45d55e81c471a04b01d22f91d842f4add77328a9
4
+ data.tar.gz: 60686f31b714c74a3c54885a5316fd4b0fb05fb0
5
+ SHA512:
6
+ metadata.gz: 7349dce60140d45dc6b8b3a333b9068ebfb8dec6b66d4db7f230ba22923698300af70f25b21d234bc11a3c35cb9c83ea4cd2f0921d3d9b86ff75e892bbb5d007
7
+ data.tar.gz: 30dee78b92f12148fcc05dde633d7926b478a41b72bd4d9659b1c3ca36473611c6e0305dc1bf998ab6fc14278b19cddd2866bf927d264310de971c72a9e34040
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
data/LICENSE ADDED
@@ -0,0 +1,63 @@
1
+ == Pgn2Fen License
2
+
3
+ Pgn2Fen is copyright Vinay Doma (vinay.doma@gmail.com) and made available
4
+ under the terms of Ruby's license, included below for your convenience.
5
+
6
+ == Ruby's License
7
+
8
+ Ruby is copyrighted free software by Yukihiro Matsumoto <matz@netlab.jp>.
9
+ You can redistribute it and/or modify it under either the terms of the GPL
10
+ (see the file GPL), or the conditions below:
11
+
12
+ 1. You may make and give away verbatim copies of the source form of the
13
+ software without restriction, provided that you duplicate all of the
14
+ original copyright notices and associated disclaimers.
15
+
16
+ 2. You may modify your copy of the software in any way, provided that
17
+ you do at least ONE of the following:
18
+
19
+ a) place your modifications in the Public Domain or otherwise
20
+ make them Freely Available, such as by posting said
21
+ modifications to Usenet or an equivalent medium, or by allowing
22
+ the author to include your modifications in the software.
23
+
24
+ b) use the modified software only within your corporation or
25
+ organization.
26
+
27
+ c) give non-standard binaries non-standard names, with
28
+ instructions on where to get the original software distribution.
29
+
30
+ d) make other distribution arrangements with the author.
31
+
32
+ 3. You may distribute the software in object code or binary form,
33
+ provided that you do at least ONE of the following:
34
+
35
+ a) distribute the binaries and library files of the software,
36
+ together with instructions (in the manual page or equivalent)
37
+ on where to get the original distribution.
38
+
39
+ b) accompany the distribution with the machine-readable source of
40
+ the software.
41
+
42
+ c) give non-standard binaries non-standard names, with
43
+ instructions on where to get the original software distribution.
44
+
45
+ d) make other distribution arrangements with the author.
46
+
47
+ 4. You may modify and include the part of the software into any other
48
+ software (possibly commercial). But some files in the distribution
49
+ are not written by the author, so that they are not under these terms.
50
+
51
+ For the list of those files and their copying conditions, see the
52
+ file LEGAL.
53
+
54
+ 5. The scripts and library files supplied as input to or produced as
55
+ output from the software do not automatically fall under the
56
+ copyright of the software, but belong to whomever generated them,
57
+ and may be sold commercially, and may be aggregated with this
58
+ software.
59
+
60
+ 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR
61
+ IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
62
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
63
+ PURPOSE.
@@ -0,0 +1,42 @@
1
+ # pgn2fen
2
+
3
+ Converts a single game chess PGN to an array of FEN strings.
4
+ The FEN follows the specification as listed on [Forsyth–Edwards Notation](http://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation).
5
+
6
+ ## Usage
7
+
8
+ ```ruby
9
+ require 'pgn2fen'
10
+ fen_array = Pgn2Fen::Game.new(pgn_string).parse_pgn().fen_array
11
+ ```
12
+
13
+ PGN header information is available in the Game object.
14
+
15
+ ```ruby
16
+ require 'pgn2fen'
17
+ game = Pgn2Fen::Game.new(pgn_string)
18
+ game.parse_pgn()
19
+ #FEN Array
20
+ puts game.fen_array
21
+
22
+ #PGN Header
23
+ puts game.event
24
+ puts game.site
25
+ puts game.date
26
+ puts game.eventdate
27
+ puts game.round
28
+ puts game.white
29
+ puts game.black
30
+ puts game.whiteelo
31
+ puts game.blackelo
32
+ puts game.result
33
+ puts game.eco
34
+ puts game.plycount
35
+ puts game.fen
36
+ ```
37
+
38
+ ## Notes
39
+
40
+ All side lines are ignored - only main game is converted.
41
+ Only a single game PGN is supported right now.
42
+
@@ -0,0 +1,14 @@
1
+ require 'rubygems'
2
+ require 'rake/testtask'
3
+
4
+ # Test --------------------------------------------------------------------
5
+ desc 'Run the test suite'
6
+ task :test do
7
+ Rake::TestTask.new do |t|
8
+ t.verbose = true
9
+ t.warning = true
10
+ t.pattern = 'test/**/*_test.rb'
11
+ end
12
+ end
13
+
14
+ task default: :test
data/TODO ADDED
@@ -0,0 +1,4 @@
1
+ # TODO
2
+
3
+ * Allow multi-game PGNs.
4
+ * Add more tests
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.3.0
@@ -0,0 +1,1053 @@
1
+ require 'set'
2
+ #require 'byebug'
3
+
4
+ module Pgn2Fen
5
+
6
+ class Game
7
+ # constants
8
+ START_FEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
9
+ HEADER_KEY_REGEX = /^\[([A-Z][A-Za-z]*)\s.*\]$/
10
+ HEADER_VALUE_REGEX = /^\[[A-Za-z]+\s"(.*)"\]$/
11
+ OPEN_PAREN = "("
12
+ CLOSE_PAREN = ")"
13
+ OPEN_BRACE = "{"
14
+ CLOSE_BRACE = "}"
15
+
16
+ # attr_accessors
17
+ # attr_accessor :event, :site, :date, :eventdate, :round, :white, :black, :whiteelo, :blackelo, :result, :eco, :plycount, :fen
18
+ attr_accessor :pgn, :fen_array
19
+ attr_accessor :can_white_castle_kingside, :can_black_castle_kingside, :can_black_castle_queenside, :potential_enpassent_ply, :halfmove, :fullmove, :promotion, :promotion_piece, :board_color_from_fen
20
+
21
+ # Board Representation
22
+ # (a8, 0) (b8, 1) (c8, 2) (d8, 3) (e8, 4) (f8, 5) (g8, 6) (h8, 7)
23
+ # (a7, 8) (b7, 9) (c7,10) (d7,11) (e7,12) (f7,13) (g7,14) (h7,15)
24
+ # (a6,16) (b6,17) (c6,18) (d6,19) (e6,20) (f6,21) (g6,22) (h6,23)
25
+ # (a5,24) (b5,25) (c5,26) (d5,27) (e5,28) (f5,29) (g5,30) (h5,31)
26
+ # (a4,32) (b4,33) (c4,34) (d4,35) (e4,36) (f4,37) (g4,38) (h4,39)
27
+ # (a3,40) (b3,41) (c3,42) (d3,43) (e3,44) (f3,45) (g3,46) (h3,47)
28
+ # (a2,48) (b2,49) (c2,50) (d2,51) (e2,52) (f2,53) (g2,54) (h2,55)
29
+ # (a1,56) (b1,57) (c1,58) (d1,59) (e1,60) (f1,61) (g1,62) (h1,63)
30
+
31
+ # static initializers
32
+ @@pgn_squares = ["a8","b8","c8","d8","e8","f8","g8","h8","a7","b7","c7","d7","e7","f7","g7","h7","a6","b6","c6","d6","e6","f6","g6","h6","a5","b5","c5","d5","e5","f5","g5","h5","a4","b4","c4","d4","e4","f4","g4","h4","a3","b3","c3","d3","e3","f3","g3","h3","a2","b2","c2","d2","e2","f2","g2","h2","a1","b1","c1","d1","e1","f1","g1","h1"];
33
+
34
+ @@number_squares = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63]
35
+
36
+ @@pgn_number_hash = Hash[@@pgn_squares.zip(@@number_squares)]
37
+ @@number_pgn_hash = Hash[@@number_squares.zip(@@pgn_squares)]
38
+
39
+ @@aFile = Set.new; @@bFile = Set.new; @@cFile = Set.new; @@dFile = Set.new;
40
+ @@eFile = Set.new; @@fFile = Set.new; @@gFile = Set.new; @@hFile = Set.new;
41
+
42
+ (0...8).each {|i|
43
+ @@aFile.add(i * 8 + 0)
44
+ @@bFile.add(i * 8 + 1)
45
+ @@cFile.add(i * 8 + 2)
46
+ @@dFile.add(i * 8 + 3)
47
+ @@eFile.add(i * 8 + 4)
48
+ @@fFile.add(i * 8 + 5)
49
+ @@gFile.add(i * 8 + 6)
50
+ @@hFile.add(i * 8 + 7)
51
+ }
52
+
53
+ @@firstRank = Set.new; @@secondRank = Set.new; @@thirdRank = Set.new; @@fourthRank = Set.new;
54
+ @@fifthRank = Set.new; @@sixthRank = Set.new; @@seventhRank = Set.new; @@eighthRank = Set.new;
55
+
56
+ (0...8).each {|i|
57
+ @@firstRank.add(8 * 7 + i)
58
+ @@secondRank.add(8 * 6 + i)
59
+ @@thirdRank.add(8 * 5 + i)
60
+ @@fourthRank.add(8 * 4 + i)
61
+ @@fifthRank.add(8 * 3 + i)
62
+ @@sixthRank.add(8 * 2 + i)
63
+ @@seventhRank.add(8 * 1 + i)
64
+ @@eighthRank.add(8 * 0 + i)
65
+ }
66
+
67
+ @@light_squares = Set.new
68
+ @@dark_squares = Set.new
69
+
70
+ [ 0, 2, 4, 6,
71
+ 9, 11, 13, 15,
72
+ 16, 18, 20, 22,
73
+ 25, 27, 29, 31,
74
+ 32, 34, 36, 38,
75
+ 41, 43, 45, 47,
76
+ 48, 50, 52, 54,
77
+ 57, 59, 61, 63 ].each {|i|
78
+ @@light_squares.add(i)
79
+ }
80
+
81
+ [ 1, 3, 5, 7,
82
+ 8, 10, 12, 14,
83
+ 17, 19, 21, 23,
84
+ 24, 26, 28, 30,
85
+ 33, 35, 37, 39,
86
+ 40, 42, 44, 46,
87
+ 49, 51, 53, 55,
88
+ 56, 58, 60, 62 ].each {|i|
89
+ @@dark_squares.add(i)
90
+ }
91
+
92
+ @@one_thru_eight = Set.new
93
+ @@a_thru_h = Set.new
94
+
95
+ ('1'..'8').each {|i| @@one_thru_eight.add(i)}
96
+ ('a'..'h').each {|i| @@a_thru_h.add(i)}
97
+
98
+ def initialize(pgn_data)
99
+ @pgn_data = pgn_data
100
+ @board = []
101
+ @board_start_fen = START_FEN
102
+ @fen = nil
103
+ @potential_enpassent_ply = nil
104
+ @halfmove = 0
105
+ @fullmove = 1
106
+ @promotion = false
107
+ @promotion_piece = nil
108
+ @board_color_from_fen = nil
109
+ end
110
+
111
+ def reset_castle_options(option)
112
+ @can_white_castle_kingside = option
113
+ @can_white_castle_queenside = option
114
+ @can_black_castle_kingside = option
115
+ @can_black_castle_queenside = option
116
+ end
117
+
118
+
119
+ def parse_headers(headers)
120
+ #puts headers
121
+ headers.each {|header|
122
+ key = HEADER_KEY_REGEX.match(header)[1]
123
+ value = HEADER_VALUE_REGEX.match(header)[1]
124
+ instance_variable_set(:"@#{key.downcase.to_sym}", value)
125
+ }
126
+ end
127
+
128
+ def parse_pgn
129
+ unless is_single_game
130
+ raise Pgn2FenError, 'Only a single game PGN is supported right now.'
131
+ end
132
+ headers = []
133
+ @pgn_data.strip!
134
+ pgn_data_array = @pgn_data.split("\n")
135
+ pgn_data_array.each {|line|
136
+ if line.index("[") == 0
137
+ headers << line
138
+ else
139
+ break
140
+ end
141
+ }
142
+ parse_headers(headers)
143
+ pgn_data_array = pgn_data_array[headers.size..-1]
144
+ # get pgn
145
+ @pgn = ""
146
+ pgn_data_array.each {|line|
147
+ @pgn = pgn.concat(line).concat(" ")
148
+ }
149
+ @pgn = clean_pgn(@pgn)
150
+
151
+ #if fen does exist, use that as start board
152
+ unless @fen.nil?
153
+ tokens = @fen.split
154
+ unless tokens.size == 6
155
+ raise Pgn2FenError, "Invalid FEN header #{@fen}"
156
+ end
157
+ @board_start_fen = @fen
158
+ @board_color_from_fen = tokens[1]
159
+ @fullmove = tokens[5].to_i
160
+ castle_options = tokens[3]
161
+ update_castle_options_from_fen(castle_options)
162
+ else
163
+ reset_castle_options(true)
164
+ end
165
+
166
+ init_board
167
+ @plies = plies_from_pgn(@pgn)
168
+ @fen_array = fen_array_from_plies(@plies)
169
+ self # allow chaining
170
+ end
171
+
172
+ def to_s
173
+ str = ""
174
+ str << "Event: #{@event}\n"
175
+ str << "Site: #{@site}\n"
176
+ str << "Date: #{@date}\n"
177
+ str << "EventDate: #{@eventdate}\n"
178
+ str << "Round: #{@round}\n"
179
+ str << "White: #{@white}\n"
180
+ str << "Black: #{@black}\n"
181
+ str << "WhiteElo: #{@whiteElo}\n"
182
+ str << "BlackElo: #{@blackElo}\n"
183
+ str << "Result: #{@result}\n"
184
+ str << "Eco: #{@eco}\n"
185
+ str << "Plycount: #{@plycount}\n"
186
+ str << "FEN: #{@fen}\n"
187
+
188
+ str << "PGN: #{@pgn}\n"
189
+ str << "Plies: #{@plies}\n"
190
+ str << "FEN Array:\n"
191
+ unless @fen_array.nil?
192
+ @fen_array.each {|fen|
193
+ str << fen << "\n"
194
+ }
195
+ end
196
+ str
197
+ end
198
+
199
+ def rank_for_hint(hint)
200
+ case hint
201
+ when '1'
202
+ return @@firstRank
203
+ when '2'
204
+ return @@secondRank
205
+ when '3'
206
+ return @@thirdRank
207
+ when '4'
208
+ return @@fourthRank
209
+ when '5'
210
+ return @@fifthRank
211
+ when '6'
212
+ return @@sixthRank
213
+ when '7'
214
+ return @@seventhRank
215
+ when '8'
216
+ return @@eighthRank
217
+ else
218
+ return nil
219
+ end
220
+ end
221
+
222
+ def file_for_hint(hint)
223
+ case hint
224
+ when 'a'
225
+ return @@aFile
226
+ when 'b'
227
+ return @@bFile
228
+ when 'c'
229
+ return @@cFile
230
+ when 'd'
231
+ return @@dFile
232
+ when 'e'
233
+ return @@eFile
234
+ when 'f'
235
+ return @@fFile
236
+ when 'g'
237
+ return @@gFile
238
+ when 'h'
239
+ return @@hFile
240
+ else
241
+ return nil
242
+ end
243
+ end
244
+
245
+ def light_square_by_pgn square_pgn
246
+ @@light_squares.include?(@@pgn_number_hash[square_pgn])
247
+ end
248
+
249
+ def dark_square_by_pgn square_pgn
250
+ @@dark_squares.include?(@@pgn_number_hash[square_pgn])
251
+ end
252
+
253
+ def light_square_by_number square_number
254
+ @@light_squares.include?(square_number)
255
+ end
256
+
257
+ def dark_square_by_number square_number
258
+ @@dark_squares.include?(square_number)
259
+ end
260
+
261
+ def init_board
262
+ @board_start_fen.split(" ").first.each_char {|c|
263
+ case c
264
+ when 'r', 'n', 'b', 'q', 'k', 'p'
265
+ @board << c
266
+ when 'R', 'N', 'B', 'Q', 'K', 'P'
267
+ @board << c
268
+ when '1', '2', '3', '4', '5', '6', '7', '8'
269
+ (0...c.to_i).each {|n| @board << "" }
270
+ else
271
+ # do nothing
272
+ end
273
+ }
274
+ end
275
+
276
+ def games_from_pgn(pgn)
277
+ games = []
278
+ pos_array = pgn.enum_for(:scan, /\[Event/).map{Regexp.last_match.begin(0)}
279
+ pos_array.each_with_index {|pos, index|
280
+ if (pos_array.size > index + 1) # all elements but last
281
+ single_game_pgn = pgn[pos..pos_array[index+1]-1]
282
+ else # last element
283
+ single_game_pgn = pgn[pos..-1]
284
+ end
285
+ games << game_from_pgn(single_game_pgn)
286
+ }
287
+ games
288
+ end
289
+
290
+ ##
291
+ # check to verify PGN includes only a single game
292
+ def is_single_game
293
+ count = @pgn_data.scan(/\[Event /).count
294
+ #puts "Event count:#{count}"
295
+ if count > 1; return false; end
296
+
297
+ # result header also includes result, hence compare with 2
298
+ count = @pgn_data.scan(/1-0/).count
299
+ #puts "1-0 count:#{count}"
300
+ if count > 2; return false; end
301
+
302
+ count = @pgn_data.scan(/0-1/).count
303
+ #puts "0-1 count:#{count}"
304
+ if count > 2; return false; end
305
+
306
+ count = @pgn_data.scan(/1\/2-1\/2/).count
307
+ #puts "1/2-1/2 count:#{count}"
308
+ if count > 2; return false; end
309
+
310
+ true
311
+ end
312
+
313
+ ##
314
+ # get plies from pgn
315
+ def plies_from_pgn(pgn)
316
+ moves = pgn.split(/[0-9]+\./)
317
+ moves.shift # remove first ""
318
+ plies = []
319
+ moves.each {|move|
320
+ move.strip!
321
+ move.gsub!(".", "")
322
+ move.split(" ").each {|ply|
323
+ plies << ply
324
+ }
325
+ }
326
+ plies
327
+ end
328
+
329
+ ##
330
+ # clean pgn - remove comments and other unrequired text
331
+ def clean_pgn(pgn)
332
+ pgn.strip!
333
+ # clean result
334
+ pgn.sub!("1-0", ""); pgn.sub!("0-1", ""); pgn.sub!("1/2-1/2", "")
335
+ # clean mate and incomplete game marker
336
+ pgn.gsub!("#", ""); pgn.gsub!("*", "")
337
+ #remove all chessbase $ characters
338
+ pgn.gsub!(/\$(\w+)/, "")
339
+ #remove all comments - content within {}
340
+ #pgn.gsub!(/(\{[^}]+\})+?/, "")
341
+ pgn = clean_comments(pgn)
342
+ #remove all subvariations - content within ()
343
+ #pgn.gsub!(/(\([^}]+\))+?/, "")
344
+ pgn = clean_subvariations(pgn)
345
+ end
346
+
347
+ def clean_comments(pgn)
348
+ remove_text_between_tokens_inclusive(pgn, OPEN_BRACE, CLOSE_BRACE)
349
+ end
350
+
351
+ def clean_subvariations(pgn)
352
+ remove_text_between_tokens_inclusive(pgn, OPEN_PAREN, CLOSE_PAREN)
353
+ end
354
+
355
+ def remove_text_between_tokens_inclusive(text, open_token, close_token)
356
+ open_token_count = 0
357
+ new_text = ""
358
+ text.split("").each {|c|
359
+ if c == open_token
360
+ open_token_count += 1
361
+ elsif c == close_token
362
+ open_token_count -= 1
363
+ else
364
+ if open_token_count == 0
365
+ new_text << c
366
+ end
367
+ end
368
+ }
369
+ new_text
370
+ end
371
+
372
+ ##
373
+ # get FEN array from plies
374
+ def fen_array_from_plies(plies)
375
+ fen_array = []
376
+ fen_array << @board_start_fen
377
+ ply_number = 1 #ply_number starts at 1 for regular game
378
+ unless @fen.nil?
379
+ if @board_color_from_fen == "w"
380
+ ply_number = (@fullmove.to_i * 2) + 1
381
+ else
382
+ ply_number = (@fullmove.to_i * 2)
383
+ end
384
+ end
385
+ plies.each{|ply|
386
+ #puts "ply=#{ply}, ply_number=#{ply_number}, move_number=#{ply_number/2 + 1}"
387
+ fen_array << fen_for_ply(ply, ply_number)
388
+ ply_number += 1
389
+ }
390
+ fen_array
391
+ end
392
+
393
+ ##
394
+ # get FEN from a ply and ply number
395
+ def fen_for_ply(ply, ply_number)
396
+ #puts ply
397
+ is_white = (ply_number % 2 != 0)
398
+ @halfmove = @halfmove + 1
399
+ long_ply = short_ply_to_long_ply(ply, is_white)
400
+
401
+ if long_ply.eql? "O-O"
402
+ if is_white
403
+ make_ply_on_board("e1g1")
404
+ make_ply_on_board("h1f1")
405
+ @can_white_castle_kingside = false
406
+ @can_white_castle_queenside = false
407
+ else
408
+ make_ply_on_board("e8g8")
409
+ make_ply_on_board("h8f8")
410
+ @can_black_castle_kingside = false
411
+ @can_black_castle_queenside = false
412
+ end
413
+ elsif long_ply.eql? "O-O-O"
414
+ if is_white
415
+ make_ply_on_board("e1c1")
416
+ make_ply_on_board("a1d1")
417
+ @can_white_castle_kingside = false
418
+ @can_white_castle_queenside = false
419
+ else
420
+ make_ply_on_board("e8c8")
421
+ make_ply_on_board("a8d8")
422
+ @can_black_castle_kingside = false
423
+ @can_black_castle_queenside = false
424
+ end
425
+ else # rest of moves
426
+ if is_white
427
+ if @can_white_castle_kingside && long_ply[0,2] == "h1"; @can_white_castle_kingside = false; end
428
+ if @can_white_castle_queenside && long_ply[0,2] == "a1"; @can_white_castle_queenside = false; end
429
+ else
430
+ if @can_black_castle_kingside && long_ply[0,2] == "h8"; @can_white_castle_kingside = false; end
431
+ if @can_black_castle_queenside && long_ply[0,2] == "a8"; @can_white_castle_queenside = false; end
432
+ end
433
+ make_ply_on_board(long_ply)
434
+ end
435
+
436
+ fen = board_to_fen
437
+ fen.concat(" ").concat(is_white ? "b": "w")
438
+ fen.concat(" ").concat(fen_castle_text)
439
+ #enpassent square
440
+ unless @potential_enpassent_ply.nil?
441
+ fen.concat(" ").concat(@potential_enpassent_ply)
442
+ @potential_enpassent_ply = nil
443
+ else
444
+ fen.concat(" ").concat("-")
445
+ end
446
+ fen.concat(" ").concat(@halfmove.to_s)
447
+ if is_white
448
+ fen.concat(" ").concat((@fullmove).to_s)
449
+ else
450
+ fen.concat(" ").concat((@fullmove += 1).to_s)
451
+ end
452
+ #pp_board
453
+ fen
454
+ end
455
+
456
+ ##
457
+ # get FEN castle text based on castling availability
458
+ def fen_castle_text
459
+ text = ""
460
+ if @can_white_castle_kingside; text.concat("K"); end
461
+ if @can_white_castle_queenside; text.concat("Q"); end
462
+ if @can_black_castle_kingside; text.concat("k"); end
463
+ if @can_black_castle_queenside; text.concat("q"); end
464
+ if text.empty?; text = "-"; end
465
+ text
466
+ end
467
+
468
+ ##
469
+ # update castling options from FEN header
470
+ def update_castle_options_from_fen castle_options
471
+ reset_castle_options(false)
472
+ if castle_options == "-"
473
+ return
474
+ end
475
+ tokens = castle_options.split("")
476
+ tokens.each {|token|
477
+ case token
478
+ when "K"
479
+ @can_white_castle_kingside = true
480
+ when "Q"
481
+ @can_white_castle_queenside = true
482
+ when "k"
483
+ @can_black_castle_kingside = true
484
+ when "q"
485
+ @can_black_castle_queenside = true
486
+ end
487
+ }
488
+ end
489
+
490
+ ##
491
+ # make a ply on the board
492
+ def make_ply_on_board(long_ply)
493
+ from_pgn = long_ply[0,2]
494
+ to_pgn = long_ply[2,2]
495
+ from_idx = @@pgn_number_hash[from_pgn]
496
+ to_idx = @@pgn_number_hash[to_pgn]
497
+ #puts "from_pgn:#{from_pgn}, to_pgn:#{to_pgn}, from_idx:#{from_idx}, to_idx:#{to_idx}"
498
+ if @promotion
499
+ if "P" == @board[from_idx] #white
500
+ @board[to_idx] = @promotion_piece.upcase
501
+ else #black
502
+ @board[to_idx] = @promotion_piece.downcase
503
+ end
504
+ @promotion = false
505
+ @promotion_piece = nil
506
+ else #general case
507
+ @board[to_idx] = @board[from_idx]
508
+ end
509
+ @board[from_idx] = ""
510
+ end
511
+
512
+ ##
513
+ # return fen representation from board
514
+ def board_to_fen
515
+ fen = ""
516
+ empty_square_counter = 0;
517
+ @board.each_with_index { |tok, idx|
518
+ if (idx % 8 == 0 && idx > 0)
519
+ if empty_square_counter != 0
520
+ fen.concat(empty_square_counter.to_s)
521
+ empty_square_counter = 0
522
+ end
523
+ fen.concat("/")
524
+ end
525
+ if tok.empty?
526
+ empty_square_counter = empty_square_counter + 1
527
+ else
528
+ if empty_square_counter != 0
529
+ fen.concat(empty_square_counter.to_s)
530
+ empty_square_counter = 0
531
+ end
532
+ fen.concat(tok)
533
+ end
534
+ }
535
+ # last squares could be empty
536
+ if empty_square_counter != 0
537
+ fen.concat(empty_square_counter.to_s)
538
+ empty_square_counter = 0
539
+ end
540
+ fen
541
+ end
542
+
543
+ def pp_board
544
+ str = ""
545
+ @board.each_with_index { |sq, idx|
546
+ if sq.empty?; sq = "*"; end
547
+ if (idx % 8 == 0 && idx != 0); str << "\n"; end
548
+ str << sq << " "
549
+ }
550
+ puts "= = = = = = = ="
551
+ puts str
552
+ puts "= = = = = = = =\n\n\n"
553
+ end
554
+
555
+ ##
556
+ # convert short ply to long ply
557
+ def short_ply_to_long_ply(ply, is_white)
558
+ from_idx = -1
559
+ to_idx = -1
560
+ from_pgn = ""
561
+ to_pgn = ""
562
+ hint = nil
563
+
564
+ if ply.eql?("O-O"); return ply; end
565
+ if ply.eql?("O-O-O"); return ply; end
566
+ unless ply.index('+').nil?; ply.sub!("+", ""); end
567
+ unless ply.index('x').nil?; ply.sub!("x", ""); @halfmove = 0; end
568
+ unless ply.index('=').nil?
569
+ @promotion = true
570
+ @promotion_piece = ply[-1]
571
+ ply = ply.chop.chop
572
+ end
573
+
574
+ if ply.length == 2 # pawn non-capture
575
+ to_pgn = ply
576
+ to_idx = @@pgn_number_hash[to_pgn]
577
+ if is_white
578
+ if !@board[to_idx+8].empty?
579
+ from_idx = to_idx+8
580
+ else
581
+ from_idx = to_idx+16
582
+ # if @board[to_idx-1].eql?"p" or @board[to_idx+1].eql?"p"
583
+ @potential_enpassent_ply = @@number_pgn_hash[from_idx-8]
584
+ # end
585
+ end
586
+ else # isBlack
587
+ if !@board[to_idx-8].empty?
588
+ from_idx = to_idx-8
589
+ else
590
+ from_idx = to_idx-16
591
+ # if @board[to_idx-1].eql?"P" or @board[to_idx+1].eql?"P"
592
+ @potential_enpassent_ply = @@number_pgn_hash[from_idx+8]
593
+ # end
594
+ end
595
+ end
596
+ from_pgn = @@number_pgn_hash[from_idx]
597
+ @halfmove = 0
598
+ return from_pgn + to_pgn
599
+ end
600
+
601
+ if ('a'..'h').include?(ply[0]) && ply.length == 3 #pawn capture, non-enpassent
602
+ to_pgn = ply[1..-1]
603
+ to_idx = @@pgn_number_hash[to_pgn]
604
+ if is_white
605
+ if @board[to_idx+7].eql?("P") && file_for_hint(ply[0]).include?(to_idx+7)
606
+ from_idx = to_idx+7
607
+ elsif @board[to_idx+9].eql?("P")&& file_for_hint(ply[0]).include?(to_idx+9)
608
+ from_idx = to_idx+9
609
+ end
610
+ else #is_black
611
+ if @board[to_idx-7].eql?("p") && file_for_hint(ply[0]).include?(to_idx-7)
612
+ from_idx = to_idx-7
613
+ elsif @board[to_idx-9].eql?("p") && file_for_hint(ply[0]).include?(to_idx-9)
614
+ from_idx = to_idx-9
615
+ end
616
+ end
617
+ if from_idx == -1; raise Pgn2FenError, "Error parsing pawn capture at ply #{ply}"; end
618
+ from_pgn = @@number_pgn_hash[from_idx]
619
+ @halfmove = 0
620
+ return from_pgn + to_pgn
621
+ end
622
+
623
+ if ply.length == 3; to_pgn = ply[1,2]; end
624
+ if ply.length() == 4
625
+ to_pgn = ply[2,2]
626
+ hint = ply[1]
627
+ end
628
+ to_idx = @@pgn_number_hash[to_pgn]
629
+
630
+ if ply[0].downcase.eql?('r'); return short_ply_to_long_ply_for_rook(to_idx, to_pgn, hint, is_white); end
631
+ if ply[0].downcase.eql?('n'); return short_ply_to_long_ply_for_knight(to_idx, to_pgn, hint, is_white); end
632
+ if ply[0].downcase.eql?('b'); return short_ply_to_long_ply_for_bishop(to_idx, to_pgn, hint, is_white); end
633
+ if ply[0].downcase.eql?('q'); return short_ply_to_long_ply_for_queen(to_idx, to_pgn, hint, is_white); end
634
+ if ply[0].downcase.eql?('k'); return short_ply_to_long_ply_for_king(to_idx, to_pgn, hint, is_white); end
635
+ return from_pgn + to_pgn
636
+ end
637
+
638
+ def short_ply_to_long_ply_for_rook(to_idx, to_pgn, hint, is_white)
639
+ from_idx = -1
640
+ from_pgn = ""
641
+ piece = is_white ? "R" : "r"
642
+ if !hint.nil?
643
+ if @@one_thru_eight.include?(hint)
644
+ from_pgn = to_pgn[0,1] + hint
645
+ end
646
+ if @@a_thru_h.include?(hint)
647
+ from_pgn = hint + to_pgn[1,1]
648
+ end
649
+ return from_pgn + to_pgn
650
+ else # no hint
651
+ # check file
652
+ up = to_idx
653
+ while up > -1 do
654
+ up = up - 8
655
+ if (up < 0)
656
+ break
657
+ end
658
+ if @board[up].eql?(piece)
659
+ from_idx = up
660
+ from_pgn = @@number_pgn_hash[from_idx]
661
+ return from_pgn + to_pgn
662
+ elsif @board[up].eql?("")
663
+ next
664
+ else
665
+ break
666
+ end
667
+ end
668
+ down = to_idx
669
+ while down < 64 do
670
+ down = down + 8
671
+ if down > 63
672
+ break
673
+ end
674
+ if @board[down].eql?(piece)
675
+ from_idx = down
676
+ from_pgn = @@number_pgn_hash[from_idx]
677
+ return from_pgn + to_pgn
678
+ elsif @board[down].eql?("")
679
+ next
680
+ else
681
+ break
682
+ end
683
+ end
684
+
685
+ # check rank
686
+ left = to_idx
687
+ while left > -1 do
688
+ left = left - 1
689
+ if left % 8 == 7
690
+ break
691
+ end
692
+ if @board[left].eql?(piece)
693
+ from_idx = left
694
+ from_pgn = @@number_pgn_hash[from_idx]
695
+ return from_pgn + to_pgn
696
+ elsif @board[left].eql?("")
697
+ next
698
+ else
699
+ break
700
+ end
701
+ end
702
+ right = to_idx
703
+ while right < 64 do
704
+ right = right + 1
705
+ if right % 8 == 0
706
+ break
707
+ end
708
+ if @board[right].eql?(piece)
709
+ from_idx = right
710
+ from_pgn = @@number_pgn_hash[from_idx]
711
+ return from_pgn + to_pgn
712
+ elsif @board[right].eql?("")
713
+ next
714
+ else
715
+ break
716
+ end
717
+ end
718
+ end
719
+ from_pgn + to_pgn
720
+ end
721
+
722
+ def short_ply_to_long_ply_for_knight(to_idx, to_pgn, hint, is_white)
723
+ from_idx = -1
724
+ from_pgn = ""
725
+ piece = is_white ? "N" : "n"
726
+ if !hint.nil?
727
+ # -17,-15, 10, -6, 6, 10, 15, 17 are possible knight moves
728
+ knight_moves = [17+to_idx, 17+to_idx, -15+to_idx, 15+to_idx, -10+to_idx, 10+to_idx, -6+to_idx, 6+to_idx]
729
+ if @@one_thru_eight.include?(hint)
730
+ rank = rank_for_hint(hint)
731
+ knight_moves = knight_moves & rank.to_a # intersection
732
+ end
733
+ if @@a_thru_h.include?(hint)
734
+ file = file_for_hint(hint)
735
+ knight_moves = knight_moves & file.to_a #intersection
736
+ end
737
+ knight_moves.each {|idx|
738
+ if @board[idx].eql?(piece)
739
+ from_idx = idx
740
+ from_pgn = @@number_pgn_hash[from_idx]
741
+ return from_pgn + to_pgn
742
+ end
743
+ }
744
+ else
745
+ # -17,-15, 10, -6, 6, 10, 15, 17 are possible knight moves
746
+ knight_moves = [-17, 17, -15, 15, -10, 10, -6, 6]
747
+ knight_moves.each {|i|
748
+ idx = to_idx + i
749
+ if (idx < 0 && idx > 63)
750
+ next
751
+ end
752
+ if @board[idx].eql?(piece)
753
+ from_idx = idx
754
+ from_pgn = @@number_pgn_hash[from_idx]
755
+ return from_pgn + to_pgn
756
+ end
757
+ }
758
+ end
759
+ #return from_pgn + to_pgn
760
+ raise Pgn2FenError, "Error parsing knight move to square #{to_pgn}"
761
+ end
762
+
763
+ def short_ply_to_long_ply_for_bishop(to_idx, to_pgn, hint, is_white)
764
+ from_idx = -1
765
+ from_pgn = ""
766
+ piece = is_white ? "B" : "b"
767
+ is_light = light_square_by_number(to_idx) ? true : false
768
+ if (!hint.nil?)
769
+ if @@one_thru_eight.include?(hint)
770
+ from_pgn = to_pgn[0,1] + h
771
+ end
772
+ if @@a_thru_h.include?(hint)
773
+ from_pgn = h + to_pgn[1,1]
774
+ end
775
+ return from_pgn + to_pgn
776
+ else
777
+ # check nw direction
778
+ nw = to_idx
779
+ while(nw > -1) do
780
+ nw = nw - 9
781
+ if (nw < 0)
782
+ break
783
+ end
784
+ # puts "nw=#{nw}"
785
+ if light_square_by_number(nw) != is_light # square colors don't match - overflow
786
+ break
787
+ elsif @board[nw].eql?(piece)
788
+ from_idx = nw
789
+ from_pgn = @@number_pgn_hash[from_idx]
790
+ return from_pgn + to_pgn
791
+ elsif @board[nw].eql?("")
792
+ next
793
+ else
794
+ break
795
+ end
796
+ end
797
+ # check ne direction
798
+ ne = to_idx
799
+ while(ne > 0) do
800
+ ne = ne - 7
801
+ if (ne < 1)
802
+ break
803
+ end
804
+ # puts "ne=#{ne}"
805
+ if light_square_by_number(ne) != is_light # square colors don't match - overflow
806
+ break
807
+ elsif @board[ne].eql?(piece)
808
+ from_idx = ne
809
+ from_pgn = @@number_pgn_hash[from_idx]
810
+ return from_pgn + to_pgn
811
+ elsif @board[ne].eql?("")
812
+ next
813
+ else
814
+ break
815
+ end
816
+ end
817
+ # check sw direction
818
+ sw = to_idx
819
+ while(sw < 63) do
820
+ sw = sw + 7
821
+ if (sw > 62)
822
+ break
823
+ end
824
+ # puts "sw=#{sw}"
825
+ if light_square_by_number(sw) != is_light # square colors don't match - overflow
826
+ break
827
+ elsif @board[sw].eql?(piece)
828
+ from_idx = sw
829
+ from_pgn = @@number_pgn_hash[from_idx]
830
+ return from_pgn + to_pgn
831
+ elsif @board[sw].eql?("")
832
+ next
833
+ else
834
+ break
835
+ end
836
+ end
837
+ # check se direction
838
+ se = to_idx
839
+ while(se < 64) do
840
+ se = se + 9
841
+ if (se > 63)
842
+ break
843
+ end
844
+ # puts "se=#{se}"
845
+ if light_square_by_number(se) != is_light # square colors don't match - overflow
846
+ break
847
+ elsif @board[se].eql?(piece)
848
+ from_idx = se
849
+ from_pgn = @@number_pgn_hash[from_idx]
850
+ return from_pgn + to_pgn
851
+ elsif @board[se].eql?("")
852
+ next
853
+ else
854
+ break
855
+ end
856
+ end
857
+ end
858
+ return from_pgn + to_pgn
859
+ end
860
+
861
+ def short_ply_to_long_ply_for_queen(to_idx, to_pgn, hint, is_white)
862
+ # check bishop type moves
863
+ from_idx = -1
864
+ from_pgn = ""
865
+ piece = is_white ? "Q" : "q"
866
+ is_light = light_square_by_number(to_idx) ? true : false
867
+ if (!hint.nil?)
868
+ if @@one_thru_eight.include?(hint)
869
+ from_pgn = to_pgn[0,1] + hint
870
+ end
871
+ if @@a_thru_h.include?(hint)
872
+ from_pgn = hint + to_pgn[1,1]
873
+ end
874
+ return from_pgn + to_pgn
875
+ else
876
+ # check nw direction
877
+ nw = to_idx
878
+ while(nw > -1) do
879
+ nw = nw - 9
880
+ if (nw < 0)
881
+ break
882
+ end
883
+ if light_square_by_number(nw) != is_light # square colors don't match - overflow
884
+ break
885
+ elsif @board[nw].eql?(piece)
886
+ from_idx = nw
887
+ from_pgn = @@number_pgn_hash[from_idx]
888
+ return from_pgn + to_pgn
889
+ elsif @board[nw].eql?("")
890
+ next
891
+ else
892
+ break
893
+ end
894
+ end
895
+ # check ne direction
896
+ ne = to_idx
897
+ while(ne > 0) do
898
+ ne = ne - 7
899
+ if (ne < 1)
900
+ break
901
+ end
902
+ if light_square_by_number(ne) != is_light # square colors don't match - overflow
903
+ break
904
+ elsif @board[ne].eql?(piece)
905
+ from_idx = ne
906
+ from_pgn = @@number_pgn_hash[from_idx]
907
+ return from_pgn + to_pgn
908
+ elsif @board[ne].eql?("")
909
+ next
910
+ else
911
+ break
912
+ end
913
+ end
914
+ # check sw direction
915
+ sw = to_idx
916
+ while(sw < 63) do
917
+ sw = sw + 7
918
+ if (sw > 62)
919
+ break
920
+ end
921
+ if light_square_by_number(sw) != is_light # square colors don't match - overflow
922
+ break
923
+ elsif @board[sw].eql?(piece)
924
+ from_idx = sw
925
+ from_pgn = @@number_pgn_hash[from_idx]
926
+ return from_pgn + to_pgn
927
+ elsif @board[sw].eql?("")
928
+ next
929
+ else
930
+ break
931
+ end
932
+ end
933
+ # check se direction
934
+ se = to_idx
935
+ while(se < 64) do
936
+ se = se + 9
937
+ if (se > 63)
938
+ break
939
+ end
940
+ if light_square_by_number(se) != is_light # square colors don't match - overflow
941
+ break
942
+ elsif @board[se].eql?(piece)
943
+ from_idx = se
944
+ from_pgn = @@number_pgn_hash[from_idx]
945
+ return from_pgn + to_pgn
946
+ elsif @board[se].eql?("")
947
+ next
948
+ else
949
+ break
950
+ end
951
+ end
952
+ end
953
+ if (from_pgn.length == 2)
954
+ return from_pgn + to_pgn
955
+ end
956
+ # check rook type moves
957
+ if !hint.nil?
958
+ if @@one_thru_eight.include?(hint)
959
+ from_pgn = to_pgn[0,1] + hint
960
+ end
961
+ if @@a_thru_h.include?(hint)
962
+ from_pgn = hint + to_pgn[1,1]
963
+ end
964
+ return from_pgn + to_pgn
965
+ else # no hint
966
+ # check file
967
+ up = to_idx
968
+ while up > -1 do
969
+ up = up - 8
970
+ if (up < 0)
971
+ break
972
+ end
973
+ if @board[up].eql?(piece)
974
+ from_idx = up
975
+ from_pgn = @@number_pgn_hash[from_idx]
976
+ return from_pgn + to_pgn
977
+ elsif @board[up].eql?("")
978
+ next
979
+ else
980
+ break
981
+ end
982
+ end
983
+ down = to_idx
984
+ while down < 64 do
985
+ down = down + 8
986
+ if down > 63
987
+ break
988
+ end
989
+ if @board[down].eql?(piece)
990
+ from_idx = down
991
+ from_pgn = @@number_pgn_hash[from_idx]
992
+ return from_pgn + to_pgn
993
+ elsif @board[down].eql?("")
994
+ next
995
+ else
996
+ break
997
+ end
998
+ end
999
+
1000
+ # check rank
1001
+ left = to_idx
1002
+ while left > -1 do
1003
+ left = left - 1
1004
+ if left % 8 == 7
1005
+ break
1006
+ end
1007
+ if @board[left].eql?(piece)
1008
+ from_idx = left
1009
+ from_pgn = @@number_pgn_hash[from_idx]
1010
+ return from_pgn + to_pgn
1011
+ elsif @board[left].eql?("")
1012
+ next
1013
+ else
1014
+ break
1015
+ end
1016
+ end
1017
+ right = to_idx
1018
+ while right < 64 do
1019
+ right = right + 1
1020
+ if right % 8 == 0
1021
+ break
1022
+ end
1023
+ if @board[right].eql?(piece)
1024
+ from_idx = right
1025
+ from_pgn = @@number_pgn_hash[from_idx]
1026
+ return from_pgn + to_pgn
1027
+ elsif @board[right].eql?("")
1028
+ next
1029
+ else
1030
+ break
1031
+ end
1032
+ end
1033
+ end
1034
+ from_pgn + to_pgn
1035
+ end
1036
+
1037
+ def short_ply_to_long_ply_for_king(to_idx, to_pgn, hint, is_white)
1038
+ from_idx = -1
1039
+ from_pgn = ""
1040
+ if is_white
1041
+ @board.reverse.each_with_index {|i,idx| if i.eql?("K"); from_idx = 63 - idx; break; end }
1042
+ else
1043
+ @board.each_with_index {|i,idx| if i.eql?("k"); from_idx = idx; break; end }
1044
+ end
1045
+ from_pgn = @@number_pgn_hash[from_idx]
1046
+ return from_pgn + to_pgn
1047
+ end
1048
+
1049
+ end
1050
+
1051
+ class Pgn2FenError < StandardError; end
1052
+
1053
+ end #end module