feen 5.0.0.beta6 → 5.0.0.beta8

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.
data/README.md CHANGED
@@ -5,352 +5,526 @@
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
- > **FEEN** (Forsyth–Edwards Enhanced Notation) support for the Ruby language.
8
+ > A Ruby library for **FEEN** (Forsyth–Edwards Enhanced Notation) - a flexible format for representing positions in two-player piece-placement games.
9
9
 
10
10
  ## What is FEEN?
11
11
 
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.
12
+ FEEN is like taking a snapshot of any board game position and turning it into a text string. Think of it as a "save file" format that works across different board games - from Chess to Shōgi to custom variants.
13
13
 
14
- This gem implements the [FEEN Specification v1.0.0](https://sashite.dev/documents/feen/1.0.0/), providing a Ruby interface for:
14
+ **Key Features:**
15
15
 
16
- - Representing positions from various games without knowledge of specific rules
17
- - Supporting boards of arbitrary dimensions (2D, 3D, and beyond)
18
- - Encoding pieces in hand with full PNN (Piece Name Notation) support
19
- - Facilitating serialization and deserialization of positions
20
- - Ensuring canonical representation for consistent data handling
16
+ - **Versatile**: Supports Chess, Shōgi, Xiangqi, and similar games
17
+ - **Bidirectional**: Convert positions to text and back
18
+ - **Compact**: Efficient representation
19
+ - **Rule-agnostic**: No knowledge of specific game rules required
20
+ - **Multi-dimensional**: Supports 2D, 3D, and higher dimensions
21
21
 
22
- ## FEEN Format
22
+ ## Installation
23
23
 
24
- A FEEN record consists of three space-separated fields:
24
+ Add this line to your application's Gemfile:
25
25
 
26
- ```
27
- <PIECE-PLACEMENT> <PIECES-IN-HAND> <GAMES-TURN>
26
+ ```ruby
27
+ gem "feen", ">= 5.0.0.beta8"
28
28
  ```
29
29
 
30
- ### Field Details
30
+ Or install it directly:
31
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
32
+ ```bash
33
+ gem install feen --pre
34
+ ```
35
35
 
36
- ## Installation
36
+ ## Quick Start
37
37
 
38
- ```ruby
39
- # In your Gemfile
40
- gem "feen", ">= 5.0.0.beta6"
41
- ```
38
+ ### Basic Example: Converting a Position to Text
42
39
 
43
- Or install manually:
40
+ ```ruby
41
+ require "feen"
44
42
 
45
- ```sh
46
- gem install feen --pre
47
- ```
43
+ # Represent a simple 3x1 board with pieces "r", "k", "r"
44
+ board = [["r", "k", "r"]]
48
45
 
49
- ## Basic Usage
46
+ feen_string = Feen.dump(
47
+ piece_placement: board,
48
+ pieces_in_hand: [], # No captured pieces
49
+ games_turn: ["GAME", "game"] # GAME player's turn
50
+ )
50
51
 
51
- ### Parsing FEEN Strings
52
+ feen_string # => "rkr / GAME/game"
53
+ ```
52
54
 
53
- Convert a FEEN string into a structured Ruby object:
55
+ ### Basic Example: Converting Text Back to Position
54
56
 
55
57
  ```ruby
56
58
  require "feen"
57
59
 
58
- feen_string = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR - CHESS/chess"
60
+ feen_string = "rkr / GAME/game"
59
61
  position = Feen.parse(feen_string)
60
62
 
61
- # Result is a hash:
62
- # {
63
- # piece_placement: [
64
- # ["r", "n", "b", "q", "k", "b", "n", "r"],
65
- # ["p", "p", "p", "p", "p", "p", "p", "p"],
66
- # ["", "", "", "", "", "", "", ""],
67
- # ["", "", "", "", "", "", "", ""],
68
- # ["", "", "", "", "", "", "", ""],
69
- # ["", "", "", "", "", "", "", ""],
70
- # ["P", "P", "P", "P", "P", "P", "P", "P"],
71
- # ["R", "N", "B", "Q", "K", "B", "N", "R"]
72
- # ],
73
- # pieces_in_hand: [],
74
- # games_turn: ["CHESS", "chess"]
75
- # }
63
+ position[:piece_placement] # => ["r", "k", "r"]
64
+ position[:pieces_in_hand] # => []
65
+ position[:games_turn] # => ["GAME", "game"]
76
66
  ```
77
67
 
78
- ### Safe Parsing
68
+ ## Understanding FEEN Format
79
69
 
80
- Parse a FEEN string without raising exceptions:
70
+ A FEEN string has exactly **three parts separated by single spaces**:
81
71
 
82
- ```ruby
83
- require "feen"
72
+ ```
73
+ <BOARD> <CAPTURED_PIECES> <TURN_INFO>
74
+ ```
84
75
 
85
- # Valid FEEN string
86
- result = Feen.safe_parse("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR - CHESS/chess")
87
- # => {piece_placement: [...], pieces_in_hand: [...], games_turn: [...]}
76
+ ### Part 1: Board Representation
88
77
 
89
- # Invalid FEEN string
90
- result = Feen.safe_parse("invalid feen string")
91
- # => nil
92
- ```
78
+ The board shows where pieces are placed:
93
79
 
94
- ### Creating FEEN Strings
80
+ - **Pieces**: Represented by letters (case matters!)
81
+ - `K` = piece belonging to first player (uppercase)
82
+ - `k` = piece belonging to second player (lowercase)
83
+ - **Empty spaces**: Represented by numbers
84
+ - `3` = three empty squares in a row
85
+ - **Ranks (rows)**: Separated by `/`
95
86
 
96
- Convert position components to a FEEN string using named arguments:
87
+ **Examples:**
97
88
 
98
89
  ```ruby
99
- require "feen"
90
+ "K" # Single piece on 1x1 board
91
+ "3" # Three empty squares
92
+ "Kqr" # Three pieces: K, q, r
93
+ "K2r" # K, two empty squares, then r
94
+ "Kqr/3/R2k" # 3x3 board with multiple ranks
95
+ ```
100
96
 
101
- # Representation of a chess board in initial position
102
- piece_placement = [
103
- ["r", "n", "b", "q", "k", "b", "n", "r"],
104
- ["p", "p", "p", "p", "p", "p", "p", "p"],
105
- ["", "", "", "", "", "", "", ""],
106
- ["", "", "", "", "", "", "", ""],
107
- ["", "", "", "", "", "", "", ""],
108
- ["", "", "", "", "", "", "", ""],
109
- ["P", "P", "P", "P", "P", "P", "P", "P"],
110
- ["R", "N", "B", "Q", "K", "B", "N", "R"]
111
- ]
97
+ ### Part 2: Captured Pieces (Pieces in Hand)
112
98
 
113
- result = Feen.dump(
114
- piece_placement: piece_placement,
115
- games_turn: %w[CHESS chess],
116
- pieces_in_hand: []
117
- )
118
- # => "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR - CHESS/chess"
119
- ```
99
+ Shows pieces that have been captured and can potentially be used again:
120
100
 
121
- ### Validation
101
+ - Format: `UPPERCASE_PIECES/lowercase_pieces`
102
+ - **Always separated by `/`** even if empty
103
+ - Count notation: `3P` means three `P` pieces
104
+ - **Base form only**: No special modifiers allowed here
122
105
 
123
- Check if a string is valid FEEN notation and in canonical form:
106
+ **Examples:**
124
107
 
125
108
  ```ruby
126
- require "feen"
109
+ "/" # No pieces captured
110
+ "P/" # First player has one P piece
111
+ "/p" # Second player has one p piece
112
+ "2PK/3p" # First player: 2 P's + 1 K, Second player: 3 p's
113
+ ```
114
+
115
+ ### Part 3: Turn Information
127
116
 
128
- # Canonical form
129
- Feen.valid?("lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL - SHOGI/shogi")
130
- # => true
117
+ Shows whose turn it is and identifies the game types:
131
118
 
132
- # Invalid syntax
133
- Feen.valid?("invalid feen string")
134
- # => false
119
+ - Format: `ACTIVE_PLAYER/INACTIVE_PLAYER`
120
+ - **One must be uppercase, other lowercase**
121
+ - The uppercase/lowercase corresponds to piece ownership
135
122
 
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)
123
+ **Examples:**
124
+
125
+ ```ruby
126
+ "CHESS/chess" # CHESS player (uppercase pieces) to move
127
+ "shogi/SHOGI" # shogi player (lowercase pieces) to move
128
+ "GAME1/game2" # Mixed game types
139
129
  ```
