feen 5.0.0.beta8 → 5.0.0.beta10
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 +284 -96
- data/lib/feen/dumper/pieces_in_hand.rb +101 -40
- data/lib/feen/dumper/style_turn.rb +71 -0
- data/lib/feen/dumper.rb +17 -17
- data/lib/feen/parser/piece_placement.rb +169 -9
- data/lib/feen/parser/pieces_in_hand.rb +55 -62
- data/lib/feen/parser/style_turn.rb +101 -0
- data/lib/feen/parser.rb +8 -8
- data/lib/feen.rb +27 -27
- metadata +33 -5
- data/lib/feen/dumper/games_turn.rb +0 -70
- data/lib/feen/parser/games_turn.rb +0 -78
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3038a2be98c26b830833d6f7fece5d050605da9ead72ee8cdedecf98e31f30ef
|
4
|
+
data.tar.gz: 2868f4ca7e04b472f059e982023129c36af9718ad819f07f09343ad34a0d611c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1ec8030d85d0c06bc11f63cdde1bc32a1ad2b709fb982cde496478d5c8dd9ea607c1b6089451e0ff3442857cb1936ded576143f9a7781419626905a04c3b8e01
|
7
|
+
data.tar.gz: 047e36e257acdca693fd88cceb99e946bfa3b0eefe86719f72f3addbc9adef543fc0ce82bcafe59dae7fbc7dad7ede27848550280de1a7adf288a89d795cf4ba
|
data/README.md
CHANGED
@@ -5,7 +5,7 @@
|
|
5
5
|

