feen 5.0.0.beta5 → 5.0.0.beta6
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/README.md +170 -52
- data/lib/feen/dumper/pieces_in_hand/errors.rb +1 -1
- data/lib/feen/dumper/pieces_in_hand.rb +42 -11
- data/lib/feen/dumper.rb +3 -3
- data/lib/feen/parser/pieces_in_hand/canonical_sorter.rb +70 -0
- data/lib/feen/parser/pieces_in_hand/errors.rb +6 -4
- data/lib/feen/parser/pieces_in_hand/pnn_patterns.rb +47 -0
- data/lib/feen/parser/pieces_in_hand.rb +129 -26
- data/lib/feen/parser.rb +4 -4
- data/lib/feen.rb +8 -8
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ce89113318c1acf675da8dc7d4af4a9ea135a6485e0e4eb8d522640149f0461a
|
4
|
+
data.tar.gz: e06ae9198a93a896594e1ed9ba0b96a8af1445a20a7f8df8b247a04d84d76e71
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 177f297b196c9a20601eca71865d3ee51389111a72da28d4d1dc08249f3017be07d50636d0a9161759067cf9e8e42dbfdc2dc472f7717130f0e20477634537da
|
7
|
+
data.tar.gz: 5060cd110c9ee95d43f952fbe55c9870e1c98171934f148947a49a6afdbf547007ddd012e24598dd3209e0eab4ea11905f61d52871e9bfa511fdc9bac78af9d6
|
data/README.md
CHANGED
@@ -14,16 +14,30 @@ FEEN (Forsyth–Edwards Enhanced Notation) is a compact, canonical, and rule-agn
|
|
14
14
|
This gem implements the [FEEN Specification v1.0.0](https://sashite.dev/documents/feen/1.0.0/), providing a Ruby interface for:
|
15
15
|
|
16
16
|
- Representing positions from various games without knowledge of specific rules
|
17
|
-
- Supporting boards of arbitrary dimensions
|
18
|
-
- Encoding pieces in hand (
|
17
|
+
- Supporting boards of arbitrary dimensions (2D, 3D, and beyond)
|
18
|
+
- Encoding pieces in hand with full PNN (Piece Name Notation) support
|
19
19
|
- Facilitating serialization and deserialization of positions
|
20
20
|
- Ensuring canonical representation for consistent data handling
|
21
21
|
|
22
|
+
## FEEN Format
|
23
|
+
|
24
|
+
A FEEN record consists of three space-separated fields:
|
25
|
+
|
26
|
+
```
|
27
|
+
<PIECE-PLACEMENT> <PIECES-IN-HAND> <GAMES-TURN>
|
28
|
+
```
|
29
|
+
|
30
|
+
### Field Details
|
31
|
+
|
32
|
+
1. **Piece Placement**: Spatial distribution of pieces on the board using [PNN notation](https://sashite.dev/documents/pnn/1.0.0/)
|
33
|
+
2. **Pieces in Hand**: Off-board pieces available for placement, sorted canonically
|
34
|
+
3. **Games Turn**: Game identifiers and active player indication
|
35
|
+
|
22
36
|
## Installation
|
23
37
|
|
24
38
|
```ruby
|
25
39
|
# In your Gemfile
|
26
|
-
gem "feen", ">= 5.0.0.
|
40
|
+
gem "feen", ">= 5.0.0.beta6"
|
27
41
|
```
|
28
42
|
|
29
43
|
Or install manually:
|
@@ -32,14 +46,6 @@ Or install manually:
|
|
32
46
|
gem install feen --pre
|
33
47
|
```
|
34
48
|
|
35
|
-
## FEEN Format
|
36
|
-
|
37
|
-
A FEEN record consists of three space-separated fields:
|
38
|
-
|
39
|
-
```
|
40
|
-
<PIECE-PLACEMENT> <PIECES-IN-HAND> <GAMES-TURN>
|
41
|
-
```
|
42
|
-
|
43
49
|
## Basic Usage
|
44
50
|
|
45
51
|
### Parsing FEEN Strings
|
@@ -49,20 +55,20 @@ Convert a FEEN string into a structured Ruby object:
|
|
49
55
|
```ruby
|
50
56
|
require "feen"
|
51
57
|
|
52
|
-
feen_string = "
|
58
|
+
feen_string = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR - CHESS/chess"
|
53
59
|
position = Feen.parse(feen_string)
|
54
60
|
|
55
61
|
# Result is a hash:
|
56
62
|
# {
|
57
63
|
# piece_placement: [
|
58
|
-
# ["r
|
64
|
+
# ["r", "n", "b", "q", "k", "b", "n", "r"],
|
59
65
|
# ["p", "p", "p", "p", "p", "p", "p", "p"],
|
60
66
|
# ["", "", "", "", "", "", "", ""],
|
61
67
|
# ["", "", "", "", "", "", "", ""],
|
62
68
|
# ["", "", "", "", "", "", "", ""],
|
63
69
|
# ["", "", "", "", "", "", "", ""],
|
64
70
|
# ["P", "P", "P", "P", "P", "P", "P", "P"],
|
65
|
-
# ["R
|
71
|
+
# ["R", "N", "B", "Q", "K", "B", "N", "R"]
|
66
72
|
# ],
|
67
73
|
# pieces_in_hand: [],
|
68
74
|
# games_turn: ["CHESS", "chess"]
|
@@ -77,7 +83,7 @@ Parse a FEEN string without raising exceptions:
|
|
77
83
|
require "feen"
|
78
84
|
|
79
85
|
# Valid FEEN string
|
80
|
-
result = Feen.safe_parse("
|
86
|
+
result = Feen.safe_parse("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR - CHESS/chess")
|
81
87
|
# => {piece_placement: [...], pieces_in_hand: [...], games_turn: [...]}
|
82
88
|
|
83
89
|
# Invalid FEEN string
|
@@ -94,14 +100,14 @@ require "feen"
|
|
94
100
|
|
95
101
|
# Representation of a chess board in initial position
|
96
102
|
piece_placement = [
|
97
|
-
["r
|
103
|
+
["r", "n", "b", "q", "k", "b", "n", "r"],
|
98
104
|
["p", "p", "p", "p", "p", "p", "p", "p"],
|
99
105
|
["", "", "", "", "", "", "", ""],
|
100
106
|
["", "", "", "", "", "", "", ""],
|
101
107
|
["", "", "", "", "", "", "", ""],
|
102
108
|
["", "", "", "", "", "", "", ""],
|
103
109
|
["P", "P", "P", "P", "P", "P", "P", "P"],
|
104
|
-
["R
|
110
|
+
["R", "N", "B", "Q", "K", "B", "N", "R"]
|
105
111
|
]
|
106
112
|
|
107
113
|
result = Feen.dump(
|
@@ -109,7 +115,7 @@ result = Feen.dump(
|
|
109
115
|
games_turn: %w[CHESS chess],
|
110
116
|
pieces_in_hand: []
|
111
117
|
)
|
112
|
-
# => "
|
118
|
+
# => "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR - CHESS/chess"
|
113
119
|
```
|
114
120
|
|
115
121
|
### Validation
|
@@ -120,16 +126,16 @@ Check if a string is valid FEEN notation and in canonical form:
|
|
120
126
|
require "feen"
|
121
127
|
|
122
128
|
# Canonical form
|
123
|
-
Feen.valid?("
|
129
|
+
Feen.valid?("lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL - SHOGI/shogi")
|
124
130
|
# => true
|
125
131
|
|
126
132
|
# Invalid syntax
|
127
133
|
Feen.valid?("invalid feen string")
|
128
134
|
# => false
|
129
135
|
|
130
|
-
# Valid syntax but non-canonical form (pieces in hand not in
|
131
|
-
Feen.valid?("
|
132
|
-
# => false
|
136
|
+
# Valid syntax but non-canonical form (pieces in hand not in canonical order)
|
137
|
+
Feen.valid?("8/8/8/8/8/8/8/8 P3K CHESS/chess")
|
138
|
+
# => false (wrong quantity sorting)
|
133
139
|
```
|
134
140
|
|
135
141
|
The `valid?` method performs two levels of validation:
|
@@ -144,47 +150,100 @@ As FEEN is rule-agnostic, it can represent positions from various board games. H
|
|
144
150
|
### International Chess
|
145
151
|
|
146
152
|
```ruby
|
147
|
-
feen_string = "
|
153
|
+
feen_string = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR - CHESS/chess"
|
148
154
|
```
|
149
155
|
|
150
|
-
In this initial chess position
|
151
|
-
|
152
|
-
- The `'` suffixes on rooks indicate an intermediate state (which might represent castling rights in chess, though FEEN doesn't define this semantics)
|
153
|
-
- The third field `CHESS/chess` indicates it's the player with uppercase pieces' turn to move
|
156
|
+
In this initial chess position, the third field `CHESS/chess` indicates it's the player with uppercase pieces' turn to move.
|
154
157
|
|
155
158
|
### Shogi (Japanese Chess)
|
156
159
|
|
157
160
|
```ruby
|
158
|
-
feen_string = "
|
161
|
+
feen_string = "lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL - SHOGI/shogi"
|
159
162
|
```
|
160
163
|
|
161
|
-
|
164
|
+
**With pieces in hand and promotions:**
|
165
|
+
|
166
|
+
```ruby
|
167
|
+
feen_string = "lnsgk3l/5g3/p1ppB2pp/9/8B/2P6/P2PPPPPP/3K3R1/5rSNL 5P2g2sln SHOGI/shogi"
|
168
|
+
```
|
162
169
|
|
170
|
+
In this shogi position:
|
163
171
|
- The format supports promotions with the `+` prefix (e.g., `+P` for a promoted pawn)
|
164
|
-
-
|
172
|
+
- `5P2g2sln` shows pieces in hand in canonical FEEN order:
|
173
|
+
- **Quantity descending**: 5 Pawns (5P), then 2 Golds (2g)
|
174
|
+
- **Alphabetically**: then Lance (l), Knight (n), Silver (s)
|
165
175
|
- `SHOGI/shogi` indicates it's Sente's (Black's, uppercase) turn to move
|
166
|
-
- `N5P2gln2s` shows the pieces in hand: Sente has a Knight (N) and 5 Pawns (5P), while Gote has 2 Golds (2g), a Lance (l), a Knight (n), and 2 Silvers (2s), all properly sorted in ASCII lexicographic order
|
167
176
|
|
168
177
|
### Makruk (Thai Chess)
|
169
178
|
|
170
179
|
```ruby
|
171
|
-
feen_string = "rnbqkbnr/8/pppppppp/8/8/PPPPPPPP/8/
|
180
|
+
feen_string = "rnbqkbnr/8/pppppppp/8/8/PPPPPPPP/8/RNBKQBNR - MAKRUK/makruk"
|
172
181
|
```
|
173
182
|
|
174
|
-
This initial Makruk position is easily represented in FEEN without needing to know the specific rules of the game.
|
175
|
-
|
176
183
|
### Xiangqi (Chinese Chess)
|
177
184
|
|
178
185
|
```ruby
|
179
186
|
feen_string = "rheagaehr/9/1c5c1/s1s1s1s1s/9/9/S1S1S1S1S/1C5C1/9/RHEAGAEHR - XIANGQI/xiangqi"
|
180
187
|
```
|
181
188
|
|
182
|
-
|
189
|
+
## Advanced Features
|
183
190
|
|
184
|
-
|
185
|
-
- The format naturally adapts to the presence of a "river" (empty space in the middle)
|
191
|
+
### Piece Name Notation (PNN) Support
|
186
192
|
|
187
|
-
|
193
|
+
FEEN supports the complete [PNN specification](https://sashite.dev/documents/pnn/1.0.0/) for representing pieces with state modifiers:
|
194
|
+
|
195
|
+
#### PNN Modifiers
|
196
|
+
|
197
|
+
- **Prefix `+`**: Enhanced state (e.g., promoted pieces in shogi)
|
198
|
+
- **Prefix `-`**: Diminished state (e.g., restricted movement)
|
199
|
+
- **Suffix `'`**: Intermediate state (e.g., castling rights, en passant eligibility)
|
200
|
+
|
201
|
+
#### Examples with PNN
|
202
|
+
|
203
|
+
```ruby
|
204
|
+
# Shogi position with promoted pieces on board
|
205
|
+
piece_placement = [
|
206
|
+
["", "", "", "", "+P", "", "", "", ""] # Promoted pawn on board
|
207
|
+
# ... other ranks
|
208
|
+
]
|
209
|
+
|
210
|
+
# Pieces in hand with PNN modifiers
|
211
|
+
pieces_in_hand = ["+P", "+P", "+P", "B'", "B'", "-p", "P"]
|
212
|
+
|
213
|
+
result = Feen.dump(
|
214
|
+
piece_placement: piece_placement,
|
215
|
+
pieces_in_hand: pieces_in_hand,
|
216
|
+
games_turn: %w[SHOGI shogi]
|
217
|
+
)
|
218
|
+
# => "8/8/8/8/4+P4/8/8/8/8 3+P2B'-pP SHOGI/shogi"
|
219
|
+
```
|
220
|
+
|
221
|
+
### Canonical Pieces in Hand Sorting
|
222
|
+
|
223
|
+
FEEN enforces canonical ordering of pieces in hand according to the specification:
|
224
|
+
|
225
|
+
1. **By quantity (descending)**
|
226
|
+
2. **By complete PNN representation (alphabetically ascending)**
|
227
|
+
|
228
|
+
```ruby
|
229
|
+
# Input pieces in any order
|
230
|
+
pieces = ["P", "B", "P", "+P", "B", "P", "+P", "+P"]
|
231
|
+
|
232
|
+
result = Feen::Dumper::PiecesInHand.dump(*pieces)
|
233
|
+
# => "3+P3P2B"
|
234
|
+
# Breakdown: 3×+P (most frequent), 3×P, 2×B (alphabetical within same quantity)
|
235
|
+
```
|
236
|
+
|
237
|
+
#### Complex Example from FEEN Specification
|
238
|
+
|
239
|
+
```ruby
|
240
|
+
# From FEEN spec v1.0.0 example
|
241
|
+
pieces = (["P"] * 10) + (["K"] * 5) + (["B"] * 3) + (["p'"] * 2) + ["+P", "-p", "R", "b", "q"]
|
242
|
+
|
243
|
+
result = Feen::Dumper::PiecesInHand.dump(*pieces)
|
244
|
+
# => "10P5K3B2p'+P-pRbq"
|
245
|
+
# Sorted by: quantity desc (10,5,3,2,1...), then alphabetical (+P,-p,R,b,q)
|
246
|
+
```
|
188
247
|
|
189
248
|
### Multi-dimensional Boards
|
190
249
|
|
@@ -193,7 +252,7 @@ FEEN supports arbitrary-dimensional board configurations:
|
|
193
252
|
```ruby
|
194
253
|
require "feen"
|
195
254
|
|
196
|
-
# 3D board
|
255
|
+
# 3D board (2×2×3 configuration)
|
197
256
|
piece_placement = [
|
198
257
|
[
|
199
258
|
%w[r n b],
|
@@ -213,26 +272,85 @@ result = Feen.dump(
|
|
213
272
|
# => "rnb/qkp//PR1/1KQ - FOO/bar"
|
214
273
|
```
|
215
274
|
|
216
|
-
###
|
275
|
+
### Hybrid Games
|
276
|
+
|
277
|
+
FEEN supports hybrid games mixing different piece sets:
|
278
|
+
|
279
|
+
```ruby
|
280
|
+
# Chess-Shogi hybrid position
|
281
|
+
feen_string = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR 3+P2B'-pP CHESS/shogi"
|
282
|
+
```
|
283
|
+
|
284
|
+
This represents a position where:
|
285
|
+
- The board uses chess-style pieces
|
286
|
+
- Pieces in hand use shogi-style promotion (`+P`) and intermediate states (`B'`, `-p`)
|
287
|
+
- Chess player to move, against shogi player
|
288
|
+
|
289
|
+
## Round-trip Consistency
|
290
|
+
|
291
|
+
FEEN.rb guarantees round-trip consistency - parsing and dumping produces identical canonical strings:
|
292
|
+
|
293
|
+
```ruby
|
294
|
+
original = "lnsgk3l/5g3/p1ppB2pp/9/8B/2P6/P2PPPPPP/3K3R1/5rSNL 5P2g2sln SHOGI/shogi"
|
295
|
+
parsed = Feen.parse(original)
|
296
|
+
dumped = Feen.dump(**parsed)
|
297
|
+
|
298
|
+
original == dumped # => true (guaranteed canonical form)
|
299
|
+
```
|
300
|
+
|
301
|
+
## Error Handling
|
302
|
+
|
303
|
+
### Validation Errors
|
304
|
+
|
305
|
+
```ruby
|
306
|
+
# Invalid PNN format
|
307
|
+
Feen::Dumper::PiecesInHand.dump("++P")
|
308
|
+
# => ArgumentError: Invalid PNN format: '++P'
|
309
|
+
|
310
|
+
# Invalid games turn
|
311
|
+
Feen.dump(
|
312
|
+
piece_placement: [["P"]],
|
313
|
+
pieces_in_hand: [],
|
314
|
+
games_turn: %w[BOTH_UPPERCASE ALSO_UPPERCASE] # Both same case
|
315
|
+
)
|
316
|
+
# => ArgumentError: One variant must be uppercase and the other lowercase
|
317
|
+
```
|
318
|
+
|
319
|
+
### Safe Operations
|
320
|
+
|
321
|
+
```ruby
|
322
|
+
# Use safe_parse for user input
|
323
|
+
user_input = gets.chomp
|
324
|
+
position = Feen.safe_parse(user_input)
|
325
|
+
|
326
|
+
if position
|
327
|
+
puts "Valid FEEN position!"
|
328
|
+
else
|
329
|
+
puts "Invalid FEEN format"
|
330
|
+
end
|
331
|
+
```
|
217
332
|
|
218
|
-
|
333
|
+
## Performance Considerations
|
219
334
|
|
220
|
-
- **
|
221
|
-
|
335
|
+
- **Parsing**: Optimized recursive descent parser with O(n) complexity
|
336
|
+
- **Validation**: Round-trip validation ensures canonical form
|
337
|
+
- **Memory**: Efficient array-based representation for large boards
|
338
|
+
- **Sorting**: In-place canonical sorting for pieces in hand
|
222
339
|
|
223
|
-
|
224
|
-
- Could represent a piece with limited movement or other restrictions
|
340
|
+
## Compatibility
|
225
341
|
|
226
|
-
- **
|
227
|
-
|
228
|
-
|
342
|
+
- **Ruby version**: >= 3.2.0
|
343
|
+
- **FEEN specification**: v1.0.0 compliant
|
344
|
+
- **PNN specification**: v1.0.0 compliant
|
345
|
+
- **Thread safety**: All operations are thread-safe (no shared mutable state)
|
229
346
|
|
230
|
-
|
347
|
+
## Related Specifications
|
231
348
|
|
232
|
-
|
349
|
+
FEEN is part of a family of specifications for abstract strategy games:
|
233
350
|
|
234
|
-
- [
|
235
|
-
- [
|
351
|
+
- [FEEN Specification v1.0.0](https://sashite.dev/documents/feen/1.0.0/) - Board position notation
|
352
|
+
- [PNN Specification v1.0.0](https://sashite.dev/documents/pnn/1.0.0/) - Piece name notation
|
353
|
+
- [GAN Specification v1.0.0](https://sashite.dev/documents/gan/1.0.0/) - Game-qualified piece identifiers
|
236
354
|
|
237
355
|
## License
|
238
356
|
|
@@ -5,7 +5,7 @@ module Feen
|
|
5
5
|
module PiecesInHand
|
6
6
|
Errors = {
|
7
7
|
invalid_type: "Piece at index: %<index>s must be a String, got type: %<type>s",
|
8
|
-
invalid_format: "Piece at index: %<index>s has an invalid format: '%<value>s'"
|
8
|
+
invalid_format: "Piece at index: %<index>s has an invalid PNN format: '%<value>s'. Expected format: [prefix]letter[suffix] where prefix is + or -, suffix is ', and letter is a-z or A-Z"
|
9
9
|
}.freeze
|
10
10
|
end
|
11
11
|
end
|
@@ -9,12 +9,14 @@ module Feen
|
|
9
9
|
module PiecesInHand
|
10
10
|
# Converts an array of piece identifiers to a FEEN-formatted pieces in hand string
|
11
11
|
#
|
12
|
-
# @param piece_chars [Array<String>] Array of
|
13
|
-
# @return [String] FEEN-formatted pieces in hand string
|
12
|
+
# @param piece_chars [Array<String>] Array of piece identifiers in full PNN format
|
13
|
+
# @return [String] FEEN-formatted pieces in hand string sorted according to FEEN specification:
|
14
|
+
# 1. By quantity (descending)
|
15
|
+
# 2. By complete PNN representation (alphabetically ascending)
|
14
16
|
# @raise [ArgumentError] If any piece identifier is invalid
|
15
17
|
# @example
|
16
|
-
# PiecesInHand.dump("P", "
|
17
|
-
# # => "
|
18
|
+
# PiecesInHand.dump("P", "P", "P", "B", "B", "+P")
|
19
|
+
# # => "3P2B+P"
|
18
20
|
#
|
19
21
|
# PiecesInHand.dump
|
20
22
|
# # => "-"
|
@@ -22,14 +24,25 @@ module Feen
|
|
22
24
|
# If no pieces in hand, return the standardized empty indicator
|
23
25
|
return NoPieces if piece_chars.empty?
|
24
26
|
|
25
|
-
# Validate each piece character according to the FEEN specification
|
27
|
+
# Validate each piece character according to the FEEN specification (full PNN support)
|
26
28
|
validated_chars = validate_piece_chars(piece_chars)
|
27
29
|
|
28
|
-
#
|
29
|
-
validated_chars.
|
30
|
+
# Count occurrences of each piece type
|
31
|
+
piece_counts = validated_chars.tally
|
32
|
+
|
33
|
+
# Sort according to FEEN specification:
|
34
|
+
# 1. By quantity (descending)
|
35
|
+
# 2. By complete PNN representation (alphabetically ascending)
|
36
|
+
sorted_pieces = piece_counts.sort do |a, b|
|
37
|
+
count_comparison = b[1] <=> a[1] # quantity descending
|
38
|
+
count_comparison.zero? ? a[0] <=> b[0] : count_comparison # then alphabetical
|
39
|
+
end
|
40
|
+
|
41
|
+
# Format the pieces sequence with proper count prefixes
|
42
|
+
format_pieces_sequence(sorted_pieces)
|
30
43
|
end
|
31
44
|
|
32
|
-
# Validates all piece characters according to FEEN specification
|
45
|
+
# Validates all piece characters according to FEEN specification with full PNN support
|
33
46
|
#
|
34
47
|
# @param piece_chars [Array<Object>] Array of piece character candidates
|
35
48
|
# @return [Array<String>] Array of validated piece characters
|
@@ -40,7 +53,7 @@ module Feen
|
|
40
53
|
end
|
41
54
|
end
|
42
55
|
|
43
|
-
# Validates a single piece character according to FEEN specification
|
56
|
+
# Validates a single piece character according to FEEN specification with full PNN support
|
44
57
|
#
|
45
58
|
# @param char [Object] Piece character candidate
|
46
59
|
# @param index [Integer] Index of the character in the original array
|
@@ -56,8 +69,12 @@ module Feen
|
|
56
69
|
)
|
57
70
|
end
|
58
71
|
|
59
|
-
# Validate format (
|
60
|
-
|
72
|
+
# Validate format (full PNN notation: [prefix]letter[suffix])
|
73
|
+
# <piece> ::= <letter> | <prefix> <letter> | <letter> <suffix> | <prefix> <letter> <suffix>
|
74
|
+
# <prefix> ::= "+" | "-"
|
75
|
+
# <suffix> ::= "'"
|
76
|
+
# <letter> ::= [a-zA-Z]
|
77
|
+
unless char.match?(/\A[-+]?[a-zA-Z]'?\z/)
|
61
78
|
raise ::ArgumentError, format(
|
62
79
|
Errors[:invalid_format],
|
63
80
|
index: index,
|
@@ -67,6 +84,20 @@ module Feen
|
|
67
84
|
|
68
85
|
char
|
69
86
|
end
|
87
|
+
|
88
|
+
# Formats the pieces sequence with proper count prefixes according to FEEN specification
|
89
|
+
#
|
90
|
+
# @param sorted_pieces [Array<Array>] Array of [piece, count] pairs sorted according to FEEN rules
|
91
|
+
# @return [String] Formatted pieces sequence
|
92
|
+
private_class_method def self.format_pieces_sequence(sorted_pieces)
|
93
|
+
sorted_pieces.map do |piece, count|
|
94
|
+
if count == 1
|
95
|
+
piece
|
96
|
+
else
|
97
|
+
"#{count}#{piece}"
|
98
|
+
end
|
99
|
+
end.join
|
100
|
+
end
|
70
101
|
end
|
71
102
|
end
|
72
103
|
end
|
data/lib/feen/dumper.rb
CHANGED
@@ -23,19 +23,19 @@ module Feen
|
|
23
23
|
# @example Creating a FEEN string for chess initial position
|
24
24
|
# Feen::Dumper.dump(
|
25
25
|
# piece_placement: [
|
26
|
-
# ["r
|
26
|
+
# ["r", "n", "b", "q", "k", "b", "n", "r"],
|
27
27
|
# ["p", "p", "p", "p", "p", "p", "p", "p"],
|
28
28
|
# ["", "", "", "", "", "", "", ""],
|
29
29
|
# ["", "", "", "", "", "", "", ""],
|
30
30
|
# ["", "", "", "", "", "", "", ""],
|
31
31
|
# ["", "", "", "", "", "", "", ""],
|
32
32
|
# ["P", "P", "P", "P", "P", "P", "P", "P"],
|
33
|
-
# ["R
|
33
|
+
# ["R", "N", "B", "Q", "K", "B", "N", "R"]
|
34
34
|
# ],
|
35
35
|
# pieces_in_hand: [],
|
36
36
|
# games_turn: ["CHESS", "chess"]
|
37
37
|
# )
|
38
|
-
# # => "
|
38
|
+
# # => "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR - CHESS/chess"
|
39
39
|
#
|
40
40
|
# @param piece_placement [Array] Board position data structure representing the spatial
|
41
41
|
# distribution of pieces across the board, where each cell
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Feen
|
4
|
+
module Parser
|
5
|
+
module PiecesInHand
|
6
|
+
# Handles canonical ordering validation for pieces in hand according to FEEN specification
|
7
|
+
module CanonicalSorter
|
8
|
+
# Validates that pieces are in canonical order according to FEEN specification:
|
9
|
+
# 1. By quantity (descending)
|
10
|
+
# 2. By complete PNN representation (alphabetically ascending)
|
11
|
+
#
|
12
|
+
# @param pieces_with_counts [Array<Hash>] Array of pieces with their counts
|
13
|
+
# @raise [ArgumentError] If pieces are not in canonical order
|
14
|
+
# @return [void]
|
15
|
+
def self.validate_order(pieces_with_counts)
|
16
|
+
return if pieces_with_counts.size <= 1
|
17
|
+
|
18
|
+
# Create the expected canonical order
|
19
|
+
canonical_order = sort_canonically(pieces_with_counts)
|
20
|
+
|
21
|
+
# Compare with actual order
|
22
|
+
pieces_with_counts.each_with_index do |piece_data, index|
|
23
|
+
canonical_piece = canonical_order[index]
|
24
|
+
|
25
|
+
next if piece_data[:piece] == canonical_piece[:piece] &&
|
26
|
+
piece_data[:count] == canonical_piece[:count]
|
27
|
+
|
28
|
+
raise ::ArgumentError, format(
|
29
|
+
Errors[:canonical_order_violation],
|
30
|
+
actual: format_pieces_sequence(pieces_with_counts),
|
31
|
+
expected: format_pieces_sequence(canonical_order)
|
32
|
+
)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Sorts pieces according to canonical FEEN order
|
37
|
+
#
|
38
|
+
# @param pieces_with_counts [Array<Hash>] Array of pieces with their counts
|
39
|
+
# @return [Array<Hash>] Canonically sorted array
|
40
|
+
def self.sort_canonically(pieces_with_counts)
|
41
|
+
pieces_with_counts.sort do |a, b|
|
42
|
+
# Primary sort: by quantity (descending)
|
43
|
+
count_comparison = b[:count] <=> a[:count]
|
44
|
+
next count_comparison unless count_comparison.zero?
|
45
|
+
|
46
|
+
# Secondary sort: by complete PNN representation (alphabetically ascending)
|
47
|
+
a[:piece] <=> b[:piece]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Formats a pieces sequence for error messages
|
52
|
+
#
|
53
|
+
# @param pieces_with_counts [Array<Hash>] Array of pieces with their counts
|
54
|
+
# @return [String] Formatted string representation
|
55
|
+
private_class_method def self.format_pieces_sequence(pieces_with_counts)
|
56
|
+
pieces_with_counts.map do |item|
|
57
|
+
count = item[:count]
|
58
|
+
piece = item[:piece]
|
59
|
+
|
60
|
+
if count == 1
|
61
|
+
piece
|
62
|
+
else
|
63
|
+
"#{count}#{piece}"
|
64
|
+
end
|
65
|
+
end.join
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -5,10 +5,12 @@ module Feen
|
|
5
5
|
module PiecesInHand
|
6
6
|
# Error messages for validation
|
7
7
|
Errors = {
|
8
|
-
invalid_type:
|
9
|
-
empty_string:
|
10
|
-
invalid_format:
|
11
|
-
|
8
|
+
invalid_type: "Pieces in hand must be a string, got %s",
|
9
|
+
empty_string: "Pieces in hand string cannot be empty",
|
10
|
+
invalid_format: "Invalid pieces in hand format: %s",
|
11
|
+
invalid_pnn_piece: "Invalid PNN piece format: '%s'. Expected format: [prefix]letter[suffix] where prefix is + or -, suffix is ', and letter is a-z or A-Z",
|
12
|
+
invalid_count: "Invalid count format: '%s'. Count cannot be '0' or '1', use the piece without count instead",
|
13
|
+
canonical_order_violation: "Pieces in hand must be in canonical order (by quantity descending, then alphabetically). Got: '%<actual>s', expected: '%<expected>s'"
|
12
14
|
}.freeze
|
13
15
|
end
|
14
16
|
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Feen
|
4
|
+
module Parser
|
5
|
+
module PiecesInHand
|
6
|
+
# Patterns for PNN (Piece Name Notation) validation and parsing
|
7
|
+
module PnnPatterns
|
8
|
+
# Basic PNN piece pattern following the specification:
|
9
|
+
# <piece> ::= <letter> | <prefix> <letter> | <letter> <suffix> | <prefix> <letter> <suffix>
|
10
|
+
# <prefix> ::= "+" | "-"
|
11
|
+
# <suffix> ::= "'"
|
12
|
+
# <letter> ::= [a-zA-Z]
|
13
|
+
PNN_PIECE_PATTERN = /\A[-+]?[a-zA-Z]'?\z/
|
14
|
+
|
15
|
+
# Pattern for valid count prefixes according to FEEN specification:
|
16
|
+
# - Cannot be "0" or "1" (use piece without prefix instead)
|
17
|
+
# - Can be 2-9 or any number with 2+ digits
|
18
|
+
VALID_COUNT_PATTERN = /\A(?:[2-9]|\d{2,})\z/
|
19
|
+
|
20
|
+
# Pattern to extract piece with optional count from pieces in hand string
|
21
|
+
# Matches: optional count followed by complete PNN piece
|
22
|
+
# Groups: (count_str, piece_str)
|
23
|
+
# Note: We need to handle the full PNN piece including modifiers
|
24
|
+
PIECE_WITH_COUNT_PATTERN = /(?:([2-9]|\d{2,}))?([-+]?[a-zA-Z]'?)/
|
25
|
+
|
26
|
+
# Complete validation pattern for pieces in hand string
|
27
|
+
# Based on the FEEN BNF specification with PNN support
|
28
|
+
# This pattern allows any sequence of pieces (uppercase/lowercase mixed) in canonical order
|
29
|
+
# It should reject invalid formats like "++P"
|
30
|
+
VALID_FORMAT_PATTERN = /\A
|
31
|
+
(?:
|
32
|
+
-| # No pieces in hand
|
33
|
+
(?:
|
34
|
+
(?:[2-9]|\d{2,})? # Optional count (2-9 or 10+)
|
35
|
+
[-+]? # Optional single prefix (+ or -)
|
36
|
+
[a-zA-Z] # Required letter
|
37
|
+
'? # Optional single suffix (')
|
38
|
+
)+ # One or more pieces
|
39
|
+
)
|
40
|
+
\z/x
|
41
|
+
|
42
|
+
# Pattern for extracting all pieces globally (used for comprehensive validation)
|
43
|
+
GLOBAL_PIECE_EXTRACTION_PATTERN = /(?:([2-9]|\d{2,}))?([-+]?[a-zA-Z]'?)/
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -2,20 +2,22 @@
|
|
2
2
|
|
3
3
|
require_relative File.join("pieces_in_hand", "errors")
|
4
4
|
require_relative File.join("pieces_in_hand", "no_pieces")
|
5
|
-
require_relative File.join("pieces_in_hand", "
|
6
|
-
require_relative File.join("pieces_in_hand", "
|
5
|
+
require_relative File.join("pieces_in_hand", "pnn_patterns")
|
6
|
+
require_relative File.join("pieces_in_hand", "canonical_sorter")
|
7
7
|
|
8
8
|
module Feen
|
9
9
|
module Parser
|
10
10
|
# Handles parsing of the pieces in hand section of a FEEN string.
|
11
11
|
# Pieces in hand represent pieces available for dropping onto the board.
|
12
|
+
# This implementation supports full PNN notation including prefixes and suffixes.
|
12
13
|
module PiecesInHand
|
13
14
|
# Parses the pieces in hand section of a FEEN string.
|
14
15
|
#
|
15
16
|
# @param pieces_in_hand_str [String] FEEN pieces in hand string
|
16
|
-
# @return [Array<String>] Array of
|
17
|
-
#
|
18
|
-
#
|
17
|
+
# @return [Array<String>] Array of piece identifiers in full PNN format,
|
18
|
+
# expanded based on their counts and sorted according to FEEN specification:
|
19
|
+
# 1. By quantity (descending)
|
20
|
+
# 2. By complete PNN representation (alphabetically ascending)
|
19
21
|
# Empty array if no pieces are in hand.
|
20
22
|
# @raise [ArgumentError] If the input string is invalid
|
21
23
|
#
|
@@ -23,13 +25,17 @@ module Feen
|
|
23
25
|
# PiecesInHand.parse("-")
|
24
26
|
# # => []
|
25
27
|
#
|
26
|
-
# @example Parse
|
27
|
-
# PiecesInHand.parse("
|
28
|
-
# # => ["
|
28
|
+
# @example Parse pieces with modifiers
|
29
|
+
# PiecesInHand.parse("3+P2B'Pn")
|
30
|
+
# # => ["+P", "+P", "+P", "B'", "B'", "P", "n"]
|
29
31
|
#
|
30
|
-
# @example Parse pieces with counts
|
31
|
-
# PiecesInHand.parse("
|
32
|
-
# # => ["
|
32
|
+
# @example Parse complex pieces with counts and modifiers
|
33
|
+
# PiecesInHand.parse("10P5K3B2p'+P-pBRbq")
|
34
|
+
# # => ["P", "P", "P", "P", "P", "P", "P", "P", "P", "P",
|
35
|
+
# # "K", "K", "K", "K", "K",
|
36
|
+
# # "B", "B", "B",
|
37
|
+
# # "p'", "p'",
|
38
|
+
# # "+P", "-p", "B", "R", "b", "q"]
|
33
39
|
def self.parse(pieces_in_hand_str)
|
34
40
|
# Validate input
|
35
41
|
validate_input_type(pieces_in_hand_str)
|
@@ -38,11 +44,13 @@ module Feen
|
|
38
44
|
# Handle the no-pieces case early
|
39
45
|
return [] if pieces_in_hand_str == NoPieces
|
40
46
|
|
41
|
-
# Extract pieces with their counts and validate the
|
47
|
+
# Extract pieces with their counts and validate the format
|
42
48
|
pieces_with_counts = extract_pieces_with_counts(pieces_in_hand_str)
|
43
|
-
validate_lexicographic_order(pieces_with_counts)
|
44
49
|
|
45
|
-
#
|
50
|
+
# Validate canonical ordering according to FEEN specification
|
51
|
+
validate_canonical_order(pieces_with_counts)
|
52
|
+
|
53
|
+
# Expand the pieces into an array maintaining the canonical order
|
46
54
|
expand_pieces(pieces_with_counts)
|
47
55
|
end
|
48
56
|
|
@@ -57,17 +65,112 @@ module Feen
|
|
57
65
|
end
|
58
66
|
|
59
67
|
# Validates that the input string matches the expected format according to FEEN specification.
|
68
|
+
# This includes validation of individual PNN pieces and overall structure.
|
60
69
|
#
|
61
70
|
# @param str [String] Input string to validate
|
62
71
|
# @raise [ArgumentError] If format is invalid
|
63
72
|
# @return [void]
|
64
73
|
private_class_method def self.validate_format(str)
|
65
|
-
return if str == NoPieces
|
74
|
+
return if str == NoPieces
|
75
|
+
|
76
|
+
# First, validate overall structure using the updated pattern
|
77
|
+
raise ::ArgumentError, format(Errors[:invalid_format], str) unless str.match?(PnnPatterns::VALID_FORMAT_PATTERN)
|
78
|
+
|
79
|
+
# Additional validation: ensure each piece component is valid PNN
|
80
|
+
# This catches cases like "++P" that might pass the overall pattern
|
81
|
+
validate_individual_pieces(str)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Validates each individual piece in the string for PNN compliance
|
85
|
+
#
|
86
|
+
# @param str [String] FEEN pieces in hand string
|
87
|
+
# @raise [ArgumentError] If any piece is invalid PNN format
|
88
|
+
# @return [void]
|
89
|
+
private_class_method def self.validate_individual_pieces(str)
|
90
|
+
original_string = str
|
91
|
+
position = 0
|
92
|
+
|
93
|
+
while position < str.length
|
94
|
+
match = str[position..].match(PnnPatterns::PIECE_WITH_COUNT_PATTERN)
|
95
|
+
|
96
|
+
unless match
|
97
|
+
# Find the problematic part
|
98
|
+
remaining = str[position..]
|
99
|
+
raise ::ArgumentError, format(Errors[:invalid_format], remaining)
|
100
|
+
end
|
101
|
+
|
102
|
+
count_str, piece = match.captures
|
66
103
|
|
67
|
-
|
104
|
+
# Skip empty matches (shouldn't happen with our pattern, but safety check)
|
105
|
+
if piece.nil? || piece.empty?
|
106
|
+
position += 1
|
107
|
+
next
|
108
|
+
end
|
109
|
+
|
110
|
+
# Validate the piece follows PNN specification
|
111
|
+
unless piece.match?(PnnPatterns::PNN_PIECE_PATTERN)
|
112
|
+
raise ::ArgumentError, format(Errors[:invalid_pnn_piece], piece)
|
113
|
+
end
|
114
|
+
|
115
|
+
# Validate count format (no "0" or "1" prefixes allowed)
|
116
|
+
if count_str && !count_str.match?(PnnPatterns::VALID_COUNT_PATTERN)
|
117
|
+
raise ::ArgumentError, format(Errors[:invalid_count], count_str)
|
118
|
+
end
|
119
|
+
|
120
|
+
position += match[0].length
|
121
|
+
end
|
122
|
+
|
123
|
+
# Final check: verify that we can reconstruct the string correctly
|
124
|
+
# by re-extracting all pieces and comparing with original
|
125
|
+
reconstructed_pieces = extract_pieces_with_counts(original_string)
|
126
|
+
reconstructed = reconstructed_pieces.map do |item|
|
127
|
+
count = item[:count]
|
128
|
+
piece = item[:piece]
|
129
|
+
count == 1 ? piece : "#{count}#{piece}"
|
130
|
+
end.join
|
131
|
+
|
132
|
+
# If reconstruction doesn't match original, there's an invalid format
|
133
|
+
return if reconstructed == original_string
|
134
|
+
# Find the first discrepancy to provide better error message
|
135
|
+
# This will catch cases like "++P" where we extract "+P" but original has extra "+"
|
136
|
+
unless original_string.length > reconstructed.length
|
137
|
+
raise ::ArgumentError, format(Errors[:invalid_format], original_string)
|
138
|
+
end
|
139
|
+
|
140
|
+
# There are extra characters - find what's invalid
|
141
|
+
original_string.sub(reconstructed, "")
|
142
|
+
# Try to identify the problematic piece
|
143
|
+
problematic_part = find_problematic_piece(original_string, reconstructed)
|
144
|
+
raise ::ArgumentError, format(Errors[:invalid_pnn_piece], problematic_part)
|
145
|
+
end
|
146
|
+
|
147
|
+
# Finds the problematic piece in the original string by comparing with reconstruction
|
148
|
+
#
|
149
|
+
# @param original [String] Original input string
|
150
|
+
# @param reconstructed [String] Reconstructed string from extracted pieces
|
151
|
+
# @return [String] The problematic piece or sequence
|
152
|
+
private_class_method def self.find_problematic_piece(original, reconstructed)
|
153
|
+
# Simple heuristic: find the first part that doesn't match
|
154
|
+
min_length = [original.length, reconstructed.length].min
|
155
|
+
|
156
|
+
# Find first difference
|
157
|
+
diff_pos = 0
|
158
|
+
diff_pos += 1 while diff_pos < min_length && original[diff_pos] == reconstructed[diff_pos]
|
159
|
+
|
160
|
+
# If difference is at start, likely extra prefix
|
161
|
+
# Look for a sequence that starts with invalid pattern like "++"
|
162
|
+
if (diff_pos == 0) && original.match?(/\A\+\+/)
|
163
|
+
return "++P" # Common case
|
164
|
+
end
|
165
|
+
|
166
|
+
# Extract a reasonable chunk around the problematic area
|
167
|
+
start_pos = [0, diff_pos - 2].max
|
168
|
+
end_pos = [original.length, diff_pos + 4].min
|
169
|
+
original[start_pos...end_pos]
|
68
170
|
end
|
69
171
|
|
70
172
|
# Extracts pieces with their counts from the FEEN string.
|
173
|
+
# Supports full PNN notation including prefixes and suffixes.
|
71
174
|
#
|
72
175
|
# @param str [String] FEEN pieces in hand string
|
73
176
|
# @return [Array<Hash>] Array of hashes with :piece and :count keys
|
@@ -76,7 +179,7 @@ module Feen
|
|
76
179
|
position = 0
|
77
180
|
|
78
181
|
while position < str.length
|
79
|
-
match = str[position..].match(
|
182
|
+
match = str[position..].match(PnnPatterns::PIECE_WITH_COUNT_PATTERN)
|
80
183
|
break unless match
|
81
184
|
|
82
185
|
count_str, piece = match.captures
|
@@ -92,24 +195,24 @@ module Feen
|
|
92
195
|
result
|
93
196
|
end
|
94
197
|
|
95
|
-
# Validates that pieces are in
|
198
|
+
# Validates that pieces are in canonical order according to FEEN specification:
|
199
|
+
# 1. By quantity (descending)
|
200
|
+
# 2. By complete PNN representation (alphabetically ascending)
|
96
201
|
#
|
97
202
|
# @param pieces_with_counts [Array<Hash>] Array of pieces with their counts
|
98
|
-
# @raise [ArgumentError] If pieces are not in
|
203
|
+
# @raise [ArgumentError] If pieces are not in canonical order
|
99
204
|
# @return [void]
|
100
|
-
private_class_method def self.
|
101
|
-
|
102
|
-
|
103
|
-
# Verify the array is sorted lexicographically
|
104
|
-
return if pieces == pieces.sort
|
205
|
+
private_class_method def self.validate_canonical_order(pieces_with_counts)
|
206
|
+
return if pieces_with_counts.size <= 1
|
105
207
|
|
106
|
-
|
208
|
+
CanonicalSorter.validate_order(pieces_with_counts)
|
107
209
|
end
|
108
210
|
|
109
211
|
# Expands the pieces based on their counts into an array.
|
212
|
+
# Maintains the canonical ordering from the input.
|
110
213
|
#
|
111
214
|
# @param pieces_with_counts [Array<Hash>] Array of pieces with their counts
|
112
|
-
# @return [Array<String>] Array of expanded pieces
|
215
|
+
# @return [Array<String>] Array of expanded pieces in canonical order
|
113
216
|
private_class_method def self.expand_pieces(pieces_with_counts)
|
114
217
|
pieces_with_counts.flat_map do |item|
|
115
218
|
Array.new(item[:count], item[:piece])
|
data/lib/feen/parser.rb
CHANGED
@@ -26,18 +26,18 @@ module Feen
|
|
26
26
|
# @raise [ArgumentError] If the FEEN string is invalid
|
27
27
|
#
|
28
28
|
# @example Parsing a standard chess initial position
|
29
|
-
# feen = "
|
29
|
+
# feen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR - CHESS/chess"
|
30
30
|
# result = Feen::Parser.parse(feen)
|
31
31
|
# # => {
|
32
32
|
# # piece_placement: [
|
33
|
-
# # ["r
|
33
|
+
# # ["r", "n", "b", "q", "k", "b", "n", "r"],
|
34
34
|
# # ["p", "p", "p", "p", "p", "p", "p", "p"],
|
35
35
|
# # ["", "", "", "", "", "", "", ""],
|
36
36
|
# # ["", "", "", "", "", "", "", ""],
|
37
37
|
# # ["", "", "", "", "", "", "", ""],
|
38
38
|
# # ["", "", "", "", "", "", "", ""],
|
39
39
|
# # ["P", "P", "P", "P", "P", "P", "P", "P"],
|
40
|
-
# # ["R
|
40
|
+
# # ["R", "N", "B", "Q", "K", "B", "N", "R"]
|
41
41
|
# # ],
|
42
42
|
# # pieces_in_hand: [],
|
43
43
|
# # games_turn: ["CHESS", "chess"]
|
@@ -76,7 +76,7 @@ module Feen
|
|
76
76
|
# @return [Hash, nil] Hash containing the parsed position data or nil if parsing fails
|
77
77
|
#
|
78
78
|
# @example Parsing a valid FEEN string
|
79
|
-
# feen = "
|
79
|
+
# feen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR - CHESS/chess"
|
80
80
|
# result = Feen::Parser.safe_parse(feen)
|
81
81
|
# # => {piece_placement: [...], pieces_in_hand: [...], games_turn: [...]}
|
82
82
|
#
|
data/lib/feen.rb
CHANGED
@@ -23,21 +23,21 @@ module Feen
|
|
23
23
|
# @raise [ArgumentError] If any parameter is invalid
|
24
24
|
# @example
|
25
25
|
# piece_placement = [
|
26
|
-
# ["r
|
26
|
+
# ["r", "n", "b", "q", "k", "b", "n", "r"],
|
27
27
|
# ["p", "p", "p", "p", "p", "p", "p", "p"],
|
28
28
|
# ["", "", "", "", "", "", "", ""],
|
29
29
|
# ["", "", "", "", "", "", "", ""],
|
30
30
|
# ["", "", "", "", "", "", "", ""],
|
31
31
|
# ["", "", "", "", "", "", "", ""],
|
32
32
|
# ["P", "P", "P", "P", "P", "P", "P", "P"],
|
33
|
-
# ["R
|
33
|
+
# ["R", "N", "B", "Q", "K", "B", "N", "R"]
|
34
34
|
# ]
|
35
35
|
# Feen.dump(
|
36
36
|
# piece_placement: piece_placement,
|
37
37
|
# pieces_in_hand: [],
|
38
38
|
# games_turn: ["CHESS", "chess"]
|
39
39
|
# )
|
40
|
-
# # => "
|
40
|
+
# # => "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR - CHESS/chess"
|
41
41
|
def self.dump(piece_placement:, pieces_in_hand:, games_turn:)
|
42
42
|
Dumper.dump(piece_placement:, pieces_in_hand:, games_turn:)
|
43
43
|
end
|
@@ -51,18 +51,18 @@ module Feen
|
|
51
51
|
# - :games_turn [Array<String>] - A two-element array with [active_variant, inactive_variant]
|
52
52
|
# @raise [ArgumentError] If the FEEN string is invalid
|
53
53
|
# @example
|
54
|
-
# feen_string = "
|
54
|
+
# feen_string = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR - CHESS/chess"
|
55
55
|
# Feen.parse(feen_string)
|
56
56
|
# # => {
|
57
57
|
# # piece_placement: [
|
58
|
-
# # ["r
|
58
|
+
# # ["r", "n", "b", "q", "k", "b", "n", "r"],
|
59
59
|
# # ["p", "p", "p", "p", "p", "p", "p", "p"],
|
60
60
|
# # ["", "", "", "", "", "", "", ""],
|
61
61
|
# # ["", "", "", "", "", "", "", ""],
|
62
62
|
# # ["", "", "", "", "", "", "", ""],
|
63
63
|
# # ["", "", "", "", "", "", "", ""],
|
64
64
|
# # ["P", "P", "P", "P", "P", "P", "P", "P"],
|
65
|
-
# # ["R
|
65
|
+
# # ["R", "N", "B", "Q", "K", "B", "N", "R"]
|
66
66
|
# # ],
|
67
67
|
# # pieces_in_hand: [],
|
68
68
|
# # games_turn: ["CHESS", "chess"]
|
@@ -80,7 +80,7 @@ module Feen
|
|
80
80
|
# @return [Hash, nil] Hash containing the parsed position data or nil if parsing fails
|
81
81
|
# @example
|
82
82
|
# # Valid FEEN string
|
83
|
-
# Feen.safe_parse("
|
83
|
+
# Feen.safe_parse("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR - CHESS/chess")
|
84
84
|
# # => {piece_placement: [...], pieces_in_hand: [...], games_turn: [...]}
|
85
85
|
#
|
86
86
|
# # Invalid FEEN string
|
@@ -104,7 +104,7 @@ module Feen
|
|
104
104
|
# @return [Boolean] True if the string is a valid and canonical FEEN string
|
105
105
|
# @example
|
106
106
|
# # Canonical form
|
107
|
-
# Feen.valid?("lnsgk3l/5g3/p1ppB2pp/9/8B/2P6/P2PPPPPP/3K3R1/5rSNL
|
107
|
+
# Feen.valid?("lnsgk3l/5g3/p1ppB2pp/9/8B/2P6/P2PPPPPP/3K3R1/5rSNL 2g2s5PNln SHOGI/shogi") # => true
|
108
108
|
#
|
109
109
|
# # Invalid syntax
|
110
110
|
# Feen.valid?("invalid feen string") # => false
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: feen
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 5.0.0.
|
4
|
+
version: 5.0.0.beta6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Cyril Kato
|
@@ -33,9 +33,11 @@ files:
|
|
33
33
|
- lib/feen/parser/games_turn/valid_games_turn_pattern.rb
|
34
34
|
- lib/feen/parser/piece_placement.rb
|
35
35
|
- lib/feen/parser/pieces_in_hand.rb
|
36
|
+
- lib/feen/parser/pieces_in_hand/canonical_sorter.rb
|
36
37
|
- lib/feen/parser/pieces_in_hand/errors.rb
|
37
38
|
- lib/feen/parser/pieces_in_hand/no_pieces.rb
|
38
39
|
- lib/feen/parser/pieces_in_hand/piece_count_pattern.rb
|
40
|
+
- lib/feen/parser/pieces_in_hand/pnn_patterns.rb
|
39
41
|
- lib/feen/parser/pieces_in_hand/valid_format_pattern.rb
|
40
42
|
homepage: https://github.com/sashite/feen.rb
|
41
43
|
licenses:
|
@@ -61,7 +63,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
61
63
|
- !ruby/object:Gem::Version
|
62
64
|
version: '0'
|
63
65
|
requirements: []
|
64
|
-
rubygems_version: 3.6.
|
66
|
+
rubygems_version: 3.6.9
|
65
67
|
specification_version: 4
|
66
68
|
summary: FEEN (Forsyth–Edwards Enhanced Notation) support for the Ruby language.
|
67
69
|
test_files: []
|