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.
- checksums.yaml +4 -4
- data/README.md +390 -216
- data/lib/feen/dumper/games_turn.rb +32 -29
- data/lib/feen/dumper/piece_placement.rb +85 -63
- data/lib/feen/dumper/pieces_in_hand.rb +91 -62
- data/lib/feen/dumper.rb +2 -2
- data/lib/feen/parser/games_turn.rb +36 -16
- data/lib/feen/parser/piece_placement.rb +78 -564
- data/lib/feen/parser/pieces_in_hand.rb +104 -138
- data/lib/feen/parser.rb +3 -3
- data/lib/feen.rb +20 -9
- metadata +1 -11
- data/lib/feen/dumper/pieces_in_hand/errors.rb +0 -12
- data/lib/feen/dumper/pieces_in_hand/no_pieces.rb +0 -10
- data/lib/feen/parser/games_turn/errors.rb +0 -14
- data/lib/feen/parser/games_turn/valid_games_turn_pattern.rb +0 -24
- data/lib/feen/parser/pieces_in_hand/canonical_sorter.rb +0 -70
- data/lib/feen/parser/pieces_in_hand/errors.rb +0 -17
- data/lib/feen/parser/pieces_in_hand/no_pieces.rb +0 -10
- data/lib/feen/parser/pieces_in_hand/piece_count_pattern.rb +0 -13
- data/lib/feen/parser/pieces_in_hand/pnn_patterns.rb +0 -47
- data/lib/feen/parser/pieces_in_hand/valid_format_pattern.rb +0 -29
data/README.md
CHANGED
@@ -5,352 +5,526 @@
|
|
5
5
|

