feen 5.0.0.beta4 → 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 +173 -54
- data/lib/feen/dumper/piece_placement.rb +1 -1
- 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/piece_placement.rb +4 -6
- 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 -4
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
@@ -12,17 +12,32 @@
|
|
12
12
|
FEEN (Forsyth–Edwards Enhanced Notation) is a compact, canonical, and rule-agnostic textual format for representing static board positions in two-player piece-placement games.
|
13
13
|
|
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
|
- Representing positions from various games without knowledge of specific rules
|
16
|
-
- Supporting boards of arbitrary dimensions
|
17
|
-
- 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
|
18
19
|
- Facilitating serialization and deserialization of positions
|
19
20
|
- Ensuring canonical representation for consistent data handling
|
20
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
|
+
|
21
36
|
## Installation
|
22
37
|
|
23
38
|
```ruby
|
24
39
|
# In your Gemfile
|
25
|
-
gem "feen", ">= 5.0.0.
|
40
|
+
gem "feen", ">= 5.0.0.beta6"
|
26
41
|
```
|
27
42
|
|
28
43
|
Or install manually:
|
@@ -31,14 +46,6 @@ Or install manually:
|
|
31
46
|
gem install feen --pre
|
32
47
|
```
|
33
48
|
|
34
|
-
## FEEN Format
|
35
|
-
|
36
|
-
A FEEN record consists of three space-separated fields:
|
37
|
-
|
38
|
-
```
|
39
|
-
<PIECE-PLACEMENT> <PIECES-IN-HAND> <GAMES-TURN>
|
40
|
-
```
|
41
|
-
|
42
49
|
## Basic Usage
|
43
50
|
|
44
51
|
### Parsing FEEN Strings
|
@@ -48,20 +55,20 @@ Convert a FEEN string into a structured Ruby object:
|
|
48
55
|
```ruby
|
49
56
|
require "feen"
|
50
57
|
|
51
|
-
feen_string = "
|
58
|
+
feen_string = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR - CHESS/chess"
|
52
59
|
position = Feen.parse(feen_string)
|
53
60
|
|
54
61
|
# Result is a hash:
|
55
62
|
# {
|
56
63
|
# piece_placement: [
|
57
|
-
# ["r", "n", "b", "q", "k
|
64
|
+
# ["r", "n", "b", "q", "k", "b", "n", "r"],
|
58
65
|
# ["p", "p", "p", "p", "p", "p", "p", "p"],
|
59
66
|
# ["", "", "", "", "", "", "", ""],
|
60
67
|
# ["", "", "", "", "", "", "", ""],
|
61
68
|
# ["", "", "", "", "", "", "", ""],
|
62
69
|
# ["", "", "", "", "", "", "", ""],
|
63
70
|
# ["P", "P", "P", "P", "P", "P", "P", "P"],
|
64
|
-
# ["R", "N", "B", "Q", "K
|
71
|
+
# ["R", "N", "B", "Q", "K", "B", "N", "R"]
|
65
72
|
# ],
|
66
73
|
# pieces_in_hand: [],
|
67
74
|
# games_turn: ["CHESS", "chess"]
|
@@ -76,7 +83,7 @@ Parse a FEEN string without raising exceptions:
|
|
76
83
|
require "feen"
|
77
84
|
|
78
85
|
# Valid FEEN string
|
79
|
-
result = Feen.safe_parse("
|
86
|
+
result = Feen.safe_parse("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR - CHESS/chess")
|
80
87
|
# => {piece_placement: [...], pieces_in_hand: [...], games_turn: [...]}
|
81
88
|
|
82
89
|
# Invalid FEEN string
|
@@ -93,14 +100,14 @@ require "feen"
|
|
93
100
|
|
94
101
|
# Representation of a chess board in initial position
|
95
102
|
piece_placement = [
|
96
|
-
["r", "n", "b", "q", "k
|
103
|
+
["r", "n", "b", "q", "k", "b", "n", "r"],
|
97
104
|
["p", "p", "p", "p", "p", "p", "p", "p"],
|
98
105
|
["", "", "", "", "", "", "", ""],
|
99
106
|
["", "", "", "", "", "", "", ""],
|
100
107
|
["", "", "", "", "", "", "", ""],
|
101
108
|
["", "", "", "", "", "", "", ""],
|
102
109
|
["P", "P", "P", "P", "P", "P", "P", "P"],
|
103
|
-
["R", "N", "B", "Q", "K
|
110
|
+
["R", "N", "B", "Q", "K", "B", "N", "R"]
|
104
111
|
]
|
105
112
|
|
106
113
|
result = Feen.dump(
|
@@ -108,7 +115,7 @@ result = Feen.dump(
|
|
108
115
|
games_turn: %w[CHESS chess],
|
109
116
|
pieces_in_hand: []
|
110
117
|
)
|
111
|
-
# => "
|
118
|
+
# => "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR - CHESS/chess"
|
112
119
|
```
|
113
120
|
|
114
121
|
### Validation
|
@@ -119,19 +126,20 @@ Check if a string is valid FEEN notation and in canonical form:
|
|
119
126
|
require "feen"
|
120
127
|
|
121
128
|
# Canonical form
|
122
|
-
Feen.valid?("
|
129
|
+
Feen.valid?("lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL - SHOGI/shogi")
|
123
130
|
# => true
|
124
131
|
|
125
132
|
# Invalid syntax
|
126
133
|
Feen.valid?("invalid feen string")
|
127
134
|
# => false
|
128
135
|
|
129
|
-
# Valid syntax but non-canonical form (pieces in hand not in
|
130
|
-
Feen.valid?("
|
131
|
-
# => 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)
|
132
139
|
```
|
133
140
|
|
134
141
|
The `valid?` method performs two levels of validation:
|
142
|
+
|
135
143
|
1. **Syntax check**: Verifies the string can be parsed as FEEN
|
136
144
|
2. **Canonicity check**: Ensures the string is in its canonical form through round-trip conversion
|
137
145
|
|
@@ -142,45 +150,101 @@ As FEEN is rule-agnostic, it can represent positions from various board games. H
|
|
142
150
|
### International Chess
|
143
151
|
|
144
152
|
```ruby
|
145
|
-
feen_string = "
|
153
|
+
feen_string = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR - CHESS/chess"
|
146
154
|
```
|
147
155
|
|
148
|
-
In this initial chess position
|
149
|
-
- The `=` suffixes on kings indicate castling rights on both sides (though FEEN doesn't define this semantics)
|
150
|
-
- 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.
|
151
157
|
|
152
158
|
### Shogi (Japanese Chess)
|
153
159
|
|
154
160
|
```ruby
|
155
|
-
feen_string = "
|
161
|
+
feen_string = "lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL - SHOGI/shogi"
|
162
|
+
```
|
163
|
+
|
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"
|
156
168
|
```
|
157
169
|
|
158
170
|
In this shogi position:
|
159
171
|
- The format supports promotions with the `+` prefix (e.g., `+P` for a promoted pawn)
|
160
|
-
-
|
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)
|
161
175
|
- `SHOGI/shogi` indicates it's Sente's (Black's, uppercase) turn to move
|
162
|
-
- `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
|
163
176
|
|
164
177
|
### Makruk (Thai Chess)
|
165
178
|
|
166
179
|
```ruby
|
167
|
-
feen_string = "rnbqkbnr/8/pppppppp/8/8/PPPPPPPP/8/
|
180
|
+
feen_string = "rnbqkbnr/8/pppppppp/8/8/PPPPPPPP/8/RNBKQBNR - MAKRUK/makruk"
|
168
181
|
```
|
169
182
|
|
170
|
-
This initial Makruk position is easily represented in FEEN without needing to know the specific rules of the game.
|
171
|
-
|
172
183
|
### Xiangqi (Chinese Chess)
|
173
184
|
|
174
185
|
```ruby
|
175
186
|
feen_string = "rheagaehr/9/1c5c1/s1s1s1s1s/9/9/S1S1S1S1S/1C5C1/9/RHEAGAEHR - XIANGQI/xiangqi"
|
176
187
|
```
|
177
188
|
|
178
|
-
In this Xiangqi position:
|
179
|
-
- The representation uses single letters for the different pieces
|
180
|
-
- The format naturally adapts to the presence of a "river" (empty space in the middle)
|
181
|
-
|
182
189
|
## Advanced Features
|
183
190
|
|
191
|
+
### Piece Name Notation (PNN) Support
|
192
|
+
|
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
|
+
```
|
247
|
+
|
184
248
|
### Multi-dimensional Boards
|
185
249
|
|
186
250
|
FEEN supports arbitrary-dimensional board configurations:
|
@@ -188,7 +252,7 @@ FEEN supports arbitrary-dimensional board configurations:
|
|
188
252
|
```ruby
|
189
253
|
require "feen"
|
190
254
|
|
191
|
-
# 3D board
|
255
|
+
# 3D board (2×2×3 configuration)
|
192
256
|
piece_placement = [
|
193
257
|
[
|
194
258
|
%w[r n b],
|
@@ -208,30 +272,85 @@ result = Feen.dump(
|
|
208
272
|
# => "rnb/qkp//PR1/1KQ - FOO/bar"
|
209
273
|
```
|
210
274
|
|
211
|
-
###
|
275
|
+
### Hybrid Games
|
212
276
|
|
213
|
-
FEEN supports
|
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
|
+
```
|
214
332
|
|
215
|
-
|
216
|
-
- Example in shogi: `+P` may represent a promoted pawn
|
333
|
+
## Performance Considerations
|
217
334
|
|
218
|
-
- **
|
219
|
-
|
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
|
220
339
|
|
221
|
-
|
222
|
-
- Example in chess: `K<` may represent a king eligible for queenside castling only
|
223
|
-
- Example in chess: `P<` may represent a pawn that may be captured _en passant_ from the left
|
340
|
+
## Compatibility
|
224
341
|
|
225
|
-
- **
|
226
|
-
|
227
|
-
|
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)
|
228
346
|
|
229
|
-
|
347
|
+
## Related Specifications
|
230
348
|
|
231
|
-
|
349
|
+
FEEN is part of a family of specifications for abstract strategy games:
|
232
350
|
|
233
|
-
- [
|
234
|
-
- [
|
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
|
235
354
|
|
236
355
|
## License
|
237
356
|
|
@@ -239,4 +358,4 @@ The [gem](https://rubygems.org/gems/feen) is available as open source under the
|
|
239
358
|
|
240
359
|
## About Sashité
|
241
360
|
|
242
|
-
This project is maintained by [Sashité](https://sashite.com/)
|
361
|
+
This project is maintained by [Sashité](https://sashite.com/) — promoting chess variants and sharing the beauty of Chinese, Japanese, and Western chess cultures.
|
@@ -7,7 +7,7 @@ module Feen
|
|
7
7
|
#
|
8
8
|
# @param piece_placement [Array] Hierarchical array representing the board where:
|
9
9
|
# - Empty squares are represented by empty strings ("")
|
10
|
-
# - Pieces are represented by strings (e.g., "r", "
|
10
|
+
# - Pieces are represented by strings (e.g., "r", "R'", "+P")
|
11
11
|
# - Dimensions are represented by nested arrays
|
12
12
|
# @return [String] FEEN piece placement string
|
13
13
|
# @raise [ArgumentError] If the piece placement structure is invalid
|
@@ -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", "n", "b", "q", "k
|
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", "N", "B", "Q", "K
|
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
|
@@ -30,10 +30,8 @@ module Feen
|
|
30
30
|
VALID_PREFIXES = [PREFIX_PROMOTION, PREFIX_DIMINISHED].freeze
|
31
31
|
|
32
32
|
# Valid piece suffixes
|
33
|
-
|
34
|
-
|
35
|
-
SUFFIX_RIGHT = ">"
|
36
|
-
VALID_SUFFIXES = [SUFFIX_EQUALS, SUFFIX_LEFT, SUFFIX_RIGHT].freeze
|
33
|
+
SUFFIX_INTERMEDIATE = "'"
|
34
|
+
VALID_SUFFIXES = [SUFFIX_INTERMEDIATE].freeze
|
37
35
|
|
38
36
|
# Build validation pattern step by step to match BNF
|
39
37
|
# <letter> ::= <letter-lowercase> | <letter-uppercase>
|
@@ -42,8 +40,8 @@ module Feen
|
|
42
40
|
# <prefix> ::= "+" | "-"
|
43
41
|
PREFIX = "[+-]"
|
44
42
|
|
45
|
-
# <suffix> ::= "
|
46
|
-
SUFFIX = "[
|
43
|
+
# <suffix> ::= "'"
|
44
|
+
SUFFIX = "[']"
|
47
45
|
|
48
46
|
# <piece> ::= <letter> | <prefix> <letter> | <letter> <suffix> | <prefix> <letter> <suffix>
|
49
47
|
PIECE = "(?:#{PREFIX}?#{LETTER}#{SUFFIX}?)"
|
@@ -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", "n", "b", "q", "k
|
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", "N", "B", "Q", "K
|
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", "n", "b", "q", "k
|
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", "N", "B", "Q", "K
|
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", "n", "b", "q", "k
|
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", "N", "B", "Q", "K
|
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:
|
@@ -46,9 +48,7 @@ metadata:
|
|
46
48
|
homepage_uri: https://github.com/sashite/feen.rb
|
47
49
|
source_code_uri: https://github.com/sashite/feen.rb
|
48
50
|
specification_uri: https://sashite.dev/documents/feen/1.0.0/
|
49
|
-
funding_uri: https://github.com/sponsors/cyril
|
50
51
|
rubygems_mfa_required: 'true'
|
51
|
-
article_uri: https://blog.cyril.email/posts/2025-05-01/introducing-feen-notation.html
|
52
52
|
rdoc_options: []
|
53
53
|
require_paths:
|
54
54
|
- lib
|
@@ -63,7 +63,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
63
63
|
- !ruby/object:Gem::Version
|
64
64
|
version: '0'
|
65
65
|
requirements: []
|
66
|
-
rubygems_version: 3.6.
|
66
|
+
rubygems_version: 3.6.9
|
67
67
|
specification_version: 4
|
68
68
|
summary: FEEN (Forsyth–Edwards Enhanced Notation) support for the Ruby language.
|
69
69
|
test_files: []
|