sashite-feen 0.1.0 → 0.3.0
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 +412 -137
- data/lib/sashite/feen/dumper/piece_placement.rb +144 -64
- data/lib/sashite/feen/dumper/pieces_in_hand.rb +124 -34
- data/lib/sashite/feen/dumper/style_turn.rb +29 -45
- data/lib/sashite/feen/dumper.rb +40 -30
- data/lib/sashite/feen/error.rb +72 -29
- data/lib/sashite/feen/hands.rb +62 -20
- data/lib/sashite/feen/parser/piece_placement.rb +324 -118
- data/lib/sashite/feen/parser/pieces_in_hand.rb +210 -44
- data/lib/sashite/feen/parser/style_turn.rb +81 -41
- data/lib/sashite/feen/parser.rb +60 -15
- data/lib/sashite/feen/placement.rb +295 -19
- data/lib/sashite/feen/position.rb +64 -13
- data/lib/sashite/feen/styles.rb +54 -57
- data/lib/sashite/feen.rb +57 -96
- metadata +1 -2
- data/lib/sashite/feen/ordering.rb +0 -16
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a8182f6c48f6a615f38d75d3060f216c06c6606b118544cb4f4f0e40b9615e89
|
|
4
|
+
data.tar.gz: 1150aacab981e71aa32aaae585ec1998cd678bb5997432135b0089394eef2482
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b8ed4a473fce336ce73cb89c684e9ea7820842b78e12766bc32440f25da7f61998c13077ce3743972ef234a85771c63f58778835dd11bfba1fd97ef63dfb7154
|
|
7
|
+
data.tar.gz: 679296955c3b032e0ebe7b9f6ca4776b42b617f67a876e5de6bee015d186e40287d9cc964a3bb2d3faec0dbaf0dd98d810e3bf522cde197d340f168ba9be3a84
|
data/README.md
CHANGED
|
@@ -1,236 +1,511 @@
|
|
|
1
|
-
|
|
1
|
+
# Feen.rb
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://github.com/sashite/feen.rb/tags)
|
|
4
|
+
[](https://rubydoc.info/github/sashite/feen.rb/main)
|
|
5
|
+

|
|
6
|
+
[](https://github.com/sashite/feen.rb/raw/main/LICENSE.md)
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
> **FEEN** (Forsyth–Edwards Enhanced Notation) implementation for the Ruby language.
|
|
9
|
+
|
|
10
|
+
## What is FEEN?
|
|
11
|
+
|
|
12
|
+
FEEN (Forsyth–Edwards Enhanced Notation) is a universal, rule-agnostic notation for representing board game positions. It extends traditional FEN to support:
|
|
13
|
+
|
|
14
|
+
- **Multiple game systems** (Chess, Shōgi, Xiangqi, and more)
|
|
15
|
+
- **Cross-style games** where players use different piece sets
|
|
16
|
+
- **Multi-dimensional boards** (2D, 3D, and beyond)
|
|
17
|
+
- **Captured pieces** (pieces-in-hand for drop mechanics)
|
|
18
|
+
- **Arbitrarily large boards** with efficient empty square encoding
|
|
19
|
+
- **Completely irregular structures** (any valid combination of ranks and separators)
|
|
20
|
+
- **Board-less positions** (positions without piece placement, useful for pure style/turn tracking)
|
|
21
|
+
|
|
22
|
+
This gem implements the [FEEN Specification v1.0.0](https://sashite.dev/specs/feen/1.0.0/) as a pure functional library with immutable data structures.
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
gem "sashite-feen"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Or install manually:
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
gem install sashite-feen
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Quick Start
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
require "sashite/feen"
|
|
6
40
|
|
|
7
|
-
|
|
41
|
+
# Parse a FEEN string into an immutable position object
|
|
42
|
+
position = Sashite::Feen.parse("+rnbq+kbn+r/+p+p+p+p+p+p+p+p/8/8/8/8/+P+P+P+P+P+P+P+P/+RNBQ+KBN+R / C/c")
|
|
8
43
|
|
|
9
|
-
|
|
44
|
+
# Access position components
|
|
45
|
+
position.placement # Board configuration
|
|
46
|
+
position.hands # Captured pieces
|
|
47
|
+
position.styles # Game styles and active player
|
|
10
48
|
|
|
11
|
-
|
|
49
|
+
# Convert placement to array based on dimensionality
|
|
50
|
+
position.placement.to_a # => [[pieces...], [pieces...], ...] for 2D boards
|
|
12
51
|
|
|
13
|
-
|
|
52
|
+
# Convert back to canonical FEEN string
|
|
53
|
+
feen_string = Sashite::Feen.dump(position) # or position.to_s
|
|
54
|
+
```
|
|
14
55
|
|
|
15
|
-
|
|
16
|
-
* **Portable & canonical**: a single, deterministic string per position.
|
|
17
|
-
* **Composable**: works nicely alongside other Sashité specs (e.g., STN for transitions).
|
|
56
|
+
## FEEN Format
|
|
18
57
|
|
|
19
|
-
FEEN
|
|
58
|
+
A FEEN string consists of three space-separated fields:
|
|
20
59
|
|
|
21
60
|
```
|
|
22
|
-
<
|
|
61
|
+
<piece-placement> <pieces-in-hand> <style-turn>
|
|
23
62
|
```
|
|
24
63
|
|
|
25
|
-
|
|
64
|
+
**Example:**
|
|
65
|
+
```txt
|
|
66
|
+
+rnbq+kbn+r/+p+p+p+p+p+p+p+p/8/8/8/8/+P+P+P+P+P+P+P+P/+RNBQ+KBN+R / C/c
|
|
67
|
+
```
|
|
26
68
|
|
|
27
|
-
|
|
69
|
+
1. **Piece placement**: Board configuration using EPIN notation with `/` separators (can be empty for board-less positions)
|
|
70
|
+
2. **Pieces in hand**: Captured pieces for each player (format: `first/second`)
|
|
71
|
+
3. **Style-turn**: Game styles and active player (format: `active/inactive`)
|
|
28
72
|
|
|
29
|
-
|
|
73
|
+
See the [FEEN Specification](https://sashite.dev/specs/feen/1.0.0/) for complete format details.
|
|
74
|
+
|
|
75
|
+
## API Reference
|
|
76
|
+
|
|
77
|
+
### Module Methods
|
|
78
|
+
|
|
79
|
+
#### `Sashite::Feen.parse(string)`
|
|
80
|
+
|
|
81
|
+
Parses a FEEN string into an immutable `Position` object.
|
|
82
|
+
|
|
83
|
+
- **Parameter**: `string` (String) - FEEN notation string
|
|
84
|
+
- **Returns**: `Position` - Immutable position object
|
|
85
|
+
- **Raises**: `Sashite::Feen::Error` subclasses on invalid input
|
|
30
86
|
|
|
31
87
|
```ruby
|
|
32
|
-
|
|
33
|
-
````
|
|
88
|
+
position = Sashite::Feen.parse("8/8/8/8/8/8/8/8 / C/c")
|
|
34
89
|
|
|
35
|
-
|
|
90
|
+
# Board-less position (empty piece placement)
|
|
91
|
+
position = Sashite::Feen.parse(" / C/c")
|
|
92
|
+
```
|
|
36
93
|
|
|
37
|
-
|
|
38
|
-
|
|
94
|
+
#### `Sashite::Feen.dump(position)`
|
|
95
|
+
|
|
96
|
+
Converts a position object into its canonical FEEN string.
|
|
97
|
+
|
|
98
|
+
- **Parameter**: `position` (Position) - Position object
|
|
99
|
+
- **Returns**: `String` - Canonical FEEN string
|
|
100
|
+
- **Guarantees**: Deterministic output (same position always produces same string)
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
feen_string = Sashite::Feen.dump(position)
|
|
39
104
|
```
|
|
40
105
|
|
|
41
|
-
|
|
106
|
+
### Position Object
|
|
107
|
+
|
|
108
|
+
The `Position` object is immutable and provides read-only access to three components:
|
|
42
109
|
|
|
43
110
|
```ruby
|
|
44
|
-
|
|
45
|
-
|
|
111
|
+
position.placement # => Placement (board configuration)
|
|
112
|
+
position.hands # => Hands (pieces in hand)
|
|
113
|
+
position.styles # => Styles (style-turn information)
|
|
114
|
+
position.to_s # => String (canonical FEEN)
|
|
46
115
|
```
|
|
47
116
|
|
|
48
|
-
|
|
117
|
+
**Equality and hashing:**
|
|
118
|
+
```ruby
|
|
119
|
+
position1 == position2 # Component-wise equality
|
|
120
|
+
position1.hash # Consistent hash for same positions
|
|
121
|
+
```
|
|
49
122
|
|
|
50
|
-
|
|
123
|
+
### Placement Object
|
|
51
124
|
|
|
52
|
-
|
|
125
|
+
Represents the board configuration as a flat array of ranks with explicit separators.
|
|
53
126
|
|
|
54
127
|
```ruby
|
|
55
|
-
|
|
128
|
+
placement.ranks # => Array<Array> - Flat array of all ranks
|
|
129
|
+
placement.separators # => Array<String> - Separators between ranks (e.g., ["/", "//"])
|
|
130
|
+
placement.dimension # => Integer - Board dimensionality (1 + max consecutive slashes)
|
|
131
|
+
placement.rank_count # => Integer - Total number of ranks
|
|
132
|
+
placement.one_dimensional? # => Boolean - True if dimension is 1
|
|
133
|
+
placement.all_pieces # => Array - All pieces (nils excluded)
|
|
134
|
+
placement.total_squares # => Integer - Total square count
|
|
135
|
+
placement.to_s # => String - Piece placement field
|
|
136
|
+
placement.to_a # => Array - Array representation (dimension-aware)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
#### `to_a` - Dimension-Aware Array Conversion
|
|
140
|
+
|
|
141
|
+
The `to_a` method returns an array representation that adapts to the board's dimensionality:
|
|
142
|
+
|
|
143
|
+
- **1D boards**: Returns a single rank array (or empty array if no ranks)
|
|
144
|
+
- **2D+ boards**: Returns array of ranks
|
|
145
|
+
|
|
146
|
+
```ruby
|
|
147
|
+
# 1D board - Returns flat array
|
|
148
|
+
feen = "K2P3k / C/c"
|
|
149
|
+
position = Sashite::Feen.parse(feen)
|
|
150
|
+
position.placement.to_a
|
|
151
|
+
# => [K, nil, nil, P, nil, nil, nil, k]
|
|
152
|
+
|
|
153
|
+
# 2D board - Returns array of arrays
|
|
154
|
+
feen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR / C/c"
|
|
155
|
+
position = Sashite::Feen.parse(feen)
|
|
156
|
+
position.placement.to_a
|
|
157
|
+
# => [[r,n,b,q,k,b,n,r], [p,p,p,p,p,p,p,p], [nil×8], ...]
|
|
158
|
+
|
|
159
|
+
# 3D board - Returns array of ranks (to be structured by application)
|
|
160
|
+
feen = "5/5//5/5 / R/r"
|
|
161
|
+
position = Sashite::Feen.parse(feen)
|
|
162
|
+
position.placement.to_a
|
|
163
|
+
# => [[nil×5], [nil×5], [nil×5], [nil×5]]
|
|
164
|
+
|
|
165
|
+
# Empty board
|
|
166
|
+
placement = Sashite::Feen::Placement.new([], [], 1)
|
|
167
|
+
placement.to_a
|
|
168
|
+
# => []
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**Other methods:**
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
# Access specific positions
|
|
175
|
+
first_rank = placement.ranks[0]
|
|
176
|
+
piece_at_a1 = first_rank[0] # Piece object or nil
|
|
177
|
+
|
|
178
|
+
# Check dimensionality
|
|
179
|
+
placement.dimension # => 2 (2D board)
|
|
180
|
+
|
|
181
|
+
# Inspect separator structure
|
|
182
|
+
placement.separators # => ["/", "/", "/", "/", "/", "/", "/"]
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Hands Object
|
|
186
|
+
|
|
187
|
+
Represents captured pieces held by each player.
|
|
188
|
+
|
|
189
|
+
```ruby
|
|
190
|
+
hands.first_player # => Array - Pieces held by first player
|
|
191
|
+
hands.second_player # => Array - Pieces held by second player
|
|
192
|
+
hands.empty? # => Boolean - True if both hands are empty
|
|
193
|
+
hands.to_s # => String - Pieces-in-hand field
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
**Example:**
|
|
197
|
+
```ruby
|
|
198
|
+
# Count pieces in hand
|
|
199
|
+
first_player_pawns = hands.first_player.count { |p| p.to_s == "P" }
|
|
200
|
+
|
|
201
|
+
# Check if any captures
|
|
202
|
+
hands.empty? # => false
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Styles Object
|
|
56
206
|
|
|
57
|
-
|
|
58
|
-
pos = Sashite::Feen.parse("<placement> <hands> <style1>/<style2>")
|
|
207
|
+
Represents game styles and indicates the active player.
|
|
59
208
|
|
|
60
|
-
|
|
61
|
-
|
|
209
|
+
```ruby
|
|
210
|
+
styles.active # => SIN identifier - Active player's style
|
|
211
|
+
styles.inactive # => SIN identifier - Inactive player's style
|
|
212
|
+
styles.to_s # => String - Style-turn field
|
|
213
|
+
```
|
|
62
214
|
|
|
63
|
-
|
|
64
|
-
|
|
215
|
+
**Example:**
|
|
216
|
+
```ruby
|
|
217
|
+
# Determine active player
|
|
218
|
+
styles.active.to_s # => "C" (first player Chess)
|
|
219
|
+
styles.inactive.to_s # => "c" (second player Chess)
|
|
65
220
|
|
|
66
|
-
#
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
221
|
+
# Check if cross-style
|
|
222
|
+
styles.active.to_s.upcase != styles.inactive.to_s.upcase
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Examples
|
|
226
|
+
|
|
227
|
+
### Chess Positions
|
|
228
|
+
|
|
229
|
+
```ruby
|
|
230
|
+
# Starting position
|
|
231
|
+
chess_start = Sashite::Feen.parse(
|
|
232
|
+
"+rnbq+kbn+r/+p+p+p+p+p+p+p+p/8/8/8/8/+P+P+P+P+P+P+P+P/+RNBQ+KBN+R / C/c"
|
|
71
233
|
)
|
|
72
234
|
|
|
73
|
-
#
|
|
74
|
-
Sashite::Feen.
|
|
235
|
+
# After 1.e4
|
|
236
|
+
after_e4 = Sashite::Feen.parse(
|
|
237
|
+
"+rnbq+kbn+r/+p+p+p+p+p+p+p+p/8/8/4P3/8/+P+P+P+P1+P+P+P/+RNBQ+KBN+R / c/C"
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
# Ruy Lopez opening
|
|
241
|
+
ruy_lopez = Sashite::Feen.parse(
|
|
242
|
+
"r1bqkbnr/+p+p+p+p1+p+p+p/2n5/1B2p3/4P3/5N2/+P+P+P+P1+P+P+P/RNBQK2R / c/C"
|
|
243
|
+
)
|
|
75
244
|
```
|
|
76
245
|
|
|
77
|
-
|
|
246
|
+
### Shōgi with Captured Pieces
|
|
78
247
|
|
|
79
|
-
|
|
248
|
+
```ruby
|
|
249
|
+
# Starting position
|
|
250
|
+
shogi_start = Sashite::Feen.parse(
|
|
251
|
+
"lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL / S/s"
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# Position with pieces in hand
|
|
255
|
+
shogi_midgame = Sashite::Feen.parse(
|
|
256
|
+
"lnsgkgsnl/1r5b1/pppp1pppp/9/4p4/9/PPPP1PPPP/1B5R1/LNSGKGSNL P/p s/S"
|
|
257
|
+
)
|
|
80
258
|
|
|
81
|
-
|
|
259
|
+
# Access captured pieces
|
|
260
|
+
position = shogi_midgame
|
|
261
|
+
position.hands.first_player # => [P] (one pawn)
|
|
262
|
+
position.hands.second_player # => [p] (one pawn)
|
|
263
|
+
|
|
264
|
+
# Count specific pieces in hand
|
|
265
|
+
position.hands.first_player.count { |p| p.to_s == "P" } # => 1
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Cross-Style Games
|
|
82
269
|
|
|
83
270
|
```ruby
|
|
84
|
-
|
|
85
|
-
Sashite::Feen.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
271
|
+
# Chess vs Makruk
|
|
272
|
+
chess_vs_makruk = Sashite::Feen.parse(
|
|
273
|
+
"rnsmksnr/8/pppppppp/8/8/8/+P+P+P+P+P+P+P+P/+RNBQ+KBN+R / C/m"
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
# Chess vs Shōgi
|
|
277
|
+
chess_vs_shogi = Sashite::Feen.parse(
|
|
278
|
+
"lnsgkgsnl/1r5b1/pppppppp/9/9/9/+P+P+P+P+P+P+P+P/+RNBQ+KBN+R / C/s"
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
# Check styles
|
|
282
|
+
position = chess_vs_makruk
|
|
283
|
+
position.styles.active.to_s # => "C" (Chess, first player)
|
|
284
|
+
position.styles.inactive.to_s # => "m" (Makruk, second player)
|
|
91
285
|
```
|
|
92
286
|
|
|
93
|
-
|
|
287
|
+
### Multi-Dimensional Boards
|
|
94
288
|
|
|
95
289
|
```ruby
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
290
|
+
# 3D Chess (Raumschach)
|
|
291
|
+
raumschach = Sashite::Feen.parse(
|
|
292
|
+
"rnknr/+p+p+p+p+p/5/5/5//buqbu/+p+p+p+p+p/5/5/5//5/5/5/5/5//5/5/5/+P+P+P+P+P/BUQBU//5/5/5/+P+P+P+P+P/RNKNR / R/r"
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
# Check dimensionality
|
|
296
|
+
raumschach.placement.dimension # => 3 (3D board)
|
|
297
|
+
raumschach.placement.ranks.size # => 25 (total ranks)
|
|
298
|
+
|
|
299
|
+
# Inspect separator structure
|
|
300
|
+
level_seps = raumschach.placement.separators.count { |s| s == "//" }
|
|
301
|
+
rank_seps = raumschach.placement.separators.count { |s| s == "/" }
|
|
302
|
+
# level_seps => 4 (separates 5 levels)
|
|
303
|
+
# rank_seps => 20 (separates ranks within levels)
|
|
100
304
|
```
|
|
101
305
|
|
|
102
|
-
|
|
306
|
+
### Irregular Boards
|
|
103
307
|
|
|
104
|
-
|
|
308
|
+
```ruby
|
|
309
|
+
# Diamond-shaped board
|
|
310
|
+
diamond = Sashite::Feen.parse("3/4/5/4/3 / G/g")
|
|
105
311
|
|
|
106
|
-
|
|
312
|
+
# Check structure
|
|
313
|
+
diamond.placement.ranks.map(&:size) # => [3, 4, 5, 4, 3]
|
|
107
314
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
315
|
+
# Very large board
|
|
316
|
+
large_board = Sashite::Feen.parse("100/100/100 / G/g")
|
|
317
|
+
large_board.placement.total_squares # => 300
|
|
111
318
|
|
|
112
|
-
|
|
319
|
+
# Single square
|
|
320
|
+
single = Sashite::Feen.parse("K / C/c")
|
|
321
|
+
single.placement.rank_count # => 1
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### Completely Irregular Structures
|
|
113
325
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
326
|
+
FEEN supports any valid combination of ranks and separators:
|
|
327
|
+
|
|
328
|
+
```ruby
|
|
329
|
+
# Extreme irregularity with variable separators
|
|
330
|
+
feen = "99999/3///K/k//r / G/g"
|
|
331
|
+
position = Sashite::Feen.parse(feen)
|
|
332
|
+
|
|
333
|
+
# Access the structure
|
|
334
|
+
position.placement.ranks.size # => 5 ranks
|
|
335
|
+
position.placement.separators # => ["/", "///", "/", "//"]
|
|
336
|
+
position.placement.dimension # => 4 (max separator is "///")
|
|
337
|
+
|
|
338
|
+
# Each rank can have different sizes
|
|
339
|
+
position.placement.ranks[0].size # => 99999
|
|
340
|
+
position.placement.ranks[1].size # => 3
|
|
341
|
+
position.placement.ranks[2].size # => 1
|
|
342
|
+
position.placement.ranks[3].size # => 1
|
|
343
|
+
position.placement.ranks[4].size # => 1
|
|
344
|
+
|
|
345
|
+
# Round-trip preservation
|
|
346
|
+
Sashite::Feen.dump(position) == feen # => true
|
|
347
|
+
```
|
|
117
348
|
|
|
118
|
-
|
|
349
|
+
### Empty Ranks
|
|
119
350
|
|
|
120
|
-
|
|
121
|
-
* Exactly **one uppercase** style (first player) and **one lowercase** style (second).
|
|
122
|
-
* The **first token is the active** player’s style.
|
|
351
|
+
FEEN supports empty ranks (ranks with no pieces):
|
|
123
352
|
|
|
124
|
-
|
|
353
|
+
```ruby
|
|
354
|
+
# Trailing separator creates empty rank
|
|
355
|
+
feen = "K/// / C/c"
|
|
356
|
+
position = Sashite::Feen.parse(feen)
|
|
125
357
|
|
|
126
|
-
|
|
358
|
+
position.placement.ranks.size # => 2
|
|
359
|
+
position.placement.ranks[0] # => [K]
|
|
360
|
+
position.placement.ranks[1] # => [] (empty rank)
|
|
361
|
+
position.placement.separators # => ["///"]
|
|
127
362
|
|
|
128
|
-
|
|
363
|
+
# Round-trip preserves structure
|
|
364
|
+
Sashite::Feen.dump(position) == feen # => true
|
|
365
|
+
```
|
|
129
366
|
|
|
130
|
-
|
|
131
|
-
* **Value objects**: `Position`, `Placement`, `Hands`, `Styles` (immutable, canonical)
|
|
132
|
-
* **Parser**: `Feen::Parser` orchestrates field parsers (`Parser::PiecePlacement`, `Parser::PiecesInHand`, `Parser::StyleTurn`)
|
|
133
|
-
* **Dumper**: `Feen::Dumper` orchestrates field dumpers (`Dumper::PiecePlacement`, `Dumper::PiecesInHand`, `Dumper::StyleTurn`)
|
|
134
|
-
* **Ordering**: `Feen::Ordering` — single comparator used by the hands dumper
|
|
135
|
-
* **Errors**: `Feen::Error` (see below)
|
|
367
|
+
### Board-less Positions
|
|
136
368
|
|
|
137
|
-
|
|
369
|
+
FEEN supports positions without piece placement, useful for tracking only style and turn information:
|
|
138
370
|
|
|
371
|
+
```ruby
|
|
372
|
+
# Position with empty board (no piece placement)
|
|
373
|
+
board_less = Sashite::Feen.parse(" / C/c")
|
|
374
|
+
|
|
375
|
+
board_less.placement.ranks.size # => 1
|
|
376
|
+
board_less.placement.dimension # => 1
|
|
377
|
+
board_less.placement.to_a # => []
|
|
378
|
+
|
|
379
|
+
# Convert back to FEEN
|
|
380
|
+
Sashite::Feen.dump(board_less) # => " / C/c"
|
|
139
381
|
```
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
└─ dumper/
|
|
158
|
-
├─ piece_placement.rb
|
|
159
|
-
├─ pieces_in_hand.rb
|
|
160
|
-
└─ style_turn.rb
|
|
382
|
+
|
|
383
|
+
### Working with Positions
|
|
384
|
+
|
|
385
|
+
```ruby
|
|
386
|
+
# Compare positions
|
|
387
|
+
position1 = Sashite::Feen.parse("8/8/8/8/8/8/8/8 / C/c")
|
|
388
|
+
position2 = Sashite::Feen.parse("8/8/8/8/8/8/8/8 / C/c")
|
|
389
|
+
position1 == position2 # => true
|
|
390
|
+
|
|
391
|
+
# Round-trip parsing
|
|
392
|
+
original = "+rnbq+kbn+r/+p+p+p+p+p+p+p+p/8/8/8/8/+P+P+P+P+P+P+P+P/+RNBQ+KBN+R / C/c"
|
|
393
|
+
position = Sashite::Feen.parse(original)
|
|
394
|
+
Sashite::Feen.dump(position) == original # => true
|
|
395
|
+
|
|
396
|
+
# Extract specific information
|
|
397
|
+
position.placement.ranks[0] # First rank (array of pieces/nils)
|
|
398
|
+
position.hands.first_player.size # Number of captured pieces
|
|
161
399
|
```
|
|
162
400
|
|
|
163
|
-
|
|
401
|
+
### State Modifiers and Derivation
|
|
164
402
|
|
|
165
|
-
|
|
403
|
+
```ruby
|
|
404
|
+
# Enhanced pieces (promoted, with special rights)
|
|
405
|
+
enhanced = Sashite::Feen.parse("+K+Q+R+B/8/8/8/8/8/8/8 / C/c")
|
|
166
406
|
|
|
167
|
-
|
|
407
|
+
# Diminished pieces (weakened, vulnerable)
|
|
408
|
+
diminished = Sashite::Feen.parse("-K-Q-R-B/8/8/8/8/8/8/8 / C/c")
|
|
168
409
|
|
|
169
|
-
|
|
410
|
+
# Foreign pieces (using opponent's style)
|
|
411
|
+
foreign = Sashite::Feen.parse("K'Q'R'B'/k'q'r'b'/8/8/8/8/8/8 / C/s")
|
|
412
|
+
```
|
|
170
413
|
|
|
171
|
-
|
|
172
|
-
* `Sashite::Feen::Error::Piece` – EPIN validation failures
|
|
173
|
-
* `Sashite::Feen::Error::Style` – SIN validation/case issues
|
|
174
|
-
* `Sashite::Feen::Error::Count` – invalid counts in hands
|
|
175
|
-
* `Sashite::Feen::Error::Bounds` – internal dimension constraints (when relevant)
|
|
176
|
-
* `Sashite::Feen::Error::Validation` – generic structural/semantic errors
|
|
414
|
+
## Error Handling
|
|
177
415
|
|
|
178
|
-
|
|
416
|
+
FEEN defines specific error classes for different validation failures:
|
|
179
417
|
|
|
180
418
|
```ruby
|
|
181
419
|
begin
|
|
182
|
-
|
|
183
|
-
rescue Sashite::Feen::Error
|
|
184
|
-
|
|
420
|
+
position = Sashite::Feen.parse("invalid feen")
|
|
421
|
+
rescue Sashite::Feen::Error => e
|
|
422
|
+
# Base error class catches all FEEN errors
|
|
423
|
+
warn "FEEN error: #{e.message}"
|
|
185
424
|
end
|
|
186
425
|
```
|
|
187
426
|
|
|
188
|
-
|
|
427
|
+
### Error Hierarchy
|
|
189
428
|
|
|
190
|
-
|
|
429
|
+
```txt
|
|
430
|
+
Sashite::Feen::Error # Base error class
|
|
431
|
+
├── Error::Syntax # Malformed FEEN structure
|
|
432
|
+
├── Error::Piece # Invalid EPIN notation
|
|
433
|
+
├── Error::Style # Invalid SIN notation
|
|
434
|
+
├── Error::Count # Invalid piece counts
|
|
435
|
+
└── Error::Validation # Other semantic violations
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### Common Errors
|
|
439
|
+
|
|
440
|
+
```ruby
|
|
441
|
+
# Syntax error - wrong field count
|
|
442
|
+
Sashite::Feen.parse("8/8/8/8/8/8/8/8 /")
|
|
443
|
+
# => Error::Syntax: "FEEN must have exactly 3 space-separated fields, got 2"
|
|
444
|
+
|
|
445
|
+
# Style error - invalid SIN
|
|
446
|
+
Sashite::Feen.parse("8/8/8/8/8/8/8/8 / 1/2")
|
|
447
|
+
# => Error::Style: "failed to parse SIN '1': invalid SIN notation: '1' (must be a single letter A-Z or a-z)"
|
|
448
|
+
|
|
449
|
+
# Count error - invalid quantity
|
|
450
|
+
Sashite::Feen.parse("8/8/8/8/8/8/8/8 0P/ C/c")
|
|
451
|
+
# => Error::Count: "piece count must be at least 1, got 0"
|
|
452
|
+
```
|
|
191
453
|
|
|
192
|
-
|
|
193
|
-
* Purely functional; all objects are frozen; methods return new values.
|
|
194
|
-
* No JSON serialization in this gem.
|
|
454
|
+
## Properties
|
|
195
455
|
|
|
196
|
-
|
|
456
|
+
- **Purely functional**: Immutable data structures, no side effects
|
|
457
|
+
- **Canonical output**: Deterministic string generation (same position → same string)
|
|
458
|
+
- **Specification compliant**: Strict adherence to [FEEN v1.0.0](https://sashite.dev/specs/feen/1.0.0/)
|
|
459
|
+
- **Minimal API**: Two methods (`parse` and `dump`) for complete functionality
|
|
460
|
+
- **Universal**: Supports any abstract strategy board game
|
|
461
|
+
- **Completely flexible**: Accepts any valid combination of ranks and separators
|
|
462
|
+
- **Perfect round-trip**: `parse(dump(position)) == position` guaranteed
|
|
463
|
+
- **Dimension-aware**: Intelligent array conversion based on board structure
|
|
464
|
+
- **Composable**: Built on [EPIN](https://github.com/sashite/epin.rb) and [SIN](https://github.com/sashite/sin.rb) specifications
|
|
465
|
+
|
|
466
|
+
## Dependencies
|
|
467
|
+
|
|
468
|
+
- [sashite-epin](https://github.com/sashite/epin.rb) — Extended Piece Identifier Notation
|
|
469
|
+
- [sashite-sin](https://github.com/sashite/sin.rb) — Style Identifier Notation
|
|
470
|
+
|
|
471
|
+
## Documentation
|
|
472
|
+
|
|
473
|
+
- [FEEN Specification v1.0.0](https://sashite.dev/specs/feen/1.0.0/) — Complete technical specification
|
|
474
|
+
- [FEEN Examples](https://sashite.dev/specs/feen/1.0.0/examples/) — Comprehensive examples
|
|
475
|
+
- [API Documentation](https://rubydoc.info/github/sashite/feen.rb/main) — Full API reference
|
|
476
|
+
- [GitHub Wiki](https://github.com/sashite/feen.rb/wiki) — Advanced usage and patterns
|
|
197
477
|
|
|
198
478
|
## Development
|
|
199
479
|
|
|
200
480
|
```sh
|
|
201
|
-
# Clone
|
|
481
|
+
# Clone the repository
|
|
202
482
|
git clone https://github.com/sashite/feen.rb.git
|
|
203
483
|
cd feen.rb
|
|
204
484
|
|
|
205
|
-
# Install
|
|
485
|
+
# Install dependencies
|
|
206
486
|
bundle install
|
|
207
487
|
|
|
208
|
-
# Run
|
|
488
|
+
# Run tests
|
|
209
489
|
ruby test.rb
|
|
210
490
|
|
|
211
|
-
# Generate
|
|
491
|
+
# Generate documentation
|
|
212
492
|
yard doc
|
|
213
493
|
```
|
|
214
494
|
|
|
215
|
-
---
|
|
216
|
-
|
|
217
495
|
## Contributing
|
|
218
496
|
|
|
219
497
|
1. Fork the repository
|
|
220
|
-
2. Create a feature branch
|
|
221
|
-
3. Add tests
|
|
222
|
-
4. Ensure
|
|
223
|
-
5. Commit
|
|
224
|
-
6. Push
|
|
225
|
-
|
|
226
|
-
---
|
|
498
|
+
2. Create a feature branch (`git checkout -b feature/new-feature`)
|
|
499
|
+
3. Add tests for your changes
|
|
500
|
+
4. Ensure all tests pass (`ruby test.rb`)
|
|
501
|
+
5. Commit your changes (`git commit -am 'Add new feature'`)
|
|
502
|
+
6. Push to the branch (`git push origin feature/new-feature`)
|
|
503
|
+
7. Create a Pull Request
|
|
227
504
|
|
|
228
505
|
## License
|
|
229
506
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
---
|
|
507
|
+
Available as open source under the [MIT License](https://opensource.org/licenses/MIT).
|
|
233
508
|
|
|
234
509
|
## About
|
|
235
510
|
|
|
236
|
-
Maintained by
|
|
511
|
+
Maintained by [Sashité](https://sashite.com/) — promoting chess variants and sharing the beauty of board game cultures.
|