feen 5.0.0.beta9 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bac9b628685776db7be71e57f80638a6479f03175da5b182154254f6aa0573e2
4
- data.tar.gz: 77f99b0ee0ce4d0a4846f5a134ab85cc35ae0919df5f75b18eb9bc688c1d1530
3
+ metadata.gz: 3038a2be98c26b830833d6f7fece5d050605da9ead72ee8cdedecf98e31f30ef
4
+ data.tar.gz: 2868f4ca7e04b472f059e982023129c36af9718ad819f07f09343ad34a0d611c
5
5
  SHA512:
6
- metadata.gz: a195a54e16d82c53e60e9001e4111800d6d6af6a04c68517599f04b4478b84b4a96ef6a0ead6034458aafc3c7aefe80a7c55cf6d9e780a0aa933cf2b8d566f16
7
- data.tar.gz: 864de377e1c97fa20f5d723865e8115468bd139b406889b44a956f55beec20f18d1e7a93fc97e033f873a0ab12c9d96f8cb82bb1ab32da48f643da0e07c6f49f
6
+ metadata.gz: 1ec8030d85d0c06bc11f63cdde1bc32a1ad2b709fb982cde496478d5c8dd9ea607c1b6089451e0ff3442857cb1936ded576143f9a7781419626905a04c3b8e01
7
+ data.tar.gz: 047e36e257acdca693fd88cceb99e946bfa3b0eefe86719f72f3addbc9adef543fc0ce82bcafe59dae7fbc7dad7ede27848550280de1a7adf288a89d795cf4ba
data/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  ![Ruby](https://github.com/sashite/feen.rb/actions/workflows/main.yml/badge.svg?branch=main)
6
6
  [![License](https://img.shields.io/github/license/sashite/feen.rb?label=License&logo=github)](https://github.com/sashite/feen.rb/raw/main/LICENSE.md)
7
7
 
8
- > A Ruby library for **FEEN** (Forsyth–Edwards Enhanced Notation) - a flexible format for representing positions in two-player piece-placement games.
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
- - **Multi-dimensional**: Supports 2D, 3D, and higher dimensions
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.beta9"
28
+ gem "feen", ">= 5.0.0.beta10"
28
29
  ```
29
30
 
30
31
  Or install it directly:
@@ -46,7 +47,7 @@ board = [["r", "k", "r"]]
46
47
  feen_string = Feen.dump(
47
48
  piece_placement: board,
48
49
  pieces_in_hand: [], # No captured pieces
49
- games_turn: ["GAME", "game"] # GAME player's turn
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[:games_turn] # => ["GAME", "game"]
66
+ position[:style_turn] # => ["GAME", "game"]
66
67
  ```
67
68
 
68
69
  ## Understanding FEEN Format
@@ -70,19 +71,23 @@ position[:games_turn] # => ["GAME", "game"]
70
71
  A FEEN string has exactly **three parts separated by single spaces**:
71
72
 
72
73
  ```
73
- <BOARD> <CAPTURED_PIECES> <TURN_INFO>
74
+ <PIECE-PLACEMENT> <PIECES-IN-HAND> <STYLE-TURN>
74
75
  ```
75
76
 
76
- ### Part 1: Board Representation
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 letters (case matters!)
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
 
@@ -92,16 +97,18 @@ The board shows where pieces are placed:
92
97
  "Kqr" # Three pieces: K, q, r
93
98
  "K2r" # K, two empty squares, then r
94
99
  "Kqr/3/R2k" # 3x3 board with multiple ranks
100
+ "+K-r/N'" # Board with piece modifiers
95
101
  ```
96
102
 
97
- ### Part 2: Captured Pieces (Pieces in Hand)
103
+ ### Part 2: Pieces in Hand
98
104
 
99
- Shows pieces that have been captured and can potentially be used again:
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
- - Count notation: `3P` means three `P` pieces
104
- - **Base form only**: No special modifiers allowed here
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
 
@@ -110,22 +117,52 @@ Shows pieces that have been captured and can potentially be used again:
110
117
  "P/" # First player has one P piece
111
118
  "/p" # Second player has one p piece
112
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
- ### Part 3: Turn Information
123
+ **Critical: Canonical Piece Sorting Algorithm**
116
124
 
117
- Shows whose turn it is and identifies the game types:
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/lowercase corresponds to piece ownership
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
163
  "CHESS/chess" # CHESS player (uppercase pieces) to move
127
164
  "shogi/SHOGI" # shogi player (lowercase pieces) to move
128
- "GAME1/game2" # GAME1 player (uppercase pieces) to move (mixed game types)
165
+ "CHESS/makruk" # Cross-style: CHESS vs makruk, CHESS to move
129
166
  ```
130
167
 
131
168
  ## Complete API Reference
@@ -138,10 +175,10 @@ 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
- - `games_turn:` [Array] - Two-element array: [active_player, inactive_player]
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
 
@@ -155,7 +192,7 @@ board = [
155
192
  feen = Feen.dump(
156
193
  piece_placement: board,
157
194
  pieces_in_hand: ["Q", "p"],
158
- games_turn: ["WHITE", "black"]
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
- - `:games_turn` - [active_player, inactive_player]
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[:games_turn]
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: [], games_turn: ["GAME", "game"] }
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")
@@ -216,7 +253,7 @@ Checks if a string is valid, canonical FEEN notation.
216
253
  ```ruby
217
254
  Feen.valid?("k/K / GAME/game") # => true
218
255
  Feen.valid?("invalid") # => false
219
- Feen.valid?("k/K P3K/ GAME/game") # => false (wrong piece order)
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
- games_turn: ["PLAYERA", "playerb"]
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
- games_turn: ["UP", "down"]
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
- games_turn: ["GAME", "game"]
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
- games_turn: ["FIRST", "second"]
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
- Captured pieces are automatically sorted in canonical order:
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
- 1. **By quantity** (most frequent first)
301
- 2. **By letter** (alphabetical within same quantity)
343
+ **Complex sorting example:**
302
344
 
303
345
  ```ruby
304
- pieces = ["B", "B", "P", "P", "P", "R", "R"]
305
- # Result: "3P2B2R/" (3P first, then 2B and 2R alphabetically)
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 (On Board Only)
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
363
  ["+P", "K", "-R"], # Enhanced pawn, King, diminished rook
317
- ["N'", "", "B"] # Knight with special state, empty, Bishop
364
+ ["N'", "", "B"] # Knight with intermediate state, empty, Bishop
318
365
  ]
319
366
 
320
- # Note: Modifiers (+, -, ') are ONLY allowed on the board
321
- # Pieces in hand must be in base form only
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"], # Base form only!
325
- games_turn: ["GAME", "game"]
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 PR/ GAME/game"
375
+ feen # => "+PK-R/N'1B P+R'/ GAME/game"
329
376
  ```
330
377
 
331
- ### Cross-Game Scenarios
378
+ ### Cross-Style Scenarios
332
379
 
333
380
  FEEN can represent positions mixing different game systems:
334
381
 
335
382
  ```ruby
336
- # FOO pieces vs bar pieces
337
- mixed_feen = Feen.dump(
338
- piece_placement: ["K", "G", "k", "r"], # Mixed piece types
339
- pieces_in_hand: ["P", "g"], # Captured from both sides
340
- games_turn: ["bar", "FOO"] # Different game systems
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
- mixed_feen # => "KGkr P/g bar/FOO"
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
@@ -352,23 +417,31 @@ mixed_feen # => "KGkr P/g bar/FOO"
352
417
  Feen.dump(
353
418
  piece_placement: "not an array", # Must be Array
354
419
  pieces_in_hand: "not an array", # Must be Array
355
- games_turn: "not an array" # Must be Array[2]
420
+ style_turn: "not an array" # Must be Array[2]
356
421
  )
357
422
  # => ArgumentError
358
423
 
359
- # ERROR: Modifiers in captured pieces
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: ["+P"], # Invalid: no modifiers allowed
363
- games_turn: ["GAME", "game"]
435
+ pieces_in_hand: [],
436
+ style_turn: ["GAME", "ALSO"] # Must be different cases
364
437
  )
365
438
  # => ArgumentError
366
439
 
367
- # ERROR: Same case in games_turn
440
+ # ERROR: Invalid style identifiers
368
441
  Feen.dump(
369
442
  piece_placement: [["K"]],
370
443
  pieces_in_hand: [],
371
- games_turn: ["GAME", "ALSO"] # Must be different cases
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
- games_turn: [current_player, opponent]
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
- games_turn: turn_info
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: [], games_turn: ["GAME", "game"] }
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
- games_turn: turn
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: Clear piece type distinctions
482
- PLAYER_1_PIECES = %w[K Q R B N P]
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
- ### 3. Round-trip Validation
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 consistency
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