egd 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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
|