feen 5.0.0.beta6 → 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: ce89113318c1acf675da8dc7d4af4a9ea135a6485e0e4eb8d522640149f0461a
4
- data.tar.gz: e06ae9198a93a896594e1ed9ba0b96a8af1445a20a7f8df8b247a04d84d76e71
3
+ metadata.gz: 0f302aa1f4504f94ed909dcb5f6759c52a8895c85e4bd4cc0c0cbff42ef6fdd4
4
+ data.tar.gz: eb77d9e6e1d1bae1c13cc0b4997e1b509e06b631001fbc8c60027da234f50494
5
5
  SHA512:
6
- metadata.gz: 177f297b196c9a20601eca71865d3ee51389111a72da28d4d1dc08249f3017be07d50636d0a9161759067cf9e8e42dbfdc2dc472f7717130f0e20477634537da
7
- data.tar.gz: 5060cd110c9ee95d43f952fbe55c9870e1c98171934f148947a49a6afdbf547007ddd012e24598dd3209e0eab4ea11905f61d52871e9bfa511fdc9bac78af9d6
6
+ metadata.gz: 331d90ea031b33aa88292680f98b5c847c467827ad2cd64f39e261c8d052a3cfefdf1f4fe734befc51ffffe7667fbe3b713a08d6952adc86869f3401726f12c5
7
+ data.tar.gz: 574801b36461caf3e971540430962344442a43027e100d76527df73345374ab624a6a2d7bd80fa9cb3917a85a2e16d4ececbca67325776c134a33d7d400ee5c7
data/README.md CHANGED
@@ -30,14 +30,14 @@ A FEEN record consists of three space-separated fields:
30
30
  ### Field Details
31
31
 
32
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
33
+ 2. **Pieces in Hand**: Off-board pieces available for placement, formatted as `"UPPERCASE/lowercase"` and sorted canonically within each section
34
34
  3. **Games Turn**: Game identifiers and active player indication
35
35
 
36
36
  ## Installation
37
37
 
38
38
  ```ruby
39
39
  # In your Gemfile
40
- gem "feen", ">= 5.0.0.beta6"
40
+ gem "feen", ">= 5.0.0.beta7"
41
41
  ```
42
42
 
43
43
  Or install manually:
@@ -55,7 +55,7 @@ Convert a FEEN string into a structured Ruby object:
55
55
  ```ruby
56
56
  require "feen"
57
57
 
58
- feen_string = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR - CHESS/chess"
58
+ feen_string = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR / CHESS/chess"
59
59
  position = Feen.parse(feen_string)
60
60
 
61
61
  # Result is a hash:
@@ -83,7 +83,7 @@ Parse a FEEN string without raising exceptions:
83
83
  require "feen"
84
84
 
85
85
  # Valid FEEN string
86
- result = Feen.safe_parse("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR - CHESS/chess")
86
+ result = Feen.safe_parse("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR / CHESS/chess")
87
87
  # => {piece_placement: [...], pieces_in_hand: [...], games_turn: [...]}
88
88
 
89
89
  # Invalid FEEN string
@@ -115,7 +115,7 @@ result = Feen.dump(
115
115
  games_turn: %w[CHESS chess],
116
116
  pieces_in_hand: []
117
117
  )
118
- # => "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR - CHESS/chess"
118
+ # => "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR / CHESS/chess"
119
119
  ```
120
120
 
121
121
  ### Validation
@@ -126,7 +126,7 @@ Check if a string is valid FEEN notation and in canonical form:
126
126
  require "feen"
127
127
 
128
128
  # Canonical form
129
- Feen.valid?("lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL - SHOGI/shogi")
129
+ Feen.valid?("lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL / SHOGI/shogi")
130
130
  # => true
131
131
 
132
132
  # Invalid syntax
@@ -134,8 +134,8 @@ Feen.valid?("invalid feen string")
134
134
  # => false
135
135
 
