feen 5.0.0.beta7 → 5.0.0.beta9

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,390 +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.beta9"
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, formatted as `"UPPERCASE/lowercase"` and sorted canonically within each section
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.beta7"
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 in uppercase section)
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" # GAME1 player (uppercase pieces) to move (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)`
157
164
 
158
- ### Shogi (Japanese Chess)
165
+ Converts a FEEN string back into position components.
159
166
 
160
- ```ruby
161
- feen_string = "lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL / SHOGI/shogi"
162
- ```
167
+ **Parameters:**
168
+
169
+ - `feen_string` [String] - Valid FEEN notation
170
+
171
+ **Returns:** Hash with keys:
163
172
 
164
- **With pieces in hand and promotions:**
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:**
165
178
 
166
179
  ```ruby
167
- feen_string = "lnsgk3l/5g3/p1ppB2pp/9/8B/2P6/P2PPPPPP/3K3R1/5rSNL 5P2G2L/2gln2s 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"]
168
190
  ```
169
191
 
170
- In this shogi position:
192
+ #### `Feen.safe_parse(feen_string)`
171
193
 
172
- - The format supports promotions with the `+` prefix (e.g., `+P` for a promoted pawn)
173
- - Pieces in hand are separated by case: `5P2G2L/2gln2s`
174
- - **Uppercase section** (Sente): 5 Pawns, 2 Golds, 2 Lances
175
- - **Lowercase section** (Gote): 2 golds, lance, knight, 2 silvers
176
- - Each section is sorted by quantity (descending) then alphabetically
177
- - `SHOGI/shogi` indicates it's Sente's (Black's, uppercase) turn to move
194
+ Like `parse()` but returns `nil` instead of raising exceptions for invalid input.
178
195
 
179
- ### Makruk (Thai Chess)
196
+ **Example:**
180
197
 
181
198
  ```ruby
182
- feen_string = "rnbqkbnr/8/pppppppp/8/8/PPPPPPPP/8/RNBKQBNR / MAKRUK/makruk"
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
183
206
  ```
184
207
 
185
- ### Xiangqi (Chinese Chess)
208
+ #### `Feen.valid?(feen_string)`
209
+
210
+ Checks if a string is valid, canonical FEEN notation.
211
+
212
+ **Returns:** Boolean
213
+
214
+ **Example:**
186
215
 
187
216
  ```ruby
188
- feen_string = "rheagaehr/9/1c5c1/s1s1s1s1s/9/9/S1S1S1S1S/1C5C1/9/RHEAGAEHR / XIANGQI/xiangqi"
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)
189
220
  ```
190
221
 
191
- ## Advanced Features
192
-
193
- ### Pieces in Hand with Case Separation
222
+ ## Working with Different Board Sizes
194
223
 
195
- FEEN uses case separation for pieces in hand to distinguish between players using the format `"UPPERCASE_PIECES/LOWERCASE_PIECES"`:
224
+ ### Standard 2D Boards
196
225
 
197
226
  ```ruby
198
- require "feen"
227
+ # 8x8 chess-like board (empty)
228
+ board = Array.new(8) { Array.new(8, "") }
199
229
 
200
- # Parse pieces in hand with case separation
201
- pieces_in_hand = Feen.parse("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR 3P2B/2pn CHESS/chess")[:pieces_in_hand]
202
- # => ["B", "B", "P", "P", "P", "n", "p", "p"] # Sorted alphabetically
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
203
234
 
204
- # Create FEEN with pieces in hand
205
- result = Feen.dump(
206
- piece_placement: [["k"], ["K"]],
207
- pieces_in_hand: ["P", "P", "B", "p", "n"],
208
- games_turn: ["TEST", "test"]
235
+ feen = Feen.dump(
236
+ piece_placement: board,
237
+ pieces_in_hand: [],
238
+ games_turn: ["PLAYERA", "playerb"]
209
239
  )
210
- # => "k/K 2BP/np TEST/test"
211
- ```
212
240
 
213
- ### Piece Name Notation (PNN) Support
241
+ feen # => "r8/9/9/9/9/9/9/9/8R / PLAYERA/playerb"
242
+ ```
214
243
 
215
- FEEN supports the complete [PNN specification](https://sashite.dev/documents/pnn/1.0.0/) for representing pieces with state modifiers:
244
+ ### 3D Boards
216
245
 
217
- #### PNN Modifiers
246
+ ```ruby
247
+ # Simple 2x2x2 cube
248
+ board_3d = [
249
+ [["a", "b"], ["c", "d"]], # First layer
250
+ [["A", "B"], ["C", "D"]] # Second layer
251
+ ]
218
252
 
219
- - **Prefix `+`**: Enhanced state (e.g., promoted pieces in shogi)
220
- - **Prefix `-`**: Diminished state (e.g., restricted movement)
221
- - **Suffix `'`**: Intermediate state (e.g., castling rights, en passant eligibility)
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
+ ```
222
260
 
223
- #### Examples with PNN
261
+ ### Irregular Boards
224
262
 
225
263
  ```ruby
226
- # Shogi position with promoted pieces on board
227
- piece_placement = [
228
- ["", "", "", "", "+P", "", "", "", ""] # Promoted pawn on board
229
- # ... 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
230
269
  ]
231
270
 
232
- # Pieces in hand with PNN modifiers - case separated
233
- 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
280
+
281
+ ### Basic Captures
234
282
 
235
- result = Feen.dump(
236
- piece_placement: piece_placement,
237
- pieces_in_hand: pieces_in_hand,
238
- games_turn: %w[SHOGI shogi]
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"]
239
292
  )
240
- # => "8/8/8/8/4+P4/8/8/8/8 3+P2B'P/-p SHOGI/shogi"
293
+ # => "k/K 3PR/2p FIRST/second"
241
294
  ```
242
295
 
243
- ### Canonical Pieces in Hand Sorting
296
+ ### Understanding Piece Sorting
297
+
298
+ Captured pieces are automatically sorted in canonical order:
299
+
300
+ 1. **By quantity** (most frequent first)
301
+ 2. **By letter** (alphabetical within same quantity)
302
+
303
+ ```ruby
304
+ pieces = ["B", "B", "P", "P", "P", "R", "R"]
305
+ # Result: "3P2B2R/" (3P first, then 2B and 2R alphabetically)
306
+ ```
244
307
 
245
- FEEN enforces canonical ordering of pieces in hand within each case section according to the specification:
308
+ ## Advanced Features
246
309
 
247
- 1. **By quantity (descending)**
248
- 2. **By complete PNN representation (alphabetically ascending)**
310
+ ### Special Piece States (On Board Only)
249
311
 
250
- The dumper organizes pieces by case first, then applies canonical sorting within each section:
312
+ For games that need special piece states, use modifiers **only on the board**:
251
313
 
252
314
  ```ruby
253
- # Input pieces in any order
254
- pieces = ["P", "b", "P", "+P", "B", "p", "+P", "+P"]
315
+ board = [
316
+ ["+P", "K", "-R"], # Enhanced pawn, King, diminished rook
317
+ ["N'", "", "B"] # Knight with special state, empty, Bishop
318
+ ]
255
319
 
256
- result = Feen.dump(
257
- piece_placement: [["k"], ["K"]],
258
- pieces_in_hand: pieces,
259
- games_turn: %w[GAME game]
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"]
260
326
  )
261
- # => "k/K 3+P2PB/bp GAME/game"
262
- # Breakdown:
263
- # - Uppercase: 3×+P (most frequent), 2×P, 1×B (alphabetical within same quantity)
264
- # - Lowercase: 1×b, 1×p (alphabetical)
327
+
328
+ feen # => "+PK-R/N'1B PR/ GAME/game"
265
329
  ```
266
330
 
267
- The parser returns pieces in simple alphabetical order for easy handling:
331
+ ### Cross-Game Scenarios
332
+
333
+ FEEN can represent positions mixing different game systems:
268
334
 
269
335
  ```ruby
270
- pieces_in_hand = Feen.parse("k/K 3+P2PB/bp GAME/game")[:pieces_in_hand]
271
- # => ["+P", "+P", "+P", "B", "P", "P", "b", "p"] # Alphabetically sorted
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
+ )
342
+
343
+ mixed_feen # => "KGkr P/g bar/FOO"
272
344
  ```
273
345
 
274
- ### Multi-dimensional Boards
346
+ ## Error Handling
275
347
 
276
- FEEN supports arbitrary-dimensional board configurations:
348
+ ### Common Errors and Solutions
277
349
 
278
350
  ```ruby
