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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: '08654fa684c9a1a58f3683757a327c67abad2656'
4
- data.tar.gz: 903d018099474b94ac8e93e92dbb7e402d7a4282
3
+ metadata.gz: c8576046875ff0741a150a8c58b4725466673c13
4
+ data.tar.gz: ffb66447b0494066c44bf56b1d7a9705139a6213
5
5
  SHA512:
6
- metadata.gz: '093c40611777d951fb712dc79b3f5d1a98a1822c832831dc045c69cd2ebd8103c332b36a89b716da31536e5c02becfde47a4cf54144d9fbb94e0065ef217a26e'
7
- data.tar.gz: 4e6f7baab0cf159afadfe3376e5dc62249c32c2622a3cf200d00c89a151d1802be5e2661596876315ddb04dd60d59263966c01f29ae29454a65a75ddf6c80bf8
6
+ metadata.gz: c1edfe986366a4820c939fd9b646362e7865df89a5ad7c764997ca38fb365d8e513b8701adeee9218ebcef8905f91435ccf3e0199347565f43d71fd7e1d96297
7
+ data.tar.gz: 615f253803aaf603f13b6dd5db52b7bde8d6646df75b5aacff40c1ff6e443f3f431a98b4ca18c00f53d08935869364c7b78b441def5abe9e325a8fbf31b38252
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- egd (1.0.0)
4
+ egd (1.0.1)
5
5
  pgn (~> 0.2.0)
6
6
 
7
7
  GEM
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) }
@@ -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
@@ -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
- class Egd::FenBuilder
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
- attr_reader :start_fen, :move
8
+ NULL_FEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1".freeze
7
9
 
8
- NULL_FEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1".freeze
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
- # 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
15
-
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
- )
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
- class Egd::FenDifferenceDiscerner
2
- # This service takes in a start and an end FEN string,
3
- # and the move in SAN,
4
- # and a tells you what kind of move occured, in more detail than SAN
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
- # Theoretically a start and end fen would suffice, but having move in SAN,
7
- # which we do, allows skipping some hard procesing parts.
30
+ # entering long processing of regular moves
8
31
 
9
- attr_reader :start_fen, :move, :end_fen
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
- # Egd::FenDifferenceDiscerner.new(start_fen:, end_fen:).call
12
- def initialize(start_fen: nil, move:, end_fen:)
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
- def call
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
- # entering long processing of regular moves
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
- def special_case_ep_capture
48
- return @ep_capture if defined?(@ep_capture)
48
+ def special_case_ep_capture
49
+ return @ep_capture if defined?(@ep_capture)
49
50
 
50
- return @ep_capture = false if Egd::Procedures.parse_fen(start_fen)[:ep_square] != to_square
51
+ return @ep_capture = false if Egd::Procedures.parse_fen(start_fen)[:ep_square] != to_square
51
52
 
52
- from = move[0] + (move[-1] == "6" ? "5" : "4")
53
+ from = move[0] + (move[-1] == "6" ? "5" : "4")
53
54
 
54
- @ep_capture = {
55
- "lran" => "#{from}x#{to_square}", # FEN
56
- "from_square" => from, # FEN
57
- "to_square" => to_square, # move
58
- "piece" => "p", # move
59
- "move_type" => "ep_capture", # FEN
60
- "captured_piece" => "p", # move
61
- }
62
- end
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
- def special_case_short_castle(player)
65
- {
66
- "lran" => "O-O", # FEN
67
- "from_square" => (player == "w" ? "e1" : "e8"), # FEN
68
- "to_square" => (player == "w" ? "g1" : "g8"), # move
69
- "piece" => "K", # move
70
- "move_type" => "short_castle", # FEN
71
- }
72
- end
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
- def special_case_long_castle(player)
75
- {
76
- "lran" => "O-O-O", # FEN
77
- "from_square" => (player == "w" ? "e1" : "e8"), # FEN
78
- "to_square" => (player == "w" ? "c1" : "c8"), # move
79
- "piece" => "K", # move
80
- "move_type" => "long_castle", # FEN
81
- }
82
- end
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
- def board1
85
- @board1 ||= Egd::FenToBoard.new(start_fen)
86
- end
85
+ def board1
86
+ @board1 ||= Egd::FenToBoard.new(start_fen)
87
+ end
87
88
 