|
6
6
|
[](https://github.com/sashite/feen.rb/raw/main/LICENSE.md)
|
7
7
|
|
8
|
-
> **FEEN** (Forsyth–Edwards Enhanced Notation)
|
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
|
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
|
-
|
14
|
+
**Key Features:**
|
15
15
|
|
16
|
-
-
|
17
|
-
-
|
18
|
-
-
|
19
|
-
-
|
20
|
-
-
|
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
|
-
##
|
22
|
+
## Installation
|
23
23
|
|
24
|
-
|
24
|
+
Add this line to your application's Gemfile:
|
25
25
|
|
26
|
-
```
|
27
|
-
|
26
|
+
```ruby
|
27
|
+
gem "feen", ">= 5.0.0.beta8"
|
28
28
|
```
|
29
29
|
|
30
|
-
|
30
|
+
Or install it directly:
|
31
31
|
|
32
|
-
|
33
|
-
|
34
|
-
|
32
|
+
```bash
|
33
|
+
gem install feen --pre
|
34
|
+
```
|
35
35
|
|
36
|
-
##
|
36
|
+
## Quick Start
|
37
37
|
|
38
|
-
|
39
|
-
# In your Gemfile
|
40
|
-
gem "feen", ">= 5.0.0.beta6"
|
41
|
-
```
|
38
|
+
### Basic Example: Converting a Position to Text
|
42
39
|
|
43
|
-
|
40
|
+
```ruby
|
41
|
+
require "feen"
|
44
42
|
|
45
|
-
|
46
|
-
|
47
|
-
```
|
43
|
+
# Represent a simple 3x1 board with pieces "r", "k", "r"
|
44
|
+
board = [["r", "k", "r"]]
|
48
45
|
|
49
|
-
|
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
|
-
|
52
|
+
feen_string # => "rkr / GAME/game"
|
53
|
+
```
|
52
54
|
|
53
|
-
|
55
|
+
### Basic Example: Converting Text Back to Position
|
54
56
|
|
55
57
|
```ruby
|
56
58
|
require "feen"
|
57
59
|
|
58
|
-
feen_string = "
|
60
|
+
feen_string = "rkr / GAME/game"
|
59
61
|
position = Feen.parse(feen_string)
|
60
62
|
|
61
|
-
#
|
62
|
-
#
|
63
|
-
#
|
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
|
-
|
68
|
+
## Understanding FEEN Format
|
79
69
|
|
80
|
-
|
70
|
+
A FEEN string has exactly **three parts separated by single spaces**:
|
81
71
|
|
82
|
-
```
|
83
|
-
|
72
|
+
```
|
73
|
+
<BOARD> <CAPTURED_PIECES> <TURN_INFO>
|
74
|
+
```
|
84
75
|
|
85
|
-
|
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
|
-
|
90
|
-
result = Feen.safe_parse("invalid feen string")
|
91
|
-
# => nil
|
92
|
-
```
|
78
|
+
The board shows where pieces are placed:
|
93
79
|
|
94
|
-
|
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
|
-
|
87
|
+
**Examples:**
|
97
88
|
|
98
89
|
```ruby
|
99
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
106
|
+
**Examples:**
|
124
107
|
|
125
108
|
```ruby
|
126
|
-
|
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
|
-
|
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
|
-
|
133
|
-
|
134
|
-
|
119
|
+
- Format: `ACTIVE_PLAYER/INACTIVE_PLAYER`
|
120
|
+
- **One must be uppercase, other lowercase**
|
121
|
+
- The uppercase/lowercase corresponds to piece ownership
|
135
122
|
|
136
|
-
|
137
|
-
|
138
|
-
|
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
|
-
|
131
|
+
## Complete API Reference
|
142
132
|
|
143
|
-
|
144
|
-
2. **Canonicity check**: Ensures the string is in its canonical form through round-trip conversion
|
133
|
+
### Core Methods
|
145
134
|
|
146
|
-
|
135
|
+
#### `Feen.dump(**options)`
|
147
136
|
|
148
|
-
|
137
|
+
Converts position components into a FEEN string.
|
149
138
|
|
150
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
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
|
-
|
214
|
+
**Example:**
|
178
215
|
|
179
216
|
```ruby
|
180
|
-
|
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
|
-
|
222
|
+
## Working with Different Board Sizes
|
223
|
+
|
224
|
+
### Standard 2D Boards
|
184
225
|
|
185
226
|
```ruby
|
186
|
-
|
187
|
-
|
227
|
+
# 8x8 chess-like board (empty)
|
228
|
+
board = Array.new(8) { Array.new(8, "") }
|
188
229
|
|
189
|
-
|
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
|
-
|
235
|
+
feen = Feen.dump(
|
236
|
+
piece_placement: board,
|
237
|
+
pieces_in_hand: [],
|
238
|
+
games_turn: ["PLAYERA", "playerb"]
|
239
|
+
)
|
192
240
|
|
193
|
-
|
241
|
+
feen # => "r8/9/9/9/9/9/9/9/8R / PLAYERA/playerb"
|
242
|
+
```
|
194
243
|
|
195
|
-
|
244
|
+
### 3D Boards
|
196
245
|
|
197
|
-
|
198
|
-
|
199
|
-
|
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
|
-
|
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
|
-
#
|
205
|
-
|
206
|
-
["", "", "",
|
207
|
-
#
|
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
|
-
|
211
|
-
|
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
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
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
|
-
# => "
|
293
|
+
# => "k/K 3PR/2p FIRST/second"
|
219
294
|
```
|
220
295
|
|
221
|
-
###
|
296
|
+
### Understanding Piece Sorting
|
222
297
|
|
223
|
-
|
298
|
+
Captured pieces are automatically sorted in canonical order:
|
224
299
|
|
225
|
-
1. **By quantity (
|
226
|
-
2. **By
|
300
|
+
1. **By quantity** (most frequent first)
|
301
|
+
2. **By letter** (alphabetical within same quantity)
|
227
302
|
|
228
303
|
```ruby
|
229
|
-
|
230
|
-
|
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
|
-
|
233
|
-
|
234
|
-
|
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
|
-
|
331
|
+
### Cross-Game Scenarios
|
332
|
+
|
333
|
+
FEEN can represent positions mixing different game systems:
|
238
334
|
|
239
335
|
```ruby
|
240
|
-
#
|
241
|
-
|
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
|
-
|
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
|
-
|
346
|
+
## Error Handling
|
249
347
|
|
250
|
-
|
348
|
+
### Common Errors and Solutions
|
251
349
|
|
252
350
|
```ruby
|
253
|
-
|
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
|
-
#
|
256
|
-
|
257
|
-
[
|
258
|
-
|
259
|
-
|
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
|
-
|
268
|
-
|
269
|
-
|
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
|
-
# =>
|
373
|
+
# => ArgumentError
|
273
374
|
```
|
274
375
|
|
275
|
-
###
|
276
|
-
|
277
|
-
FEEN supports hybrid games mixing different piece sets:
|
376
|
+
### Safe Parsing for User Input
|
278
377
|
|
279
378
|
```ruby
|
280
|
-
|
281
|
-
|
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
|
-
|
285
|
-
|
286
|
-
|
287
|
-
- Chess player to move, against shogi player
|
391
|
+
## Real-World Examples
|
392
|
+
|
393
|
+
### Save/Load Game State
|
288
394
|
|
289
|
-
|
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
|
-
|
417
|
+
### Position Database
|
292
418
|
|
293
419
|
```ruby
|
294
|
-
|
295
|
-
|
296
|
-
|
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
|
-
|
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
|
-
##
|
456
|
+
## Best Practices
|
302
457
|
|
303
|
-
###
|
458
|
+
### 1. Always Validate Input
|
304
459
|
|
305
460
|
```ruby
|
306
|
-
|
307
|
-
|
308
|
-
|
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
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
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
|
-
###
|
494
|
+
### 3. Round-trip Validation
|
320
495
|
|
321
496
|
```ruby
|
322
|
-
|
323
|
-
|
324
|
-
position = Feen.
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
525
|
+
## Contributing
|
350
526
|
|
351
|
-
|
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
|
|