egd 1.0.0 → 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +1 -0
- data/egd.gemspec +1 -1
- data/lib/egd/builder.rb +82 -0
- data/lib/egd/fen_builder.rb +18 -17
- data/lib/egd/fen_difference_discerner.rb +161 -159
- data/lib/egd/fen_to_board.rb +35 -33
- data/lib/egd/pgn_parser.rb +144 -143
- data/lib/egd/position_feature_discerner.rb +18 -16
- data/lib/egd/procedures.rb +44 -42
- data/lib/egd/version.rb +1 -1
- data/lib/egd.rb +1 -81
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c8576046875ff0741a150a8c58b4725466673c13
|
4
|
+
data.tar.gz: ffb66447b0494066c44bf56b1d7a9705139a6213
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c1edfe986366a4820c939fd9b646362e7865df89a5ad7c764997ca38fb365d8e513b8701adeee9218ebcef8905f91435ccf3e0199347565f43d71fd7e1d96297
|
7
|
+
data.tar.gz: 615f253803aaf603f13b6dd5db52b7bde8d6646df75b5aacff40c1ff6e443f3f431a98b4ca18c00f53d08935869364c7b78b441def5abe9e325a8fbf31b38252
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -149,6 +149,7 @@ EGD tries to provide the maximum of meta-information about a move a programmed s
|
|
149
149
|
Currently outputted keys are:
|
150
150
|
```rb
|
151
151
|
"move" => {
|
152
|
+
"player"=>"w", # w for White and b for Black
|
152
153
|
"san" => "exd6", # the Short Algebraic Notation from provided PGN
|
153
154
|
"lran" => "e5xd6", # EGDs semi-custom Long Reversible Algebraic Notation
|
154
155
|
"from_square" => "e5",
|
data/egd.gemspec
CHANGED
@@ -16,7 +16,7 @@ Gem::Specification.new do |spec|
|
|
16
16
|
spec.license = "BSD"
|
17
17
|
|
18
18
|
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
19
|
-
f.match(%r{^(test|spec|features)/})
|
19
|
+
f.match?(%r{^(test|spec|features)/})
|
20
20
|
end
|
21
21
|
spec.bindir = "exe"
|
22
22
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
data/lib/egd/builder.rb
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
module Egd
|
2
|
+
class Builder
|
3
|
+
# This is the real deal
|
4
|
+
# Takes in a PGN string and returns a Ruby or JSON hash representation of the game in EGD
|
5
|
+
|
6
|
+
attr_reader :pgn
|
7
|
+
|
8
|
+
def initialize(pgn)
|
9
|
+
@pgn = pgn
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_h
|
13
|
+
return @to_h if defined?(@to_h)
|
14
|
+
|
15
|
+
@to_h = {}
|
16
|
+
@to_h["game_tags"] = game_tags
|
17
|
+
@to_h["moves"] = {}
|
18
|
+
|
19
|
+
@previous_fen = Egd::FenBuilder::NULL_FEN
|
20
|
+
|
21
|
+
moves.each_with_object(@to_h) do |move, mem|
|
22
|
+
transition_key = "#{move[%r'\A\d+']}#{move.match?(%r'\.\.') ? "b" : "w"}" #=> "1w"
|
23
|
+
|
24
|
+
san = move.match(%r'\A(?:\d+\.(?:\s*\.\.)?\s+)(?<san>\S+)\z')[:san] #=> "e4"
|
25
|
+
end_fen = Egd::FenBuilder.new(start_fen: @previous_fen, move: move).call
|
26
|
+
|
27
|
+
current_transition = {
|
28
|
+
"start_position" => {
|
29
|
+
"fen" => @previous_fen,
|
30
|
+
"features" => {}, # TODO, no features can be discerned before the move yet
|
31
|
+
},
|
32
|
+
"move" => {
|
33
|
+
"player" => transition_key[%r'\D\z'], #=> "w"
|
34
|
+
"san" => san,
|
35
|
+
}.merge(
|
36
|
+
Egd::FenDifferenceDiscerner.new(
|
37
|
+
start_fen: @previous_fen, move: san, end_fen: end_fen
|
38
|
+
).call
|
39
|
+
),
|
40
|
+
"end_position" => {
|
41
|
+
"fen" => end_fen,
|
42
|
+
"features" => Egd::PositionFeatureDiscerner.new(
|
43
|
+
move: move, end_fen: end_fen
|
44
|
+
).call
|
45
|
+
}
|
46
|
+
}
|
47
|
+
|
48
|
+
# leave this breadcrumb for next run through loop
|
49
|
+
@previous_fen = current_transition.dig("end_position", "fen")
|
50
|
+
|
51
|
+
mem["moves"][transition_key] = current_transition
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def to_json
|
56
|
+
@to_json ||= to_h.to_json
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
def game_tags
|
61
|
+
@game_tags ||= parsed_pgn[:game_tags] || {}
|
62
|
+
end
|
63
|
+
|
64
|
+
def moves
|
65
|
+
return @moves if defined?(@moves)
|
66
|
+
|
67
|
+
@moves = []
|
68
|
+
|
69
|
+
parsed_pgn[:moves].each do |move_row|
|
70
|
+
@moves << "#{move_row[:num]}. #{move_row[:w]}"
|
71
|
+
@moves << "#{move_row[:num]}. .. #{move_row[:b]}" if move_row[:b]
|
72
|
+
end
|
73
|
+
|
74
|
+
@moves
|
75
|
+
end
|
76
|
+
|
77
|
+
def parsed_pgn
|
78
|
+
@parsed_pgn ||= Egd::PgnParser.new(pgn).call
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
end
|
data/lib/egd/fen_builder.rb
CHANGED
@@ -1,23 +1,24 @@
|
|
1
|
+
module Egd
|
2
|
+
class FenBuilder
|
3
|
+
# This service takes in a FEN string and a chess move in algebraic notation.
|
4
|
+
# Outputs the FEN of the resulting position
|
1
5
|
|
2
|
-
|
3
|
-
# This service takes in a FEN string and a chess move in algebraic notation.
|
4
|
-
# Outputs the FEN of the resulting position
|
6
|
+
attr_reader :start_fen, :move
|
5
7
|
|
6
|
-
|
8
|
+
NULL_FEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1".freeze
|
7
9
|
|
8
|
-
|
10
|
+
# Egd::FenBuilder.new(start_fen: nil, move:).call
|
11
|
+
def initialize(start_fen: nil, move: nil)
|
12
|
+
@start_fen = start_fen || NULL_FEN
|
13
|
+
@move = move.to_s.gsub(%r'\A\d+\.\s*\.*\s*', "")
|
14
|
+
end
|
9
15
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
@fen ||= (
|
18
|
-
move != "" ?
|
19
|
-
PGN::FEN.new(start_fen).to_position.move(move).to_fen.to_s :
|
20
|
-
@start_fen
|
21
|
-
)
|
16
|
+
def call
|
17
|
+
@fen ||= (
|
18
|
+
move != "" ?
|
19
|
+
PGN::FEN.new(start_fen).to_position.move(move).to_fen.to_s :
|
20
|
+
@start_fen
|
21
|
+
)
|
22
|
+
end
|
22
23
|
end
|
23
24
|
end
|
@@ -1,204 +1,206 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
1
|
+
module Egd
|
2
|
+
class FenDifferenceDiscerner
|
3
|
+
# This service takes in a start and an end FEN string,
|
4
|
+
# and the move in SAN,
|
5
|
+
# and a tells you what kind of move occured, in more detail than SAN
|
6
|
+
|
7
|
+
# Theoretically a start and end fen would suffice, but having move in SAN,
|
8
|
+
# which we do, allows skipping some hard procesing parts.
|
9
|
+
|
10
|
+
attr_reader :start_fen, :move, :end_fen
|
11
|
+
|
12
|
+
# Egd::FenDifferenceDiscerner.new(start_fen:, end_fen:).call
|
13
|
+
def initialize(start_fen: nil, move:, end_fen:)
|
14
|
+
@start_fen = start_fen || Egd::FenBuilder::NULL_FEN
|
15
|
+
@move = move
|
16
|
+
@end_fen = end_fen
|
17
|
+
end
|
18
|
+
|
19
|
+
def call
|
20
|
+
# quickreturn with possible special cases
|
21
|
+
case move
|
22
|
+
when "O-O"
|
23
|
+
return special_case_short_castle(Egd::Procedures.parse_fen(start_fen)[:to_move])
|
24
|
+
when "O-O-O"
|
25
|
+
return special_case_long_castle(Egd::Procedures.parse_fen(start_fen)[:to_move])
|
26
|
+
when %r'\A[a-h]x[a-h]\d' # pawn x pawn, possible en-passant
|
27
|
+
return special_case_ep_capture if special_case_ep_capture
|
28
|
+
end
|
5
29
|
|
6
|
-
|
7
|
-
# which we do, allows skipping some hard procesing parts.
|
30
|
+
# entering long processing of regular moves
|
8
31
|
|
9
|
-
|
32
|
+
changes = {
|
33
|
+
"lran" => lran, # FEN
|
34
|
+
"from_square" => from_square, # FEN # b2
|
35
|
+
"to_square" => to_square, # move b3
|
36
|
+
"piece" => piece, # move p
|
37
|
+
"move_type" => move_type,
|
38
|
+
}
|
10
39
|
|
11
|
-
|
12
|
-
|
13
|
-
@start_fen = start_fen || Egd::FenBuilder::NULL_FEN
|
14
|
-
@move = move
|
15
|
-
@end_fen = end_fen
|
16
|
-
end
|
40
|
+
changes.merge!("captured_piece" => captured_piece) if captured_piece
|
41
|
+
changes.merge!("promotion" => promotion) if promotion
|
17
42
|
|
18
|
-
|
19
|
-
# quickreturn with possible special cases
|
20
|
-
case move
|
21
|
-
when "O-O"
|
22
|
-
return special_case_short_castle(Egd::Procedures.parse_fen(start_fen)[:to_move])
|
23
|
-
when "O-O-O"
|
24
|
-
return special_case_long_castle(Egd::Procedures.parse_fen(start_fen)[:to_move])
|
25
|
-
when %r'\A[a-h]x[a-h]\d' # pawn x pawn, possible en-passant
|
26
|
-
return special_case_ep_capture if special_case_ep_capture
|
43
|
+
changes
|
27
44
|
end
|
28
45
|
|
29
|
-
|
30
|
-
|
31
|
-
changes = {
|
32
|
-
"lran" => lran, # FEN
|
33
|
-
"from_square" => from_square, # FEN # b2
|
34
|
-
"to_square" => to_square, # move b3
|
35
|
-
"piece" => piece, # move p
|
36
|
-
"move_type" => move_type,
|
37
|
-
}
|
38
|
-
|
39
|
-
changes.merge!("captured_piece" => captured_piece) if captured_piece
|
40
|
-
changes.merge!("promotion" => promotion) if promotion
|
41
|
-
|
42
|
-
changes
|
43
|
-
end
|
44
|
-
|
45
|
-
private
|
46
|
+
private
|
46
47
|
|
47
|
-
|
48
|
-
|
48
|
+
def special_case_ep_capture
|
49
|
+
return @ep_capture if defined?(@ep_capture)
|
49
50
|
|
50
|
-
|
51
|
+
return @ep_capture = false if Egd::Procedures.parse_fen(start_fen)[:ep_square] != to_square
|
51
52
|
|
52
|
-
|
53
|
+
from = move[0] + (move[-1] == "6" ? "5" : "4")
|
53
54
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
55
|
+
@ep_capture = {
|
56
|
+
"lran" => "#{from}x#{to_square}", # FEN
|
57
|
+
"from_square" => from, # FEN
|
58
|
+
"to_square" => to_square, # move
|
59
|
+
"piece" => "p", # move
|
60
|
+
"move_type" => "ep_capture", # FEN
|
61
|
+
"captured_piece" => "p", # move
|
62
|
+
}
|
63
|
+
end
|
63
64
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
65
|
+
def special_case_short_castle(player)
|
66
|
+
{
|
67
|
+
"lran" => "O-O", # FEN
|
68
|
+
"from_square" => (player == "w" ? "e1" : "e8"), # FEN
|
69
|
+
"to_square" => (player == "w" ? "g1" : "g8"), # move
|
70
|
+
"piece" => "K", # move
|
71
|
+
"move_type" => "short_castle", # FEN
|
72
|
+
}
|
73
|
+
end
|
73
74
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
75
|
+
def special_case_long_castle(player)
|
76
|
+
{
|
77
|
+
"lran" => "O-O-O", # FEN
|
78
|
+
"from_square" => (player == "w" ? "e1" : "e8"), # FEN
|
79
|
+
"to_square" => (player == "w" ? "c1" : "c8"), # move
|
80
|
+
"piece" => "K", # move
|
81
|
+
"move_type" => "long_castle", # FEN
|
82
|
+
}
|
83
|
+
end
|
83
84
|
|
84
|
-
|
85
|
-
|
86
|
-
|
85
|
+
def board1
|
86
|
+
@board1 ||= Egd::FenToBoard.new(start_fen)
|
87
|
+
end
|
87
88
|
|
88
|
-
|
89
|
-
|
90
|
-
|
89
|
+
def board2
|
90
|
+
@board2 ||= Egd::FenToBoard.new(end_fen)
|
91
|
+
end
|
91
92
|
|
92
|
-
|
93
|
-
|
93
|
+
def changed_squares
|
94
|
+
return @changed_squares if defined?(@changed_squares)
|
94
95
|
|
95
|
-
|
96
|
+
@changed_squares = []
|
96
97
|
|
97
|
-
|
98
|
-
|
98
|
+
(1..64).to_a.each do |fen_index|
|
99
|
+
i = fen_index - 1
|
99
100
|
|
100
|
-
|
101
|
-
|
101
|
+
if board1.boardline[i] != board2.boardline[i]
|
102
|
+
square = Egd::Procedures.fen_index_to_square(fen_index)
|
102
103
|
|
103
|
-
|
104
|
-
|
105
|
-
|
104
|
+
@changed_squares << {
|
105
|
+
square: square, from: board1.boardline[i], to: board2.boardline[i]
|
106
|
+
}
|
107
|
+
end
|
106
108
|
end
|
107
|
-
end
|
108
109
|
|
109
|
-
|
110
|
-
|
110
|
+
@changed_squares
|
111
|
+
end
|
111
112
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
113
|
+
def from_square
|
114
|
+
@from_square ||= changed_squares.reject do |hash|
|
115
|
+
hash[:square] == to_square
|
116
|
+
end.detect do |hash|
|
117
|
+
hash[:to] == "-"
|
118
|
+
end[:square]
|
119
|
+
end
|
119
120
|
|
120
|
-
|
121
|
-
|
122
|
-
|
121
|
+
def to_square
|
122
|
+
@to_square ||= move.match(%r'\A(?<basemove>.*\d)(?<drek>.*)?\z')[:basemove][-2..-1]
|
123
|
+
end
|
123
124
|
|
124
|
-
|
125
|
-
|
125
|
+
def piece
|
126
|
+
return @piece if defined?(@piece)
|
126
127
|
|
127
|
-
|
128
|
+
possible_piece = move[0]
|
128
129
|
|
129
|
-
|
130
|
+
@piece = (Egd::SAN_CHESS_PIECES.include?(possible_piece) ? possible_piece : "p" )
|
130
131
|
|
131
|
-
|
132
|
+
@piece << bishop_color(to_square) if @piece == "B"
|
132
133
|
|
133
|
-
|
134
|
-
|
134
|
+
@piece
|
135
|
+
end
|
135
136
|
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
137
|
+
def move_type
|
138
|
+
@move_type ||=
|
139
|
+
case move
|
140
|
+
when %r'x.*='i
|
141
|
+
"promotion_capture"
|
142
|
+
when %r'x'i
|
143
|
+
"capture"
|
144
|
+
when %r'='i
|
145
|
+
"promotion"
|
146
|
+
else
|
147
|
+
"move"
|
148
|
+
end
|
149
|
+
end
|
149
150
|
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
151
|
+
def captured_piece
|
152
|
+
@captured_piece ||=
|
153
|
+
if move_type[%r'capture']
|
154
|
+
captured_piece = changed_squares.detect do |hash|
|
155
|
+
hash[:square] == to_square
|
156
|
+
end[:from].upcase
|
156
157
|
|
157
|
-
|
158
|
+
captured_piece.downcase! if captured_piece[%r'p'i]
|
158
159
|
|
159
|
-
|
160
|
+
captured_piece << bishop_color(to_square) if captured_piece == "B"
|
160
161
|
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
162
|
+
captured_piece
|
163
|
+
else
|
164
|
+
nil
|
165
|
+
end
|
166
|
+
end
|
166
167
|
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
168
|
+
def promotion
|
169
|
+
@promotion ||=
|
170
|
+
if move_type[%r'promotion']
|
171
|
+
promoted_to = move.match(%r'=(?<promo>.)\z')[:promo]
|
171
172
|
|
172
|
-
|
173
|
+
promoted_to << bishop_color(to_square) if promoted_to == "B"
|
173
174
|
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
175
|
+
promoted_to
|
176
|
+
else
|
177
|
+
nil
|
178
|
+
end
|
179
|
+
end
|
179
180
|
|
180
|
-
|
181
|
-
|
181
|
+
def lran
|
182
|
+
return @lran if defined?(@lran)
|
182
183
|
|
183
|
-
|
184
|
+
@lran = "#{piece_in_lran(piece)}#{from_square}"
|
184
185
|
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
186
|
+
@lran << (
|
187
|
+
captured_piece ?
|
188
|
+
"x#{piece_in_lran(captured_piece)}#{to_square}" :
|
189
|
+
"-#{to_square}"
|
190
|
+
)
|
190
191
|
|
191
|
-
|
192
|
+
@lran << "=#{promotion[0]}" if promotion
|
192
193
|
|
193
|
-
|
194
|
-
|
194
|
+
@lran
|
195
|
+
end
|
195
196
|
|
196
|
-
|
197
|
-
|
198
|
-
|
197
|
+
def piece_in_lran(pc)
|
198
|
+
pc == "p" ? "" : pc[0]
|
199
|
+
end
|
199
200
|
|
200
|
-
|
201
|
-
|
202
|
-
|
201
|
+
def bishop_color(on_square)
|
202
|
+
Egd::Procedures.square_color(on_square) == "w" ? "l" : "d"
|
203
|
+
end
|
203
204
|
|
205
|
+
end
|
204
206
|
end
|
data/lib/egd/fen_to_board.rb
CHANGED
@@ -1,45 +1,47 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
1
|
+
module Egd
|
2
|
+
class FenToBoard
|
3
|
+
# this service parses a FEN string into a hash-representation of a chess board and pieces
|
4
|
+
# So you can do
|
5
|
+
# board = Egd::FenToBoard.new(fen_string)
|
6
|
+
# board["b3"] #=> "P" # as in white pawn
|
6
7
|
|
7
|
-
|
8
|
+
attr_reader :fen
|
8
9
|
|
9
|
-
|
10
|
+
LETTER_VALUES = %w|_ a b c d e f g h|.freeze
|
10
11
|
|
11
12
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
# Egd::FenToBoard.new(fen_string)["b2"]
|
14
|
+
def initialize(fen)
|
15
|
+
@fen = fen
|
16
|
+
end
|
16
17
|
|
17
|
-
|
18
|
-
|
19
|
-
|
18
|
+
def [](square)
|
19
|
+
board_hash[square]
|
20
|
+
end
|
20
21
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
22
|
+
def boardline
|
23
|
+
# this replaces numbers with corresponding amount of dashes
|
24
|
+
@boardline ||= parsed_fen[:board].gsub(%r'\d') do |match|
|
25
|
+
"-" * match.to_i
|
26
|
+
end.gsub("/", "")
|
27
|
+
end #=> "rnbqkbnrpppp...."
|
27
28
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
29
|
+
private
|
30
|
+
def parsed_fen
|
31
|
+
@parsed_fen ||= Egd::Procedures.parse_fen(fen)
|
32
|
+
end
|
32
33
|
|
33
|
-
|
34
|
-
|
34
|
+
def board_hash
|
35
|
+
return @board_hash if defined?(@board_hash)
|
35
36
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
37
|
+
look_up_square_behavior = ->(hash, key) {
|
38
|
+
hash[key] = boardline[
|
39
|
+
Egd::Procedures.square_to_fen_index(key) - 1
|
40
|
+
]
|
41
|
+
}
|
41
42
|
|
42
|
-
|
43
|
-
|
43
|
+
@board_hash = Hash.new(&look_up_square_behavior)
|
44
|
+
end
|
44
45
|
|
46
|
+
end
|
45
47
|
end
|
data/lib/egd/pgn_parser.rb
CHANGED
@@ -1,177 +1,178 @@
|
|
1
1
|
# Thanks to https://github.com/jedld/pgn_parser
|
2
|
+
module Egd
|
3
|
+
class PgnParser
|
4
|
+
# This service takes in a PGN string and parses it
|
5
|
+
# Returns the game tags (headers of the PGN file) and
|
6
|
+
# the *actual* moves made in SAN
|
7
|
+
|
8
|
+
attr_reader :headers, :pgn_content
|
9
|
+
|
10
|
+
def initialize(pgn_content)
|
11
|
+
@pgn_content = pgn_content
|
12
|
+
@headers = []
|
13
|
+
@movelist = []
|
14
|
+
@game_attributes = {}
|
15
|
+
end
|
2
16
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
while (current_index < @pgn_content.size)
|
23
|
-
current_char = @pgn_content[current_index]
|
24
|
-
current_index += 1
|
25
|
-
|
26
|
-
if state == :initial
|
27
|
-
if current_char == '['
|
28
|
-
state = :start_parse_header
|
29
|
-
next
|
30
|
-
elsif (current_char == ' ' || current_char == "\n" || current_char == "\r")
|
31
|
-
next
|
32
|
-
else
|
33
|
-
break
|
17
|
+
def call
|
18
|
+
current_index = 0
|
19
|
+
state = :initial
|
20
|
+
buffer = ''
|
21
|
+
|
22
|
+
while (current_index < @pgn_content.size)
|
23
|
+
current_char = @pgn_content[current_index]
|
24
|
+
current_index += 1
|
25
|
+
|
26
|
+
if state == :initial
|
27
|
+
if current_char == '['
|
28
|
+
state = :start_parse_header
|
29
|
+
next
|
30
|
+
elsif (current_char == ' ' || current_char == "\n" || current_char == "\r")
|
31
|
+
next
|
32
|
+
else
|
33
|
+
break
|
34
|
+
end
|
34
35
|
end
|
35
|
-
end
|
36
36
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
37
|
+
if state == :start_parse_header
|
38
|
+
if current_char == ']'
|
39
|
+
state = :initial
|
40
|
+
hd = parse_header(buffer)
|
41
|
+
@headers << hd
|
42
|
+
@game_attributes[hd[:type]] = hd[:value]
|
43
|
+
buffer = ''
|
44
|
+
next
|
45
|
+
else
|
46
|
+
buffer << current_char
|
47
|
+
next
|
48
|
+
end
|
48
49
|
end
|
49
50
|
end
|
50
|
-
end
|
51
51
|
|
52
|
-
|
52
|
+
@movelist = simple_parse_moves
|
53
53
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
54
|
+
hash = {moves: @movelist}
|
55
|
+
hash.merge!(game_tags: @game_attributes) if @game_attributes.any?
|
56
|
+
hash
|
57
|
+
end
|
58
58
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
59
|
+
private
|
60
|
+
|
61
|
+
def parse_header(header)
|
62
|
+
event_type = ""
|
63
|
+
event_value = ""
|
64
|
+
state = :parse_type
|
65
|
+
current_index = 0
|
66
|
+
buffer = ''
|
67
|
+
|
68
|
+
while (current_index < header.size)
|
69
|
+
current_char = header[current_index]
|
70
|
+
current_index += 1
|
71
|
+
|
72
|
+
if state == :parse_type
|
73
|
+
if current_char == ' '
|
74
|
+
event_type = buffer.dup
|
75
|
+
buffer = ''
|
76
|
+
state = :start_parse_value
|
77
|
+
next
|
78
|
+
else
|
79
|
+
buffer << current_char
|
80
|
+
next
|
81
|
+
end
|
82
|
+
elsif state == :start_parse_value
|
83
|
+
if current_char == '"'
|
84
|
+
state = :parse_value
|
85
|
+
next
|
86
|
+
else
|
87
|
+
next
|
88
|
+
end
|
89
|
+
elsif state == :parse_value
|
90
|
+
if current_char=='"'
|
91
|
+
event_value = buffer.dup
|
92
|
+
buffer = ''
|
93
|
+
else
|
94
|
+
buffer << current_char
|
95
|
+
end
|
95
96
|
end
|
96
97
|
end
|
97
|
-
end
|
98
98
|
|
99
|
-
|
100
|
-
|
99
|
+
{type: event_type, value: event_value}
|
100
|
+
end
|
101
101
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
102
|
+
def simple_parse_moves
|
103
|
+
move_line =
|
104
|
+
pgn_content.split("\n").map do |line|
|
105
|
+
line unless line.strip[0] == "["
|
106
|
+
end.compact.join(" ").
|
107
|
+
gsub(%r'((1\-0)|(0\-1)|(1/2\-1/2)|(\*))\s*\z', "") # cut away game termination
|
108
108
|
|
109
|
-
|
110
|
-
|
111
|
-
|
109
|
+
# strip out comments and alternatives
|
110
|
+
while move_line.gsub!(%r'\{[^{}]*\}', ""); end
|
111
|
+
while move_line.gsub!(%r'\([^()]*\)', ""); end
|
112
112
|
|
113
|
-
|
114
|
-
|
113
|
+
# strip out "$n"-like annotations
|
114
|
+
move_line.gsub!(%r'\$\d+ ', " ")
|
115
115
|
|
116
|
-
|
117
|
-
|
116
|
+
# strip out ?! -like annotations
|
117
|
+
move_line.gsub!(%r'[?!]+ ', " ")
|
118
118
|
|
119
|
-
|
120
|
-
|
119
|
+
# strip out +/- like annotations
|
120
|
+
move_line.gsub!(%r'(./.)|(= )|(\+\−)|(\-\+)|(\∞)', "")
|
121
121
|
|
122
|
-
|
123
|
-
|
122
|
+
# squish whitespace
|
123
|
+
move_line = move_line.strip.gsub(%r'\s{2,}', " ")
|
124
124
|
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
125
|
+
# check if move line consists of legit chars only
|
126
|
+
if !move_line.match?(%r'\A(?:[[:alnum:]]|[=\-+.#\* ])+\z')
|
127
|
+
raise(
|
128
|
+
"The PGN move portion has weird characters even after cleaning it.\n"\
|
129
|
+
"Is the PGN valid?\n"\
|
130
|
+
"The moves after cleaning came out as:\n#{move_line}"
|
131
|
+
)
|
132
|
+
end
|
133
133
|
|
134
|
-
|
134
|
+
moves = []
|
135
135
|
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
136
|
+
while move_line.match?(%r'\d+\.')
|
137
|
+
parsed_moves = move_line.match(%r'\A
|
138
|
+
(?<move_number>\d+)\.(?<move_chunk>.*?)(?:(?<remainder>\d+\..*\z)|\z)
|
139
|
+
'x)
|
140
140
|
|
141
|
-
|
142
|
-
|
143
|
-
|
141
|
+
move_number = parsed_moves[:move_number]
|
142
|
+
move_chunk = parsed_moves[:move_chunk] #=> " e4 c5 "
|
143
|
+
move_line = parsed_moves[:remainder].to_s.strip
|
144
144
|
|
145
|
-
|
145
|
+
# a good place to DEBUG
|
146
146
|
|
147
|
-
|
147
|
+
next if !move_chunk
|
148
148
|
|
149
|
-
|
150
|
-
|
149
|
+
move_chunk = move_chunk.to_s.gsub(%r'\.{2}\s?', ".. ") # formats Black move ".."
|
150
|
+
move_line = move_line.to_s.strip
|
151
151
|
|
152
|
-
|
152
|
+
number_var = "@_#{move_number}"
|
153
153
|
|
154
|
-
|
155
|
-
|
154
|
+
w = move_chunk.strip.split(" ")[0]
|
155
|
+
b = move_chunk.strip.split(" ")[1]
|
156
156
|
|
157
|
-
|
158
|
-
|
159
|
-
|
157
|
+
options = {}
|
158
|
+
options.merge!(:w=>w) unless w.match?(%r'\.{2}')
|
159
|
+
options.merge!(:b=>b) if b
|
160
160
|
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
161
|
+
instance_variable_set(
|
162
|
+
"@_#{move_number}",
|
163
|
+
instance_variable_get("@_#{move_number}") ?
|
164
|
+
instance_variable_get("@_#{move_number}").merge(options) :
|
165
|
+
{:num=>move_number.to_i}.merge(options)
|
166
|
+
)
|
167
167
|
|
168
|
-
|
169
|
-
|
170
|
-
|
168
|
+
moves << instance_variable_get("@_#{move_number}") if instance_variable_get("@_#{move_number}")[:b]
|
169
|
+
@last = instance_variable_get("@_#{move_number}")
|
170
|
+
end
|
171
171
|
|
172
|
-
|
173
|
-
|
172
|
+
# offload last to moves since there may not have been a black move
|
173
|
+
moves << @last if !@last[:b]
|
174
174
|
|
175
|
-
|
175
|
+
moves
|
176
|
+
end
|
176
177
|
end
|
177
178
|
end
|
@@ -1,25 +1,27 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
1
|
+
module Egd
|
2
|
+
class PositionFeatureDiscerner
|
3
|
+
# This service takes in a move and the resulting FEN string
|
4
|
+
# and outputs a hash of features of the resulting position
|
4
5
|
|
5
|
-
|
6
|
-
|
6
|
+
# Currently minimal function,
|
7
|
+
# Only looks at supplied move and tells whether the position is a check or checkmate.
|
7
8
|
|
8
|
-
|
9
|
+
attr_reader :move, :end_fen
|
9
10
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
11
|
+
def initialize(move:, end_fen:)
|
12
|
+
@move = move
|
13
|
+
@end_fen = end_fen
|
14
|
+
end
|
14
15
|
|
15
|
-
|
16
|
-
|
16
|
+
def call
|
17
|
+
return @features if defined?(@features)
|
17
18
|
|
18
|
-
|
19
|
+
@features = {}
|
19
20
|
|
20
|
-
|
21
|
-
|
21
|
+
@features.merge!("check" => true, "checkmate" => true) if move[%r'#\z']
|
22
|
+
@features.merge!("check" => true) if move[%r'\+\z']
|
22
23
|
|
23
|
-
|
24
|
+
@features
|
25
|
+
end
|
24
26
|
end
|
25
27
|
end
|
data/lib/egd/procedures.rb
CHANGED
@@ -1,53 +1,55 @@
|
|
1
|
-
module Egd
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
1
|
+
module Egd
|
2
|
+
module Procedures
|
3
|
+
# This module has global methods for ease of working with chess data
|
4
|
+
|
5
|
+
extend self
|
6
|
+
|
7
|
+
# Egd::Procedures.parse_fen(fen)
|
8
|
+
def parse_fen(fen)
|
9
|
+
match = fen.split(%r'\s+') # FEN lines are delimited with whitespace, splitting on that
|
10
|
+
|
11
|
+
{
|
12
|
+
board: match[0],
|
13
|
+
to_move: match[1],
|
14
|
+
castling: match[2],
|
15
|
+
ep_square: match[3],
|
16
|
+
halfmove: match[4],
|
17
|
+
fullmove: match[5]
|
18
|
+
}
|
19
|
+
end
|
19
20
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
21
|
+
# Egd::Procedures.square_to_fen_index("b2")
|
22
|
+
def square_to_fen_index(square)
|
23
|
+
column = square[0]
|
24
|
+
row = square[1]
|
24
25
|
|
25
|
-
|
26
|
-
|
27
|
-
|
26
|
+
row_value = Egd::COLUMN_HEIGHT - row.to_i
|
27
|
+
row_value * Egd::ROW_LENGTH + Egd::FenToBoard::LETTER_VALUES.index(column)
|
28
|
+
end # a8 -> 1, a7 -> 9, h1 -> 64
|
28
29
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
30
|
+
# Egd::Procedures.fen_index_to_square(index)
|
31
|
+
def fen_index_to_square(index)
|
32
|
+
# 3 -> c8
|
33
|
+
row = Egd::COLUMN_HEIGHT - ((index - 1) / Egd::COLUMN_HEIGHT) # => 8
|
33
34
|
|
34
|
-
|
35
|
+
column_index = index - ((Egd::COLUMN_HEIGHT - row) * Egd::ROW_LENGTH)
|
35
36
|
|
36
|
-
|
37
|
+
column = Egd::FenToBoard::LETTER_VALUES[column_index] # => "c"
|
37
38
|
|
38
|
-
|
39
|
-
|
39
|
+
"#{column}#{row}"
|
40
|
+
end # 1 -> "a8", 64 -> "h1"
|
40
41
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
42
|
+
# Egd::Procedures.square_color("a7")
|
43
|
+
def square_color(square)
|
44
|
+
column = square[0] #=> a
|
45
|
+
row = square[1] #=> 8
|
45
46
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
47
|
+
if %w|a c e g|.include?(column)
|
48
|
+
row.to_i.even? ? "w" : "b"
|
49
|
+
else
|
50
|
+
row.to_i.odd? ? "w" : "b"
|
51
|
+
end
|
50
52
|
end
|
51
|
-
end
|
52
53
|
|
54
|
+
end
|
53
55
|
end
|
data/lib/egd/version.rb
CHANGED
data/lib/egd.rb
CHANGED
@@ -7,6 +7,7 @@ require "egd/fen_to_board"
|
|
7
7
|
require "egd/fen_difference_discerner"
|
8
8
|
require "egd/position_feature_discerner"
|
9
9
|
require "egd/pgn_parser"
|
10
|
+
require "egd/builder"
|
10
11
|
require "egd/version"
|
11
12
|
|
12
13
|
module Egd
|
@@ -17,85 +18,4 @@ module Egd
|
|
17
18
|
def self.root
|
18
19
|
Pathname.new(File.expand_path('../..', __FILE__))
|
19
20
|
end
|
20
|
-
|
21
|
-
class Builder
|
22
|
-
# This is the real deal
|
23
|
-
# Takes in a PGN string and returns a Ruby or JSON hash representation of the game in EGD
|
24
|
-
|
25
|
-
attr_reader :pgn
|
26
|
-
|
27
|
-
def initialize(pgn)
|
28
|
-
@pgn = pgn
|
29
|
-
end
|
30
|
-
|
31
|
-
def to_h
|
32
|
-
return @to_h if defined?(@to_h)
|
33
|
-
|
34
|
-
@to_h = {}
|
35
|
-
@to_h["game_tags"] = game_tags
|
36
|
-
@to_h["moves"] = {}
|
37
|
-
|
38
|
-
@previous_fen = Egd::FenBuilder::NULL_FEN
|
39
|
-
|
40
|
-
moves.each_with_object(@to_h) do |move, mem|
|
41
|
-
transition_key = "#{move[%r'\A\d+']}#{move.match?(%r'\.\.') ? "b" : "w"}" #=> "1w"
|
42
|
-
|
43
|
-
san = move.match(%r'\A(?:\d+\.(?:\s*\.\.)?\s+)(?<san>\S+)\z')[:san] #=> "e4"
|
44
|
-
end_fen = Egd::FenBuilder.new(start_fen: @previous_fen, move: move).call
|
45
|
-
|
46
|
-
current_transition = {
|
47
|
-
"start_position" => {
|
48
|
-
"fen" => @previous_fen,
|
49
|
-
"features" => {}, # TODO, no features can be discerned before the move yet
|
50
|
-
},
|
51
|
-
"move" => {
|
52
|
-
"player" => transition_key[%r'\D\z'], #=> "w"
|
53
|
-
"san" => san,
|
54
|
-
}.merge(
|
55
|
-
Egd::FenDifferenceDiscerner.new(
|
56
|
-
start_fen: @previous_fen, move: san, end_fen: end_fen
|
57
|
-
).call
|
58
|
-
),
|
59
|
-
"end_position" => {
|
60
|
-
"fen" => end_fen,
|
61
|
-
"features" => Egd::PositionFeatureDiscerner.new(
|
62
|
-
move: move, end_fen: end_fen
|
63
|
-
).call
|
64
|
-
}
|
65
|
-
}
|
66
|
-
|
67
|
-
# leave this breadcrumb for next run through loop
|
68
|
-
@previous_fen = current_transition.dig("end_position", "fen")
|
69
|
-
|
70
|
-
mem["moves"][transition_key] = current_transition
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
|
-
def to_json
|
75
|
-
@to_json ||= to_h.to_json
|
76
|
-
end
|
77
|
-
|
78
|
-
private
|
79
|
-
def game_tags
|
80
|
-
@game_tags ||= parsed_pgn[:game_tags] || {}
|
81
|
-
end
|
82
|
-
|
83
|
-
def moves
|
84
|
-
return @moves if defined?(@moves)
|
85
|
-
|
86
|
-
@moves = []
|
87
|
-
|
88
|
-
parsed_pgn[:moves].each do |move_row|
|
89
|
-
@moves << "#{move_row[:num]}. #{move_row[:w]}"
|
90
|
-
@moves << "#{move_row[:num]}. .. #{move_row[:b]}" if move_row[:b]
|
91
|
-
end
|
92
|
-
|
93
|
-
@moves
|
94
|
-
end
|
95
|
-
|
96
|
-
def parsed_pgn
|
97
|
-
@parsed_pgn ||= Egd::PgnParser.new(pgn).call
|
98
|
-
end
|
99
|
-
|
100
|
-
end
|
101
21
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: egd
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Epigene
|
@@ -129,6 +129,7 @@ files:
|
|
129
129
|
- bin/setup
|
130
130
|
- egd.gemspec
|
131
131
|
- lib/egd.rb
|
132
|
+
- lib/egd/builder.rb
|
132
133
|
- lib/egd/fen_builder.rb
|
133
134
|
- lib/egd/fen_difference_discerner.rb
|
134
135
|
- lib/egd/fen_to_board.rb
|