feen 5.0.0.beta4 → 5.0.0.beta6

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: de87bfc9b071e9f1ff14b6f7378ba0a6f9f78cdb9a9bfe32ca00f2494a1be2cd
4
- data.tar.gz: fd4d19e8a3b7fb845f081b127a859ff0791c8c11c05e1efb4846152044f67c18
3
+ metadata.gz: ce89113318c1acf675da8dc7d4af4a9ea135a6485e0e4eb8d522640149f0461a
4
+ data.tar.gz: e06ae9198a93a896594e1ed9ba0b96a8af1445a20a7f8df8b247a04d84d76e71
5
5
  SHA512:
6
- metadata.gz: e9fe95297b8577a3bf171a865deca0dc9c2eed91a7d039c04f97077d53d3fa912ca553f28f448f0dd987087926d3ca5225624d7f2d8d08a9275a16a11efe26e0
7
- data.tar.gz: b82b7e00f5ece6f5e5859750226be3c74db23e694c8e53def8c8c656fe2fb3ecba7f0f4166cf3d9a4f6c7b4ab7af3cf6544da87a2684e438a389d96e04b3dc75
6
+ metadata.gz: 177f297b196c9a20601eca71865d3ee51389111a72da28d4d1dc08249f3017be07d50636d0a9161759067cf9e8e42dbfdc2dc472f7717130f0e20477634537da
7
+ data.tar.gz: 5060cd110c9ee95d43f952fbe55c9870e1c98171934f148947a49a6afdbf547007ddd012e24598dd3209e0eab4ea11905f61d52871e9bfa511fdc9bac78af9d6
data/README.md CHANGED
@@ -12,17 +12,32 @@
12
12
  FEEN (Forsyth–Edwards Enhanced Notation) is a compact, canonical, and rule-agnostic textual format for representing static board positions in two-player piece-placement games.
13
13
 
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
  - Representing positions from various games without knowledge of specific rules
16
- - Supporting boards of arbitrary dimensions
17
- - 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
18
19
  - Facilitating serialization and deserialization of positions
19
20
  - Ensuring canonical representation for consistent data handling
20
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, sorted canonically
34
+ 3. **Games Turn**: Game identifiers and active player indication
35
+
21
36
  ## Installation
22
37
 
23
38
  ```ruby
24
39
  # In your Gemfile
25
- gem "feen", ">= 5.0.0.beta4"
40
+ gem "feen", ">= 5.0.0.beta6"
26
41
  ```
27
42
 
28
43
  Or install manually:
@@ -31,14 +46,6 @@ Or install manually:
31
46
  gem install feen --pre
32
47
  ```
33
48
 
34
- ## FEEN Format
35
-
36
- A FEEN record consists of three space-separated fields:
37
-
38
- ```
39
- <PIECE-PLACEMENT> <PIECES-IN-HAND> <GAMES-TURN>
40
- ```
41
-
42
49
  ## Basic Usage
43
50
 
44
51
  ### Parsing FEEN Strings
@@ -48,20 +55,20 @@ Convert a FEEN string into a structured Ruby object:
48
55
  ```ruby
49
56
  require "feen"
50
57
 
51
- feen_string = "rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR - CHESS/chess"
58
+ feen_string = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR - CHESS/chess"
52
59
  position = Feen.parse(feen_string)
53
60
 
54
61
  # Result is a hash:
55
62
  # {
56
63
  # piece_placement: [
57
- # ["r", "n", "b", "q", "k=", "b", "n", "r"],
64
+ # ["r", "n", "b", "q", "k", "b", "n", "r"],
58
65
  # ["p", "p", "p", "p", "p", "p", "p", "p"],
59
66
  # ["", "", "", "", "", "", "", ""],
60
67
  # ["", "", "", "", "", "", "", ""],
61
68
  # ["", "", "", "", "", "", "", ""],
62
69
  # ["", "", "", "", "", "", "", ""],
63
70
  # ["P", "P", "P", "P", "P", "P", "P", "P"],
64
- # ["R", "N", "B", "Q", "K=", "B", "N", "R"]
71
+ # ["R", "N", "B", "Q", "K", "B", "N", "R"]
65
72
  # ],
66
73
  # pieces_in_hand: [],
67
74
  # games_turn: ["CHESS", "chess"]
@@ -76,7 +83,7 @@ Parse a FEEN string without raising exceptions:
76
83
  require "feen"
77
84
 
78
85
  # Valid FEEN string
79
- result = Feen.safe_parse("rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR - CHESS/chess")
86
+ result = Feen.safe_parse("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR - CHESS/chess")
80
87
  # => {piece_placement: [...], pieces_in_hand: [...], games_turn: [...]}
81
88
 
82
89
  # Invalid FEEN string
@@ -93,14 +100,14 @@ require "feen"
93
100
 
94
101
  # Representation of a chess board in initial position