140
130
 
141
- The `valid?` method performs two levels of validation:
131
+ ## Complete API Reference
142
132
 
143
- 1. **Syntax check**: Verifies the string can be parsed as FEEN
144
- 2. **Canonicity check**: Ensures the string is in its canonical form through round-trip conversion
133
+ ### Core Methods
145
134
 
146
- ## Game Examples
135
+ #### `Feen.dump(**options)`
147
136
 
148
- As FEEN is rule-agnostic, it can represent positions from various board games. Here are some examples:
137
+ Converts position components into a FEEN string.
149
138
 
150
- ### International Chess
139
+ **Parameters:**
140
+ - `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]
143
+
144
+ **Returns:** String - FEEN notation
145
+
146
+ **Example:**
151
147
 
152
148
  ```ruby
153
- feen_string = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR - CHESS/chess"
149
+ board = [
150
+ ["r", "n", "k", "n", "r"], # Back rank
151
+ ["", "", "", "", ""], # Empty rank
152
+ ["P", "P", "P", "P", "P"] # Front rank
153
+ ]
154
+
155
+ feen = Feen.dump(
156
+ piece_placement: board,
157
+ pieces_in_hand: ["Q", "p"],
158
+ games_turn: ["WHITE", "black"]
159
+ )
160
+ # => "rnknr/5/PPPPP Q/p WHITE/black"
154
161
  ```
155
162
 
156
- In this initial chess position, the third field `CHESS/chess` indicates it's the player with uppercase pieces' turn to move.
163
+ #### `Feen.parse(feen_string)`
164
+
165
+ Converts a FEEN string back into position components.
166
+
167
+ **Parameters:**
168
+
169
+ - `feen_string` [String] - Valid FEEN notation
170
+
171
+ **Returns:** Hash with keys:
157
172
 
158
- ### Shogi (Japanese Chess)
173
+ - `:piece_placement` - The board as nested arrays
174
+ - `:pieces_in_hand` - Captured pieces as array of strings
175
+ - `:games_turn` - [active_player, inactive_player]
176
+
177
+ **Example:**
159
178
 
160
179
  ```ruby
