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.
- checksums.yaml +4 -4
- data/README.md +381 -245
- 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 +32 -54
- data/lib/feen/dumper.rb +2 -2
- data/lib/feen/parser/games_turn.rb +40 -20
- data/lib/feen/parser/piece_placement.rb +232 -558
- data/lib/feen/parser/pieces_in_hand.rb +93 -103
- data/lib/feen/parser.rb +3 -3
- data/lib/feen.rb +20 -9
- metadata +3 -6
- data/lib/feen/dumper/pieces_in_hand/errors.rb +0 -12
- 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/errors.rb +0 -20
- data/lib/feen/parser/pieces_in_hand/pnn_patterns.rb +0 -89
data/README.md
CHANGED
@@ -5,390 +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.beta9"
|
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.beta7"
|
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" # GAME1 player (uppercase pieces) to move (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)`
|
157
164
|
|
158
|
-
|
165
|
+
Converts a FEEN string back into position components.
|
159
166
|
|
160
|
-
|
161
|
-
|
162
|
-
|
167
|
+
**Parameters:**
|
168
|
+
|
169
|
+
- `feen_string` [String] - Valid FEEN notation
|
170
|
+
|
171
|
+
**Returns:** Hash with keys:
|
163
172
|
|
164
|
-
|
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
|
-
|
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
|
-
|
192
|
+
#### `Feen.safe_parse(feen_string)`
|
171
193
|
|
172
|
-
|
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
|
-
|
196
|
+
**Example:**
|
180
197
|
|
181
198
|
```ruby
|
182
|
-
|
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
|
-
|
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
|
-
|
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
|
-
##
|
192
|
-
|
193
|
-
### Pieces in Hand with Case Separation
|
222
|
+
## Working with Different Board Sizes
|
194
223
|
|
195
|
-
|
224
|
+
### Standard 2D Boards
|
196
225
|
|
197
226
|
```ruby
|
198
|
-
|
227
|
+
# 8x8 chess-like board (empty)
|
228
|
+
board = Array.new(8) { Array.new(8, "") }
|
199
229
|
|
200
|
-
#
|
201
|
-
|
202
|
-
|
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
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
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
|
-
|
241
|
+
feen # => "r8/9/9/9/9/9/9/9/8R / PLAYERA/playerb"
|
242
|
+
```
|
214
243
|
|
215
|
-
|
244
|
+
### 3D Boards
|
216
245
|
|
217
|
-
|
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
|
-
|
220
|
-
|
221
|
-
|
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
|
-
|
261
|
+
### Irregular Boards
|
224
262
|
|
225
263
|
```ruby
|
226
|
-
#
|
227
|
-
|
228
|
-
["", "", "",
|
229
|
-
#
|
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
|
-
|
233
|
-
|
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
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
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
|
-
# => "
|
293
|
+
# => "k/K 3PR/2p FIRST/second"
|
241
294
|
```
|
242
295
|
|
243
|
-
###
|
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
|
-
|
308
|
+
## Advanced Features
|
246
309
|
|
247
|
-
|
248
|
-
2. **By complete PNN representation (alphabetically ascending)**
|
310
|
+
### Special Piece States (On Board Only)
|
249
311
|
|
250
|
-
|
312
|
+
For games that need special piece states, use modifiers **only on the board**:
|
251
313
|
|
252
314
|
```ruby
|
253
|
-
|
254
|
-
|
315
|
+
board = [
|
316
|
+
["+P", "K", "-R"], # Enhanced pawn, King, diminished rook
|
317
|
+
["N'", "", "B"] # Knight with special state, empty, Bishop
|
318
|
+
]
|
255
319
|
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
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
|
-
|
262
|
-
#
|
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
|
-
|
331
|
+
### Cross-Game Scenarios
|
332
|
+
|
333
|
+
FEEN can represent positions mixing different game systems:
|
268
334
|
|
269
335
|
```ruby
|
270
|
-
|
271
|
-
|
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
|
-
|
346
|
+
## Error Handling
|
275
347
|
|
276
|
-
|
348
|
+
### Common Errors and Solutions
|
277
349
|
|
278
350
|
```ruby
|
279
|
-
|
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
|
-
#
|
282
|
-
|
283
|
-
[
|
284
|
-
|
285
|
-
|
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
|
-
|
294
|
-
|
295
|
-
|
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
|
-
# =>
|
373
|
+
# => ArgumentError
|
299
374
|
```
|
300
375
|
|
301
|
-
###
|
302
|
-
|
303
|
-
FEEN supports hybrid games mixing different piece sets:
|
376
|
+
### Safe Parsing for User Input
|
304
377
|
|
305
378
|
```ruby
|
306
|
-
|
307
|
-
|
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
|
-
|
391
|
+
## Real-World Examples
|
311
392
|
|
312
|
-
|
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
|
-
|
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
|
-
|
417
|
+
### Position Database
|
320
418
|
|
321
419
|
```ruby
|
322
|
-
|
323
|
-
|
324
|
-
|
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
|
-
|
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
|
-
##
|
456
|
+
## Best Practices
|
330
457
|
|
331
|
-
###
|
458
|
+
### 1. Always Validate Input
|
332
459
|
|
333
460
|
```ruby
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
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
|
-
|
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
|
-
|
351
|
-
|
352
|
-
|
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
|
-
###
|
494
|
+
### 3. Round-trip Validation
|
356
495
|
|
357
496
|
```ruby
|
358
|
-
|
359
|
-
|
360
|
-
position = Feen.
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
else
|
366
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
525
|
+
## Contributing
|
388
526
|
|
389
|
-
|
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
|
|