feen 5.0.0.beta5 → 5.0.0.beta7

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: 28b60c3d60fd3241172fee9e143c53e6dca04082a2efe7f0fa7559b85dc6eac0
4
- data.tar.gz: a15999144de7bcd5637b6efca4ed1c4a8ae7d3757d6d583123dfaf44468d6d35
3
+ metadata.gz: 0f302aa1f4504f94ed909dcb5f6759c52a8895c85e4bd4cc0c0cbff42ef6fdd4
4
+ data.tar.gz: eb77d9e6e1d1bae1c13cc0b4997e1b509e06b631001fbc8c60027da234f50494
5
5
  SHA512:
6
- metadata.gz: 861669bea6101dba5eefcf6062b9e4e0b37152618e7ffa9c1ba963eb37b2f49cad804f4c1bbee31bd030bf4ddd9602563df7e54cc244384d37e1f45ec748c5ff
7
- data.tar.gz: 6f00dcfc09fd378fde0fbe3cd36b078718eab1fa27e208d474637ef89853a7543be4027116d623d0e037c340f0deeae90ce337404db09c953faecba852f56ece
6
+ metadata.gz: 331d90ea031b33aa88292680f98b5c847c467827ad2cd64f39e261c8d052a3cfefdf1f4fe734befc51ffffe7667fbe3b713a08d6952adc86869f3401726f12c5
7
+ data.tar.gz: 574801b36461caf3e971540430962344442a43027e100d76527df73345374ab624a6a2d7bd80fa9cb3917a85a2e16d4ececbca67325776c134a33d7d400ee5c7
data/README.md CHANGED
@@ -14,16 +14,30 @@ FEEN (Forsyth–Edwards Enhanced Notation) is a compact, canonical, and rule-agn
14
14
  This gem implements the [FEEN Specification v1.0.0](https://sashite.dev/documents/feen/1.0.0/), providing a Ruby interface for:
15
15
 
16
16
  - Representing positions from various games without knowledge of specific rules
17
- - Supporting boards of arbitrary dimensions
18
- - Encoding pieces in hand (as used in Shogi)
17
+ - Supporting boards of arbitrary dimensions (2D, 3D, and beyond)
18
+ - Encoding pieces in hand with full PNN (Piece Name Notation) support
19
19
  - Facilitating serialization and deserialization of positions
20
20
  - Ensuring canonical representation for consistent data handling
21
21
 
22
+ ## FEEN Format
23
+
24
+ A FEEN record consists of three space-separated fields:
25
+
26
+ ```
27
+ <PIECE-PLACEMENT> <PIECES-IN-HAND> <GAMES-TURN>
28
+ ```
29
+
30
+ ### Field Details
31
+
32
+ 1. **Piece Placement**: Spatial distribution of pieces on the board using [PNN notation](https://sashite.dev/documents/pnn/1.0.0/)
33
+ 2. **Pieces in Hand**: Off-board pieces available for placement, formatted as `"UPPERCASE/lowercase"` and sorted canonically within each section
34
+ 3. **Games Turn**: Game identifiers and active player indication
35
+
22
36
  ## Installation
23
37
 
24
38
  ```ruby
25
39
  # In your Gemfile
26
- gem "feen", ">= 5.0.0.beta5"
40
+ gem "feen", ">= 5.0.0.beta7"
27
41
  ```
28
42
 
29
43
  Or install manually:
@@ -32,14 +46,6 @@ Or install manually:
32
46
  gem install feen --pre
33
47
  ```
34
48
 
35
- ## FEEN Format
36
-
37
- A FEEN record consists of three space-separated fields:
38
-
39
- ```
40
- <PIECE-PLACEMENT> <PIECES-IN-HAND> <GAMES-TURN>
41
- ```
42
-
43
49
  ## Basic Usage
44
50
 
45
51
  ### Parsing FEEN Strings
@@ -49,20 +55,20 @@ Convert a FEEN string into a structured Ruby object:
49
55
  ```ruby
50
56
  require "feen"
51
57
 
52
- feen_string = "r'nbqkbnr'/pppppppp/8/8/8/8/PPPPPPPP/R'NBQKBNR' - CHESS/chess"
58
+ feen_string = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR / CHESS/chess"
53
59
  position = Feen.parse(feen_string)
54
60
 
55
61
  # Result is a hash:
56
62
  # {
57
63
  # piece_placement: [
58
- # ["r'", "n", "b", "q", "k", "b", "n", "r'"],
64
+ # ["r", "n", "b", "q", "k", "b", "n", "r"],
59
65
  # ["p", "p", "p", "p", "p", "p", "p", "p"],
60
66
  # ["", "", "", "", "", "", "", ""],
61
67
  # ["", "", "", "", "", "", "", ""],
62
68
  # ["", "", "", "", "", "", "", ""],
63
69
  # ["", "", "", "", "", "", "", ""],
64
70
  # ["P", "P", "P", "P", "P", "P", "P", "P"],
65
- # ["R'", "N", "B", "Q", "K", "B", "N", "R'"]
71
+ # ["R", "N", "B", "Q", "K", "B", "N", "R"]
66
72
  # ],
67
73
  # pieces_in_hand: [],
68
74
  # games_turn: ["CHESS", "chess"]
@@ -77,7 +83,7 @@ Parse a FEEN string without raising exceptions:
77
83
  require "feen"
78
84
 
79
85
  # Valid FEEN string
80
- result = Feen.safe_parse("r'nbqkbnr'/pppppppp/8/8/8/8/PPPPPPPP/R'NBQKBNR' - CHESS/chess")
86
+ result = Feen.safe_parse("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR / CHESS/chess")
81
87
  # => {piece_placement: [...], pieces_in_hand: [...], games_turn: [...]}
82
88
 
83
89
  # Invalid FEEN string
@@ -94,14 +100,14 @@ require "feen"
94
100
 
95
101
  # Representation of a chess board in initial position
96
102
  piece_placement = [
97
- ["r'", "n", "b", "q", "k", "b", "n", "r'"],
103
+ ["r", "n", "b", "q", "k", "b", "n", "r"],
98
104
  ["p", "p", "p", "p", "p", "p", "p", "p"],
99
105
  ["", "", "", "", "", "", "", ""],
100
106
  ["", "", "", "", "", "", "", ""],
101
107
  ["", "", "", "", "", "", "", ""],
102
108
  ["", "", "", "", "", "", "", ""],
103
109
  ["P", "P", "P", "P", "P", "P", "P", "P"],
104
- ["R'", "N", "B", "Q", "K", "B", "N", "R'"]
110
+ ["R", "N", "B", "Q", "K", "B", "N", "R"]
105
111
  ]
106
112
 
107
113
  result = Feen.dump(
@@ -109,7 +115,7 @@ result = Feen.dump(
109
115
  games_turn: %w[CHESS chess],
110
116
  pieces_in_hand: []
111
117
  )
112
- # => "r'nbqkbnr'/pppppppp/8/8/8/8/PPPPPPPP/R'NBQKBNR' - CHESS/chess"
118
+ # => "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR / CHESS/chess"
113
119
  ```
114
120
 
115
121
  ### Validation
@@ -120,16 +126,16 @@ Check if a string is valid FEEN notation and in canonical form:
120
126
  require "feen"
121
127
 
122
128
  # Canonical form
123
- Feen.valid?("lnsgk3l/5g3/p1ppB2pp/9/8B/2P6/P2PPPPPP/3K3R1/5rSNL N5P2gln2s SHOGI/shogi")
129
+ Feen.valid?("lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL / SHOGI/shogi")
124
130
  # => true
125
131
 
126
132
  # Invalid syntax
127
133
  Feen.valid?("invalid feen string")
128
134
  # => false
129
135
 
130
- # Valid syntax but non-canonical form (pieces in hand not in lexicographic order)
131
- Feen.valid?("lnsgk3l/5g3/p1ppB2pp/9/8B/2P6/P2PPPPPP/3K3R1/5rSNL N5P2gn2sl SHOGI/shogi")
132
- # => false
136
+ # Valid syntax but non-canonical form (pieces in hand not in canonical order)
137
+ Feen.valid?("8/8/8/8/8/8/8/8 P3K/ CHESS/chess")
138
+ # => false (wrong quantity sorting in uppercase section)
133
139
  ```
134
140
 
135
141
  The `valid?` method performs two levels of validation:
@@ -144,47 +150,126 @@ As FEEN is rule-agnostic, it can represent positions from various board games. H
144
150
  ### International Chess
145
151
 
146
152
  ```ruby
147
- feen_string = "r'nbqkbnr'/pppppppp/8/8/8/8/PPPPPPPP/R'NBQKBNR' - CHESS/chess"
153
+ feen_string = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR / CHESS/chess"
148
154
  ```
149
155
 
150
- In this initial chess position:
151
-
152
- - The `'` suffixes on rooks indicate an intermediate state (which might represent castling rights in chess, though FEEN doesn't define this semantics)
153
- - The third field `CHESS/chess` indicates it's the player with uppercase pieces' turn to move
156
+ In this initial chess position, the third field `CHESS/chess` indicates it's the player with uppercase pieces' turn to move.
154
157
 
155
158
  ### Shogi (Japanese Chess)
156
159
 
157
160
  ```ruby
158
- feen_string = "lnsgk3l/5g3/p1ppB2pp/9/8B/2P6/P2PPPPPP/3K3R1/5rSNL N5P2gln2s SHOGI/shogi"
161
+ feen_string = "lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL / SHOGI/shogi"
162
+ ```
163
+
164
+ **With pieces in hand and promotions:**
165
+
166
+ ```ruby
167
+ feen_string = "lnsgk3l/5g3/p1ppB2pp/9/8B/2P6/P2PPPPPP/3K3R1/5rSNL 5P2G2L/2gln2s SHOGI/shogi"
159
168
  ```
160
169
 
161
170
  In this shogi position:
162
171
 
163
172
  - The format supports promotions with the `+` prefix (e.g., `+P` for a promoted pawn)
164
- - The notation allows for pieces in hand, indicated in the second field
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
165
177
  - `SHOGI/shogi` indicates it's Sente's (Black's, uppercase) turn to move
166
- - `N5P2gln2s` shows the pieces in hand: Sente has a Knight (N) and 5 Pawns (5P), while Gote has 2 Golds (2g), a Lance (l), a Knight (n), and 2 Silvers (2s), all properly sorted in ASCII lexicographic order
167
178
 
168
179
  ### Makruk (Thai Chess)
169
180
 
170
181
  ```ruby
171
- feen_string = "rnbqkbnr/8/pppppppp/8/8/PPPPPPPP/8/RNBQKBNR - MAKRUK/makruk"
182
+ feen_string = "rnbqkbnr/8/pppppppp/8/8/PPPPPPPP/8/RNBKQBNR / MAKRUK/makruk"
172
183
  ```
173
184
 
174
- This initial Makruk position is easily represented in FEEN without needing to know the specific rules of the game.
175
-
176
185
  ### Xiangqi (Chinese Chess)
177
186
 
178
187
  ```ruby
179
- feen_string = "rheagaehr/9/1c5c1/s1s1s1s1s/9/9/S1S1S1S1S/1C5C1/9/RHEAGAEHR - XIANGQI/xiangqi"
188
+ feen_string = "rheagaehr/9/1c5c1/s1s1s1s1s/9/9/S1S1S1S1S/1C5C1/9/RHEAGAEHR / XIANGQI/xiangqi"
180
189
  ```
181
190
 
182
- In this Xiangqi position:
191
+ ## Advanced Features
192
+
193
+ ### Pieces in Hand with Case Separation
183
194
 
184
- - The representation uses single letters for the different pieces
185
- - The format naturally adapts to the presence of a "river" (empty space in the middle)
195
+ FEEN uses case separation for pieces in hand to distinguish between players using the format `"UPPERCASE_PIECES/LOWERCASE_PIECES"`:
186
196
 
187
- ## Advanced Features
197
+ ```ruby
198
+ require "feen"
199
+
200
+ # Parse pieces in hand with case separation
201
+ pieces_in_hand = Feen.parse("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR 3P2B/2pn CHESS/chess")[:pieces_in_hand]
202
+ # => ["B", "B", "P", "P", "P", "n", "p", "p"] # Sorted alphabetically
203
+
204
+ # Create FEEN with pieces in hand
205
+ result = Feen.dump(
206
+ piece_placement: [["k"], ["K"]],
207
+ pieces_in_hand: ["P", "P", "B", "p", "n"],
208
+ games_turn: ["TEST", "test"]
209
+ )
210
+ # => "k/K 2BP/np TEST/test"
211
+ ```
212
+
213
+ ### Piece Name Notation (PNN) Support
214
+
215
+ FEEN supports the complete [PNN specification](https://sashite.dev/documents/pnn/1.0.0/) for representing pieces with state modifiers:
216
+
217
+ #### PNN Modifiers
218
+
219
+ - **Prefix `+`**: Enhanced state (e.g., promoted pieces in shogi)
220
+ - **Prefix `-`**: Diminished state (e.g., restricted movement)
221
+ - **Suffix `'`**: Intermediate state (e.g., castling rights, en passant eligibility)
222
+
223
+ #### Examples with PNN
224
+
225
+ ```ruby
226
+ # Shogi position with promoted pieces on board
227
+ piece_placement = [
228
+ ["", "", "", "", "+P", "", "", "", ""] # Promoted pawn on board
229
+ # ... other ranks
230
+ ]
231
+
232
+ # Pieces in hand with PNN modifiers - case separated
233
+ pieces_in_hand = ["+P", "+P", "+P", "B'", "B'", "-p", "P"]
234
+
235
+ result = Feen.dump(
236
+ piece_placement: piece_placement,
237
+ pieces_in_hand: pieces_in_hand,
238
+ games_turn: %w[SHOGI shogi]
239
+ )
240
+ # => "8/8/8/8/4+P4/8/8/8/8 3+P2B'P/-p SHOGI/shogi"
241
+ ```
242
+
243
+ ### Canonical Pieces in Hand Sorting
244
+
245
+ FEEN enforces canonical ordering of pieces in hand within each case section according to the specification:
246
+
247
+ 1. **By quantity (descending)**
248
+ 2. **By complete PNN representation (alphabetically ascending)**
249
+
250
+ The dumper organizes pieces by case first, then applies canonical sorting within each section:
251
+
252
+ ```ruby
253
+ # Input pieces in any order
254
+ pieces = ["P", "b", "P", "+P", "B", "p", "+P", "+P"]
255
+
256
+ result = Feen.dump(
257
+ piece_placement: [["k"], ["K"]],
258
+ pieces_in_hand: pieces,
259
+ games_turn: %w[GAME game]
260
+ )
261
+ # => "k/K 3+P2PB/bp GAME/game"
262
+ # Breakdown:
263
+ # - Uppercase: 3×+P (most frequent), 2×P, 1×B (alphabetical within same quantity)
264
+ # - Lowercase: 1×b, 1×p (alphabetical)
265
+ ```
266
+
267
+ The parser returns pieces in simple alphabetical order for easy handling:
268
+
269
+ ```ruby
270
+ pieces_in_hand = Feen.parse("k/K 3+P2PB/bp GAME/game")[:pieces_in_hand]
271
+ # => ["+P", "+P", "+P", "B", "P", "P", "b", "p"] # Alphabetically sorted
272
+ ```
188
273
 
189
274
  ### Multi-dimensional Boards
190
275
 
@@ -193,7 +278,7 @@ FEEN supports arbitrary-dimensional board configurations:
193
278
  ```ruby
194
279
  require "feen"
195
280
 
196
- # 3D board
281
+ # 3D board (2×2×3 configuration)
197
282
  piece_placement = [
198
283
  [
199
284
  %w[r n b],
@@ -210,29 +295,100 @@ result = Feen.dump(
210
295
  games_turn: %w[FOO bar],
211
296
  pieces_in_hand: []
212
297
  )
213
- # => "rnb/qkp//PR1/1KQ - FOO/bar"
298
+ # => "rnb/qkp//PR1/1KQ / FOO/bar"
299
+ ```
300
+
301
+ ### Hybrid Games
302
+
303
+ FEEN supports hybrid games mixing different piece sets:
304
+
305
+ ```ruby
306
+ # Chess-Shogi hybrid position
307
+ feen_string = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR 3+P2B'/p CHESS/shogi"
308
+ ```
309
+
310
+ This represents a position where:
311
+
312
+ - The board uses chess-style pieces
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
316
+
317
+ ## Round-trip Consistency
318
+
319
+ FEEN.rb guarantees round-trip consistency - parsing and dumping produces identical canonical strings:
320
+
321
+ ```ruby
322
+ original = "lnsgk3l/5g3/p1ppB2pp/9/8B/2P6/P2PPPPPP/3K3R1/5rSNL 5P2G2L/2gln2s SHOGI/shogi"
323
+ parsed = Feen.parse(original)
324
+ dumped = Feen.dump(**parsed)
325
+
326
+ original == dumped # => true (guaranteed canonical form)
327
+ ```
328
+
329
+ ## Error Handling
330
+
331
+ ### Validation Errors
332
+
333
+ ```ruby
334
+ # Invalid PNN format
335
+ Feen.dump(
336
+ piece_placement: [["k"]],
337
+ pieces_in_hand: ["++P"], # Invalid: double prefix
338
+ games_turn: %w[GAME game]
339
+ )
340
+ # => ArgumentError: Invalid format at index: 0, value: '++P'
341
+
342
+ # Invalid games turn
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
349
+
350
+ # Invalid pieces in hand format (parsing)
351
+ Feen.parse("8/8/8/8/8/8/8/8 NoSeparator CHESS/chess")
352
+ # => ArgumentError: Invalid pieces in hand format: NoSeparator
214
353
  ```
215
354
 
216
- ### Piece Modifiers
355
+ ### Safe Operations
356
+
357
+ ```ruby
358
+ # Use safe_parse for user input
359
+ user_input = gets.chomp
360
+ position = Feen.safe_parse(user_input)
361
+
362
+ if position
363
+ puts "Valid FEEN position!"
364
+ puts "Pieces in hand: #{position[:pieces_in_hand]}"
365
+ else
366
+ puts "Invalid FEEN format"
367
+ end
368
+ ```
217
369
 
218
- FEEN supports prefixes and suffixes for pieces to denote various states or capabilities:
370
+ ## Performance Considerations
219
371
 
220
- - **Prefix `+`**: Enhanced state
221
- - Example in shogi: `+P` may represent a promoted pawn
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
222
377
 
223
- - **Prefix `-`**: Diminished state
224
- - Could represent a piece with limited movement or other restrictions
378
+ ## Compatibility
225
379
 
226
- - **Suffix `'`**: Intermediate state
227
- - Example in chess: `R'` may represent a rook that has intermediate status (such as castling eligibility)
228
- - Example in chess: `P'` may represent a pawn that may be captured _en passant_
380
+ - **Ruby version**: >= 3.2.0
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)
229
384
 
230
- These modifiers have no defined semantics in the FEEN specification itself but provide a flexible framework for representing piece-specific conditions while maintaining FEEN's rule-agnostic nature.
385
+ ## Related Specifications
231
386
 
232
- ## Documentation
387
+ FEEN is part of a family of specifications for abstract strategy games:
233
388
 
234
- - [Official FEEN Specification](https://sashite.dev/documents/feen/1.0.0/)
235
- - [API Documentation](https://rubydoc.info/github/sashite/feen.rb/main)
389
+ - [FEEN Specification v1.0.0](https://sashite.dev/documents/feen/1.0.0/) - Board position notation
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
236
392
 
237
393
  ## License
238
394
 
@@ -5,7 +5,7 @@ module Feen
5
5
  module PiecesInHand
6
6
  Errors = {
7
7
  invalid_type: "Piece at index: %<index>s must be a String, got type: %<type>s",
8
- invalid_format: "Piece at index: %<index>s has an invalid format: '%<value>s'"
8
+ invalid_format: "Piece at index: %<index>s has an invalid PNN format: '%<value>s'. Expected format: [prefix]letter[suffix] where prefix is + or -, suffix is ', and letter is a-z or A-Z"
9
9
  }.freeze
10
10
  end
11
11
  end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative File.join("pieces_in_hand", "no_pieces")
4
3
  require_relative File.join("pieces_in_hand", "errors")
5
4
 
6
5
  module Feen
@@ -9,24 +8,102 @@ module Feen
9
8
  module PiecesInHand
10
9
  # Converts an array of piece identifiers to a FEEN-formatted pieces in hand string
11
10
  #
12
- # @param piece_chars [Array<String>] Array of single-character piece identifiers
13
- # @return [String] FEEN-formatted pieces in hand string
11
+ # @param piece_chars [Array<String>] Array of piece identifiers (e.g., ["P", "p", "B", "B", "p", "+P"])
12
+ # @return [String] FEEN-formatted pieces in hand string following the format:
13
+ # - Groups pieces by case: uppercase first, then lowercase, separated by "/"
14
+ # - Within each group, sorts by quantity (descending), then alphabetically (ascending)
15
+ # - Uses count notation for quantities > 1 (e.g., "3P" instead of "PPP")
14
16
  # @raise [ArgumentError] If any piece identifier is invalid
15
17
  # @example
16
- # PiecesInHand.dump("P", "p", "B")
17
- # # => "BPp"
18
+ # PiecesInHand.dump("P", "P", "P", "B", "B", "p", "p", "p", "p", "p")
19
+ # # => "3P2B/5p"
20
+ #
21
+ # PiecesInHand.dump("p", "P", "B")
22
+ # # => "BP/p"
18
23
  #
19
24
  # PiecesInHand.dump
20
- # # => "-"
25
+ # # => "/"
21
26
  def self.dump(*piece_chars)
22
- # If no pieces in hand, return the standardized empty indicator
23
- return NoPieces if piece_chars.empty?
24
-
25
27
  # Validate each piece character according to the FEEN specification
26
28
  validated_chars = validate_piece_chars(piece_chars)
27
29
 
28
- # Sort pieces in ASCII lexicographic order and join them
29
- validated_chars.sort.join
30
+ # Group pieces by case
31
+ uppercase_pieces, lowercase_pieces = group_pieces_by_case(validated_chars)
32
+
33
+ # Format each group according to FEEN specification
34
+ uppercase_formatted = format_pieces_group(uppercase_pieces)
35
+ lowercase_formatted = format_pieces_group(lowercase_pieces)
36
+
37
+ # Combine with separator
38
+ "#{uppercase_formatted}/#{lowercase_formatted}"
39
+ end
40
+
41
+ # Groups pieces by case (uppercase vs lowercase)
42
+ #
43
+ # @param pieces [Array<String>] Array of validated piece identifiers
44
+ # @return [Array<Array<String>, Array<String>>] Two arrays: [uppercase_pieces, lowercase_pieces]
45
+ private_class_method def self.group_pieces_by_case(pieces)
46
+ uppercase_pieces = pieces.select { |piece| piece_is_uppercase?(piece) }
47
+ lowercase_pieces = pieces.select { |piece| piece_is_lowercase?(piece) }
48
+
49
+ [uppercase_pieces, lowercase_pieces]
50
+ end
51
+
52
+ # Determines if a piece belongs to the uppercase group
53
+ # A piece is considered uppercase if its main letter is uppercase (ignoring prefixes/suffixes)
54
+ #
55
+ # @param piece [String] Piece identifier (e.g., "P", "+P", "P'", "+P'")
56
+ # @return [Boolean] True if the piece's main letter is uppercase
57
+ private_class_method def self.piece_is_uppercase?(piece)
58
+ # Extract the main letter (skip prefixes like + or -)
59
+ main_letter = piece.gsub(/\A[+-]/, "").gsub(/'\z/, "")
60
+ main_letter.match?(/[A-Z]/)
61
+ end
62
+
63
+ # Determines if a piece belongs to the lowercase group
64
+ # A piece is considered lowercase if its main letter is lowercase (ignoring prefixes/suffixes)
65
+ #
66
+ # @param piece [String] Piece identifier (e.g., "p", "+p", "p'", "+p'")
67
+ # @return [Boolean] True if the piece's main letter is lowercase
68
+ private_class_method def self.piece_is_lowercase?(piece)
69
+ # Extract the main letter (skip prefixes like + or -)
70
+ main_letter = piece.gsub(/\A[+-]/, "").gsub(/'\z/, "")
71
+ main_letter.match?(/[a-z]/)
72
+ end
73
+
74
+ # Formats a group of pieces according to FEEN specification
75
+ #
76
+ # @param pieces [Array<String>] Array of pieces from the same case group
77
+ # @return [String] Formatted string for this group (e.g., "3P2B", "5pq")
78
+ private_class_method def self.format_pieces_group(pieces)
79
+ return "" if pieces.empty?
80
+
81
+ # Count occurrences of each piece type
82
+ piece_counts = pieces.each_with_object(Hash.new(0)) do |piece, counts|
83
+ counts[piece] += 1
84
+ end
85
+
86
+ # Sort by count (descending) then alphabetically (ascending)
87
+ sorted_pieces = piece_counts.sort do |a, b|
88
+ piece_a, count_a = a
89
+ piece_b, count_b = b
90
+
91
+ # Primary sort: by count (descending)
92
+ count_comparison = count_b <=> count_a
93
+ next count_comparison unless count_comparison.zero?
94
+
95
+ # Secondary sort: by piece name (ascending)
96
+ piece_a <=> piece_b
97
+ end
98
+
99
+ # Format each piece with its count
100
+ sorted_pieces.map do |piece, count|
101
+ if count == 1
102
+ piece
103
+ else
104
+ "#{count}#{piece}"
105
+ end
106
+ end.join
30
107
  end
31
108
 
32
109
  # Validates all piece characters according to FEEN specification
@@ -41,6 +118,10 @@ module Feen
41
118
  end
42
119
 
43
120
  # Validates a single piece character according to FEEN specification
121
+ # Supports full PNN notation: [prefix]letter[suffix] where:
122
+ # - prefix can be "+" or "-"
123
+ # - letter must be a-z or A-Z
124
+ # - suffix can be "'"
44
125
  #
45
126
  # @param char [Object] Piece character candidate
46
127
  # @param index [Integer] Index of the character in the original array
@@ -56,8 +137,9 @@ module Feen
56
137
  )
57
138
  end
58
139
 
59
- # Validate format (single alphabetic character)
60
- unless char.match?(/\A[a-zA-Z]\z/)
140
+ # Validate format using PNN pattern: [prefix]letter[suffix]
141
+ # where prefix is +/-, letter is a-zA-Z, suffix is '
142
+ unless char.match?(/\A[+-]?[a-zA-Z]'?\z/)
61
143
  raise ::ArgumentError, format(
62
144
  Errors[:invalid_format],
63
145
  index: index,
data/lib/feen/dumper.rb CHANGED
@@ -23,19 +23,19 @@ module Feen
23
23
  # @example Creating a FEEN string for chess initial position
24
24
  # Feen::Dumper.dump(
25
25
  # piece_placement: [
26
- # ["r'", "n", "b", "q", "k", "b", "n", "r'"],
26
+ # ["r", "n", "b", "q", "k", "b", "n", "r"],
27
27
  # ["p", "p", "p", "p", "p", "p", "p", "p"],
28
28
  # ["", "", "", "", "", "", "", ""],
29
29
  # ["", "", "", "", "", "", "", ""],
30
30
  # ["", "", "", "", "", "", "", ""],
31
31
  # ["", "", "", "", "", "", "", ""],
32
32
  # ["P", "P", "P", "P", "P", "P", "P", "P"],
33
- # ["R'", "N", "B", "Q", "K", "B", "N", "R'"]
33
+ # ["R", "N", "B", "Q", "K", "B", "N", "R"]
34
34
  # ],
35
35
  # pieces_in_hand: [],
36
36
  # games_turn: ["CHESS", "chess"]
37
37
  # )
38
- # # => "r'nbqkbnr'/pppppppp/8/8/8/8/PPPPPPPP/R'NBQKBNR' - CHESS/chess"
38
+ # # => "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR - CHESS/chess"
39
39
  #
40
40
  # @param piece_placement [Array] Board position data structure representing the spatial
41
41
  # distribution of pieces across the board, where each cell
@@ -5,10 +5,15 @@ module Feen
5
5
  module PiecesInHand
6
6
  # Error messages for validation
7
7
  Errors = {
8
- invalid_type: "Pieces in hand must be a string, got %s",
9
- empty_string: "Pieces in hand string cannot be empty",
10
- invalid_format: "Invalid pieces in hand format: %s",
11
- sorting_error: "Pieces in hand must be in ASCII lexicographic order"
8
+ invalid_type: "Pieces in hand must be a string, got %s",
9
+ empty_string: "Pieces in hand string cannot be empty",
10
+ invalid_format: "Invalid pieces in hand format: %s",
11
+ invalid_pnn_piece: "Invalid PNN piece format: '%s'. Expected format: [prefix]letter[suffix] where prefix is + or -, suffix is ', and letter is a-z or A-Z",
12
+ invalid_count: "Invalid count format: '%s'. Count cannot be '0' or '1', use the piece without count instead",
13
+ canonical_order_violation: "Pieces in hand must be in canonical order (by quantity descending, then alphabetically). Got: '%<actual>s', expected: '%<expected>s'",
14
+ missing_separator: "Pieces in hand format must contain exactly one '/' separator. Got: %s",
15
+ wrong_case_in_section: "Piece '%<piece>s' has wrong case for %<section>s section",
16
+ invalid_section_format: "Invalid format in %<section>s section: %<content>s"
12
17
  }.freeze
13
18
  end
14
19
  end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Feen
4
+ module Parser
5
+ module PiecesInHand
6
+ # Patterns for PNN (Piece Name Notation) validation and parsing
7
+ module PnnPatterns
8
+ # Basic PNN piece pattern following the specification:
9
+ # <piece> ::= <letter> | <prefix> <letter> | <letter> <suffix> | <prefix> <letter> <suffix>
10
+ # <prefix> ::= "+" | "-"
11
+ # <suffix> ::= "'"
12
+ # <letter> ::= [a-zA-Z]
13
+ PNN_PIECE_PATTERN = /\A[-+]?[a-zA-Z]'?\z/
14
+
15
+ # Pattern for valid count prefixes according to FEEN specification:
16
+ # - Cannot be "0" or "1" (use piece without prefix instead)
17
+ # - Can be 2-9 or any number with 2+ digits
18
+ VALID_COUNT_PATTERN = /\A(?:[2-9]|\d{2,})\z/
19
+
20
+ # Pattern to extract piece with optional count from pieces in hand string
21
+ # Matches: optional count followed by complete PNN piece
22
+ # Groups: (count_str, piece_str)
23
+ # Note: We need to handle the full PNN piece including modifiers
24
+ PIECE_WITH_COUNT_PATTERN = /(?:([2-9]|\d{2,}))?([-+]?[a-zA-Z]'?)/
25
+
26
+ # Pattern for uppercase pieces only (used for uppercase section validation)
27
+ UPPERCASE_PIECE_PATTERN = /[-+]?[A-Z]'?/
28
+
29
+ # Pattern for lowercase pieces only (used for lowercase section validation)
30
+ LOWERCASE_PIECE_PATTERN = /[-+]?[a-z]'?/
31
+
32
+ # Pattern for uppercase section: sequence of uppercase pieces with optional counts
33
+ # Format: [count]piece[count]piece... where pieces are uppercase
34
+ UPPERCASE_SECTION_PATTERN = /\A
35
+ (?:
36
+ (?:[2-9]|\d{2,})? # Optional count (2-9 or 10+)
37
+ [-+]? # Optional single prefix (+ or -)
38
+ [A-Z] # Required uppercase letter
39
+ '? # Optional single suffix (')
40
+ )+ # One or more uppercase pieces
41
+ \z/x
42
+
43
+ # Pattern for lowercase section: sequence of lowercase pieces with optional counts
44
+ # Format: [count]piece[count]piece... where pieces are lowercase
45
+ LOWERCASE_SECTION_PATTERN = /\A
46
+ (?:
47
+ (?:[2-9]|\d{2,})? # Optional count (2-9 or 10+)
48
+ [-+]? # Optional single prefix (+ or -)
49
+ [a-z] # Required lowercase letter
50
+ '? # Optional single suffix (')
51
+ )+ # One or more lowercase pieces
52
+ \z/x
53
+
54
+ # Complete validation pattern for pieces in hand string with case separation
55
+ # Based on the FEEN BNF specification with PNN support
56
+ # Format: "UPPERCASE_PIECES/LOWERCASE_PIECES"
57
+ # Either section can be empty, but the "/" separator is mandatory
58
+ VALID_FORMAT_PATTERN = %r{\A
59
+ (?:
60
+ (?: # Uppercase section (optional)
61
+ (?:[2-9]|\d{2,})? # Optional count (2-9 or 10+)
62
+ [-+]? # Optional single prefix (+ or -)
63
+ [A-Z] # Required uppercase letter
64
+ '? # Optional single suffix (')
65
+ )* # Zero or more uppercase pieces
66
+ )
67
+ / # Mandatory separator
68
+ (?:
69
+ (?: # Lowercase section (optional)
70
+ (?:[2-9]|\d{2,})? # Optional count (2-9 or 10+)
71
+ [-+]? # Optional single prefix (+ or -)
72
+ [a-z] # Required lowercase letter
73
+ '? # Optional single suffix (')
74
+ )* # Zero or more lowercase pieces
75
+ )
76
+ \z}x
77
+
78
+ # Pattern for extracting all pieces globally (used for comprehensive validation)
79
+ GLOBAL_PIECE_EXTRACTION_PATTERN = /(?:([2-9]|\d{2,}))?([-+]?[a-zA-Z]'?)/
80
+
81
+ # Pattern specifically for uppercase pieces with counts (for section parsing)
82
+ UPPERCASE_PIECE_WITH_COUNT_PATTERN = /(?:([2-9]|\d{2,}))?([-+]?[A-Z]'?)/
83
+
84
+ # Pattern specifically for lowercase pieces with counts (for section parsing)
85
+ LOWERCASE_PIECE_WITH_COUNT_PATTERN = /(?:([2-9]|\d{2,}))?([-+]?[a-z]'?)/
86
+ end
87
+ end
88
+ end
89
+ end
@@ -1,49 +1,54 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative File.join("pieces_in_hand", "errors")
4
- require_relative File.join("pieces_in_hand", "no_pieces")
5
- require_relative File.join("pieces_in_hand", "piece_count_pattern")
6
- require_relative File.join("pieces_in_hand", "valid_format_pattern")
4
+ require_relative File.join("pieces_in_hand", "pnn_patterns")
7
5
 
8
6
  module Feen
9
7
  module Parser
10
8
  # Handles parsing of the pieces in hand section of a FEEN string.
11
9
  # Pieces in hand represent pieces available for dropping onto the board.
10
+ # This implementation supports full PNN notation including prefixes and suffixes.
11
+ # Format: "UPPERCASE_PIECES/LOWERCASE_PIECES"
12
12
  module PiecesInHand
13
13
  # Parses the pieces in hand section of a FEEN string.
14
14
  #
15
- # @param pieces_in_hand_str [String] FEEN pieces in hand string
16
- # @return [Array<String>] Array of single-character piece identifiers in the
17
- # format specified in the FEEN string (no prefixes or suffixes), expanded
18
- # based on their counts and sorted in ASCII lexicographic order.
15
+ # @param pieces_in_hand_str [String] FEEN pieces in hand string in format "UPPERCASE/lowercase"
16
+ # @return [Array<String>] Array of piece identifiers in full PNN format,
17
+ # expanded based on their counts and sorted alphabetically.
19
18
  # Empty array if no pieces are in hand.
20
19
  # @raise [ArgumentError] If the input string is invalid
21
20
  #
22
21
  # @example Parse no pieces in hand
23
- # PiecesInHand.parse("-")
22
+ # PiecesInHand.parse("/")
24
23
  # # => []
25
24
  #
26
- # @example Parse multiple pieces in hand
27
- # PiecesInHand.parse("BN2Pb")
28
- # # => ["B", "N", "P", "P", "b"]
25
+ # @example Parse pieces with case separation
26
+ # PiecesInHand.parse("3P2B/p")
27
+ # # => ["B", "B", "P", "P", "P", "p"]
29
28
  #
30
- # @example Parse pieces with counts
31
- # PiecesInHand.parse("N5P2b")
32
- # # => ["N", "P", "P", "P", "P", "P", "b", "b"]
29
+ # @example Parse complex pieces with counts and modifiers
30
+ # PiecesInHand.parse("10P5K3B/2p'+p-pbq")
31
+ # # => ["+p", "-p", "B", "B", "B", "K", "K", "K", "K", "K",
32
+ # # "P", "P", "P", "P", "P", "P", "P", "P", "P", "P",
33
+ # # "b", "p'", "p'", "q"]
33
34
  def self.parse(pieces_in_hand_str)
34
35
  # Validate input
35
36
  validate_input_type(pieces_in_hand_str)
36
37
  validate_format(pieces_in_hand_str)
37
38
 
38
39
  # Handle the no-pieces case early
39
- return [] if pieces_in_hand_str == NoPieces
40
+ return [] if pieces_in_hand_str == "/"
40
41
 
41
- # Extract pieces with their counts and validate the order
42
- pieces_with_counts = extract_pieces_with_counts(pieces_in_hand_str)
43
- validate_lexicographic_order(pieces_with_counts)
42
+ # Split by the separator to get uppercase and lowercase sections
43
+ uppercase_section, lowercase_section = pieces_in_hand_str.split("/", 2)
44
44
 
45
- # Expand the pieces into an array and maintain lexicographic ordering
46
- expand_pieces(pieces_with_counts)
45
+ # Parse each section separately
46
+ uppercase_pieces = parse_pieces_section(uppercase_section || "", :uppercase)
47
+ lowercase_pieces = parse_pieces_section(lowercase_section || "", :lowercase)
48
+
49
+ # Combine all pieces and sort them alphabetically
50
+ all_pieces = uppercase_pieces + lowercase_pieces
51
+ all_pieces.sort
47
52
  end
48
53
 
49
54
  # Validates that the input is a non-empty string.
@@ -57,26 +62,130 @@ module Feen
57
62
  end
58
63
 
59
64
  # Validates that the input string matches the expected format according to FEEN specification.
65
+ # Format must be: "UPPERCASE_PIECES/LOWERCASE_PIECES"
60
66
  #
61
67
  # @param str [String] Input string to validate
62
68
  # @raise [ArgumentError] If format is invalid
63
69
  # @return [void]
64
70
  private_class_method def self.validate_format(str)
65
- return if str == NoPieces || str.match?(ValidFormatPattern)
71
+ # Must contain exactly one "/" separator
72
+ parts_count = str.count("/")
73
+ raise ::ArgumentError, format(Errors[:invalid_format], str) unless parts_count == 1
74
+
75
+ uppercase_section, lowercase_section = str.split("/", 2)
76
+
77
+ # Each section can be empty, but if not empty, must follow PNN patterns
78
+ validate_section_format(uppercase_section, :uppercase) unless uppercase_section.empty?
79
+ validate_section_format(lowercase_section, :lowercase) unless lowercase_section.empty?
80
+ end
81
+
82
+ # Validates the format of a specific section (uppercase or lowercase)
83
+ #
84
+ # @param section [String] The section to validate
85
+ # @param case_type [Symbol] Either :uppercase or :lowercase
86
+ # @raise [ArgumentError] If the section format is invalid
87
+ # @return [void]
88
+ private_class_method def self.validate_section_format(section, case_type)
89
+ return if section.empty?
90
+
91
+ # Build the appropriate pattern based on case type
92
+ case_pattern = case case_type
93
+ when :uppercase
94
+ PnnPatterns::UPPERCASE_SECTION_PATTERN
95
+ when :lowercase
96
+ PnnPatterns::LOWERCASE_SECTION_PATTERN
97
+ else
98
+ raise ArgumentError, "Invalid case type: #{case_type}"
99
+ end
100
+
101
+ # Validate overall section pattern
102
+ raise ::ArgumentError, format(Errors[:invalid_format], section) unless section.match?(case_pattern)
103
+
104
+ # Validate individual pieces in the section
105
+ validate_individual_pieces_in_section(section, case_type)
106
+ end
107
+
108
+ # Validates each individual piece in a section for PNN compliance
109
+ #
110
+ # @param section [String] FEEN pieces section string
111
+ # @param case_type [Symbol] Either :uppercase or :lowercase
112
+ # @raise [ArgumentError] If any piece is invalid PNN format
113
+ # @return [void]
114
+ private_class_method def self.validate_individual_pieces_in_section(section, case_type)
115
+ position = 0
116
+
117
+ while position < section.length
118
+ match = section[position..].match(PnnPatterns::PIECE_WITH_COUNT_PATTERN)
119
+
120
+ unless match
121
+ remaining = section[position..]
122
+ raise ::ArgumentError, format(Errors[:invalid_format], remaining)
123
+ end
124
+
125
+ count_str, piece = match.captures
126
+
127
+ # Skip empty matches (shouldn't happen with our pattern, but safety check)
128
+ if piece.nil? || piece.empty?
129
+ position += 1
130
+ next
131
+ end
132
+
133
+ # Validate the piece follows PNN specification
134
+ unless piece.match?(PnnPatterns::PNN_PIECE_PATTERN)
135
+ raise ::ArgumentError, format(Errors[:invalid_pnn_piece], piece)
136
+ end
137
+
138
+ # Validate count format (no "0" or "1" prefixes allowed)
139
+ if count_str && !count_str.match?(PnnPatterns::VALID_COUNT_PATTERN)
140
+ raise ::ArgumentError, format(Errors[:invalid_count], count_str)
141
+ end
142
+
143
+ # Validate that the piece matches the expected case
144
+ piece_case = piece_is_uppercase?(piece) ? :uppercase : :lowercase
145
+ unless piece_case == case_type
146
+ case_name = case_type == :uppercase ? "uppercase" : "lowercase"
147
+ raise ::ArgumentError, "#{case_name.capitalize} section contains #{piece_case} piece: '#{piece}'"
148
+ end
66
149
 
67
- raise ::ArgumentError, format(Errors[:invalid_format], str)
150
+ position += match[0].length
151
+ end
152
+ end
153
+
154
+ # Determines if a piece belongs to the uppercase group
155
+ #
156
+ # @param piece [String] Piece identifier (e.g., "P", "+P", "P'", "+P'")
157
+ # @return [Boolean] True if the piece's main letter is uppercase
158
+ private_class_method def self.piece_is_uppercase?(piece)
159
+ # Extract the main letter (skip prefixes like + or -)
160
+ main_letter = piece.gsub(/\A[+-]/, "").gsub(/'\z/, "")
161
+ main_letter.match?(/[A-Z]/)
162
+ end
163
+
164
+ # Parses a specific section (uppercase or lowercase) and returns expanded pieces
165
+ #
166
+ # @param section [String] The section string to parse
167
+ # @param case_type [Symbol] Either :uppercase or :lowercase (for validation)
168
+ # @return [Array<String>] Array of expanded pieces from this section
169
+ private_class_method def self.parse_pieces_section(section, _case_type)
170
+ return [] if section.empty?
171
+
172
+ # Extract pieces with their counts
173
+ pieces_with_counts = extract_pieces_with_counts_from_section(section)
174
+
175
+ # Expand the pieces into an array (no canonical order validation needed)
176
+ expand_pieces(pieces_with_counts)
68
177
  end
69
178
 
70
- # Extracts pieces with their counts from the FEEN string.
179
+ # Extracts pieces with their counts from a section string.
71
180
  #
72
- # @param str [String] FEEN pieces in hand string
181
+ # @param section [String] FEEN pieces section string
73
182
  # @return [Array<Hash>] Array of hashes with :piece and :count keys
74
- private_class_method def self.extract_pieces_with_counts(str)
183
+ private_class_method def self.extract_pieces_with_counts_from_section(section)
75
184
  result = []
76
185
  position = 0
77
186
 
78
- while position < str.length
79
- match = str[position..].match(PieceCountPattern)
187
+ while position < section.length
188
+ match = section[position..].match(PnnPatterns::PIECE_WITH_COUNT_PATTERN)
80
189
  break unless match
81
190
 
82
191
  count_str, piece = match.captures
@@ -92,20 +201,6 @@ module Feen
92
201
  result
93
202
  end
94
203
 
95
- # Validates that pieces are in lexicographic ASCII order.
96
- #
97
- # @param pieces_with_counts [Array<Hash>] Array of pieces with their counts
98
- # @raise [ArgumentError] If pieces are not in lexicographic order
99
- # @return [void]
100
- private_class_method def self.validate_lexicographic_order(pieces_with_counts)
101
- pieces = pieces_with_counts.map { |item| item[:piece] }
102
-
103
- # Verify the array is sorted lexicographically
104
- return if pieces == pieces.sort
105
-
106
- raise ::ArgumentError, Errors[:sorting_error]
107
- end
108
-
109
204
  # Expands the pieces based on their counts into an array.
110
205
  #
111
206
  # @param pieces_with_counts [Array<Hash>] Array of pieces with their counts
data/lib/feen/parser.rb CHANGED
@@ -26,18 +26,18 @@ module Feen
26
26
  # @raise [ArgumentError] If the FEEN string is invalid
27
27
  #
28
28
  # @example Parsing a standard chess initial position
29
- # feen = "r'nbqkbnr'/pppppppp/8/8/8/8/PPPPPPPP/R'NBQKBNR' - CHESS/chess"
29
+ # feen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR - CHESS/chess"
30
30
  # result = Feen::Parser.parse(feen)
31
31
  # # => {
32
32
  # # piece_placement: [
33
- # # ["r'", "n", "b", "q", "k", "b", "n", "r'"],
33
+ # # ["r", "n", "b", "q", "k", "b", "n", "r"],
34
34
  # # ["p", "p", "p", "p", "p", "p", "p", "p"],
35
35
  # # ["", "", "", "", "", "", "", ""],
36
36
  # # ["", "", "", "", "", "", "", ""],
37
37
  # # ["", "", "", "", "", "", "", ""],
38
38
  # # ["", "", "", "", "", "", "", ""],
39
39
  # # ["P", "P", "P", "P", "P", "P", "P", "P"],
40
- # # ["R'", "N", "B", "Q", "K", "B", "N", "R'"]
40
+ # # ["R", "N", "B", "Q", "K", "B", "N", "R"]
41
41
  # # ],
42
42
  # # pieces_in_hand: [],
43
43
  # # games_turn: ["CHESS", "chess"]
@@ -76,7 +76,7 @@ module Feen
76
76
  # @return [Hash, nil] Hash containing the parsed position data or nil if parsing fails
77
77
  #
78
78
  # @example Parsing a valid FEEN string
79
- # feen = "r'nbqkbnr'/pppppppp/8/8/8/8/PPPPPPPP/R'NBQKBNR' - CHESS/chess"
79
+ # feen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR - CHESS/chess"
80
80
  # result = Feen::Parser.safe_parse(feen)
81
81
  # # => {piece_placement: [...], pieces_in_hand: [...], games_turn: [...]}
82
82
  #
data/lib/feen.rb CHANGED
@@ -23,21 +23,21 @@ module Feen
23
23
  # @raise [ArgumentError] If any parameter is invalid
24
24
  # @example
25
25
  # piece_placement = [
26
- # ["r'", "n", "b", "q", "k", "b", "n", "r'"],
26
+ # ["r", "n", "b", "q", "k", "b", "n", "r"],
27
27
  # ["p", "p", "p", "p", "p", "p", "p", "p"],
28
28
  # ["", "", "", "", "", "", "", ""],
29
29
  # ["", "", "", "", "", "", "", ""],
30
30
  # ["", "", "", "", "", "", "", ""],
31
31
  # ["", "", "", "", "", "", "", ""],
32
32
  # ["P", "P", "P", "P", "P", "P", "P", "P"],
33
- # ["R'", "N", "B", "Q", "K", "B", "N", "R'"]
33
+ # ["R", "N", "B", "Q", "K", "B", "N", "R"]
34
34
  # ]
35
35
  # Feen.dump(
36
36
  # piece_placement: piece_placement,
37
37
  # pieces_in_hand: [],
38
38
  # games_turn: ["CHESS", "chess"]
39
39
  # )
40
- # # => "r'nbqkbnr'/pppppppp/8/8/8/8/PPPPPPPP/R'NBQKBNR' - CHESS/chess"
40
+ # # => "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR - CHESS/chess"
41
41
  def self.dump(piece_placement:, pieces_in_hand:, games_turn:)
42
42
  Dumper.dump(piece_placement:, pieces_in_hand:, games_turn:)
43
43
  end
@@ -51,18 +51,18 @@ module Feen
51
51
  # - :games_turn [Array<String>] - A two-element array with [active_variant, inactive_variant]
52
52
  # @raise [ArgumentError] If the FEEN string is invalid
53
53
  # @example
54
- # feen_string = "r'nbqkbnr'/pppppppp/8/8/8/8/PPPPPPPP/R'NBQKBNR' - CHESS/chess"
54
+ # feen_string = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR - CHESS/chess"
55
55
  # Feen.parse(feen_string)
56
56
  # # => {
57
57
  # # piece_placement: [
58
- # # ["r'", "n", "b", "q", "k", "b", "n", "r'"],
58
+ # # ["r", "n", "b", "q", "k", "b", "n", "r"],
59
59
  # # ["p", "p", "p", "p", "p", "p", "p", "p"],
60
60
  # # ["", "", "", "", "", "", "", ""],
61
61
  # # ["", "", "", "", "", "", "", ""],
62
62
  # # ["", "", "", "", "", "", "", ""],
63
63
  # # ["", "", "", "", "", "", "", ""],
64
64
  # # ["P", "P", "P", "P", "P", "P", "P", "P"],
65
- # # ["R'", "N", "B", "Q", "K", "B", "N", "R'"]
65
+ # # ["R", "N", "B", "Q", "K", "B", "N", "R"]
66
66
  # # ],
67
67
  # # pieces_in_hand: [],
68
68
  # # games_turn: ["CHESS", "chess"]
@@ -80,7 +80,7 @@ module Feen
80
80
  # @return [Hash, nil] Hash containing the parsed position data or nil if parsing fails
81
81
  # @example
82
82
  # # Valid FEEN string
83
- # Feen.safe_parse("r'nbqkbnr'/pppppppp/8/8/8/8/PPPPPPPP/R'NBQKBNR' - CHESS/chess")
83
+ # Feen.safe_parse("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR - CHESS/chess")
84
84
  # # => {piece_placement: [...], pieces_in_hand: [...], games_turn: [...]}
85
85
  #
86
86
  # # Invalid FEEN string
@@ -104,7 +104,7 @@ module Feen
104
104
  # @return [Boolean] True if the string is a valid and canonical FEEN string
105
105
  # @example
106
106
  # # Canonical form
107
- # Feen.valid?("lnsgk3l/5g3/p1ppB2pp/9/8B/2P6/P2PPPPPP/3K3R1/5rSNL N5P2gln2s SHOGI/shogi") # => true
107
+ # Feen.valid?("lnsgk3l/5g3/p1ppB2pp/9/8B/2P6/P2PPPPPP/3K3R1/5rSNL 2g2s5PNln SHOGI/shogi") # => true
108
108
  #
109
109
  # # Invalid syntax
110
110
  # Feen.valid?("invalid feen string") # => false
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: feen
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.0.0.beta5
4
+ version: 5.0.0.beta7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cyril Kato
@@ -26,7 +26,6 @@ files:
26
26
  - lib/feen/dumper/piece_placement.rb
27
27
  - lib/feen/dumper/pieces_in_hand.rb
28
28
  - lib/feen/dumper/pieces_in_hand/errors.rb
29
- - lib/feen/dumper/pieces_in_hand/no_pieces.rb
30
29
  - lib/feen/parser.rb
31
30
  - lib/feen/parser/games_turn.rb
32
31
  - lib/feen/parser/games_turn/errors.rb
@@ -34,9 +33,7 @@ files:
34
33
  - lib/feen/parser/piece_placement.rb
35
34
  - lib/feen/parser/pieces_in_hand.rb
36
35
  - lib/feen/parser/pieces_in_hand/errors.rb
37
- - lib/feen/parser/pieces_in_hand/no_pieces.rb
38
- - lib/feen/parser/pieces_in_hand/piece_count_pattern.rb
39
- - lib/feen/parser/pieces_in_hand/valid_format_pattern.rb
36
+ - lib/feen/parser/pieces_in_hand/pnn_patterns.rb
40
37
  homepage: https://github.com/sashite/feen.rb
41
38
  licenses:
42
39
  - MIT
@@ -61,7 +58,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
61
58
  - !ruby/object:Gem::Version
62
59
  version: '0'
63
60
  requirements: []
64
- rubygems_version: 3.6.7
61
+ rubygems_version: 3.6.9
65
62
  specification_version: 4
66
63
  summary: FEEN (Forsyth–Edwards Enhanced Notation) support for the Ruby language.
67
64
  test_files: []
@@ -1,10 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Feen
4
- module Dumper
5
- module PiecesInHand
6
- # Character used to represent no pieces in hand
7
- NoPieces = "-"
8
- end
9
- end
10
- end
@@ -1,10 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Feen
4
- module Parser
5
- module PiecesInHand
6
- # Character used to represent no pieces in hand
7
- NoPieces = "-"
8
- end
9
- end
10
- end
@@ -1,13 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Feen
4
- module Parser
5
- module PiecesInHand
6
- # Regex to extract piece counts from pieces in hand string
7
- # Matches either:
8
- # - A single piece character with no count (e.g., "P")
9
- # - A count followed by a piece character (e.g., "5P")
10
- PieceCountPattern = /(?:([2-9]|\d{2,}))?([A-Za-z])/
11
- end
12
- end
13
- end
@@ -1,29 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Feen
4
- module Parser
5
- module PiecesInHand
6
- # Valid pattern for pieces in hand based on BNF specification.
7
- #
8
- # The FEEN format specifies these rules for numeric prefixes:
9
- # - Cannot start with "0"
10
- # - Cannot be exactly "1" (use the letter without prefix instead)
11
- # - Can be 2-9 or any number with 2+ digits (10, 11, etc.)
12
- #
13
- # The pattern matches either:
14
- # - A single digit from 2-9
15
- # - OR any number with two or more digits (10, 11, 27, 103, etc.)
16
- #
17
- # @return [Regexp] Regular expression for validating pieces in hand format
18
- ValidFormatPattern = /\A
19
- (?:
20
- -| # No pieces in hand
21
- (?: # Or sequence of pieces
22
- (?:(?:[2-9]|\d{2,})?[A-Z])* # Uppercase pieces (optional)
23
- (?:(?:[2-9]|\d{2,})?[a-z])* # Lowercase pieces (optional)
24
- )
25
- )
26
- \z/x
27
- end
28
- end
29
- end