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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5bcbcddcc2de55695f9a203e30fa5d3d236deaf7c0e3975011ae1a47f389cd7c
4
- data.tar.gz: 98b184086642d30761cf4646dbeac1ca0a88860c11d18a15866f01e7f0b0e040
3
+ metadata.gz: a8182f6c48f6a615f38d75d3060f216c06c6606b118544cb4f4f0e40b9615e89
4
+ data.tar.gz: 1150aacab981e71aa32aaae585ec1998cd678bb5997432135b0089394eef2482
5
5
  SHA512:
6
- metadata.gz: cb41ec980de1a7ae5237da821f0a3be0cabe50768cb0dceec8988465336512a0705aec6f5aa9acfe1b0cf43770996581355029be8623a19d8a44dc737c628d83
7
- data.tar.gz: 95507ce051c09837ca92da2b9447b2ef3f3c9a34fdc4e412cce0ed8e32595acf91acdbae37d2ed313315db9c0e29069edae0ec1c71770b3d1f853f20896a3c0c
6
+ metadata.gz: b8ed4a473fce336ce73cb89c684e9ea7820842b78e12766bc32440f25da7f61998c13077ce3743972ef234a85771c63f58778835dd11bfba1fd97ef63dfb7154
7
+ data.tar.gz: 679296955c3b032e0ebe7b9f6ca4776b42b617f67a876e5de6bee015d186e40287d9cc964a3bb2d3faec0dbaf0dd98d810e3bf522cde197d340f168ba9be3a84
data/README.md CHANGED
@@ -1,236 +1,511 @@
1
- Here you go — a lean, easy-to-read README that reflects the new “API / parser / dumper” design without drowning the reader in details.
1
+ # Feen.rb
2
2
 