279
- 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
280
358
 
281
- # 3D board (2×2×3 configuration)
282
- piece_placement = [
283
- [
284
- %w[r n b],
285
- %w[q k p]
286
- ],
287
- [
288
- ["P", "R", ""],
289
- ["", "K", "Q"]
290
- ]
291
- ]
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
292
366
 
293
- result = Feen.dump(
294
- piece_placement: piece_placement,
295
- games_turn: %w[FOO bar],
296
- 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
297
372
  )
298
- # => "rnb/qkp//PR1/1KQ / FOO/bar"
373
+ # => ArgumentError
299
374
  ```
300
375
 
301
- ### Hybrid Games
302
-
303
- FEEN supports hybrid games mixing different piece sets:
376
+ ### Safe Parsing for User Input
304
377
 
305
378
  ```ruby
306
- # Chess-Shogi hybrid position
307
- feen_string = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR 3+P2B'/p 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
308
389
  ```
309
390
 
310
- This represents a position where:
391
+ ## Real-World Examples
311
392
 
312
- - The board uses chess-style pieces
313
- - Pieces in hand use shogi-style promotion (`+P`) and intermediate states (`B'`)
314
- - Chess player to move, against shogi player
315
- - Case separation shows which player has which pieces
393
+ ### Save/Load Game State
316
394
 