161
- feen_string = "lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL - SHOGI/shogi"
180
+ position = Feen.parse("rnknr/5/PPPPP Q/p WHITE/black")
181
+
182
+ position[:piece_placement]
183
+ # => [["r", "n", "k", "n", "r"], ["", "", "", "", ""], ["P", "P", "P", "P", "P"]]
184
+
185
+ position[:pieces_in_hand]
186
+ # => ["Q", "p"]
187
+
188
+ position[:games_turn]
189
+ # => ["WHITE", "black"]
162
190
  ```
163
191
 
164
- **With pieces in hand and promotions:**
192
+ #### `Feen.safe_parse(feen_string)`
193
+
194
+ Like `parse()` but returns `nil` instead of raising exceptions for invalid input.
195
+
196
+ **Example:**
165
197
 
166
198
  ```ruby
167
- feen_string = "lnsgk3l/5g3/p1ppB2pp/9/8B/2P6/P2PPPPPP/3K3R1/5rSNL 5P2g2sln SHOGI/shogi"
199
+ # Valid input
200
+ result = Feen.safe_parse("k/K / GAME/game")
201
+ # => { piece_placement: [["k"], ["K"]], pieces_in_hand: [], games_turn: ["GAME", "game"] }
202
+
203
+ # Invalid input
204
+ result = Feen.safe_parse("invalid")
205
+ # => nil
168
206
  ```
169
207
 
170
- In this shogi position:
171
- - The format supports promotions with the `+` prefix (e.g., `+P` for a promoted pawn)
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)
175
- - `SHOGI/shogi` indicates it's Sente's (Black's, uppercase) turn to move
208
+ #### `Feen.valid?(feen_string)`
209
+
210
+ Checks if a string is valid, canonical FEEN notation.
211
+
212
+ **Returns:** Boolean
176
213
 
177
- ### Makruk (Thai Chess)
214
+ **Example:**
178
215
 
179
216
  ```ruby
