pgn2fen 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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