95
102
  piece_placement = [
96
- ["r", "n", "b", "q", "k=", "b", "n", "r"],
103
+ ["r", "n", "b", "q", "k", "b", "n", "r"],
97
104
  ["p", "p", "p", "p", "p", "p", "p", "p"],
98
105
  ["", "", "", "", "", "", "", ""],
99
106
  ["", "", "", "", "", "", "", ""],
100
107
  ["", "", "", "", "", "", "", ""],
101
108
  ["", "", "", "", "", "", "", ""],
102
109
  ["P", "P", "P", "P", "P", "P", "P", "P"],
103
- ["R", "N", "B", "Q", "K=", "B", "N", "R"]
110
+ ["R", "N", "B", "Q", "K", "B", "N", "R"]
104
111
  ]
105
112
 
106
113
  result = Feen.dump(
@@ -108,7 +115,7 @@ result = Feen.dump(
108
115
  games_turn: %w[CHESS chess],
109
116
  pieces_in_hand: []
110
117
  )
111
- # => "rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR - CHESS/chess"
118
+ # => "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR - CHESS/chess"
112
119
  ```
113
120
 
114
121
  ### Validation
@@ -119,19 +126,20 @@ Check if a string is valid FEEN notation and in canonical form:
119
126
  require "feen"
120
127
 
121
128
  # Canonical form
122
- 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")
123
130
  # => true
124
131
 
125
132
  # Invalid syntax
126
133
  Feen.valid?("invalid feen string")
127
134
  # => false
128
135
 
129
- # Valid syntax but non-canonical form (pieces in hand not in lexicographic order)
130
- Feen.valid?("lnsgk3l/5g3/p1ppB2pp/9/8B/2P6/P2PPPPPP/3K3R1/5rSNL N5P2gn2sl SHOGI/shogi")
131
- # => 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)
132
139
  ```
133
140
 
134
141
  The `valid?` method performs two levels of validation:
142
+
135
143
  1. **Syntax check**: Verifies the string can be parsed as FEEN
136
144
  2. **Canonicity check**: Ensures the string is in its canonical form through round-trip conversion
137
145
 
@@ -142,45 +150,101 @@ As FEEN is rule-agnostic, it can represent positions from various board games. H
142
150
  ### International Chess
143
151
 
144
152
  ```ruby
145
- feen_string = "rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR - CHESS/chess"
153
+ feen_string = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR - CHESS/chess"
146
154
  ```
147
155
 
148
- In this initial chess position:
149
- - The `=` suffixes on kings indicate castling rights on both sides (though FEEN doesn't define this semantics)
150
- - 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.
151
157
 
152
158
  ### Shogi (Japanese Chess)
153
159
 
154
160
  ```ruby
155
- 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 5P2g2sln SHOGI/shogi"
156
168
  ```
157
169
 
158
170
  In this shogi position:
159
171
  - The format supports promotions with the `+` prefix (e.g., `+P` for a promoted pawn)
160
- - The notation allows for pieces in hand, indicated in the second field
172
+ - `5P2g2sln` shows pieces in hand in canonical FEEN order:
173
+ - **Quantity descending**: 5 Pawns (5P), then 2 Golds (2g)
174
+ - **Alphabetically**: then Lance (l), Knight (n), Silver (s)
161
175
  - `SHOGI/shogi` indicates it's Sente's (Black's, uppercase) turn to move
162
- - `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
163
176
 
164
177
  ### Makruk (Thai Chess)
165
178
 
166
179
  ```ruby
167
- feen_string = "rnbqkbnr/8/pppppppp/8/8/PPPPPPPP/8/RNBQKBNR - MAKRUK/makruk"
180
+ feen_string = "rnbqkbnr/8/pppppppp/8/8/PPPPPPPP/8/RNBKQBNR - MAKRUK/makruk"
168
181
  ```
169
182
 
170
- This initial Makruk position is easily represented in FEEN without needing to know the specific rules of the game.
171
-
172
183
  ### Xiangqi (Chinese Chess)
173
184
 
174
185
  ```ruby
175
186
  feen_string = "rheagaehr/9/1c5c1/s1s1s1s1s/9/9/S1S1S1S1S/1C5C1/9/RHEAGAEHR - XIANGQI/xiangqi"
176
187
  ```
177
188
 
178
- In this Xiangqi position:
179
- - The representation uses single letters for the different pieces
180
- - The format naturally adapts to the presence of a "river" (empty space in the middle)
181
-
182
189
  ## Advanced Features
183
190
 
191
+ ### Piece Name Notation (PNN) Support
192
+
193
+ FEEN supports the complete [PNN specification](https://sashite.dev/documents/pnn/1.0.0/) for representing pieces with state modifiers:
194
+
195
+ #### PNN Modifiers
196
+
197
+ - **Prefix `+`**: Enhanced state (e.g., promoted pieces in shogi)
198
+ - **Prefix `-`**: Diminished state (e.g., restricted movement)
199
+ - **Suffix `'`**: Intermediate state (e.g., castling rights, en passant eligibility)
200
+
201
+ #### Examples with PNN
202
+
203
+ ```ruby
204
+ # Shogi position with promoted pieces on board
205
+ piece_placement = [
206
+ ["", "", "", "", "+P", "", "", "", ""] # Promoted pawn on board
207
+ # ... other ranks
208
+ ]
209
+
210
+ # Pieces in hand with PNN modifiers
211
+ pieces_in_hand = ["+P", "+P", "+P", "B'", "B'", "-p", "P"]
212
+
213
+ result = Feen.dump(
214
+ piece_placement: piece_placement,
215
+ pieces_in_hand: pieces_in_hand,
216
+ games_turn: %w[SHOGI shogi]
217
+ )
218
+ # => "8/8/8/8/4+P4/8/8/8/8 3+P2B'-pP SHOGI/shogi"
219
+ ```
220
+
221
+ ### Canonical Pieces in Hand Sorting
222
+
223
+ FEEN enforces canonical ordering of pieces in hand according to the specification:
224
+
225
+ 1. **By quantity (descending)**
226
+ 2. **By complete PNN representation (alphabetically ascending)**
227
+
228
+ ```ruby
229
+ # Input pieces in any order
230
+ pieces = ["P", "B", "P", "+P", "B", "P", "+P", "+P"]
231
+
232
+ result = Feen::Dumper::PiecesInHand.dump(*pieces)
233
+ # => "3+P3P2B"
234
+ # Breakdown: 3×+P (most frequent), 3×P, 2×B (alphabetical within same quantity)
235
+ ```
236
+
237
+ #### Complex Example from FEEN Specification
238
+
239
+ ```ruby
240
+ # From FEEN spec v1.0.0 example
241
+ pieces = (["P"] * 10) + (["K"] * 5) + (["B"] * 3) + (["p'"] * 2) + ["+P", "-p", "R", "b", "q"]
242
+
243
+ result = Feen::Dumper::PiecesInHand.dump(*pieces)
244
+ # => "10P5K3B2p'+P-pRbq"
245
+ # Sorted by: quantity desc (10,5,3,2,1...), then alphabetical (+P,-p,R,b,q)
246
+ ```
247
+
184
248
  ### Multi-dimensional Boards
185
249
 
186
250
  FEEN supports arbitrary-dimensional board configurations:
@@ -188,7 +252,7 @@ FEEN supports arbitrary-dimensional board configurations:
188
252
  ```ruby
189
253
  require "feen"
190
254
 
191
- # 3D board
255
+ # 3D board (2×2×3 configuration)
192
256
  piece_placement = [
193
257
  [
194
258
  %w[r n b],
@@ -208,30 +272,85 @@ result = Feen.dump(
208
272
  # => "rnb/qkp//PR1/1KQ - FOO/bar"
209
273
  ```
210
274
 
211
- ### Piece Modifiers
275
+ ### Hybrid Games
212
276
 
213
- FEEN supports prefixes and suffixes for pieces to denote various states or capabilities:
277
+ FEEN supports hybrid games mixing different piece sets:
278
+
279
+ ```ruby
280
+ # Chess-Shogi hybrid position
281
+ feen_string = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR 3+P2B'-pP CHESS/shogi"
282
+ ```
283
+
284
+ This represents a position where:
285
+ - The board uses chess-style pieces
286
+ - Pieces in hand use shogi-style promotion (`+P`) and intermediate states (`B'`, `-p`)
287
+ - Chess player to move, against shogi player
288
+
289
+ ## Round-trip Consistency
290
+
291
+ FEEN.rb guarantees round-trip consistency - parsing and dumping produces identical canonical strings:
292
+
293
+ ```ruby
294
+ original = "lnsgk3l/5g3/p1ppB2pp/9/8B/2P6/P2PPPPPP/3K3R1/5rSNL 5P2g2sln SHOGI/shogi"
295
+ parsed = Feen.parse(original)
296
+ dumped = Feen.dump(**parsed)
297
+
298
+ original == dumped # => true (guaranteed canonical form)
299
+ ```
300
+
301
+ ## Error Handling
302
+
303
+ ### Validation Errors
304
+
305
+ ```ruby
306
+ # Invalid PNN format
307
+ Feen::Dumper::PiecesInHand.dump("++P")
308
+ # => ArgumentError: Invalid PNN format: '++P'
309
+
310
+ # Invalid games turn
311
+ Feen.dump(
312
+ piece_placement: [["P"]],
313
+ pieces_in_hand: [],
314
+ games_turn: %w[BOTH_UPPERCASE ALSO_UPPERCASE] # Both same case
315
+ )
316
+ # => ArgumentError: One variant must be uppercase and the other lowercase
317
+ ```
318
+
319
+ ### Safe Operations
320
+
321
+ ```ruby
322
+ # Use safe_parse for user input
323
+ user_input = gets.chomp
324
+ position = Feen.safe_parse(user_input)
325
+
326
+ if position
327
+ puts "Valid FEEN position!"
328
+ else
329
+ puts "Invalid FEEN format"
330
+ end
331
+ ```
214
332
 
215
- - **Prefix `+`**: May indicate promotion or special state
216
- - Example in shogi: `+P` may represent a promoted pawn
333
+ ## Performance Considerations
217
334
 
218
- - **Suffix `=`**: May indicate dual-option status
219
- - Example in chess: `K=` may represent a king eligible for both kingside and queenside castling
335
+ - **Parsing**: Optimized recursive descent parser with O(n) complexity
336
+ - **Validation**: Round-trip validation ensures canonical form
337
+ - **Memory**: Efficient array-based representation for large boards
338
+ - **Sorting**: In-place canonical sorting for pieces in hand
220
339
 
221
- - **Suffix `<`**: May indicate left-side constraint
222
- - Example in chess: `K<` may represent a king eligible for queenside castling only
223
- - Example in chess: `P<` may represent a pawn that may be captured _en passant_ from the left
340
+ ## Compatibility
224
341
 
225
- - **Suffix `>`**: May indicate right-side constraint
226
- - Example in chess: `K>` may represent a king eligible for kingside castling only
227
- - Example in chess: `P>` may represent a pawn that may be captured en passant from the right
342
+ - **Ruby version**: >= 3.2.0
343
+ - **FEEN specification**: v1.0.0 compliant
344
+ - **PNN specification**: v1.0.0 compliant
345
+ - **Thread safety**: All operations are thread-safe (no shared mutable state)
228
346
 
229
- 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.
347
+ ## Related Specifications
230
348
 
231
- ## Documentation
349
+ FEEN is part of a family of specifications for abstract strategy games:
232
350
 
233
- - [Official FEEN Specification](https://sashite.dev/documents/feen/1.0.0/)
234
- - [API Documentation](https://rubydoc.info/github/sashite/feen.rb/main)
351
+ - [FEEN Specification v1.0.0](https://sashite.dev/documents/feen/1.0.0/) - Board position notation
352
+ - [PNN Specification v1.0.0](https://sashite.dev/documents/pnn/1.0.0/) - Piece name notation
353
+ - [GAN Specification v1.0.0](https://sashite.dev/documents/gan/1.0.0/) - Game-qualified piece identifiers
235
354
 
236
355
  ## License
237
356
 
@@ -239,4 +358,4 @@ The [gem](https://rubygems.org/gems/feen) is available as open source under the
239
358
 
240
359
  ## About Sashité
241
360
 
242
- This project is maintained by [Sashité](https://sashite.com/) - a project dedicated to promoting chess variants and sharing the beauty of Chinese, Japanese, and Western chess cultures.
361
+ This project is maintained by [Sashité](https://sashite.com/) promoting chess variants and sharing the beauty of Chinese, Japanese, and Western chess cultures.
@@ -7,7 +7,7 @@ module Feen
7
7
  #
8
8
  # @param piece_placement [Array] Hierarchical array representing the board where:
9
9
  # - Empty squares are represented by empty strings ("")
10
- # - Pieces are represented by strings (e.g., "r", "K=", "+P")
10
+ # - Pieces are represented by strings (e.g., "r", "R'", "+P")
11
11
  # - Dimensions are represented by nested arrays
12
12
  # @return [String] FEEN piece placement string
13
13
  # @raise [ArgumentError] If the piece placement structure is invalid
@@ -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
@@ -9,12 +9,14 @@ module Feen
9
9
  module PiecesInHand
10
10
  # Converts an array of piece identifiers to a FEEN-formatted pieces in hand string
11
11
  #
12
- # @param piece_chars [Array<String>] Array of single-character piece identifiers
13
- # @return [String] FEEN-formatted pieces in hand string
12
+ # @param piece_chars [Array<String>] Array of piece identifiers in full PNN format
13
+ # @return [String] FEEN-formatted pieces in hand string sorted according to FEEN specification:
14
+ # 1. By quantity (descending)
15
+ # 2. By complete PNN representation (alphabetically ascending)
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")
19
+ # # => "3P2B+P"
18
20
  #
19
21
  # PiecesInHand.dump
20
22
  # # => "-"
@@ -22,14 +24,25 @@ module Feen
22
24
  # If no pieces in hand, return the standardized empty indicator
23
25
  return NoPieces if piece_chars.empty?
24
26
 
25
- # Validate each piece character according to the FEEN specification
27
+ # Validate each piece character according to the FEEN specification (full PNN support)
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
+ # Count occurrences of each piece type
31
+ piece_counts = validated_chars.tally
32
+
33
+ # Sort according to FEEN specification:
34
+ # 1. By quantity (descending)
35
+ # 2. By complete PNN representation (alphabetically ascending)
36
+ sorted_pieces = piece_counts.sort do |a, b|
37
+ count_comparison = b[1] <=> a[1] # quantity descending
38
+ count_comparison.zero? ? a[0] <=> b[0] : count_comparison # then alphabetical
39
+ end
40
+
41
+ # Format the pieces sequence with proper count prefixes
42
+ format_pieces_sequence(sorted_pieces)
30
43
  end
31
44
 
32
- # Validates all piece characters according to FEEN specification
45
+ # Validates all piece characters according to FEEN specification with full PNN support
33
46
  #
34
47
  # @param piece_chars [Array<Object>] Array of piece character candidates
35
48
  # @return [Array<String>] Array of validated piece characters
@@ -40,7 +53,7 @@ module Feen
40
53
  end
41
54
  end
42
55
 
43
- # Validates a single piece character according to FEEN specification
56
+ # Validates a single piece character according to FEEN specification with full PNN support
44
57
  #
45
58
  # @param char [Object] Piece character candidate
46
59
  # @param index [Integer] Index of the character in the original array
@@ -56,8 +69,12 @@ module Feen
56
69
  )
57
70
  end
58
71
 
59
- # Validate format (single alphabetic character)
60
- unless char.match?(/\A[a-zA-Z]\z/)
72
+ # Validate format (full PNN notation: [prefix]letter[suffix])
73
+ # <piece> ::= <letter> | <prefix> <letter> | <letter> <suffix> | <prefix> <letter> <suffix>
74
+ # <prefix> ::= "+" | "-"
75
+ # <suffix> ::= "'"
76
+ # <letter> ::= [a-zA-Z]
77
+ unless char.match?(/\A[-+]?[a-zA-Z]'?\z/)
61
78
  raise ::ArgumentError, format(
62
79
  Errors[:invalid_format],
63
80
  index: index,
@@ -67,6 +84,20 @@ module Feen
67
84
 
68
85
  char
69
86
  end
87
+
88
+ # Formats the pieces sequence with proper count prefixes according to FEEN specification
89
+ #
90
+ # @param sorted_pieces [Array<Array>] Array of [piece, count] pairs sorted according to FEEN rules
91
+ # @return [String] Formatted pieces sequence
92
+ private_class_method def self.format_pieces_sequence(sorted_pieces)
93
+ sorted_pieces.map do |piece, count|
94
+ if count == 1
95
+ piece
96
+ else
97
+ "#{count}#{piece}"
98
+ end
99
+ end.join
100
+ end
70
101
  end
71
102
  end
72
103
  end
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
- # # => "rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR - 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
@@ -30,10 +30,8 @@ module Feen
30
30
  VALID_PREFIXES = [PREFIX_PROMOTION, PREFIX_DIMINISHED].freeze
31
31
 
32
32
  # Valid piece suffixes
33
- SUFFIX_EQUALS = "="
34
- SUFFIX_LEFT = "<"
35
- SUFFIX_RIGHT = ">"
36
- VALID_SUFFIXES = [SUFFIX_EQUALS, SUFFIX_LEFT, SUFFIX_RIGHT].freeze
33
+ SUFFIX_INTERMEDIATE = "'"
34
+ VALID_SUFFIXES = [SUFFIX_INTERMEDIATE].freeze
37
35
 
38
36
  # Build validation pattern step by step to match BNF
39
37
  # <letter> ::= <letter-lowercase> | <letter-uppercase>
@@ -42,8 +40,8 @@ module Feen
42
40
  # <prefix> ::= "+" | "-"
43
41
  PREFIX = "[+-]"
44
42
 
45
- # <suffix> ::= "=" | "<" | ">"
46
- SUFFIX = "[=<>]"
43
+ # <suffix> ::= "'"
44
+ SUFFIX = "[']"
47
45
 
48
46
  # <piece> ::= <letter> | <prefix> <letter> | <letter> <suffix> | <prefix> <letter> <suffix>
49
47
  PIECE = "(?:#{PREFIX}?#{LETTER}#{SUFFIX}?)"
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Feen
4
+ module Parser
5
+ module PiecesInHand
6
+ # Handles canonical ordering validation for pieces in hand according to FEEN specification
7
+ module CanonicalSorter
8
+ # Validates that pieces are in canonical order according to FEEN specification:
9
+ # 1. By quantity (descending)
10
+ # 2. By complete PNN representation (alphabetically ascending)
11
+ #
12
+ # @param pieces_with_counts [Array<Hash>] Array of pieces with their counts
13
+ # @raise [ArgumentError] If pieces are not in canonical order
14
+ # @return [void]
15
+ def self.validate_order(pieces_with_counts)
16
+ return if pieces_with_counts.size <= 1
17
+
18
+ # Create the expected canonical order
19
+ canonical_order = sort_canonically(pieces_with_counts)
20
+
21
+ # Compare with actual order
22
+ pieces_with_counts.each_with_index do |piece_data, index|
23
+ canonical_piece = canonical_order[index]
24
+
25
+ next if piece_data[:piece] == canonical_piece[:piece] &&
26
+ piece_data[:count] == canonical_piece[:count]
27
+
28
+ raise ::ArgumentError, format(
29
+ Errors[:canonical_order_violation],
30
+ actual: format_pieces_sequence(pieces_with_counts),
31
+ expected: format_pieces_sequence(canonical_order)
32
+ )
33
+ end
34
+ end
35
+
36
+ # Sorts pieces according to canonical FEEN order
37
+ #
38
+ # @param pieces_with_counts [Array<Hash>] Array of pieces with their counts
39
+ # @return [Array<Hash>] Canonically sorted array
40
+ def self.sort_canonically(pieces_with_counts)
41
+ pieces_with_counts.sort do |a, b|
42
+ # Primary sort: by quantity (descending)
43
+ count_comparison = b[:count] <=> a[:count]
44
+ next count_comparison unless count_comparison.zero?
45
+
46
+ # Secondary sort: by complete PNN representation (alphabetically ascending)
47
+ a[:piece] <=> b[:piece]
48
+ end
49
+ end
50
+
51
+ # Formats a pieces sequence for error messages
52
+ #
53
+ # @param pieces_with_counts [Array<Hash>] Array of pieces with their counts
54
+ # @return [String] Formatted string representation
55
+ private_class_method def self.format_pieces_sequence(pieces_with_counts)
56
+ pieces_with_counts.map do |item|
57
+ count = item[:count]
58
+ piece = item[:piece]
59
+
60
+ if count == 1
61
+ piece
62
+ else
63
+ "#{count}#{piece}"
64
+ end
65
+ end.join
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -5,10 +5,12 @@ 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'"
12
14
  }.freeze
13
15
  end
14
16
  end
@@ -0,0 +1,47 @@
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
+ # Complete validation pattern for pieces in hand string
27
+ # Based on the FEEN BNF specification with PNN support
28
+ # This pattern allows any sequence of pieces (uppercase/lowercase mixed) in canonical order
29
+ # It should reject invalid formats like "++P"
30
+ VALID_FORMAT_PATTERN = /\A
31
+ (?:
32
+ -| # No pieces in hand
33
+ (?:
34
+ (?:[2-9]|\d{2,})? # Optional count (2-9 or 10+)
35
+ [-+]? # Optional single prefix (+ or -)
36
+ [a-zA-Z] # Required letter
37
+ '? # Optional single suffix (')
38
+ )+ # One or more pieces
39
+ )
40
+ \z/x
41
+
42
+ # Pattern for extracting all pieces globally (used for comprehensive validation)
43
+ GLOBAL_PIECE_EXTRACTION_PATTERN = /(?:([2-9]|\d{2,}))?([-+]?[a-zA-Z]'?)/
44
+ end
45
+ end
46
+ end
47
+ end
@@ -2,20 +2,22 @@
2
2
 
3
3
  require_relative File.join("pieces_in_hand", "errors")
4
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")
5
+ require_relative File.join("pieces_in_hand", "pnn_patterns")
6
+ require_relative File.join("pieces_in_hand", "canonical_sorter")
7
7
 
8
8
  module Feen
9
9
  module Parser
10
10
  # Handles parsing of the pieces in hand section of a FEEN string.
11
11
  # Pieces in hand represent pieces available for dropping onto the board.
12
+ # This implementation supports full PNN notation including prefixes and suffixes.
12
13
  module PiecesInHand
13
14
  # Parses the pieces in hand section of a FEEN string.
14
15
  #
15
16
  # @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.
17
+ # @return [Array<String>] Array of piece identifiers in full PNN format,
18
+ # expanded based on their counts and sorted according to FEEN specification:
19
+ # 1. By quantity (descending)
20
+ # 2. By complete PNN representation (alphabetically ascending)
19
21
  # Empty array if no pieces are in hand.
20
22
  # @raise [ArgumentError] If the input string is invalid
21
23
  #
@@ -23,13 +25,17 @@ module Feen
23
25
  # PiecesInHand.parse("-")
24
26
  # # => []
25
27
  #
26
- # @example Parse multiple pieces in hand
27
- # PiecesInHand.parse("BN2Pb")
28
- # # => ["B", "N", "P", "P", "b"]
28
+ # @example Parse pieces with modifiers
29
+ # PiecesInHand.parse("3+P2B'Pn")
30
+ # # => ["+P", "+P", "+P", "B'", "B'", "P", "n"]
29
31
  #
30
- # @example Parse pieces with counts
31
- # PiecesInHand.parse("N5P2b")
32
- # # => ["N", "P", "P", "P", "P", "P", "b", "b"]
32
+ # @example Parse complex pieces with counts and modifiers
33
+ # PiecesInHand.parse("10P5K3B2p'+P-pBRbq")
34
+ # # => ["P", "P", "P", "P", "P", "P", "P", "P", "P", "P",
35
+ # # "K", "K", "K", "K", "K",
36
+ # # "B", "B", "B",
37
+ # # "p'", "p'",
38
+ # # "+P", "-p", "B", "R", "b", "q"]
33
39
  def self.parse(pieces_in_hand_str)
34
40
  # Validate input
35
41
  validate_input_type(pieces_in_hand_str)
@@ -38,11 +44,13 @@ module Feen
38
44
  # Handle the no-pieces case early
39
45
  return [] if pieces_in_hand_str == NoPieces
40
46
 
41
- # Extract pieces with their counts and validate the order
47
+ # Extract pieces with their counts and validate the format
42
48
  pieces_with_counts = extract_pieces_with_counts(pieces_in_hand_str)
43
- validate_lexicographic_order(pieces_with_counts)
44
49
 
45
- # Expand the pieces into an array and maintain lexicographic ordering
50
+ # Validate canonical ordering according to FEEN specification
51
+ validate_canonical_order(pieces_with_counts)
52
+
53
+ # Expand the pieces into an array maintaining the canonical order
46
54
  expand_pieces(pieces_with_counts)
47
55
  end
48
56
 
@@ -57,17 +65,112 @@ module Feen
57
65
  end
58
66
 
59
67
  # Validates that the input string matches the expected format according to FEEN specification.
68
+ # This includes validation of individual PNN pieces and overall structure.
60
69
  #
61
70
  # @param str [String] Input string to validate
62
71
  # @raise [ArgumentError] If format is invalid
63
72
  # @return [void]
64
73
  private_class_method def self.validate_format(str)
65
- return if str == NoPieces || str.match?(ValidFormatPattern)
74
+ return if str == NoPieces
75
+
76
+ # First, validate overall structure using the updated pattern
77
+ raise ::ArgumentError, format(Errors[:invalid_format], str) unless str.match?(PnnPatterns::VALID_FORMAT_PATTERN)
78
+
79
+ # Additional validation: ensure each piece component is valid PNN
80
+ # This catches cases like "++P" that might pass the overall pattern
81
+ validate_individual_pieces(str)
82
+ end
83
+
84
+ # Validates each individual piece in the string for PNN compliance
85
+ #
86
+ # @param str [String] FEEN pieces in hand string
87
+ # @raise [ArgumentError] If any piece is invalid PNN format
88
+ # @return [void]
89
+ private_class_method def self.validate_individual_pieces(str)
90
+ original_string = str
91
+ position = 0
92
+
93
+ while position < str.length
94
+ match = str[position..].match(PnnPatterns::PIECE_WITH_COUNT_PATTERN)
95
+
96
+ unless match
97
+ # Find the problematic part
98
+ remaining = str[position..]
99
+ raise ::ArgumentError, format(Errors[:invalid_format], remaining)
100
+ end
101
+
102
+ count_str, piece = match.captures
66
103
 
67
- raise ::ArgumentError, format(Errors[:invalid_format], str)
104
+ # Skip empty matches (shouldn't happen with our pattern, but safety check)
105
+ if piece.nil? || piece.empty?
106
+ position += 1
107
+ next
108
+ end
109
+
110
+ # Validate the piece follows PNN specification
111
+ unless piece.match?(PnnPatterns::PNN_PIECE_PATTERN)
112
+ raise ::ArgumentError, format(Errors[:invalid_pnn_piece], piece)
113
+ end
114
+
115
+ # Validate count format (no "0" or "1" prefixes allowed)
116
+ if count_str && !count_str.match?(PnnPatterns::VALID_COUNT_PATTERN)
117
+ raise ::ArgumentError, format(Errors[:invalid_count], count_str)
118
+ end
119
+
120
+ position += match[0].length
121
+ end
122
+
123
+ # Final check: verify that we can reconstruct the string correctly
124
+ # by re-extracting all pieces and comparing with original
125
+ reconstructed_pieces = extract_pieces_with_counts(original_string)
126
+ reconstructed = reconstructed_pieces.map do |item|
127
+ count = item[:count]
128
+ piece = item[:piece]
129
+ count == 1 ? piece : "#{count}#{piece}"
130
+ end.join
131
+
132
+ # If reconstruction doesn't match original, there's an invalid format
133
+ return if reconstructed == original_string
134
+ # Find the first discrepancy to provide better error message
135
+ # This will catch cases like "++P" where we extract "+P" but original has extra "+"
136
+ unless original_string.length > reconstructed.length
137
+ raise ::ArgumentError, format(Errors[:invalid_format], original_string)
138
+ end
139
+
140
+ # There are extra characters - find what's invalid
141
+ original_string.sub(reconstructed, "")
142
+ # Try to identify the problematic piece
143
+ problematic_part = find_problematic_piece(original_string, reconstructed)
144
+ raise ::ArgumentError, format(Errors[:invalid_pnn_piece], problematic_part)
145
+ end
146
+
147
+ # Finds the problematic piece in the original string by comparing with reconstruction
148
+ #
149
+ # @param original [String] Original input string
150
+ # @param reconstructed [String] Reconstructed string from extracted pieces
151
+ # @return [String] The problematic piece or sequence
152
+ private_class_method def self.find_problematic_piece(original, reconstructed)
153
+ # Simple heuristic: find the first part that doesn't match
154
+ min_length = [original.length, reconstructed.length].min
155
+
156
+ # Find first difference
157
+ diff_pos = 0
158
+ diff_pos += 1 while diff_pos < min_length && original[diff_pos] == reconstructed[diff_pos]
159
+
160
+ # If difference is at start, likely extra prefix
161
+ # Look for a sequence that starts with invalid pattern like "++"
162
+ if (diff_pos == 0) && original.match?(/\A\+\+/)
163
+ return "++P" # Common case
164
+ end
165
+
166
+ # Extract a reasonable chunk around the problematic area
167
+ start_pos = [0, diff_pos - 2].max
168
+ end_pos = [original.length, diff_pos + 4].min
169
+ original[start_pos...end_pos]
68
170
  end
69
171
 
70
172
  # Extracts pieces with their counts from the FEEN string.
173
+ # Supports full PNN notation including prefixes and suffixes.
71
174
  #
72
175
  # @param str [String] FEEN pieces in hand string
73
176
  # @return [Array<Hash>] Array of hashes with :piece and :count keys
@@ -76,7 +179,7 @@ module Feen
76
179
  position = 0
77
180
 
78
181
  while position < str.length
79
- match = str[position..].match(PieceCountPattern)
182
+ match = str[position..].match(PnnPatterns::PIECE_WITH_COUNT_PATTERN)
80
183
  break unless match
81
184
 
82
185
  count_str, piece = match.captures
@@ -92,24 +195,24 @@ module Feen
92
195
  result
93
196
  end
94
197
 
95
- # Validates that pieces are in lexicographic ASCII order.
198
+ # Validates that pieces are in canonical order according to FEEN specification:
199
+ # 1. By quantity (descending)
200
+ # 2. By complete PNN representation (alphabetically ascending)
96
201
  #
97
202
  # @param pieces_with_counts [Array<Hash>] Array of pieces with their counts
98
- # @raise [ArgumentError] If pieces are not in lexicographic order
203
+ # @raise [ArgumentError] If pieces are not in canonical order
99
204
  # @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
205
+ private_class_method def self.validate_canonical_order(pieces_with_counts)
206
+ return if pieces_with_counts.size <= 1
105
207
 
106
- raise ::ArgumentError, Errors[:sorting_error]
208
+ CanonicalSorter.validate_order(pieces_with_counts)
107
209
  end
108
210
 
109
211
  # Expands the pieces based on their counts into an array.
212
+ # Maintains the canonical ordering from the input.
110
213
  #
111
214
  # @param pieces_with_counts [Array<Hash>] Array of pieces with their counts
112
- # @return [Array<String>] Array of expanded pieces
215
+ # @return [Array<String>] Array of expanded pieces in canonical order
113
216
  private_class_method def self.expand_pieces(pieces_with_counts)
114
217
  pieces_with_counts.flat_map do |item|
115
218
  Array.new(item[:count], item[:piece])
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 = "rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR - 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 = "rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR - 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
- # # => "rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR - 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 = "rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR - 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("rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR - 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.beta4
4
+ version: 5.0.0.beta6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cyril Kato
@@ -33,9 +33,11 @@ files:
33
33
  - lib/feen/parser/games_turn/valid_games_turn_pattern.rb
34
34
  - lib/feen/parser/piece_placement.rb
35
35
  - lib/feen/parser/pieces_in_hand.rb
36
+ - lib/feen/parser/pieces_in_hand/canonical_sorter.rb
36
37
  - lib/feen/parser/pieces_in_hand/errors.rb
37
38
  - lib/feen/parser/pieces_in_hand/no_pieces.rb
38
39
  - lib/feen/parser/pieces_in_hand/piece_count_pattern.rb
40
+ - lib/feen/parser/pieces_in_hand/pnn_patterns.rb
39
41
  - lib/feen/parser/pieces_in_hand/valid_format_pattern.rb
40
42
  homepage: https://github.com/sashite/feen.rb
41
43
  licenses:
@@ -46,9 +48,7 @@ metadata:
46
48
  homepage_uri: https://github.com/sashite/feen.rb
47
49
  source_code_uri: https://github.com/sashite/feen.rb
48
50
  specification_uri: https://sashite.dev/documents/feen/1.0.0/
49
- funding_uri: https://github.com/sponsors/cyril
50
51
  rubygems_mfa_required: 'true'
51
- article_uri: https://blog.cyril.email/posts/2025-05-01/introducing-feen-notation.html
52
52
  rdoc_options: []
53
53
  require_paths:
54
54
  - lib
@@ -63,7 +63,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
63
63
  - !ruby/object:Gem::Version
64
64
  version: '0'
65
65
  requirements: []
66
- rubygems_version: 3.6.7
66
+ rubygems_version: 3.6.9
67
67
  specification_version: 4
68
68
  summary: FEEN (Forsyth–Edwards Enhanced Notation) support for the Ruby language.
69
69
  test_files: []