180
- feen_string = "rnbqkbnr/8/pppppppp/8/8/PPPPPPPP/8/RNBKQBNR - MAKRUK/makruk"
217
+ Feen.valid?("k/K / GAME/game") # => true
218
+ Feen.valid?("invalid") # => false
219
+ Feen.valid?("k/K P3K/ GAME/game") # => false (wrong piece order)
181
220
  ```
182
221
 
183
- ### Xiangqi (Chinese Chess)
222
+ ## Working with Different Board Sizes
223
+
224
+ ### Standard 2D Boards
184
225
 
185
226
  ```ruby
186
- feen_string = "rheagaehr/9/1c5c1/s1s1s1s1s/9/9/S1S1S1S1S/1C5C1/9/RHEAGAEHR - XIANGQI/xiangqi"
187
- ```
227
+ # 8x8 chess-like board (empty)
228
+ board = Array.new(8) { Array.new(8, "") }
188
229
 
189
- ## Advanced Features
230
+ # 9x9 board with pieces in corners
231
+ board = Array.new(9) { Array.new(9, "") }
232
+ board[0][0] = "r" # Top-left
233
+ board[8][8] = "R" # Bottom-right
190
234
 
191
- ### Piece Name Notation (PNN) Support
235
+ feen = Feen.dump(
236
+ piece_placement: board,
237
+ pieces_in_hand: [],
238
+ games_turn: ["PLAYERA", "playerb"]
239
+ )
192
240
 
193
- FEEN supports the complete [PNN specification](https://sashite.dev/documents/pnn/1.0.0/) for representing pieces with state modifiers:
241
+ feen # => "r8/9/9/9/9/9/9/9/8R / PLAYERA/playerb"
242
+ ```
194
243
 
195
- #### PNN Modifiers
244
+ ### 3D Boards
196
245
 
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)
246
+ ```ruby
247
+ # Simple 2x2x2 cube
248
+ board_3d = [
249
+ [["a", "b"], ["c", "d"]], # First layer
250
+ [["A", "B"], ["C", "D"]] # Second layer
251
+ ]
200
252
 
201
- #### Examples with PNN
253
+ feen = Feen.dump(
254
+ piece_placement: board_3d,
255
+ pieces_in_hand: [],
256
+ games_turn: ["UP", "down"]
257
+ )
258
+ # => "ab/cd//AB/CD / UP/down"
259
+ ```
260
+
261
+ ### Irregular Boards
202
262
 
