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 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