136
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)
137
+ Feen.valid?("8/8/8/8/8/8/8/8 P3K/ CHESS/chess")
138
+ # => false (wrong quantity sorting in uppercase section)
139
139
  ```
140
140
 
141
141
  The `valid?` method performs two levels of validation:
@@ -150,7 +150,7 @@ As FEEN is rule-agnostic, it can represent positions from various board games. H
150
150
  ### International Chess
151
151
 
152
152
  ```ruby
153
- feen_string = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR - CHESS/chess"
153
+ feen_string = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR / CHESS/chess"
154
154
  ```
155
155
 
156
156
  In this initial chess position, the third field `CHESS/chess` indicates it's the player with uppercase pieces' turn to move.
@@ -158,36 +158,58 @@ In this initial chess position, the third field `CHESS/chess` indicates it's the
158
158
  ### Shogi (Japanese Chess)
159
159
 
160
160
  ```ruby
161
- feen_string = "lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL - SHOGI/shogi"
161
+ feen_string = "lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL / SHOGI/shogi"
162
162
  ```
163
163
 
164
164
  **With pieces in hand and promotions:**
165
165
 
166
166
  ```ruby
167
- feen_string = "lnsgk3l/5g3/p1ppB2pp/9/8B/2P6/P2PPPPPP/3K3R1/5rSNL 5P2g2sln SHOGI/shogi"
167
+ feen_string = "lnsgk3l/5g3/p1ppB2pp/9/8B/2P6/P2PPPPPP/3K3R1/5rSNL 5P2G2L/2gln2s SHOGI/shogi"
168
168
  ```
169
169
 
170
170
  In this shogi position:
171
+
171
172
  - The format supports promotions with the `+` prefix (e.g., `+P` for a promoted pawn)
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)
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
175
177
  - `SHOGI/shogi` indicates it's Sente's (Black's, uppercase) turn to move
176
178
 
177
179
  ### Makruk (Thai Chess)
178
180
 
179
181
  ```ruby
180
- feen_string = "rnbqkbnr/8/pppppppp/8/8/PPPPPPPP/8/RNBKQBNR - MAKRUK/makruk"
182
+ feen_string = "rnbqkbnr/8/pppppppp/8/8/PPPPPPPP/8/RNBKQBNR / MAKRUK/makruk"
181
183
  ```
182
184
 
183
185
  ### Xiangqi (Chinese Chess)
184
186
 
185
187
  ```ruby
186
- 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"
187
189
  ```
188
190
 
189
191
  ## Advanced Features
190
192
 
193
+ ### Pieces in Hand with Case Separation
194
+
195
+ FEEN uses case separation for pieces in hand to distinguish between players using the format `"UPPERCASE_PIECES/LOWERCASE_PIECES"`:
196
+
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
+
191
213
  ### Piece Name Notation (PNN) Support
192
214
 
193
215
  FEEN supports the complete [PNN specification](https://sashite.dev/documents/pnn/1.0.0/) for representing pieces with state modifiers:
@@ -207,7 +229,7 @@ piece_placement = [
207
229
  # ... other ranks
208
230
  ]
209
231
 
210
- # Pieces in hand with PNN modifiers
232
+ # Pieces in hand with PNN modifiers - case separated
211
233
  pieces_in_hand = ["+P", "+P", "+P", "B'", "B'", "-p", "P"]
212
234
 
213
235
  result = Feen.dump(
@@ -215,34 +237,38 @@ result = Feen.dump(
215
237
  pieces_in_hand: pieces_in_hand,
216
238
  games_turn: %w[SHOGI shogi]
217
239
  )
218
- # => "8/8/8/8/4+P4/8/8/8/8 3+P2B'-pP SHOGI/shogi"
240
+ # => "8/8/8/8/4+P4/8/8/8/8 3+P2B'P/-p SHOGI/shogi"
219
241
  ```
220
242
 
221
243
  ### Canonical Pieces in Hand Sorting
222
244
 
223
- FEEN enforces canonical ordering of pieces in hand according to the specification:
245
+ FEEN enforces canonical ordering of pieces in hand within each case section according to the specification:
224
246
 
225
247
  1. **By quantity (descending)**
226
248
  2. **By complete PNN representation (alphabetically ascending)**