|
6
6
|
[](https://github.com/sashite/feen.rb/raw/main/LICENSE.md)
|
7
7
|
|
8
|
-
> A Ruby library for **FEEN** (Forsyth–Edwards Enhanced Notation) - a
|
8
|
+
> A Ruby library for **FEEN** (Forsyth–Edwards Enhanced Notation) - a compact, canonical, and rule-agnostic textual format for representing static board positions in two-player piece-placement games.
|
9
9
|
|
10
10
|
## What is FEEN?
|
11
11
|
|
@@ -13,18 +13,19 @@ FEEN is like taking a snapshot of any board game position and turning it into a
|
|
13
13
|
|
14
14
|
**Key Features:**
|
15
15
|
|
16
|
-
- **Versatile**: Supports Chess, Shōgi, Xiangqi, and similar games
|
17
|
-
- **Bidirectional**: Convert positions to text and back
|
18
|
-
- **Compact**: Efficient representation
|
19
16
|
- **Rule-agnostic**: No knowledge of specific game rules required
|
20
|
-
- **
|
17
|
+
- **Canonical**: Equivalent positions yield identical strings
|
18
|
+
- **Cross-style support**: Handles hybrid configurations with different piece sets
|
19
|
+
- **Multi-dimensional**: Supports 2D, 3D, and higher dimensional boards
|
20
|
+
- **Captured pieces**: Full support for pieces-in-hand mechanics
|
21
|
+
- **Compact**: Efficient representation with compression for empty spaces
|
21
22
|
|
22
23
|
## Installation
|
23
24
|
|
24
25
|
Add this line to your application's Gemfile:
|
25
26
|
|
26
27
|
```ruby
|
27
|
-
gem "feen", ">= 5.0.0.
|
28
|
+
gem "feen", ">= 5.0.0.beta10"
|
28
29
|
```
|
29
30
|
|
30
31
|
Or install it directly:
|
@@ -45,8 +46,8 @@ board = [["r", "k", "r"]]
|
|
45
46
|
|
46
47
|
feen_string = Feen.dump(
|
47
48
|
piece_placement: board,
|
48
|
-
pieces_in_hand: [],
|
49
|
-
|
49
|
+
pieces_in_hand: [], # No captured pieces
|
50
|
+
style_turn: ["GAME", "game"] # GAME player's turn
|
50
51
|
)
|
51
52
|
|
52
53
|
feen_string # => "rkr / GAME/game"
|
@@ -62,7 +63,7 @@ position = Feen.parse(feen_string)
|
|
62
63
|
|
63
64
|
position[:piece_placement] # => ["r", "k", "r"]
|
64
65
|
position[:pieces_in_hand] # => []
|
65
|
-
position[:
|
66
|
+
position[:style_turn] # => ["GAME", "game"]
|
66
67
|
```
|
67
68
|
|
68
69
|
## Understanding FEEN Format
|
@@ -70,62 +71,98 @@ position[:games_turn] # => ["GAME", "game"]
|
|
70
71
|
A FEEN string has exactly **three parts separated by single spaces**:
|
71
72
|
|
72
73
|
```
|
73
|
-
<
|
74
|
+
<PIECE-PLACEMENT> <PIECES-IN-HAND> <STYLE-TURN>
|
74
75
|
```
|
75
76
|
|
76
|
-
### Part 1:
|
77
|
+
### Part 1: Piece Placement
|
77
78
|
|
78
|
-
The board shows where pieces are placed:
|
79
|
+
The board shows where pieces are placed, always from the point of view of the player who plays first in the initial position:
|
79
80
|
|
80
|
-
- **Pieces**: Represented by
|
81
|
-
- `K` = piece belonging to first player (uppercase)
|
82
|
-
- `k` = piece belonging to second player (lowercase)
|
81
|
+
- **Pieces**: Represented by PNN notation (case matters!)
|
82
|
+
- `K` = piece belonging to first player (uppercase style)
|
83
|
+
- `k` = piece belonging to second player (lowercase style)
|
84
|
+
- `+P` = enhanced piece (modifier allowed on board only)
|
85
|
+
- `-R` = diminished piece (modifier allowed on board only)
|
86
|
+
- `N'` = intermediate state piece (modifier allowed on board only)
|
83
87
|
- **Empty spaces**: Represented by numbers
|
84
88
|
- `3` = three empty squares in a row
|
85
89
|
- **Ranks (rows)**: Separated by `/`
|
90
|
+
- **Higher dimensions**: Use multiple `/` characters (`//`, `///`, etc.)
|
86
91
|
|
87
92
|
**Examples:**
|
88
93
|
|
89
94
|
```ruby
|
90
|
-
"K"
|
91
|
-
"3"
|
92
|
-
"Kqr"
|
93
|
-
"K2r"
|
94
|
-
"Kqr/3/R2k"
|
95
|
+
"K" # Single piece on 1x1 board
|
96
|
+
"3" # Three empty squares
|
97
|
+
"Kqr" # Three pieces: K, q, r
|
98
|
+
"K2r" # K, two empty squares, then r
|
99
|
+
"Kqr/3/R2k" # 3x3 board with multiple ranks
|
100
|
+
"+K-r/N'" # Board with piece modifiers
|
95
101
|
```
|
96
102
|
|
97
|
-
### Part 2:
|
103
|
+
### Part 2: Pieces in Hand
|
98
104
|
|
99
|
-
Shows pieces that have been captured and
|
105
|
+
Shows pieces that have been captured and are available for future placement:
|
100
106
|
|
101
107
|
- Format: `UPPERCASE_PIECES/lowercase_pieces`
|
102
108
|
- **Always separated by `/`** even if empty
|
103
|
-
-
|
104
|
-
-
|
109
|
+
- **Base form only**: No modifiers allowed (captured pieces revert to base type)
|
110
|
+
- Count notation: `3P` means three `P` pieces (never `1P` for single pieces)
|
111
|
+
- **Canonical sorting**: By quantity (descending), then alphabetical
|
105
112
|
|
106
113
|
**Examples:**
|
107
114
|
|
108
115
|
```ruby
|
109
|
-
"/"
|
110
|
-
"P/"
|
111
|
-
"/p"
|
112
|
-
"2PK/3p"
|
116
|
+
"/" # No pieces captured
|
117
|
+
"P/" # First player has one P piece
|
118
|
+
"/p" # Second player has one p piece
|
119
|
+
"2PK/3p" # First player: 2 P's + 1 K, Second player: 3 p's
|
120
|
+
"3P2RK/2pb" # Sorted by quantity, then alphabetical
|
113
121
|
```
|
114
122
|
|
115
|
-
|
123
|
+
**Critical: Canonical Piece Sorting Algorithm**
|
116
124
|
|
117
|
-
|
125
|
+
Captured pieces are automatically sorted according to the FEEN specification:
|
126
|
+
|
127
|
+
1. **By player**: Uppercase pieces first, then lowercase pieces (separated by `/`)
|
128
|
+
2. **By quantity** (descending): Most frequent pieces first
|
129
|
+
3. **By base letter** (ascending): Alphabetical within same quantity
|
130
|
+
4. **By prefix** (specific order): For same base letter and quantity: `-`, `+`, then no prefix
|
131
|
+
5. **By suffix** (specific order): For same prefix: no suffix, then `'`
|
132
|
+
|
133
|
+
**Detailed sorting example:**
|
134
|
+
|
135
|
+
```ruby
|
136
|
+
# Input pieces: PP+P'PPP+P'KS'S-PB+B+B+P'BBBPPPSP-P-P'-PRB
|
137
|
+
# Step 1 - Group by base letter and modifiers:
|
138
|
+
# B: +B+B+BBBBB = 2+B + 5B
|
139
|
+
# K: K = K
|
140
|
+
# P: -P-P-P-P' + +P'+P'+P' + PPPPPPPPP = 3-P + -P' + 3+P' + 9P
|
141
|
+
# R: R = R
|
142
|
+
# S: SS + S' = 2S + S'
|
143
|
+
|
144
|
+
# Step 2 - Sort by quantity (desc), then letter (asc), then prefix/suffix:
|
145
|
+
# Result: "2+B5BK3-P-P'3+P'9PR2SS'"
|
146
|
+
|
147
|
+
# Canonical form: "2+B5BK3-P-P'3+P'9PR2SS'/"
|
148
|
+
```
|
149
|
+
|
150
|
+
### Part 3: Style Turn
|
151
|
+
|
152
|
+
Identifies the style type associated with each player and whose turn it is:
|
118
153
|
|
119
154
|
- Format: `ACTIVE_PLAYER/INACTIVE_PLAYER`
|
120
|
-
- **One must be uppercase, other lowercase**
|
121
|
-
- The uppercase
|
155
|
+
- **One must be uppercase, other lowercase** (semantically significant casing)
|
156
|
+
- The uppercase name identifies the style system for uppercase pieces
|
157
|
+
- The lowercase name identifies the style system for lowercase pieces
|
158
|
+
- First name refers to the player to move
|
122
159
|
|
123
160
|
**Examples:**
|
124
161
|
|
125
162
|
```ruby
|
126
|
-
"CHESS/chess"
|
127
|
-
"shogi/SHOGI"
|
128
|
-
"
|
163
|
+
"CHESS/chess" # CHESS player (uppercase pieces) to move
|
164
|
+
"shogi/SHOGI" # shogi player (lowercase pieces) to move
|
165
|
+
"CHESS/makruk" # Cross-style: CHESS vs makruk, CHESS to move
|
129
166
|
```
|
130
167
|
|
131
168
|
## Complete API Reference
|
@@ -138,24 +175,24 @@ Converts position components into a FEEN string.
|
|
138
175
|
|
139
176
|
**Parameters:**
|
140
177
|
- `piece_placement:` [Array] - Nested array representing the board
|
141
|
-
- `pieces_in_hand:` [Array] - List of captured pieces (strings)
|
142
|
-
- `
|
178
|
+
- `pieces_in_hand:` [Array] - List of captured pieces (strings, base form only)
|
179
|
+
- `style_turn:` [Array] - Two-element array: [active_player, inactive_player]
|
143
180
|
|
144
|
-
**Returns:** String - FEEN notation
|
181
|
+
**Returns:** String - Canonical FEEN notation
|
145
182
|
|
146
183
|
**Example:**
|
147
184
|
|
148
185
|
```ruby
|
149
186
|
board = [
|
150
|
-
["r", "n", "k", "n", "r"],
|
151
|
-
["", "", "", "", ""],
|
152
|
-
["P", "P", "P", "P", "P"]
|
187
|
+
["r", "n", "k", "n", "r"], # Back rank
|
188
|
+
["", "", "", "", ""], # Empty rank
|
189
|
+
["P", "P", "P", "P", "P"] # Front rank
|
153
190
|
]
|
154
191
|
|
155
192
|
feen = Feen.dump(
|
156
193
|
piece_placement: board,
|
157
194
|
pieces_in_hand: ["Q", "p"],
|
158
|
-
|
195
|
+
style_turn: ["WHITE", "black"]
|
159
196
|
)
|
160
197
|
# => "rnknr/5/PPPPP Q/p WHITE/black"
|
161
198
|
```
|
@@ -172,7 +209,7 @@ Converts a FEEN string back into position components.
|
|
172
209
|
|
173
210
|
- `:piece_placement` - The board as nested arrays
|
174
211
|
- `:pieces_in_hand` - Captured pieces as array of strings
|
175
|
-
- `:
|
212
|
+
- `:style_turn` - [active_player, inactive_player]
|
176
213
|
|
177
214
|
**Example:**
|
178
215
|
|
@@ -185,7 +222,7 @@ position[:piece_placement]
|
|
185
222
|
position[:pieces_in_hand]
|
186
223
|
# => ["Q", "p"]
|
187
224
|
|
188
|
-
position[:
|
225
|
+
position[:style_turn]
|
189
226
|
# => ["WHITE", "black"]
|
190
227
|
```
|
191
228
|
|
@@ -198,7 +235,7 @@ Like `parse()` but returns `nil` instead of raising exceptions for invalid input
|
|
198
235
|
```ruby
|
199
236
|
# Valid input
|
200
237
|
result = Feen.safe_parse("k/K / GAME/game")
|
201
|
-
# => { piece_placement: [["k"], ["K"]], pieces_in_hand: [],
|
238
|
+
# => { piece_placement: [["k"], ["K"]], pieces_in_hand: [], style_turn: ["GAME", "game"] }
|
202
239
|
|
203
240
|
# Invalid input
|
204
241
|
result = Feen.safe_parse("invalid")
|
@@ -214,9 +251,9 @@ Checks if a string is valid, canonical FEEN notation.
|
|
214
251
|
**Example:**
|
215
252
|
|
216
253
|
```ruby
|
217
|
-
Feen.valid?("k/K / GAME/game")
|
218
|
-
Feen.valid?("invalid")
|
219
|
-
Feen.valid?("k/K
|
254
|
+
Feen.valid?("k/K / GAME/game") # => true
|
255
|
+
Feen.valid?("invalid") # => false
|
256
|
+
Feen.valid?("k/K 3PK/ GAME/game") # => false (wrong piece order)
|
220
257
|
```
|
221
258
|
|
222
259
|
## Working with Different Board Sizes
|
@@ -235,7 +272,7 @@ board[8][8] = "R" # Bottom-right
|
|
235
272
|
feen = Feen.dump(
|
236
273
|
piece_placement: board,
|
237
274
|
pieces_in_hand: [],
|
238
|
-
|
275
|
+
style_turn: ["PLAYERA", "playerb"]
|
239
276
|
)
|
240
277
|
|
241
278
|
feen # => "r8/9/9/9/9/9/9/9/8R / PLAYERA/playerb"
|
@@ -253,7 +290,7 @@ board_3d = [
|
|
253
290
|
feen = Feen.dump(
|
254
291
|
piece_placement: board_3d,
|
255
292
|
pieces_in_hand: [],
|
256
|
-
|
293
|
+
style_turn: ["UP", "down"]
|
257
294
|
)
|
258
295
|
# => "ab/cd//AB/CD / UP/down"
|
259
296
|
```
|
@@ -271,7 +308,7 @@ irregular_board = [
|
|
271
308
|
feen = Feen.dump(
|
272
309
|
piece_placement: irregular_board,
|
273
310
|
pieces_in_hand: [],
|
274
|
-
|
311
|
+
style_turn: ["GAME", "game"]
|
275
312
|
)
|
276
313
|
# => "rkr/pp/PPPP / GAME/game"
|
277
314
|
```
|
@@ -288,59 +325,87 @@ captured = ["P", "P", "P", "R", "p", "p"]
|
|
288
325
|
feen = Feen.dump(
|
289
326
|
piece_placement: [["k"], ["K"]], # Minimal board
|
290
327
|
pieces_in_hand: captured,
|
291
|
-
|
328
|
+
style_turn: ["FIRST", "second"]
|
292
329
|
)
|
293
330
|
# => "k/K 3PR/2p FIRST/second"
|
294
331
|
```
|
295
332
|
|
296
|
-
### Understanding Piece Sorting
|
333
|
+
### Understanding Canonical Piece Sorting
|
334
|
+
|
335
|
+
Captured pieces are automatically sorted in canonical order according to the FEEN specification:
|
297
336
|
|
298
|
-
|
337
|
+
1. **By player**: Uppercase pieces first, then lowercase pieces (separated by `/`)
|
338
|
+
2. **By quantity** (descending): Most frequent pieces first
|
339
|
+
3. **By base letter** (ascending): Alphabetical within same quantity
|
340
|
+
4. **By prefix** (specific order): For same base letter and quantity: `-`, `+`, then no prefix
|
341
|
+
5. **By suffix** (specific order): For same prefix: no suffix, then `'`
|
299
342
|
|
300
|
-
|
301
|
-
2. **By letter** (alphabetical within same quantity)
|
343
|
+
**Complex sorting example:**
|
302
344
|
|
303
345
|
```ruby
|
304
|
-
|
305
|
-
|
346
|
+
# Mixed pieces with modifiers
|
347
|
+
pieces = ["-B", "+B", "+B", "B", "B", "B", "B", "B", "K", "-P", "-P", "-P", "-P'", "+P'", "+P'", "+P'", "P", "P", "P", "P", "P", "P", "P", "P", "P", "R", "S", "S", "S'", "b", "p"]
|
348
|
+
|
349
|
+
# After canonical sorting: "2+B5BK3-P-P'3+P'9PR2SS'/bp"
|
350
|
+
# Breakdown:
|
351
|
+
# - Uppercase: 2+B (2 enhanced B), 5B (5 regular B), K (1 King), 3-P (3 diminished P), -P' (1 diminished P with intermediate state), 3+P' (3 enhanced P with intermediate state), 9P (9 regular P), R (1 Rook), 2S (2 regular S), S' (1 S with intermediate state)
|
352
|
+
# - Lowercase: b (1 bishop), p (1 pawn)
|
306
353
|
```
|
307
354
|
|
308
355
|
## Advanced Features
|
309
356
|
|
310
|
-
### Special Piece States (
|
357
|
+
### Special Piece States (Board Only)
|
311
358
|
|
312
|
-
For games that need special piece states, use modifiers **only on the board**:
|
359
|
+
For games that need special piece states, use PNN modifiers **only on the board**:
|
313
360
|
|
314
361
|
```ruby
|
315
362
|
board = [
|
316
|
-
["+P", "K", "-R"],
|
317
|
-
["N'", "", "B"]
|
363
|
+
["+P", "K", "-R"], # Enhanced pawn, King, diminished rook
|
364
|
+
["N'", "", "B"] # Knight with intermediate state, empty, Bishop
|
318
365
|
]
|
319
366
|
|
320
|
-
# Note: Modifiers
|
321
|
-
# Pieces in hand
|
367
|
+
# Note: Modifiers are allowed on the board
|
368
|
+
# Pieces in hand may or may not have modifiers depending on game rules
|
322
369
|
feen = Feen.dump(
|
323
370
|
piece_placement: board,
|
324
|
-
pieces_in_hand: ["P", "R"], #
|
325
|
-
|
371
|
+
pieces_in_hand: ["P", "+R'"], # Modifiers allowed in hand per FEEN spec
|
372
|
+
style_turn: ["GAME", "game"]
|
326
373
|
)
|
327
374
|
|
328
|
-
feen # => "+PK-R/N'1B
|
375
|
+
feen # => "+PK-R/N'1B P+R'/ GAME/game"
|
329
376
|
```
|
330
377
|
|
331
|
-
### Cross-
|
378
|
+
### Cross-Style Scenarios
|
332
379
|
|
333
380
|
FEEN can represent positions mixing different game systems:
|
334
381
|
|
335
382
|
```ruby
|
336
|
-
#
|
337
|
-
|
338
|
-
piece_placement: ["K", "
|
339
|
-
pieces_in_hand: ["P", "
|
340
|
-
|
383
|
+
# CHESS pieces vs makruk pieces
|
384
|
+
cross_style_feen = Feen.dump(
|
385
|
+
piece_placement: [["K", "Q", "k", "m"]], # Mixed piece types
|
386
|
+
pieces_in_hand: ["P", "s"], # Captured from both sides
|
387
|
+
style_turn: ["CHESS", "makruk"] # Different game systems
|
341
388
|
)
|
342
389
|
|
343
|
-
|
390
|
+
cross_style_feen # => "KQkm P/s CHESS/makruk"
|
391
|
+
```
|
392
|
+
|
393
|
+
### Dynamic Piece Ownership
|
394
|
+
|
395
|
+
FEEN supports piece ownership changes through capture and redeployment:
|
396
|
+
|
397
|
+
```ruby
|
398
|
+
# A piece's current owner is determined by its case
|
399
|
+
# Regardless of its original style system
|
400
|
+
board = [["r", "K"]] # lowercase 'r' owned by second player
|
401
|
+
# uppercase 'K' owned by first player
|
402
|
+
|
403
|
+
feen = Feen.dump(
|
404
|
+
piece_placement: board,
|
405
|
+
pieces_in_hand: [],
|
406
|
+
style_turn: ["CHESS", "shogi"] # Cross-style game
|
407
|
+
)
|
408
|
+
# => "rK / CHESS/shogi"
|
344
409
|
```
|
345
410
|
|
346
411
|
## Error Handling
|
@@ -350,25 +415,33 @@ mixed_feen # => "KGkr P/g bar/FOO"
|
|
350
415
|
```ruby
|
351
416
|
# ERROR: Wrong argument types
|
352
417
|
Feen.dump(
|
353
|
-
piece_placement: "not an array",
|
354
|
-
pieces_in_hand: "not an array",
|
355
|
-
|
418
|
+
piece_placement: "not an array", # Must be Array
|
419
|
+
pieces_in_hand: "not an array", # Must be Array
|
420
|
+
style_turn: "not an array" # Must be Array[2]
|
356
421
|
)
|
357
422
|
# => ArgumentError
|
358
423
|
|
359
|
-
# ERROR:
|
424
|
+
# ERROR: Invalid pieces in captured pieces (if validation enabled)
|
425
|
+
Feen.dump(
|
426
|
+
piece_placement: [["K"]],
|
427
|
+
pieces_in_hand: ["invalid_piece"], # Must follow PNN specification
|
428
|
+
style_turn: ["GAME", "game"]
|
429
|
+
)
|
430
|
+
# => ArgumentError (if validation enabled)
|
431
|
+
|
432
|
+
# ERROR: Same case in style_turn
|
360
433
|
Feen.dump(
|
361
434
|
piece_placement: [["K"]],
|
362
|
-
pieces_in_hand: [
|
363
|
-
|
435
|
+
pieces_in_hand: [],
|
436
|
+
style_turn: ["GAME", "ALSO"] # Must be different cases
|
364
437
|
)
|
365
438
|
# => ArgumentError
|
366
439
|
|
367
|
-
# ERROR:
|
440
|
+
# ERROR: Invalid style identifiers
|
368
441
|
Feen.dump(
|
369
442
|
piece_placement: [["K"]],
|
370
443
|
pieces_in_hand: [],
|
371
|
-
|
444
|
+
style_turn: ["game-1", "game2"] # Must follow SNN specification
|
372
445
|
)
|
373
446
|
# => ArgumentError
|
374
447
|
```
|
@@ -390,6 +463,69 @@ end
|
|
390
463
|
|
391
464
|
## Real-World Examples
|
392
465
|
|
466
|
+
### International Chess Starting Position
|
467
|
+
|
468
|
+
```ruby
|
469
|
+
chess_start = Feen.dump(
|
470
|
+
piece_placement: [
|
471
|
+
["r", "n", "b", "q", "k", "b", "n", "r"],
|
472
|
+
["p", "p", "p", "p", "p", "p", "p", "p"],
|
473
|
+
["", "", "", "", "", "", "", ""],
|
474
|
+
["", "", "", "", "", "", "", ""],
|
475
|
+
["", "", "", "", "", "", "", ""],
|
476
|
+
["", "", "", "", "", "", "", ""],
|
477
|
+
["P", "P", "P", "P", "P", "P", "P", "P"],
|
478
|
+
["R", "N", "B", "Q", "K", "B", "N", "R"]
|
479
|
+
],
|
480
|
+
pieces_in_hand: [],
|
481
|
+
style_turn: ["CHESS", "chess"]
|
482
|
+
)
|
483
|
+
# => "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR / CHESS/chess"
|
484
|
+
```
|
485
|
+
|
486
|
+
### Japanese Shōgi Starting Position
|
487
|
+
|
488
|
+
```ruby
|
489
|
+
shogi_start = Feen.dump(
|
490
|
+
piece_placement: [
|
491
|
+
["l", "n", "s", "g", "k", "g", "s", "n", "l"],
|
492
|
+
["", "r", "", "", "", "", "", "b", ""],
|
493
|
+
["p", "p", "p", "p", "p", "p", "p", "p", "p"],
|
494
|
+
["", "", "", "", "", "", "", "", ""],
|
495
|
+
["", "", "", "", "", "", "", "", ""],
|
496
|
+
["", "", "", "", "", "", "", "", ""],
|
497
|
+
["P", "P", "P", "P", "P", "P", "P", "P", "P"],
|
498
|
+
["", "B", "", "", "", "", "", "R", ""],
|
499
|
+
["L", "N", "S", "G", "K", "G", "S", "N", "L"]
|
500
|
+
],
|
501
|
+
pieces_in_hand: [],
|
502
|
+
style_turn: ["SHOGI", "shogi"]
|
503
|
+
)
|
504
|
+
# => "lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL / SHOGI/shogi"
|
505
|
+
```
|
506
|
+
|
507
|
+
### Shōgi Position with Captured Pieces
|
508
|
+
|
509
|
+
```ruby
|
510
|
+
# Game in progress with captured pieces
|
511
|
+
shogi_midgame = Feen.dump(
|
512
|
+
piece_placement: [
|
513
|
+
["l", "n", "s", "g", "k", "g", "s", "n", "l"],
|
514
|
+
["", "r", "", "", "", "", "", "", ""],
|
515
|
+
["p", "p", "p", "p", "p", "p", "p", "p", "p"],
|
516
|
+
["", "", "", "", "", "", "", "", ""],
|
517
|
+
["", "", "", "", "", "", "", "", ""],
|
518
|
+
["", "", "", "", "", "", "", "", ""],
|
519
|
+
["P", "P", "P", "P", "P", "P", "P", "P", "P"],
|
520
|
+
["", "B", "", "", "", "", "", "R", ""],
|
521
|
+
["L", "N", "S", "G", "K", "G", "S", "N", "L"]
|
522
|
+
],
|
523
|
+
pieces_in_hand: ["B", "P", "P", "b", "p"], # Captured pieces
|
524
|
+
style_turn: ["SHOGI", "shogi"]
|
525
|
+
)
|
526
|
+
# => "lnsgkgsnl/1r7/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL 2PB/bp SHOGI/shogi"
|
527
|
+
```
|
528
|
+
|
393
529
|
### Save/Load Game State
|
394
530
|
|
395
531
|
```ruby
|
@@ -398,7 +534,7 @@ class GameState
|
|
398
534
|
feen = Feen.dump(
|
399
535
|
piece_placement: board,
|
400
536
|
pieces_in_hand: captured,
|
401
|
-
|
537
|
+
style_turn: [current_player, opponent]
|
402
538
|
)
|
403
539
|
|
404
540
|
File.write("game_save.feen", feen)
|
@@ -426,7 +562,7 @@ class PositionDatabase
|
|
426
562
|
feen = Feen.dump(
|
427
563
|
piece_placement: board,
|
428
564
|
pieces_in_hand: captured,
|
429
|
-
|
565
|
+
style_turn: turn_info
|
430
566
|
)
|
431
567
|
|
432
568
|
@positions[name] = feen
|
@@ -450,7 +586,7 @@ end
|
|
450
586
|
db = PositionDatabase.new
|
451
587
|
db.store_position("start", [["r", "k", "r"]], [], ["GAME", "game"])
|
452
588
|
position = db.retrieve_position("start")
|
453
|
-
# => {piece_placement: [["r", "k", "r"]], pieces_in_hand: [],
|
589
|
+
# => { piece_placement: [["r", "k", "r"]], pieces_in_hand: [], style_turn: ["GAME", "game"] }
|
454
590
|
```
|
455
591
|
|
456
592
|
## Best Practices
|
@@ -467,7 +603,7 @@ def create_feen_safely(board, captured, turn)
|
|
467
603
|
Feen.dump(
|
468
604
|
piece_placement: board,
|
469
605
|
pieces_in_hand: captured,
|
470
|
-
|
606
|
+
style_turn: turn
|
471
607
|
)
|
472
608
|
rescue ArgumentError => e
|
473
609
|
puts "FEEN creation failed: #{e.message}"
|
@@ -475,27 +611,44 @@ rescue ArgumentError => e
|
|
475
611
|
end
|
476
612
|
```
|
477
613
|
|
478
|
-
### 2. Use Consistent Naming
|
614
|
+
### 2. Use Consistent Style Naming
|
479
615
|
|
480
616
|
```ruby
|
481
|
-
# Good:
|
482
|
-
|
483
|
-
PLAYER_2_PIECES = %w[k q r b n p]
|
484
|
-
|
485
|
-
# Good: Descriptive game identifiers
|
486
|
-
GAME_TYPES = {
|
617
|
+
# Good: Follow SNN specification conventions
|
618
|
+
STYLE_IDENTIFIERS = {
|
487
619
|
chess_white: "CHESS",
|
488
620
|
chess_black: "chess",
|
489
621
|
shogi_sente: "SHOGI",
|
490
|
-
shogi_gote: "shogi"
|
622
|
+
shogi_gote: "shogi",
|
623
|
+
xiangqi_red: "XIANGQI",
|
624
|
+
xiangqi_black: "xiangqi"
|
491
625
|
}
|
626
|
+
|
627
|
+
# Good: Clear piece type distinctions following PNN
|
628
|
+
CHESS_PIECES = %w[K Q R B N P] # Uppercase for first player
|
629
|
+
CHESS_PIECES_LOWER = %w[k q r b n p] # Lowercase for second player
|
630
|
+
```
|
631
|
+
|
632
|
+
### 3. Handle Cross-Style Scenarios Carefully
|
633
|
+
|
634
|
+
```ruby
|
635
|
+
def validate_cross_style_position(feen_string)
|
636
|
+
position = Feen.parse(feen_string)
|
637
|
+
styles = position[:style_turn]
|
638
|
+
|
639
|
+
# Check if it's a cross-style game
|
640
|
+
if styles[0].downcase != styles[1].downcase
|
641
|
+
puts "Cross-style game detected: #{styles[0]} vs #{styles[1]}"
|
642
|
+
# Consider piece identity ambiguity implications
|
643
|
+
end
|
644
|
+
end
|
492
645
|
```
|
493
646
|
|
494
|
-
###
|
647
|
+
### 4. Round-trip Validation
|
495
648
|
|
496
649
|
```ruby
|
497
650
|
def verify_feen_consistency(original_feen)
|
498
|
-
# Parse and re-dump to check
|
651
|
+
# Parse and re-dump to check canonical format
|
499
652
|
position = Feen.parse(original_feen)
|
500
653
|
regenerated = Feen.dump(**position)
|
501
654
|
|
@@ -509,17 +662,52 @@ def verify_feen_consistency(original_feen)
|
|
509
662
|
end
|
510
663
|
```
|
511
664
|
|
665
|
+
## FEEN Specification Compliance
|
666
|
+
|
667
|
+
This library implements **FEEN v1.0.0** specification with the following features:
|
668
|
+
|
669
|
+
### Core Properties ✓
|
670
|
+
- Rule-agnostic representation
|
671
|
+
- Canonical format enforcement
|
672
|
+
- Cross-style/hybrid position support
|
673
|
+
- Multi-dimensional board support
|
674
|
+
- Two-player limitation (exactly)
|
675
|
+
- 26-piece limit per player (a-z, A-Z)
|
676
|
+
|
677
|
+
### Field Support ✓
|
678
|
+
- **Piece Placement**: Full PNN notation with modifiers on board
|
679
|
+
- **Pieces in Hand**: Full PNN notation with modifiers (as per specification), canonical sorting
|
680
|
+
- **Style Turn**: SNN-compliant identifiers with semantic casing
|
681
|
+
|
682
|
+
### Advanced Features ✓
|
683
|
+
- Dynamic piece ownership through capture
|
684
|
+
- Irregular board shapes
|
685
|
+
- 3D and higher-dimensional boards
|
686
|
+
- Empty space compression
|
687
|
+
- Proper dimension separators (`/`, `//`, `///`)
|
688
|
+
- **Strict canonical piece sorting** per FEEN specification
|
689
|
+
|
690
|
+
### Canonical Sorting Implementation ✓
|
691
|
+
The library implements the exact sorting algorithm specified in FEEN v1.0.0:
|
692
|
+
1. Player separation (uppercase/lowercase)
|
693
|
+
2. Quantity (descending)
|
694
|
+
3. Base letter (ascending)
|
695
|
+
4. Prefix order: `-`, `+`, no prefix
|
696
|
+
5. Suffix order: no suffix, `'`
|
697
|
+
|
512
698
|
## Compatibility and Performance
|
513
699
|
|
514
700
|
- **Ruby Version**: >= 3.2.0
|
515
701
|
- **Thread Safety**: All operations are thread-safe
|
516
702
|
- **Memory**: Efficient array-based representation
|
517
703
|
- **Performance**: O(n) parsing and generation complexity
|
704
|
+
- **Format**: Full compliance with FEEN v1.0.0 specification
|
518
705
|
|
519
706
|
## Related Resources
|
520
707
|
|
521
708
|
- [FEEN Specification v1.0.0](https://sashite.dev/documents/feen/1.0.0/) - Complete format specification
|
522
709
|
- [PNN Specification v1.0.0](https://sashite.dev/documents/pnn/1.0.0/) - Piece notation details
|
710
|
+
- [SNN Specification v1.0.0](https://sashite.dev/documents/snn/1.0.0/) - Style name notation
|
523
711
|
- [GAN Specification v1.0.0](https://sashite.dev/documents/gan/1.0.0/) - Game-qualified identifiers
|
524
712
|
|
525
713
|
## Contributing
|