317
- ## 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
+ ```
318
416
 
319
- FEEN.rb guarantees round-trip consistency - parsing and dumping produces identical canonical strings:
417
+ ### Position Database
320
418
 
321
419
  ```ruby
322
- original = "lnsgk3l/5g3/p1ppB2pp/9/8B/2P6/P2PPPPPP/3K3R1/5rSNL 5P2G2L/2gln2s SHOGI/shogi"
323
- parsed = Feen.parse(original)
324
- 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
325
448
 
326
- 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"] }
327
454
  ```
328
455
 
329
- ## Error Handling
456
+ ## Best Practices
330
457
 
331
- ### Validation Errors
458
+ ### 1. Always Validate Input
332
459
 
333
460
  ```ruby
334
- # Invalid PNN format
335
- Feen.dump(
336
- piece_placement: [["k"]],
337
- pieces_in_hand: ["++P"], # Invalid: double prefix
338
- games_turn: %w[GAME game]
339
- )
340
- # => ArgumentError: Invalid format at index: 0, value: '++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
+ ```
341
477
 
342
- # Invalid games turn
343
- Feen.dump(
344
- piece_placement: [["P"]],
345
- pieces_in_hand: [],
346
- games_turn: %w[BOTH_UPPERCASE ALSO_UPPERCASE] # Both same case
347
- )
348
- # => ArgumentError: One variant must be uppercase and the other lowercase
478
+ ### 2. Use Consistent Naming
349
479
 
350
- # Invalid pieces in hand format (parsing)
351
- Feen.parse("8/8/8/8/8/8/8/8 NoSeparator CHESS/chess")
352
- # => ArgumentError: Invalid pieces in hand format: NoSeparator
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
+ }
353
492
  ```
354
493
 
355
- ### Safe Operations
494
+ ### 3. Round-trip Validation
356
495
 
357
496
  ```ruby
358
- # Use safe_parse for user input
359
- user_input = gets.chomp
360
- position = Feen.safe_parse(user_input)
361
-
362
- if position
363
- puts "Valid FEEN position!"
364
- puts "Pieces in hand: #{position[:pieces_in_hand]}"
365
- else
366
- 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
367
509
  end
368
510
  ```
369
511
 
370
- ## Performance Considerations
371
-
372
- - **Parsing**: Optimized recursive descent parser with O(n) complexity
373
- - **Case separation**: Efficient single-pass processing for pieces in hand
374
- - **Validation**: Round-trip validation ensures canonical form
375
- - **Memory**: Efficient array-based representation for large boards
376
- - **Sorting**: In-place canonical sorting for pieces in hand
512
+ ## Compatibility and Performance
377
513
 
378
- ## 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
379
518
 
380
- - **Ruby version**: >= 3.2.0
381
- - **FEEN specification**: v1.0.0 compliant
382
- - **PNN specification**: v1.0.0 compliant
383
- - **Thread safety**: All operations are thread-safe (no shared mutable state)
519
+ ## Related Resources
384
520
 
385
- ## 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
386
524
 
387
- FEEN is part of a family of specifications for abstract strategy games:
525
+ ## Contributing
388
526
 
389
- - [FEEN Specification v1.0.0](https://sashite.dev/documents/feen/1.0.0/) - Board position notation
390
- - [PNN Specification v1.0.0](https://sashite.dev/documents/pnn/1.0.0/) - Piece name notation
391
- - [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.
392
528
 
393
529
  ## License
394
530