227
249
 
250
+ The dumper organizes pieces by case first, then applies canonical sorting within each section:
251
+
228
252
  ```ruby
229
253
  # Input pieces in any order
230
- pieces = ["P", "B", "P", "+P", "B", "P", "+P", "+P"]
254
+ pieces = ["P", "b", "P", "+P", "B", "p", "+P", "+P"]
231
255
 
232
- result = Feen::Dumper::PiecesInHand.dump(*pieces)
233
- # => "3+P3P2B"
234
- # Breakdown: 3×+P (most frequent), 3×P, 2×B (alphabetical within same quantity)
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)
235
265
  ```
236
266
 
237
- #### Complex Example from FEEN Specification
267
+ The parser returns pieces in simple alphabetical order for easy handling:
238
268
 
239
269
  ```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)
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
246
272
  ```
247
273
 
248
274
  ### Multi-dimensional Boards
@@ -269,7 +295,7 @@ result = Feen.dump(
269
295
  games_turn: %w[FOO bar],
270
296
  pieces_in_hand: []
271
297
  )
272
- # => "rnb/qkp//PR1/1KQ - FOO/bar"
298
+ # => "rnb/qkp//PR1/1KQ / FOO/bar"
273
299
  ```
274
300
 
275
301
  ### Hybrid Games
@@ -278,20 +304,22 @@ FEEN supports hybrid games mixing different piece sets:
278
304
 
279
305
  ```ruby
280
306
  # Chess-Shogi hybrid position
281
- feen_string = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR 3+P2B'-pP CHESS/shogi"
307
+ feen_string = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR 3+P2B'/p CHESS/shogi"
282
308
  ```
283
309
 
284
310
  This represents a position where:
311
+
285
312
  - The board uses chess-style pieces
286
- - Pieces in hand use shogi-style promotion (`+P`) and intermediate states (`B'`, `-p`)
313
+ - Pieces in hand use shogi-style promotion (`+P`) and intermediate states (`B'`)
287
314
  - Chess player to move, against shogi player
315
+ - Case separation shows which player has which pieces
288
316
 
289
317
  ## Round-trip Consistency
290
318
 
291
319
  FEEN.rb guarantees round-trip consistency - parsing and dumping produces identical canonical strings:
292
320
 
293
321
  ```ruby
294
- original = "lnsgk3l/5g3/p1ppB2pp/9/8B/2P6/P2PPPPPP/3K3R1/5rSNL 5P2g2sln SHOGI/shogi"
322
+ original = "lnsgk3l/5g3/p1ppB2pp/9/8B/2P6/P2PPPPPP/3K3R1/5rSNL 5P2G2L/2gln2s SHOGI/shogi"
295
323
  parsed = Feen.parse(original)
296
324
  dumped = Feen.dump(**parsed)
297
325
 
@@ -304,8 +332,12 @@ original == dumped # => true (guaranteed canonical form)
304
332
 
305
333
  ```ruby
306
334
  # Invalid PNN format
307
- Feen::Dumper::PiecesInHand.dump("++P")
308
- # => ArgumentError: Invalid PNN format: '++P'
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'
309
341
 
310
342
  # Invalid games turn
