feen 5.0.0.beta5 → 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: 28b60c3d60fd3241172fee9e143c53e6dca04082a2efe7f0fa7559b85dc6eac0
4
- data.tar.gz: a15999144de7bcd5637b6efca4ed1c4a8ae7d3757d6d583123dfaf44468d6d35
3
+ metadata.gz: ce89113318c1acf675da8dc7d4af4a9ea135a6485e0e4eb8d522640149f0461a
4
+ data.tar.gz: e06ae9198a93a896594e1ed9ba0b96a8af1445a20a7f8df8b247a04d84d76e71
5
5
  SHA512:
6
- metadata.gz: 861669bea6101dba5eefcf6062b9e4e0b37152618e7ffa9c1ba963eb37b2f49cad804f4c1bbee31bd030bf4ddd9602563df7e54cc244384d37e1f45ec748c5ff
7
- data.tar.gz: 6f00dcfc09fd378fde0fbe3cd36b078718eab1fa27e208d474637ef89853a7543be4027116d623d0e037c340f0deeae90ce337404db09c953faecba852f56ece
6
+ metadata.gz: 177f297b196c9a20601eca71865d3ee51389111a72da28d4d1dc08249f3017be07d50636d0a9161759067cf9e8e42dbfdc2dc472f7717130f0e20477634537da
7
+ data.tar.gz: 5060cd110c9ee95d43f952fbe55c9870e1c98171934f148947a49a6afdbf547007ddd012e24598dd3209e0eab4ea11905f61d52871e9bfa511fdc9bac78af9d6
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, sorted canonically
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.beta6"
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)
133
139
  ```
134
140
 
135
141
  The `valid?` method performs two levels of validation:
@@ -144,47 +150,100 @@ 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"
159
162
  ```
160
163
 
161
- In this shogi position:
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"
168
+ ```
162
169
 
170
+ In this shogi position:
163
171
  - 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
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)
165
175
  - `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
176
 
168
177
  ### Makruk (Thai Chess)
169
178
 
170
179
  ```ruby
171
- 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"
172
181
  ```
173
182
 
174
- This initial Makruk position is easily represented in FEEN without needing to know the specific rules of the game.
175
-
176
183
  ### Xiangqi (Chinese Chess)
177
184
 
178
185
  ```ruby
179
186
  feen_string = "rheagaehr/9/1c5c1/s1s1s1s1s/9/9/S1S1S1S1S/1C5C1/9/RHEAGAEHR - XIANGQI/xiangqi"
180
187
  ```
181
188
 
182
- In this Xiangqi position:
189
+ ## Advanced Features
183
190
 
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)
191
+ ### Piece Name Notation (PNN) Support
186
192
 
187
- ## Advanced Features
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
+ ```
188
247
 
189
248
  ### Multi-dimensional Boards
190
249
 
@@ -193,7 +252,7 @@ FEEN supports arbitrary-dimensional board configurations:
193
252
  ```ruby
194
253
  require "feen"
195
254
 
196
- # 3D board
255
+ # 3D board (2×2×3 configuration)
197
256
  piece_placement = [
198
257
  [
199
258
  %w[r n b],
@@ -213,26 +272,85 @@ result = Feen.dump(
213
272
  # => "rnb/qkp//PR1/1KQ - FOO/bar"
214
273
  ```
215
274
 
216
- ### Piece Modifiers
275
+ ### Hybrid Games
276
+
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
+ ```
217
332
 
218
- FEEN supports prefixes and suffixes for pieces to denote various states or capabilities:
333
+ ## Performance Considerations
219
334
 
220
- - **Prefix `+`**: Enhanced state
221
- - Example in shogi: `+P` may represent a promoted pawn
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
222
339
 
223
- - **Prefix `-`**: Diminished state
224
- - Could represent a piece with limited movement or other restrictions
340
+ ## Compatibility
225
341
 
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_
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)
229
346
 
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.
347
+ ## Related Specifications
231
348
 
232
- ## Documentation
349
+ FEEN is part of a family of specifications for abstract strategy games:
233
350
 
234
- - [Official FEEN Specification](https://sashite.dev/documents/feen/1.0.0/)
235
- - [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
236
354
 
237
355
  ## License
238
356
 
@@ -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
- # # => "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
@@ -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 = "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.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:
@@ -61,7 +63,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
61
63
  - !ruby/object:Gem::Version
62
64
  version: '0'
63
65
  requirements: []
64
- rubygems_version: 3.6.7
66
+ rubygems_version: 3.6.9
65
67
  specification_version: 4
66
68
  summary: FEEN (Forsyth–Edwards Enhanced Notation) support for the Ruby language.
67
69
  test_files: []