sashite-feen 0.2.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 +436 -42
- data/lib/sashite/feen/dumper/piece_placement.rb +107 -50
- data/lib/sashite/feen/dumper/pieces_in_hand.rb +1 -0
- data/lib/sashite/feen/hands.rb +2 -2
- data/lib/sashite/feen/parser/piece_placement.rb +190 -176
- data/lib/sashite/feen/parser/pieces_in_hand.rb +7 -13
- data/lib/sashite/feen/parser.rb +11 -2
- data/lib/sashite/feen/placement.rb +260 -30
- metadata +1 -1
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
|
@@ -9,7 +9,15 @@
|
|
|
9
9
|
|
|
10
10
|
## What is FEEN?
|
|
11
11
|
|
|
12
|
-
FEEN (Forsyth–Edwards Enhanced Notation) is a universal, rule-agnostic notation for representing board game positions. It extends traditional FEN to support
|
|
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)
|
|
13
21
|
|
|
14
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.
|
|
15
23
|
|
|
@@ -19,94 +27,480 @@ This gem implements the [FEEN Specification v1.0.0](https://sashite.dev/specs/fe
|
|
|
19
27
|
gem "sashite-feen"
|
|
20
28
|
```
|
|
21
29
|
|
|
22
|
-
|
|
30
|
+
Or install manually:
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
gem install sashite-feen
|
|
34
|
+
```
|
|
23
35
|
|
|
24
|
-
|
|
36
|
+
## Quick Start
|
|
25
37
|
|
|
26
38
|
```ruby
|
|
27
39
|
require "sashite/feen"
|
|
28
40
|
|
|
29
|
-
# Parse a FEEN string into
|
|
41
|
+
# Parse a FEEN string into an immutable position object
|
|
30
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")
|
|
31
43
|
|
|
32
|
-
#
|
|
33
|
-
|
|
44
|
+
# Access position components
|
|
45
|
+
position.placement # Board configuration
|
|
46
|
+
position.hands # Captured pieces
|
|
47
|
+
position.styles # Game styles and active player
|
|
48
|
+
|
|
49
|
+
# Convert placement to array based on dimensionality
|
|
50
|
+
position.placement.to_a # => [[pieces...], [pieces...], ...] for 2D boards
|
|
51
|
+
|
|
52
|
+
# Convert back to canonical FEEN string
|
|
53
|
+
feen_string = Sashite::Feen.dump(position) # or position.to_s
|
|
34
54
|
```
|
|
35
55
|
|
|
36
|
-
|
|
56
|
+
## FEEN Format
|
|
57
|
+
|
|
58
|
+
A FEEN string consists of three space-separated fields:
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
<piece-placement> <pieces-in-hand> <style-turn>
|
|
62
|
+
```
|
|
63
|
+
|
|
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
|
+
```
|
|
68
|
+
|
|
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`)
|
|
72
|
+
|
|
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
|
|
37
78
|
|
|
38
79
|
#### `Sashite::Feen.parse(string)`
|
|
39
80
|
|
|
40
|
-
Parses a FEEN string
|
|
81
|
+
Parses a FEEN string into an immutable `Position` object.
|
|
41
82
|
|
|
42
|
-
- **
|
|
43
|
-
- **Returns**: `
|
|
83
|
+
- **Parameter**: `string` (String) - FEEN notation string
|
|
84
|
+
- **Returns**: `Position` - Immutable position object
|
|
44
85
|
- **Raises**: `Sashite::Feen::Error` subclasses on invalid input
|
|
45
86
|
|
|
87
|
+
```ruby
|
|
88
|
+
position = Sashite::Feen.parse("8/8/8/8/8/8/8/8 / C/c")
|
|
89
|
+
|
|
90
|
+
# Board-less position (empty piece placement)
|
|
91
|
+
position = Sashite::Feen.parse(" / C/c")
|
|
92
|
+
```
|
|
93
|
+
|
|
46
94
|
#### `Sashite::Feen.dump(position)`
|
|
47
95
|
|
|
48
|
-
Converts a position object into its canonical FEEN string
|
|
96
|
+
Converts a position object into its canonical FEEN string.
|
|
49
97
|
|
|
50
|
-
- **
|
|
51
|
-
- **Returns**: Canonical FEEN string
|
|
98
|
+
- **Parameter**: `position` (Position) - Position object
|
|
99
|
+
- **Returns**: `String` - Canonical FEEN string
|
|
52
100
|
- **Guarantees**: Deterministic output (same position always produces same string)
|
|
53
101
|
|
|
102
|
+
```ruby
|
|
103
|
+
feen_string = Sashite::Feen.dump(position)
|
|
104
|
+
```
|
|
105
|
+
|
|
54
106
|
### Position Object
|
|
55
107
|
|
|
56
|
-
The `Position` object
|
|
108
|
+
The `Position` object is immutable and provides read-only access to three components:
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
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)
|
|
115
|
+
```
|
|
57
116
|
|
|
117
|
+
**Equality and hashing:**
|
|
58
118
|
```ruby
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
position.styles # => Styles object (style-turn information)
|
|
62
|
-
position.to_s # => Canonical FEEN string (equivalent to dump)
|
|
119
|
+
position1 == position2 # Component-wise equality
|
|
120
|
+
position1.hash # Consistent hash for same positions
|
|
63
121
|
```
|
|
64
122
|
|
|
65
|
-
|
|
123
|
+
### Placement Object
|
|
66
124
|
|
|
67
|
-
|
|
125
|
+
Represents the board configuration as a flat array of ranks with explicit separators.
|
|
68
126
|
|
|
127
|
+
```ruby
|
|
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)
|
|
69
137
|
```
|
|
70
|
-
|
|
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
|
+
# => []
|
|
71
169
|
```
|
|
72
170
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
206
|
+
|
|
207
|
+
Represents game styles and indicates the active player.
|
|
208
|
+
|
|
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
|
+
```
|
|
214
|
+
|
|
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)
|
|
220
|
+
|
|
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"
|
|
233
|
+
)
|
|
234
|
+
|
|
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
|
+
)
|
|
244
|
+
```
|
|
76
245
|
|
|
77
|
-
|
|
246
|
+
### Shōgi with Captured Pieces
|
|
247
|
+
|
|
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
|
+
)
|
|
258
|
+
|
|
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
|
|
269
|
+
|
|
270
|
+
```ruby
|
|
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)
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Multi-Dimensional Boards
|
|
288
|
+
|
|
289
|
+
```ruby
|
|
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)
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### Irregular Boards
|
|
307
|
+
|
|
308
|
+
```ruby
|
|
309
|
+
# Diamond-shaped board
|
|
310
|
+
diamond = Sashite::Feen.parse("3/4/5/4/3 / G/g")
|
|
311
|
+
|
|
312
|
+
# Check structure
|
|
313
|
+
diamond.placement.ranks.map(&:size) # => [3, 4, 5, 4, 3]
|
|
314
|
+
|
|
315
|
+
# Very large board
|
|
316
|
+
large_board = Sashite::Feen.parse("100/100/100 / G/g")
|
|
317
|
+
large_board.placement.total_squares # => 300
|
|
318
|
+
|
|
319
|
+
# Single square
|
|
320
|
+
single = Sashite::Feen.parse("K / C/c")
|
|
321
|
+
single.placement.rank_count # => 1
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### Completely Irregular Structures
|
|
325
|
+
|
|
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
|
+
```
|
|
348
|
+
|
|
349
|
+
### Empty Ranks
|
|
350
|
+
|
|
351
|
+
FEEN supports empty ranks (ranks with no pieces):
|
|
352
|
+
|
|
353
|
+
```ruby
|
|
354
|
+
# Trailing separator creates empty rank
|
|
355
|
+
feen = "K/// / C/c"
|
|
356
|
+
position = Sashite::Feen.parse(feen)
|
|
357
|
+
|
|
358
|
+
position.placement.ranks.size # => 2
|
|
359
|
+
position.placement.ranks[0] # => [K]
|
|
360
|
+
position.placement.ranks[1] # => [] (empty rank)
|
|
361
|
+
position.placement.separators # => ["///"]
|
|
362
|
+
|
|
363
|
+
# Round-trip preserves structure
|
|
364
|
+
Sashite::Feen.dump(position) == feen # => true
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### Board-less Positions
|
|
368
|
+
|
|
369
|
+
FEEN supports positions without piece placement, useful for tracking only style and turn information:
|
|
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"
|
|
381
|
+
```
|
|
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
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### State Modifiers and Derivation
|
|
402
|
+
|
|
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")
|
|
406
|
+
|
|
407
|
+
# Diminished pieces (weakened, vulnerable)
|
|
408
|
+
diminished = Sashite::Feen.parse("-K-Q-R-B/8/8/8/8/8/8/8 / C/c")
|
|
409
|
+
|
|
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
|
+
```
|
|
78
413
|
|
|
79
414
|
## Error Handling
|
|
80
415
|
|
|
81
|
-
|
|
416
|
+
FEEN defines specific error classes for different validation failures:
|
|
417
|
+
|
|
418
|
+
```ruby
|
|
419
|
+
begin
|
|
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}"
|
|
424
|
+
end
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### Error Hierarchy
|
|
82
428
|
|
|
83
429
|
```txt
|
|
84
|
-
Sashite::Feen::Error
|
|
85
|
-
├── Error::Syntax
|
|
86
|
-
├── Error::Piece
|
|
87
|
-
├── Error::Style
|
|
88
|
-
├── Error::Count
|
|
89
|
-
└── Error::Validation
|
|
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"
|
|
90
452
|
```
|
|
91
453
|
|
|
92
454
|
## Properties
|
|
93
455
|
|
|
94
456
|
- **Purely functional**: Immutable data structures, no side effects
|
|
95
|
-
- **Canonical output**: Deterministic string generation
|
|
96
|
-
- **Specification compliant**: Strict adherence to FEEN v1.0.0
|
|
97
|
-
- **Minimal API**: Two methods for complete functionality
|
|
98
|
-
- **
|
|
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
|
|
99
465
|
|
|
100
466
|
## Dependencies
|
|
101
467
|
|
|
102
|
-
- [sashite-epin](https://github.com/sashite/epin.rb)
|
|
103
|
-
- [sashite-sin](https://github.com/sashite/sin.rb)
|
|
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
|
|
104
470
|
|
|
105
471
|
## Documentation
|
|
106
472
|
|
|
107
|
-
- [FEEN Specification v1.0.0](https://sashite.dev/specs/feen/1.0.0/)
|
|
108
|
-
- [FEEN Examples](https://sashite.dev/specs/feen/1.0.0/examples/)
|
|
109
|
-
- [API Documentation](https://rubydoc.info/github/sashite/feen.rb/main)
|
|
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
|
|
477
|
+
|
|
478
|
+
## Development
|
|
479
|
+
|
|
480
|
+
```sh
|
|
481
|
+
# Clone the repository
|
|
482
|
+
git clone https://github.com/sashite/feen.rb.git
|
|
483
|
+
cd feen.rb
|
|
484
|
+
|
|
485
|
+
# Install dependencies
|
|
486
|
+
bundle install
|
|
487
|
+
|
|
488
|
+
# Run tests
|
|
489
|
+
ruby test.rb
|
|
490
|
+
|
|
491
|
+
# Generate documentation
|
|
492
|
+
yard doc
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
## Contributing
|
|
496
|
+
|
|
497
|
+
1. Fork the repository
|
|
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
|
|
110
504
|
|
|
111
505
|
## License
|
|
112
506
|
|
|
@@ -114,4 +508,4 @@ Available as open source under the [MIT License](https://opensource.org/licenses
|
|
|
114
508
|
|
|
115
509
|
## About
|
|
116
510
|
|
|
117
|
-
Maintained by [Sashité](https://sashite.com/)
|
|
511
|
+
Maintained by [Sashité](https://sashite.com/) — promoting chess variants and sharing the beauty of board game cultures.
|