311
343
  Feen.dump(
@@ -314,6 +346,10 @@ Feen.dump(
314
346
  games_turn: %w[BOTH_UPPERCASE ALSO_UPPERCASE] # Both same case
315
347
  )
316
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
317
353
  ```
318
354
 
319
355
  ### Safe Operations
@@ -325,6 +361,7 @@ position = Feen.safe_parse(user_input)
325
361
 
326
362
  if position
327
363
  puts "Valid FEEN position!"
364
+ puts "Pieces in hand: #{position[:pieces_in_hand]}"
328
365
  else
329
366
  puts "Invalid FEEN format"
330
367
  end
@@ -333,6 +370,7 @@ end
333
370
  ## Performance Considerations
334
371
 
335
372
  - **Parsing**: Optimized recursive descent parser with O(n) complexity
373
+ - **Case separation**: Efficient single-pass processing for pieces in hand
336
374
  - **Validation**: Round-trip validation ensures canonical form
337
375
  - **Memory**: Efficient array-based representation for large boards
338
376
  - **Sorting**: In-place canonical sorting for pieces in hand
@@ -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,40 +8,105 @@ 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 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)
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")
16
16
  # @raise [ArgumentError] If any piece identifier is invalid
17
17
  # @example
18
- # PiecesInHand.dump("P", "P", "P", "B", "B", "+P")
19
- # # => "3P2B+P"
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"
20
23
  #
21
24
  # PiecesInHand.dump
22
- # # => "-"
25
+ # # => "/"
23
26
  def self.dump(*piece_chars)
24
- # If no pieces in hand, return the standardized empty indicator
25
- return NoPieces if piece_chars.empty?
26
-
27
- # Validate each piece character according to the FEEN specification (full PNN support)
27
+ # Validate each piece character according to the FEEN specification
28
28
  validated_chars = validate_piece_chars(piece_chars)
29
29
 
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
+
30
81
  # Count occurrences of each piece type
31
- piece_counts = validated_chars.tally
82
+ piece_counts = pieces.each_with_object(Hash.new(0)) do |piece, counts|
83
+ counts[piece] += 1
84
+ end
32
85
 
33
- # Sort according to FEEN specification:
34
- # 1. By quantity (descending)
35
- # 2. By complete PNN representation (alphabetically ascending)
86
+ # Sort by count (descending) then alphabetically (ascending)
36
87
  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
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
39
97
  end
40
98
 
41
- # Format the pieces sequence with proper count prefixes
42
- format_pieces_sequence(sorted_pieces)
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
43
107
  end
44
108
 
45
- # Validates all piece characters according to FEEN specification with full PNN support
109
+ # Validates all piece characters according to FEEN specification
46
110
  #
47
111
  # @param piece_chars [Array<Object>] Array of piece character candidates
48
112
  # @return [Array<String>] Array of validated piece characters
@@ -53,7 +117,11 @@ module Feen
53
117
  end
54
118
  end
55
119
 
56
- # Validates a single piece character according to FEEN specification with full PNN support
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 "'"
57
125
  #
58
126
  # @param char [Object] Piece character candidate
59
127
  # @param index [Integer] Index of the character in the original array
@@ -69,12 +137,9 @@ module Feen
69
137
  )
70
138
  end
71
139
 
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/)
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/)
78
143
  raise ::ArgumentError, format(
79
144
  Errors[:invalid_format],
80
145
  index: index,
@@ -84,20 +149,6 @@ module Feen
84
149
 
85
150
  char
86
151
  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
101
152
  end
102
153
  end
103
154
  end
@@ -10,7 +10,10 @@ module Feen
10
10
  invalid_format: "Invalid pieces in hand format: %s",
11
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
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'"
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"
14
17
  }.freeze
15
18
  end
16
19
  end
@@ -23,24 +23,66 @@ module Feen
23
23
  # Note: We need to handle the full PNN piece including modifiers
24
24
  PIECE_WITH_COUNT_PATTERN = /(?:([2-9]|\d{2,}))?([-+]?[a-zA-Z]'?)/
25
25
 
26
- # Complete validation pattern for pieces in hand string
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
27
55
  # 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
56
+ # Format: "UPPERCASE_PIECES/LOWERCASE_PIECES"
57
+ # Either section can be empty, but the "/" separator is mandatory
58
+ VALID_FORMAT_PATTERN = %r{\A
31
59
  (?:
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
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
39
66
  )
40
- \z/x
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
41
77
 
42
78
  # Pattern for extracting all pieces globally (used for comprehensive validation)
43
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]'?)/
44
86
  end
45
87
  end
46
88
  end
@@ -1,57 +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
4
  require_relative File.join("pieces_in_hand", "pnn_patterns")
6
- require_relative File.join("pieces_in_hand", "canonical_sorter")
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.
12
10
  # This implementation supports full PNN notation including prefixes and suffixes.
11
+ # Format: "UPPERCASE_PIECES/LOWERCASE_PIECES"
13
12
  module PiecesInHand
14
13
  # Parses the pieces in hand section of a FEEN string.
15
14
  #
16
- # @param pieces_in_hand_str [String] FEEN pieces in hand string
15
+ # @param pieces_in_hand_str [String] FEEN pieces in hand string in format "UPPERCASE/lowercase"
17
16
  # @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)
17
+ # expanded based on their counts and sorted alphabetically.
21
18
  # Empty array if no pieces are in hand.
22
19
  # @raise [ArgumentError] If the input string is invalid
23
20
  #
24
21
  # @example Parse no pieces in hand
25
- # PiecesInHand.parse("-")
22
+ # PiecesInHand.parse("/")
26
23
  # # => []
27
24
  #
28
- # @example Parse pieces with modifiers
29
- # PiecesInHand.parse("3+P2B'Pn")
30
- # # => ["+P", "+P", "+P", "B'", "B'", "P", "n"]
25
+ # @example Parse pieces with case separation
26
+ # PiecesInHand.parse("3P2B/p")
27
+ # # => ["B", "B", "P", "P", "P", "p"]
31
28
  #
32
29
  # @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"]
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"]
39
34
  def self.parse(pieces_in_hand_str)
40
35
  # Validate input
41
36
  validate_input_type(pieces_in_hand_str)
42
37
  validate_format(pieces_in_hand_str)
43
38
 
44
39
  # Handle the no-pieces case early
45
- return [] if pieces_in_hand_str == NoPieces
40
+ return [] if pieces_in_hand_str == "/"
46
41
 
47
- # Extract pieces with their counts and validate the format
48
- pieces_with_counts = extract_pieces_with_counts(pieces_in_hand_str)
42
+ # Split by the separator to get uppercase and lowercase sections
43
+ uppercase_section, lowercase_section = pieces_in_hand_str.split("/", 2)
49
44
 
50
- # Validate canonical ordering according to FEEN specification
51
- validate_canonical_order(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)
52
48
 
53
- # Expand the pieces into an array maintaining the canonical order
54
- expand_pieces(pieces_with_counts)
49
+ # Combine all pieces and sort them alphabetically
50
+ all_pieces = uppercase_pieces + lowercase_pieces
51
+ all_pieces.sort
55
52
  end
56
53
 
57
54
  # Validates that the input is a non-empty string.
@@ -65,37 +62,63 @@ module Feen
65
62
  end
66
63
 
67
64
  # Validates that the input string matches the expected format according to FEEN specification.
68
- # This includes validation of individual PNN pieces and overall structure.
65
+ # Format must be: "UPPERCASE_PIECES/LOWERCASE_PIECES"
69
66
  #
70
67
  # @param str [String] Input string to validate
71
68
  # @raise [ArgumentError] If format is invalid
72
69
  # @return [void]
73
70
  private_class_method def self.validate_format(str)
74
- return if str == NoPieces
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)
75
76
 
76
- # First, validate overall structure using the updated pattern
77
- raise ::ArgumentError, format(Errors[:invalid_format], str) unless str.match?(PnnPatterns::VALID_FORMAT_PATTERN)
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
78
81
 
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
+ # 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)
82
106
  end
83
107
 
84
- # Validates each individual piece in the string for PNN compliance
108
+ # Validates each individual piece in a section for PNN compliance
85
109
  #
86
- # @param str [String] FEEN pieces in hand string
110
+ # @param section [String] FEEN pieces section string
111
+ # @param case_type [Symbol] Either :uppercase or :lowercase
87
112
  # @raise [ArgumentError] If any piece is invalid PNN format
88
113
  # @return [void]
89
- private_class_method def self.validate_individual_pieces(str)
90
- original_string = str
114
+ private_class_method def self.validate_individual_pieces_in_section(section, case_type)
91
115
  position = 0
92
116
 
93
- while position < str.length
94
- match = str[position..].match(PnnPatterns::PIECE_WITH_COUNT_PATTERN)
117
+ while position < section.length
118
+ match = section[position..].match(PnnPatterns::PIECE_WITH_COUNT_PATTERN)
95
119
 
96
120
  unless match
97
- # Find the problematic part
98
- remaining = str[position..]
121
+ remaining = section[position..]
99
122
  raise ::ArgumentError, format(Errors[:invalid_format], remaining)
100
123
  end
101
124
 
@@ -117,69 +140,52 @@ module Feen
117
140
  raise ::ArgumentError, format(Errors[:invalid_count], count_str)
118
141
  end
119
142
 
120
- position += match[0].length
121
- end
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
122
149
 
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)
150
+ position += match[0].length
138
151
  end
152
+ end
139
153
 
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)
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]/)
145
162
  end
146
163
 
147
- # Finds the problematic piece in the original string by comparing with reconstruction
164
+ # Parses a specific section (uppercase or lowercase) and returns expanded pieces
148
165
  #
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
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)
165
174
 
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]
175
+ # Expand the pieces into an array (no canonical order validation needed)
176
+ expand_pieces(pieces_with_counts)
170
177
  end
171
178
 
172
- # Extracts pieces with their counts from the FEEN string.
173
- # Supports full PNN notation including prefixes and suffixes.
179
+ # Extracts pieces with their counts from a section string.
174
180
  #
175
- # @param str [String] FEEN pieces in hand string
181
+ # @param section [String] FEEN pieces section string
176
182
  # @return [Array<Hash>] Array of hashes with :piece and :count keys
177
- private_class_method def self.extract_pieces_with_counts(str)
183
+ private_class_method def self.extract_pieces_with_counts_from_section(section)
178
184
  result = []
179
185
  position = 0
180
186
 
181
- while position < str.length
182
- match = str[position..].match(PnnPatterns::PIECE_WITH_COUNT_PATTERN)
187
+ while position < section.length
188
+ match = section[position..].match(PnnPatterns::PIECE_WITH_COUNT_PATTERN)
183
189
  break unless match
184
190
 
185
191
  count_str, piece = match.captures
@@ -195,24 +201,10 @@ module Feen
195
201
  result
196
202
  end
197
203
 
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)
201
- #
202
- # @param pieces_with_counts [Array<Hash>] Array of pieces with their counts
203
- # @raise [ArgumentError] If pieces are not in canonical order
204
- # @return [void]
205
- private_class_method def self.validate_canonical_order(pieces_with_counts)
206
- return if pieces_with_counts.size <= 1
207
-
208
- CanonicalSorter.validate_order(pieces_with_counts)
209
- end
210
-
211
204
  # Expands the pieces based on their counts into an array.
212
- # Maintains the canonical ordering from the input.
213
205
  #
214
206
  # @param pieces_with_counts [Array<Hash>] Array of pieces with their counts
215
- # @return [Array<String>] Array of expanded pieces in canonical order
207
+ # @return [Array<String>] Array of expanded pieces
216
208
  private_class_method def self.expand_pieces(pieces_with_counts)
217
209
  pieces_with_counts.flat_map do |item|
218
210
  Array.new(item[:count], item[:piece])
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.beta6
4
+ version: 5.0.0.beta7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cyril Kato
@@ -26,19 +26,14 @@ 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
33
32
  - lib/feen/parser/games_turn/valid_games_turn_pattern.rb
34
33
  - lib/feen/parser/piece_placement.rb
35
34
  - lib/feen/parser/pieces_in_hand.rb
36
- - lib/feen/parser/pieces_in_hand/canonical_sorter.rb
37
35
  - lib/feen/parser/pieces_in_hand/errors.rb
38
- - lib/feen/parser/pieces_in_hand/no_pieces.rb
39
- - lib/feen/parser/pieces_in_hand/piece_count_pattern.rb
40
36
  - lib/feen/parser/pieces_in_hand/pnn_patterns.rb
41
- - lib/feen/parser/pieces_in_hand/valid_format_pattern.rb
42
37
  homepage: https://github.com/sashite/feen.rb
43
38
  licenses:
44
39
  - MIT
@@ -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,70 +0,0 @@
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
@@ -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