3
- ---
3
+ [![Version](https://img.shields.io/github/v/tag/sashite/feen.rb?label=Version&logo=github)](https://github.com/sashite/feen.rb/tags)
4
+ [![Yard documentation](https://img.shields.io/badge/Yard-documentation-blue.svg?logo=github)](https://rubydoc.info/github/sashite/feen.rb/main)
5
+ ![Ruby](https://github.com/sashite/feen.rb/actions/workflows/main.yml/badge.svg?branch=main)
6
+ [![License](https://img.shields.io/github/license/sashite/feen.rb?label=License&logo=github)](https://github.com/sashite/feen.rb/raw/main/LICENSE.md)
4
7
 
5
- # Feen.rb
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
- > **FEEN** Forsyth–Edwards Enhanced Notation for rule-agnostic board positions (Chess, Shōgi-like, Xiangqi-like, variants).
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
- Purely functional, immutable Ruby implementation built on top of **EPIN** (piece identifiers) and **SIN** (style identifiers).
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
- ## Why FEEN?
52
+ # Convert back to canonical FEEN string
53
+ feen_string = Sashite::Feen.dump(position) # or position.to_s
54
+ ```
14
55
 
15
- * **Rule-agnostic**: expresses a board **position** without baking in game rules.
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 strings have **three space-separated fields**:
58
+ A FEEN string consists of three space-separated fields:
20
59
 
21
60
  ```
22
- <piece_placement> <pieces_in_hand> <style_turn>
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
- ## Installation
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
- Add to your `Gemfile`:
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
- gem "sashite-feen"
33
- ````
88
+ position = Sashite::Feen.parse("8/8/8/8/8/8/8/8 / C/c")
34
89
 
35
- Then:
90
+ # Board-less position (empty piece placement)
91
+ position = Sashite::Feen.parse(" / C/c")
92
+ ```
36
93
 
37
- ```sh
38
- bundle install
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
- This gem depends on:
106
+ ### Position Object
107
+
108
+ The `Position` object is immutable and provides read-only access to three components:
42
109
 
43
110
  ```ruby
44
- gem "sashite-epin"
45
- gem "sashite-sin"
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
- Bundler will install them automatically when you use `sashite-feen`.
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
- ## Quick start
125
+ Represents the board configuration as a flat array of ranks with explicit separators.
53
126
 
54
127
  ```ruby
55
- require "sashite/feen"
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
- # Parse
58
- pos = Sashite::Feen.parse("<placement> <hands> <style1>/<style2>")
207
+ Represents game styles and indicates the active player.
59
208
 
60
- # Validate
61
- Sashite::Feen.valid?("<your FEEN>") # => true/false
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
- # Normalize (parse → canonical dump)
64
- Sashite::Feen.normalize("<your FEEN>") # => canonical FEEN string
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
- # Build from fields (strings)
67
- pos = Sashite::Feen.build(
68
- piece_placement: "<placement>",
69
- pieces_in_hand: "<bagFirst>/<bagSecond>", # empty bags allowed: "/"
70
- style_turn: "<activeSIN>/<inactiveSIN>"
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
- # Dump a Position (canonical)
74
- Sashite::Feen.dump(pos) # => "<placement> <hands> <style1>/<style2>"
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
- > **Tip:** FEEN itself does not do JSON; keep it minimal and functional. Serialize externally if needed.
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
- ## Public API
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
- Sashite::Feen.parse(str) # => Position (or raises Sashite::Feen::Error)
85
- Sashite::Feen.valid?(str) # => Boolean
86
- Sashite::Feen.dump(position) # => String (canonical)
87
- Sashite::Feen.normalize(str) # => String (dump(parse(str)))
88
- Sashite::Feen.build(
89
- piece_placement:, pieces_in_hand:, style_turn:
90
- ) # => Position
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
- Position value-objects are immutable:
287
+ ### Multi-Dimensional Boards
94
288
 
95
289
  ```ruby
96
- pos.placement # => Sashite::Feen::Placement
97
- pos.hands # => Sashite::Feen::Hands
98
- pos.styles # => Sashite::Feen::Styles
99
- pos.to_s # => canonical FEEN string (same as dump(pos))
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
- ## Canonicalization (short rules)
308
+ ```ruby
309
+ # Diamond-shaped board
310
+ diamond = Sashite::Feen.parse("3/4/5/4/3 / G/g")
105
311
 
106
- * **Piece placement (field 1)**
312
+ # Check structure
313
+ diamond.placement.ranks.map(&:size) # => [3, 4, 5, 4, 3]
107
314
 
108
- * Consecutive empties compress to digits `1..9`; runs `>9` are split into `"9"` + remainder.
109
- * Digit `0` in empties is invalid.
110
- * EPIN tokens are validated via `sashite-epin` and re-emitted canonically.
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
- * **Pieces in hand (field 2)**
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
- * Two concatenated bags separated by `/` (either side may be empty).
115
- * Counts are aggregated; `1` is omitted in output.
116
- * Deterministic sort per EPIN: quantity ↓, letter ↑ (case-insensitive), uppercase before lowercase, prefix `-` < `+` < none, suffix none < `'`.
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
- * **Style-turn (field 3)**
349
+ ### Empty Ranks
119
350
 
120
- * Exactly two SIN tokens separated by `/`.
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
- ## Design overview
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
- The gem is small, layered, and testable:
363
+ # Round-trip preserves structure
364
+ Sashite::Feen.dump(position) == feen # => true
365
+ ```
129
366
 
130
- * **API**: `Sashite::Feen` (parse / valid? / dump / normalize / build)
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
- ### Project layout
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
- lib/
141
- ├─ sashite-feen.rb
142
- └─ sashite/
143
- ├─ feen.rb # Public API
144
- └─ feen/
145
- ├─ error.rb
146
- ├─ position.rb
147
- ├─ placement.rb
148
- ├─ hands.rb
149
- ├─ styles.rb
150
- ├─ ordering.rb
151
- ├─ parser.rb
152
- ├─ parser/
153
- │ ├─ piece_placement.rb
154
- │ ├─ pieces_in_hand.rb
155
- │ └─ style_turn.rb
156
- ├─ dumper.rb
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
- > Version is defined outside of `lib/sashite/feen/version.rb` (e.g., `VERSION.semver`).
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
- ## Errors
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
- Rescue at the granularity you need:
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
- * `Sashite::Feen::Error::Syntax` – tokenization/field arity
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
- Example:
416
+ FEEN defines specific error classes for different validation failures:
179
417
 
180
418
  ```ruby
181
419
  begin
182
- pos = Sashite::Feen.parse(str)
183
- rescue Sashite::Feen::Error::Style => e
184
- warn "Bad style-turn: #{e.message}"
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
- ## Dependencies & compatibility
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
- * Runtime: `sashite-epin`, `sashite-sin`
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 smoke tests
488
+ # Run tests
209
489
  ruby test.rb
210
490
 
211
- # Generate YARD docs
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: `git checkout -b feat/my-change`
221
- 3. Add tests covering your changes
222
- 4. Ensure everything is green (lint, tests, docs)
223
- 5. Commit with a conventional message
224
- 6. Push and open a Pull Request
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
- Open source under the [MIT License](https://opensource.org/licenses/MIT).
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 **Sashité** — promoting chess variants and sharing the beauty of board-game cultures.
511
+ Maintained by [Sashité](https://sashite.com/) — promoting chess variants and sharing the beauty of board game cultures.