203
263
  ```ruby
204
- # Shogi position with promoted pieces on board
205
- piece_placement = [
206
- ["", "", "", "", "+P", "", "", "", ""] # Promoted pawn on board
207
- # ... other ranks
264
+ # Different sized ranks are allowed
265
+ irregular_board = [
266
+ ["r", "k", "r"], # 3 squares
267
+ ["p", "p"], # 2 squares
268
+ ["P", "P", "P", "P"] # 4 squares
208
269
  ]
209
270
 
210
- # Pieces in hand with PNN modifiers
211
- pieces_in_hand = ["+P", "+P", "+P", "B'", "B'", "-p", "P"]
271
+ feen = Feen.dump(
272
+ piece_placement: irregular_board,
273
+ pieces_in_hand: [],
274
+ games_turn: ["GAME", "game"]
275
+ )
276
+ # => "rkr/pp/PPPP / GAME/game"
277
+ ```
278
+
279
+ ## Working with Captured Pieces
212
280
 
213
- result = Feen.dump(
214
- piece_placement: piece_placement,
215
- pieces_in_hand: pieces_in_hand,
216
- games_turn: %w[SHOGI shogi]
281
+ ### Basic Captures
282
+
283
+ ```ruby
284
+ # Player 1 captured 3 pawns and 1 rook
285
+ # Player 2 captured 2 pawns
286
+ captured = ["P", "P", "P", "R", "p", "p"]
287
+
288
+ feen = Feen.dump(
289
+ piece_placement: [["k"], ["K"]], # Minimal board
290
+ pieces_in_hand: captured,
291
+ games_turn: ["FIRST", "second"]
217
292
  )
218
- # => "8/8/8/8/4+P4/8/8/8/8 3+P2B'-pP SHOGI/shogi"
293
+ # => "k/K 3PR/2p FIRST/second"
219
294
  ```
220
295
 
221
- ### Canonical Pieces in Hand Sorting
296
+ ### Understanding Piece Sorting
222
297
 
223
- FEEN enforces canonical ordering of pieces in hand according to the specification:
298
+ Captured pieces are automatically sorted in canonical order:
224
299
 
225
- 1. **By quantity (descending)**
226
- 2. **By complete PNN representation (alphabetically ascending)**
300
+ 1. **By quantity** (most frequent first)
301
+ 2. **By letter** (alphabetical within same quantity)
227
302
 
228
303
  ```ruby
229
- # Input pieces in any order
230
- pieces = ["P", "B", "P", "+P", "B", "P", "+P", "+P"]
304
+ pieces = ["B", "B", "P", "P", "P", "R", "R"]
305
+ # Result: "3P2B2R/" (3P first, then 2B and 2R alphabetically)
306
+ ```
307
+
308
+ ## Advanced Features
309
+
310
+ ### Special Piece States (On Board Only)
311
+
312
+ For games that need special piece states, use modifiers **only on the board**:
231
313
 
232
- result = Feen::Dumper::PiecesInHand.dump(*pieces)
233
- # => "3+P3P2B"
234
- # Breakdown: 3×+P (most frequent), 3×P, 2×B (alphabetical within same quantity)
314
+ ```ruby
315
+ board = [
316
+ ["+P", "K", "-R"], # Enhanced pawn, King, diminished rook
317
+ ["N'", "", "B"] # Knight with special state, empty, Bishop
318
+ ]
319
+
320
+ # Note: Modifiers (+, -, ') are ONLY allowed on the board
321
+ # Pieces in hand must be in base form only
322
+ feen = Feen.dump(
323
+ piece_placement: board,
324
+ pieces_in_hand: ["P", "R"], # Base form only!
325
+ games_turn: ["GAME", "game"]
326
+ )
327
+
328
+ feen # => "+PK-R/N'1B PR/ GAME/game"
235
329
  ```
236
330
 
237
- #### Complex Example from FEEN Specification
331
+ ### Cross-Game Scenarios
332
+
333
+ FEEN can represent positions mixing different game systems:
238
334
 
239
335
  ```ruby
240
- # From FEEN spec v1.0.0 example
241
- pieces = (["P"] * 10) + (["K"] * 5) + (["B"] * 3) + (["p'"] * 2) + ["+P", "-p", "R", "b", "q"]
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
341
+ )
242
342
 
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)
343
+ mixed_feen # => "KGkr P/g bar/FOO"
246
344
  ```
247
345
 
248
- ### Multi-dimensional Boards
346
+ ## Error Handling
249
347
 
250
- FEEN supports arbitrary-dimensional board configurations:
348
+ ### Common Errors and Solutions
251
349
 
252
350
  ```ruby
253
- require "feen"
351
+ # ERROR: Wrong argument types
352
+ Feen.dump(
353
+ piece_placement: "not an array", # Must be Array
354
+ pieces_in_hand: "not an array", # Must be Array
355
+ games_turn: "not an array" # Must be Array[2]
356
+ )
357
+ # => ArgumentError
254
358
 
255
- # 3D board (2×2×3 configuration)
256
- piece_placement = [
257
- [
258
- %w[r n b],
259
- %w[q k p]
260
- ],
261
- [
262
- ["P", "R", ""],
263
- ["", "K", "Q"]
264
- ]
265
- ]
359
+ # ERROR: Modifiers in captured pieces
360
+ Feen.dump(
361
+ piece_placement: [["K"]],
362
+ pieces_in_hand: ["+P"], # Invalid: no modifiers allowed
363
+ games_turn: ["GAME", "game"]
364
+ )
365
+ # => ArgumentError
266
366
 
267
- result = Feen.dump(
268
- piece_placement: piece_placement,
269
- games_turn: %w[FOO bar],
270
- pieces_in_hand: []
367
+ # ERROR: Same case in games_turn
368
+ Feen.dump(
369
+ piece_placement: [["K"]],
370
+ pieces_in_hand: [],
371
+ games_turn: ["GAME", "ALSO"] # Must be different cases
271
372
  )
272
- # => "rnb/qkp//PR1/1KQ - FOO/bar"
373
+ # => ArgumentError
273
374
  ```
274
375
 
275
- ### Hybrid Games
276
-
277
- FEEN supports hybrid games mixing different piece sets:
376
+ ### Safe Parsing for User Input
278
377
 
279
378
  ```ruby
280
- # Chess-Shogi hybrid position
281
- feen_string = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR 3+P2B'-pP CHESS/shogi"
379
+ def process_user_feen(user_input)
380
+ position = Feen.safe_parse(user_input)
381
+
382
+ if position
383
+ puts "Valid position with #{position[:pieces_in_hand].size} captured pieces"
384
+ # Process the position...
385
+ else
386
+ puts "Invalid FEEN format. Please check your input."
387
+ end
388
+ end
282
389
  ```
283
390
 
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
391
+ ## Real-World Examples
392
+
393
+ ### Save/Load Game State
288
394
 
289
- ## Round-trip Consistency
395
+ ```ruby
396
+ class GameState
397
+ def save_position(board, captured, current_player, opponent)
398
+ feen = Feen.dump(
399
+ piece_placement: board,
400
+ pieces_in_hand: captured,
401
+ games_turn: [current_player, opponent]
402
+ )
403
+
404
+ File.write("game_save.feen", feen)
405
+ end
406
+
407
+ def load_position(filename)
408
+ feen_string = File.read(filename)
409
+ Feen.parse(feen_string)
410
+ rescue => e
411
+ warn "Could not load game: #{e.message}"
412
+ nil
413
+ end
414
+ end
415
+ ```
290
416
 
291
- FEEN.rb guarantees round-trip consistency - parsing and dumping produces identical canonical strings:
417
+ ### Position Database
292
418
 
293
419
  ```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)
420
+ class PositionDatabase
421
+ def initialize
422
+ @positions = {}
423
+ end
424
+
425
+ def store_position(name, board, captured, turn_info)
426
+ feen = Feen.dump(
427
+ piece_placement: board,
428
+ pieces_in_hand: captured,
429
+ games_turn: turn_info
430
+ )
431
+
432
+ @positions[name] = feen
433
+ end
434
+
435
+ def retrieve_position(name)
436
+ feen = @positions[name]
437
+ return nil unless feen
438
+
439
+ Feen.parse(feen)
440
+ end
441
+
442
+ def validate_all_positions
443
+ @positions.each do |name, feen|
444
+ puts "Invalid position: #{name}" unless Feen.valid?(feen)
445
+ end
446
+ end
447
+ end
297
448
 
298
- original == dumped # => true (guaranteed canonical form)
449
+ # Usage example:
450
+ db = PositionDatabase.new
451
+ db.store_position("start", [["r", "k", "r"]], [], ["GAME", "game"])
452
+ position = db.retrieve_position("start")
453
+ # => {piece_placement: [["r", "k", "r"]], pieces_in_hand: [], games_turn: ["GAME", "game"]}
299
454
  ```
300
455
 
301
- ## Error Handling
456
+ ## Best Practices
302
457
 
303
- ### Validation Errors
458
+ ### 1. Always Validate Input
304
459
 
305
460
  ```ruby
306
- # Invalid PNN format
307
- Feen::Dumper::PiecesInHand.dump("++P")
308
- # => ArgumentError: Invalid PNN format: '++P'
461
+ def create_feen_safely(board, captured, turn)
462
+ # Validate before creating
463
+ return nil unless board.is_a?(Array)
464
+ return nil unless captured.is_a?(Array)
465
+ return nil unless turn.is_a?(Array) && turn.size == 2
466
+
467
+ Feen.dump(
468
+ piece_placement: board,
469
+ pieces_in_hand: captured,
470
+ games_turn: turn
471
+ )
472
+ rescue ArgumentError => e
473
+ puts "FEEN creation failed: #{e.message}"
474
+ nil
475
+ end
476
+ ```
309
477
 
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
478
+ ### 2. Use Consistent Naming
479
+
480
+ ```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 = {
487
+ chess_white: "CHESS",
488
+ chess_black: "chess",
489
+ shogi_sente: "SHOGI",
490
+ shogi_gote: "shogi"
491
+ }
317
492
  ```
318
493
 
319
- ### Safe Operations
494
+ ### 3. Round-trip Validation
320
495
 
321
496
  ```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"
497
+ def verify_feen_consistency(original_feen)
498
+ # Parse and re-dump to check consistency
499
+ position = Feen.parse(original_feen)
500
+ regenerated = Feen.dump(**position)
501
+
502
+ if original_feen == regenerated
503
+ puts "✓ FEEN is canonical"
504
+ else
505
+ puts "✗ FEEN inconsistency detected"
506
+ puts "Original: #{original_feen}"
507
+ puts "Regenerated: #{regenerated}"
508
+ end
330
509
  end
331
510
  ```
332
511
 
333
- ## Performance Considerations
334
-
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
512
+ ## Compatibility and Performance
339
513
 
340
- ## Compatibility
514
+ - **Ruby Version**: >= 3.2.0
515
+ - **Thread Safety**: All operations are thread-safe
516
+ - **Memory**: Efficient array-based representation
517
+ - **Performance**: O(n) parsing and generation complexity
341
518
 
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)
519
+ ## Related Resources
346
520
 
347
- ## Related Specifications
521
+ - [FEEN Specification v1.0.0](https://sashite.dev/documents/feen/1.0.0/) - Complete format specification
522
+ - [PNN Specification v1.0.0](https://sashite.dev/documents/pnn/1.0.0/) - Piece notation details
523
+ - [GAN Specification v1.0.0](https://sashite.dev/documents/gan/1.0.0/) - Game-qualified identifiers
348
524
 
349
- FEEN is part of a family of specifications for abstract strategy games:
525
+ ## Contributing
350
526
 
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
527
+ Bug reports and pull requests are welcome on GitHub at https://github.com/sashite/feen.rb.
354
528
 
355
529
  ## License
356
530