88
- def board2
89
- @board2 ||= Egd::FenToBoard.new(end_fen)
90
- end
89
+ def board2
90
+ @board2 ||= Egd::FenToBoard.new(end_fen)
91
+ end
91
92
 
92
- def changed_squares
93
- return @changed_squares if defined?(@changed_squares)
93
+ def changed_squares
94
+ return @changed_squares if defined?(@changed_squares)
94
95
 
95
- @changed_squares = []
96
+ @changed_squares = []
96
97
 
97
- (1..64).to_a.each do |fen_index|
98
- i = fen_index - 1
98
+ (1..64).to_a.each do |fen_index|
99
+ i = fen_index - 1
99
100
 
100
- if board1.boardline[i] != board2.boardline[i]
101
- square = Egd::Procedures.fen_index_to_square(fen_index)
101
+ if board1.boardline[i] != board2.boardline[i]
102
+ square = Egd::Procedures.fen_index_to_square(fen_index)
102
103
 
103
- @changed_squares << {
104
- square: square, from: board1.boardline[i], to: board2.boardline[i]
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
- @changed_squares
110
- end
110
+ @changed_squares
111
+ end
111
112
 
112
- def from_square
113
- @from_square ||= changed_squares.reject do |hash|
114
- hash[:square] == to_square
115
- end.detect do |hash|
116
- hash[:to] == "-"
117
- end[:square]
118
- end
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
- def to_square
121
- @to_square ||= move.match(%r'\A(?<basemove>.*\d)(?<drek>.*)?\z')[:basemove][-2..-1]
122
- end
121
+ def to_square
122
+ @to_square ||= move.match(%r'\A(?<basemove>.*\d)(?<drek>.*)?\z')[:basemove][-2..-1]
123
+ end
123
124
 
124
- def piece
125
- return @piece if defined?(@piece)
125
+ def piece
126
+ return @piece if defined?(@piece)
126
127
 
127
- possible_piece = move[0]
128
+ possible_piece = move[0]
128
129
 
129
- @piece = (Egd::SAN_CHESS_PIECES.include?(possible_piece) ? possible_piece : "p" )
130
+ @piece = (Egd::SAN_CHESS_PIECES.include?(possible_piece) ? possible_piece : "p" )
130
131
 
131
- @piece << bishop_color(to_square) if @piece == "B"
132
+ @piece << bishop_color(to_square) if @piece == "B"
132
133
 
133
- @piece
134
- end
134
+ @piece
135
+ end
135
136
 
136
- def move_type
137
- @move_type ||=
138
- case move
139
- when %r'x.*='i
140
- "promotion_capture"
141
- when %r'x'i
142
- "capture"
143
- when %r'='i
144
- "promotion"
145
- else
146
- "move"
147
- end
148
- end
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
- def captured_piece
151
- @captured_piece ||=
152
- if move_type[%r'capture']
153
- captured_piece = changed_squares.detect do |hash|
154
- hash[:square] == to_square
155
- end[:from].upcase
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
- captured_piece.downcase! if captured_piece[%r'p'i]
158
+ captured_piece.downcase! if captured_piece[%r'p'i]
158
159
 
159
- captured_piece << bishop_color(to_square) if captured_piece == "B"
160
+ captured_piece << bishop_color(to_square) if captured_piece == "B"
160
161
 
161
- captured_piece
162
- else
163
- nil
164
- end
165
- end
162
+ captured_piece
163
+ else
164
+ nil
165
+ end
166
+ end
166
167
 
167
- def promotion
168
- @promotion ||=
169
- if move_type[%r'promotion']
170
- promoted_to = move.match(%r'=(?<promo>.)\z')[:promo]
168
+ def promotion
169
+ @promotion ||=
170
+ if move_type[%r'promotion']
171
+ promoted_to = move.match(%r'=(?<promo>.)\z')[:promo]
171
172
 
172
- promoted_to << bishop_color(to_square) if promoted_to == "B"
173
+ promoted_to << bishop_color(to_square) if promoted_to == "B"
173
174
 
174
- promoted_to
175
- else
176
- nil
177
- end
178
- end
175
+ promoted_to
176
+ else
177
+ nil
178
+ end
179
+ end
179
180
 
180
- def lran
181
- return @lran if defined?(@lran)
181
+ def lran
182
+ return @lran if defined?(@lran)
182
183
 
183
- @lran = "#{piece_in_lran(piece)}#{from_square}"
184
+ @lran = "#{piece_in_lran(piece)}#{from_square}"
184
185
 
185
- @lran << (
186
- captured_piece ?
187
- "x#{piece_in_lran(captured_piece)}#{to_square}" :
188
- "-#{to_square}"
189
- )
186
+ @lran << (
187
+ captured_piece ?
188
+ "x#{piece_in_lran(captured_piece)}#{to_square}" :
189
+ "-#{to_square}"
190
+ )
190
191
 
191
- @lran << "=#{promotion[0]}" if promotion
192
+ @lran << "=#{promotion[0]}" if promotion
192
193
 
193
- @lran
194
- end
194
+ @lran
195
+ end
195
196
 
196
- def piece_in_lran(pc)
197
- pc == "p" ? "" : pc[0]
198
- end
197
+ def piece_in_lran(pc)
198
+ pc == "p" ? "" : pc[0]
199
+ end
199
200
 
200
- def bishop_color(on_square)
201
- Egd::Procedures.square_color(on_square) == "w" ? "l" : "d"
202
- end
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
@@ -1,45 +1,47 @@
1
- class Egd::FenToBoard
2
- # this service parses a FEN string into a hash-representation of a chess board and pieces
3
- # So you can do
4
- # board = Egd::FenToBoard.new(fen_string)
5
- # board["b3"] #=> "P" # as in white pawn
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
- attr_reader :fen
8
+ attr_reader :fen
8
9
 
9
- LETTER_VALUES = %w|_ a b c d e f g h|.freeze
10
+ LETTER_VALUES = %w|_ a b c d e f g h|.freeze
10
11
 
11
12
 
12
- # Egd::FenToBoard.new(fen_string)["b2"]
13
- def initialize(fen)
14
- @fen = fen
15
- end
13
+ # Egd::FenToBoard.new(fen_string)["b2"]
14
+ def initialize(fen)
15
+ @fen = fen
16
+ end
16
17
 
17
- def [](square)
18
- board_hash[square]
19
- end
18
+ def [](square)
19
+ board_hash[square]
20
+ end
20
21
 
21
- def boardline
22
- # this replaces numbers with corresponding amount of dashes
23
- @boardline ||= parsed_fen[:board].gsub(%r'\d') do |match|
24
- "-" * match.to_i
25
- end.gsub("/", "")
26
- end #=> "rnbqkbnrpppp...."
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
- private
29
- def parsed_fen
30
- @parsed_fen ||= Egd::Procedures.parse_fen(fen)
31
- end
29
+ private
30
+ def parsed_fen
31
+ @parsed_fen ||= Egd::Procedures.parse_fen(fen)
32
+ end
32
33
 
33
- def board_hash
34
- return @board_hash if defined?(@board_hash)
34
+ def board_hash
35
+ return @board_hash if defined?(@board_hash)
35
36
 
36
- look_up_square_behavior = ->(hash, key) {
37
- hash[key] = boardline[
38
- Egd::Procedures.square_to_fen_index(key) - 1
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
- @board_hash = Hash.new(&look_up_square_behavior)
43
- end
43
+ @board_hash = Hash.new(&look_up_square_behavior)
44
+ end
44
45
 
46
+ end
45
47
  end
@@ -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
- class Egd::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
16
-
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
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
- 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
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
- @movelist = simple_parse_moves
52
+ @movelist = simple_parse_moves
53
53
 
54
- hash = {moves: @movelist}
55
- hash.merge!(game_tags: @game_attributes) if @game_attributes.any?
56
- hash
57
- end
54
+ hash = {moves: @movelist}
55
+ hash.merge!(game_tags: @game_attributes) if @game_attributes.any?
56
+ hash
57
+ end
58
58
 
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
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
- {type: event_type, value: event_value}
100
- end
99
+ {type: event_type, value: event_value}
100
+ end
101
101
 
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
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
- # strip out comments and alternatives
110
- while move_line.gsub!(%r'\{[^{}]*\}', ""); end
111
- while move_line.gsub!(%r'\([^()]*\)', ""); end
109
+ # strip out comments and alternatives
110
+ while move_line.gsub!(%r'\{[^{}]*\}', ""); end
111
+ while move_line.gsub!(%r'\([^()]*\)', ""); end
112
112
 
113
- # strip out "$n"-like annotations
114
- move_line.gsub!(%r'\$\d+ ', " ")
113
+ # strip out "$n"-like annotations
114
+ move_line.gsub!(%r'\$\d+ ', " ")
115
115
 
116
- # strip out ?! -like annotations
117
- move_line.gsub!(%r'[?!]+ ', " ")
116
+ # strip out ?! -like annotations
117
+ move_line.gsub!(%r'[?!]+ ', " ")
118
118
 
119
- # strip out +/- like annotations
120
- move_line.gsub!(%r'(./.)|(= )|(\+\−)|(\-\+)|(\∞)', "")
119
+ # strip out +/- like annotations
120
+ move_line.gsub!(%r'(./.)|(= )|(\+\−)|(\-\+)|(\∞)', "")
121
121
 
122
- # squish whitespace
123
- move_line = move_line.strip.gsub(%r'\s{2,}', " ")
122
+ # squish whitespace
123
+ move_line = move_line.strip.gsub(%r'\s{2,}', " ")
124
124
 
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
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
- moves = []
134
+ moves = []
135
135
 
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)
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
- move_number = parsed_moves[:move_number]
142
- move_chunk = parsed_moves[:move_chunk] #=> " e4 c5 "
143
- move_line = parsed_moves[:remainder].to_s.strip
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
- # a good place to DEBUG
145
+ # a good place to DEBUG
146
146
 
147
- next if !move_chunk
147
+ next if !move_chunk
148
148
 
149
- move_chunk = move_chunk.to_s.gsub(%r'\.{2}\s?', ".. ") # formats Black move ".."
150
- move_line = move_line.to_s.strip
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
- number_var = "@_#{move_number}"
152
+ number_var = "@_#{move_number}"
153
153
 
154
- w = move_chunk.strip.split(" ")[0]
155
- b = move_chunk.strip.split(" ")[1]
154
+ w = move_chunk.strip.split(" ")[0]
155
+ b = move_chunk.strip.split(" ")[1]
156
156
 
157
- options = {}
158
- options.merge!(:w=>w) unless w.match?(%r'\.{2}')
159
- options.merge!(:b=>b) if b
157
+ options = {}
158
+ options.merge!(:w=>w) unless w.match?(%r'\.{2}')
159
+ options.merge!(:b=>b) if b
160
160
 
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
- )
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
- moves << instance_variable_get("@_#{move_number}") if instance_variable_get("@_#{move_number}")[:b]
169
- @last = instance_variable_get("@_#{move_number}")
170
- end
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
- # offload last to moves since there may not have been a black move
173
- moves << @last if !@last[:b]
172
+ # offload last to moves since there may not have been a black move
173
+ moves << @last if !@last[:b]
174
174
 
175
- moves
175
+ moves
176
+ end
176
177
  end
177
178
  end
@@ -1,25 +1,27 @@
1
- class Egd::PositionFeatureDiscerner
2
- # This service takes in a move and the resulting FEN string
3
- # and outputs a hash of features of the resulting position
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
- # Currently minimal function,
6
- # Only looks at supplied move and tells whether the position is a check or checkmate.
6
+ # Currently minimal function,
7
+ # Only looks at supplied move and tells whether the position is a check or checkmate.
7
8
 
8
- attr_reader :move, :end_fen
9
+ attr_reader :move, :end_fen
9
10
 
10
- def initialize(move:, end_fen:)
11
- @move = move
12
- @end_fen = end_fen
13
- end
11
+ def initialize(move:, end_fen:)
12
+ @move = move
13
+ @end_fen = end_fen
14
+ end
14
15
 
15
- def call
16
- return @features if defined?(@features)
16
+ def call
17
+ return @features if defined?(@features)
17
18
 
18
- @features = {}
19
+ @features = {}
19
20
 
20
- @features.merge!("check" => true, "checkmate" => true) if move[%r'#\z']
21
- @features.merge!("check" => true) if move[%r'\+\z']
21
+ @features.merge!("check" => true, "checkmate" => true) if move[%r'#\z']
22
+ @features.merge!("check" => true) if move[%r'\+\z']
22
23
 
23
- @features
24
+ @features
25
+ end
24
26
  end
25
27
  end
@@ -1,53 +1,55 @@
1
- module Egd::Procedures
2
- # This module has global methods for ease of working with chess data
3
-
4
- extend self
5
-
6
- # Egd::Procedures.parse_fen(fen)
7
- def parse_fen(fen)
8
- match = fen.split(%r'\s+') # FEN lines are delimited with whitespace, splitting on that
9
-
10
- {
11
- board: match[0],
12
- to_move: match[1],
13
- castling: match[2],
14
- ep_square: match[3],
15
- halfmove: match[4],
16
- fullmove: match[5]
17
- }
18
- end
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
- # Egd::Procedures.square_to_fen_index("b2")
21
- def square_to_fen_index(square)
22
- column = square[0]
23
- row = square[1]
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
- row_value = Egd::COLUMN_HEIGHT - row.to_i
26
- row_value * Egd::ROW_LENGTH + Egd::FenToBoard::LETTER_VALUES.index(column)
27
- end # a8 -> 1, a7 -> 9, h1 -> 64
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
- # Egd::Procedures.fen_index_to_square(index)
30
- def fen_index_to_square(index)
31
- # 3 -> c8
32
- row = Egd::COLUMN_HEIGHT - ((index - 1) / Egd::COLUMN_HEIGHT) # => 8
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
- column_index = index - ((Egd::COLUMN_HEIGHT - row) * Egd::ROW_LENGTH)
35
+ column_index = index - ((Egd::COLUMN_HEIGHT - row) * Egd::ROW_LENGTH)
35
36
 
36
- column = Egd::FenToBoard::LETTER_VALUES[column_index] # => "c"
37
+ column = Egd::FenToBoard::LETTER_VALUES[column_index] # => "c"
37
38
 
38
- "#{column}#{row}"
39
- end # 1 -> "a8", 64 -> "h1"
39
+ "#{column}#{row}"
40
+ end # 1 -> "a8", 64 -> "h1"
40
41
 
41
- # Egd::Procedures.square_color("a7")
42
- def square_color(square)
43
- column = square[0] #=> a
44
- row = square[1] #=> 8
42
+ # Egd::Procedures.square_color("a7")
43
+ def square_color(square)
44
+ column = square[0] #=> a
45
+ row = square[1] #=> 8
45
46
 
46
- if %w|a c e g|.include?(column)
47
- row.to_i.even? ? "w" : "b"
48
- else
49
- row.to_i.odd? ? "w" : "b"
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
@@ -1,3 +1,3 @@
1
1
  module Egd
2
- VERSION = "1.0.0"
2
+ VERSION = "1.0.1"
3
3
  end
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.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