feen 5.0.0.beta8 → 5.0.0.beta9
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 +4 -4
- data/README.md +32 -32
- data/lib/feen/dumper/games_turn.rb +1 -1
- data/lib/feen/parser/games_turn.rb +7 -7
- data/lib/feen/parser/piece_placement.rb +169 -9
- data/lib/feen/parser/pieces_in_hand.rb +35 -19
- metadata +3 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bac9b628685776db7be71e57f80638a6479f03175da5b182154254f6aa0573e2
|
4
|
+
data.tar.gz: 77f99b0ee0ce4d0a4846f5a134ab85cc35ae0919df5f75b18eb9bc688c1d1530
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a195a54e16d82c53e60e9001e4111800d6d6af6a04c68517599f04b4478b84b4a96ef6a0ead6034458aafc3c7aefe80a7c55cf6d9e780a0aa933cf2b8d566f16
|
7
|
+
data.tar.gz: 864de377e1c97fa20f5d723865e8115468bd139b406889b44a956f55beec20f18d1e7a93fc97e033f873a0ab12c9d96f8cb82bb1ab32da48f643da0e07c6f49f
|
data/README.md
CHANGED
@@ -24,7 +24,7 @@ FEEN is like taking a snapshot of any board game position and turning it into a
|
|
24
24
|
Add this line to your application's Gemfile:
|
25
25
|
|
26
26
|
```ruby
|
27
|
-
gem "feen", ">= 5.0.0.
|
27
|
+
gem "feen", ">= 5.0.0.beta9"
|
28
28
|
```
|
29
29
|
|
30
30
|
Or install it directly:
|
@@ -45,8 +45,8 @@ board = [["r", "k", "r"]]
|
|
45
45
|
|
46
46
|
feen_string = Feen.dump(
|
47
47
|
piece_placement: board,
|
48
|
-
pieces_in_hand: [],
|
49
|
-
games_turn: ["GAME", "game"]
|
48
|
+
pieces_in_hand: [], # No captured pieces
|
49
|
+
games_turn: ["GAME", "game"] # GAME player's turn
|
50
50
|
)
|
51
51
|
|
52
52
|
feen_string # => "rkr / GAME/game"
|
@@ -87,11 +87,11 @@ The board shows where pieces are placed:
|
|
87
87
|
**Examples:**
|
88
88
|
|
89
89
|
```ruby
|
90
|
-
"K"
|
91
|
-
"3"
|
92
|
-
"Kqr"
|
93
|
-
"K2r"
|
94
|
-
"Kqr/3/R2k"
|
90
|
+
"K" # Single piece on 1x1 board
|
91
|
+
"3" # Three empty squares
|
92
|
+
"Kqr" # Three pieces: K, q, r
|
93
|
+
"K2r" # K, two empty squares, then r
|
94
|
+
"Kqr/3/R2k" # 3x3 board with multiple ranks
|
95
95
|
```
|
96
96
|
|
97
97
|
### Part 2: Captured Pieces (Pieces in Hand)
|
@@ -106,10 +106,10 @@ Shows pieces that have been captured and can potentially be used again:
|
|
106
106
|
**Examples:**
|
107
107
|
|
108
108
|
```ruby
|
109
|
-
"/"
|
110
|
-
"P/"
|
111
|
-
"/p"
|
112
|
-
"2PK/3p"
|
109
|
+
"/" # No pieces captured
|
110
|
+
"P/" # First player has one P piece
|
111
|
+
"/p" # Second player has one p piece
|
112
|
+
"2PK/3p" # First player: 2 P's + 1 K, Second player: 3 p's
|
113
113
|
```
|
114
114
|
|
115
115
|
### Part 3: Turn Information
|
@@ -123,9 +123,9 @@ Shows whose turn it is and identifies the game types:
|
|
123
123
|
**Examples:**
|
124
124
|
|
125
125
|
```ruby
|
126
|
-
"CHESS/chess"
|
127
|
-
"shogi/SHOGI"
|
128
|
-
"GAME1/game2"
|
126
|
+
"CHESS/chess" # CHESS player (uppercase pieces) to move
|
127
|
+
"shogi/SHOGI" # shogi player (lowercase pieces) to move
|
128
|
+
"GAME1/game2" # GAME1 player (uppercase pieces) to move (mixed game types)
|
129
129
|
```
|
130
130
|
|
131
131
|
## Complete API Reference
|
@@ -147,9 +147,9 @@ Converts position components into a FEEN string.
|
|
147
147
|
|
148
148
|
```ruby
|
149
149
|
board = [
|
150
|
-
["r", "n", "k", "n", "r"],
|
151
|
-
["", "", "", "", ""],
|
152
|
-
["P", "P", "P", "P", "P"]
|
150
|
+
["r", "n", "k", "n", "r"], # Back rank
|
151
|
+
["", "", "", "", ""], # Empty rank
|
152
|
+
["P", "P", "P", "P", "P"] # Front rank
|
153
153
|
]
|
154
154
|
|
155
155
|
feen = Feen.dump(
|
@@ -214,9 +214,9 @@ Checks if a string is valid, canonical FEEN notation.
|
|
214
214
|
**Example:**
|
215
215
|
|
216
216
|
```ruby
|
217
|
-
Feen.valid?("k/K / GAME/game")
|
218
|
-
Feen.valid?("invalid")
|
219
|
-
Feen.valid?("k/K P3K/ GAME/game")
|
217
|
+
Feen.valid?("k/K / GAME/game") # => true
|
218
|
+
Feen.valid?("invalid") # => false
|
219
|
+
Feen.valid?("k/K P3K/ GAME/game") # => false (wrong piece order)
|
220
220
|
```
|
221
221
|
|
222
222
|
## Working with Different Board Sizes
|
@@ -313,8 +313,8 @@ For games that need special piece states, use modifiers **only on the board**:
|
|
313
313
|
|
314
314
|
```ruby
|
315
315
|
board = [
|
316
|
-
["+P", "K", "-R"],
|
317
|
-
["N'", "", "B"]
|
316
|
+
["+P", "K", "-R"], # Enhanced pawn, King, diminished rook
|
317
|
+
["N'", "", "B"] # Knight with special state, empty, Bishop
|
318
318
|
]
|
319
319
|
|
320
320
|
# Note: Modifiers (+, -, ') are ONLY allowed on the board
|
@@ -335,9 +335,9 @@ FEEN can represent positions mixing different game systems:
|
|
335
335
|
```ruby
|
336
336
|
# FOO pieces vs bar pieces
|
337
337
|
mixed_feen = Feen.dump(
|
338
|
-
piece_placement: ["K", "G", "k", "r"],
|
339
|
-
pieces_in_hand: ["P", "g"],
|
340
|
-
games_turn: ["bar", "FOO"]
|
338
|
+
piece_placement: ["K", "G", "k", "r"], # Mixed piece types
|
339
|
+
pieces_in_hand: ["P", "g"], # Captured from both sides
|
340
|
+
games_turn: ["bar", "FOO"] # Different game systems
|
341
341
|
)
|
342
342
|
|
343
343
|
mixed_feen # => "KGkr P/g bar/FOO"
|
@@ -350,16 +350,16 @@ mixed_feen # => "KGkr P/g bar/FOO"
|
|
350
350
|
```ruby
|
351
351
|
# ERROR: Wrong argument types
|
352
352
|
Feen.dump(
|
353
|
-
piece_placement: "not an array",
|
354
|
-
pieces_in_hand: "not an array",
|
355
|
-
games_turn: "not an array"
|
353
|
+
piece_placement: "not an array", # Must be Array
|
354
|
+
pieces_in_hand: "not an array", # Must be Array
|
355
|
+
games_turn: "not an array" # Must be Array[2]
|
356
356
|
)
|
357
357
|
# => ArgumentError
|
358
358
|
|
359
359
|
# ERROR: Modifiers in captured pieces
|
360
360
|
Feen.dump(
|
361
361
|
piece_placement: [["K"]],
|
362
|
-
pieces_in_hand: ["+P"],
|
362
|
+
pieces_in_hand: ["+P"], # Invalid: no modifiers allowed
|
363
363
|
games_turn: ["GAME", "game"]
|
364
364
|
)
|
365
365
|
# => ArgumentError
|
@@ -368,7 +368,7 @@ Feen.dump(
|
|
368
368
|
Feen.dump(
|
369
369
|
piece_placement: [["K"]],
|
370
370
|
pieces_in_hand: [],
|
371
|
-
games_turn: ["GAME", "ALSO"]
|
371
|
+
games_turn: ["GAME", "ALSO"] # Must be different cases
|
372
372
|
)
|
373
373
|
# => ArgumentError
|
374
374
|
```
|
@@ -450,7 +450,7 @@ end
|
|
450
450
|
db = PositionDatabase.new
|
451
451
|
db.store_position("start", [["r", "k", "r"]], [], ["GAME", "game"])
|
452
452
|
position = db.retrieve_position("start")
|
453
|
-
# => {piece_placement: [["r", "k", "r"]], pieces_in_hand: [], games_turn: ["GAME", "game"]}
|
453
|
+
# => { piece_placement: [["r", "k", "r"]], pieces_in_hand: [], games_turn: ["GAME", "game"] }
|
454
454
|
```
|
455
455
|
|
456
456
|
## Best Practices
|
@@ -25,7 +25,7 @@ module Feen
|
|
25
25
|
# # => "CHESS/chess"
|
26
26
|
#
|
27
27
|
# @example Invalid - same casing
|
28
|
-
# GamesTurn.dump("CHESS", "
|
28
|
+
# GamesTurn.dump("CHESS", "MAKRUK")
|
29
29
|
# # => ArgumentError: One variant must be uppercase and the other lowercase
|
30
30
|
def self.dump(active_variant, inactive_variant)
|
31
31
|
validate_variants(active_variant, inactive_variant)
|
@@ -15,8 +15,8 @@ module Feen
|
|
15
15
|
# <games-turn> ::= <game-id-uppercase> "/" <game-id-lowercase>
|
16
16
|
# | <game-id-lowercase> "/" <game-id-uppercase>
|
17
17
|
VALID_GAMES_TURN_PATTERN = %r{
|
18
|
-
\A
|
19
|
-
(?:
|
18
|
+
\A # Start of string
|
19
|
+
(?: # Non-capturing group for alternatives
|
20
20
|
(?<uppercase_first>[A-Z]+) # Named group: uppercase identifier first
|
21
21
|
/ # Separator
|
22
22
|
(?<lowercase_second>[a-z]+) # Named group: lowercase identifier second
|
@@ -25,7 +25,7 @@ module Feen
|
|
25
25
|
/ # Separator
|
26
26
|
(?<uppercase_second>[A-Z]+) # Named group: uppercase identifier second
|
27
27
|
)
|
28
|
-
\z
|
28
|
+
\z # End of string
|
29
29
|
}x
|
30
30
|
|
31
31
|
# Parses the games turn section of a FEEN string
|
@@ -35,12 +35,12 @@ module Feen
|
|
35
35
|
# @raise [ArgumentError] If the input string is invalid
|
36
36
|
#
|
37
37
|
# @example Valid games turn string with uppercase first
|
38
|
-
# GamesTurn.parse("CHESS/
|
39
|
-
# # => ["CHESS", "
|
38
|
+
# GamesTurn.parse("CHESS/ogi")
|
39
|
+
# # => ["CHESS", "ogi"]
|
40
40
|
#
|
41
41
|
# @example Valid games turn string with lowercase first
|
42
|
-
# GamesTurn.parse("chess/
|
43
|
-
# # => ["chess", "
|
42
|
+
# GamesTurn.parse("chess/OGI")
|
43
|
+
# # => ["chess", "OGI"]
|
44
44
|
def self.parse(games_turn_str)
|
45
45
|
validate_input_type(games_turn_str)
|
46
46
|
|
@@ -3,6 +3,14 @@
|
|
3
3
|
module Feen
|
4
4
|
module Parser
|
5
5
|
# Handles parsing of the piece placement section of a FEEN string
|
6
|
+
#
|
7
|
+
# This module is responsible for converting the first field of a FEEN string
|
8
|
+
# (piece placement) into a hierarchical array structure representing the board.
|
9
|
+
# It supports arbitrary dimensions and handles both pieces (with optional PNN modifiers)
|
10
|
+
# and empty squares (represented by numbers).
|
11
|
+
#
|
12
|
+
# @see https://sashite.dev/documents/feen/1.0.0/ FEEN Specification
|
13
|
+
# @see https://sashite.dev/documents/pnn/1.0.0/ PNN Specification
|
6
14
|
module PiecePlacement
|
7
15
|
# Simplified error messages
|
8
16
|
ERRORS = {
|
@@ -16,11 +24,78 @@ module Feen
|
|
16
24
|
|
17
25
|
# Parses the piece placement section of a FEEN string
|
18
26
|
#
|
27
|
+
# Converts a FEEN piece placement string into a hierarchical array structure
|
28
|
+
# representing the board where empty squares are represented by empty strings
|
29
|
+
# and pieces are represented by strings containing their PNN identifier and
|
30
|
+
# optional modifiers.
|
31
|
+
#
|
19
32
|
# @param piece_placement_str [String] FEEN piece placement string
|
20
33
|
# @return [Array] Hierarchical array structure representing the board where:
|
21
34
|
# - Empty squares are represented by empty strings ("")
|
22
35
|
# - Pieces are represented by strings containing their identifier and optional modifiers
|
23
36
|
# @raise [ArgumentError] If the input string is invalid
|
37
|
+
#
|
38
|
+
# @example Parse a simple 2D chess position (initial position)
|
39
|
+
# PiecePlacement.parse("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR")
|
40
|
+
# # => [
|
41
|
+
# # ["r", "n", "b", "q", "k", "b", "n", "r"],
|
42
|
+
# # ["p", "p", "p", "p", "p", "p", "p", "p"],
|
43
|
+
# # ["", "", "", "", "", "", "", ""],
|
44
|
+
# # ["", "", "", "", "", "", "", ""],
|
45
|
+
# # ["", "", "", "", "", "", "", ""],
|
46
|
+
# # ["", "", "", "", "", "", "", ""],
|
47
|
+
# # ["P", "P", "P", "P", "P", "P", "P", "P"],
|
48
|
+
# # ["R", "N", "B", "Q", "K", "B", "N", "R"]
|
49
|
+
# # ]
|
50
|
+
#
|
51
|
+
# @example Parse a single rank with mixed pieces and empty squares
|
52
|
+
# PiecePlacement.parse("r2k1r")
|
53
|
+
# # => ["r", "", "", "k", "", "r"]
|
54
|
+
#
|
55
|
+
# @example Parse pieces with PNN modifiers (promoted pieces in Shogi)
|
56
|
+
# PiecePlacement.parse("+P+Bk")
|
57
|
+
# # => ["+P", "+B", "k"]
|
58
|
+
#
|
59
|
+
# @example Parse a 3D board structure (2 planes of 2x2)
|
60
|
+
# PiecePlacement.parse("rn/pp//RN/PP")
|
61
|
+
# # => [
|
62
|
+
# # [["r", "n"], ["p", "p"]],
|
63
|
+
# # [["R", "N"], ["P", "P"]]
|
64
|
+
# # ]
|
65
|
+
#
|
66
|
+
# @example Parse complex Shogi position with promoted pieces
|
67
|
+
# PiecePlacement.parse("9/9/9/9/4+P4/9/5+B3/9/9")
|
68
|
+
# # => [
|
69
|
+
# # ["", "", "", "", "", "", "", "", ""],
|
70
|
+
# # ["", "", "", "", "", "", "", "", ""],
|
71
|
+
# # ["", "", "", "", "", "", "", "", ""],
|
72
|
+
# # ["", "", "", "", "", "", "", "", ""],
|
73
|
+
# # ["", "", "", "", "+P", "", "", "", ""],
|
74
|
+
# # ["", "", "", "", "", "", "", "", ""],
|
75
|
+
# # ["", "", "", "", "", "+B", "", "", ""],
|
76
|
+
# # ["", "", "", "", "", "", "", "", ""],
|
77
|
+
# # ["", "", "", "", "", "", "", "", ""]
|
78
|
+
# # ]
|
79
|
+
#
|
80
|
+
# @example Parse irregular board shapes (different rank sizes)
|
81
|
+
# PiecePlacement.parse("rnbqkbnr/ppppppp/8")
|
82
|
+
# # => [
|
83
|
+
# # ["r", "n", "b", "q", "k", "b", "n", "r"], # 8 cells
|
84
|
+
# # ["p", "p", "p", "p", "p", "p", "p"], # 7 cells
|
85
|
+
# # ["", "", "", "", "", "", "", ""] # 8 cells
|
86
|
+
# # ]
|
87
|
+
#
|
88
|
+
# @example Parse large numbers of empty squares
|
89
|
+
# PiecePlacement.parse("15")
|
90
|
+
# # => ["", "", "", "", "", "", "", "", "", "", "", "", "", "", ""]
|
91
|
+
#
|
92
|
+
# @example Parse pieces with all PNN modifier types
|
93
|
+
# PiecePlacement.parse("+P'-R'k")
|
94
|
+
# # => ["+P'", "-R'", "k"]
|
95
|
+
# # Where:
|
96
|
+
# # - "+P'" = enhanced state with intermediate suffix
|
97
|
+
# # - "-R'" = diminished state with intermediate suffix
|
98
|
+
# # - "k" = base piece without modifiers
|
24
99
|
def self.parse(piece_placement_str)
|
25
100
|
validate_input(piece_placement_str)
|
26
101
|
parse_structure(piece_placement_str)
|
@@ -28,9 +103,24 @@ module Feen
|
|
28
103
|
|
29
104
|
# Validates the input string for basic requirements
|
30
105
|
#
|
106
|
+
# Ensures the input is a non-empty string containing only valid FEEN characters.
|
107
|
+
# Valid characters include: letters (a-z, A-Z), digits (0-9), and modifiers (+, -, ').
|
108
|
+
#
|
31
109
|
# @param str [String] Input string to validate
|
32
110
|
# @raise [ArgumentError] If the string is invalid
|
33
111
|
# @return [void]
|
112
|
+
#
|
113
|
+
# @example Valid input
|
114
|
+
# validate_input("rnbqkbnr/pppppppp/8/8")
|
115
|
+
# # => (no error)
|
116
|
+
#
|
117
|
+
# @example Invalid input (empty string)
|
118
|
+
# validate_input("")
|
119
|
+
# # => ArgumentError: Piece placement string cannot be empty
|
120
|
+
#
|
121
|
+
# @example Invalid input (wrong type)
|
122
|
+
# validate_input(123)
|
123
|
+
# # => ArgumentError: Piece placement must be a string, got Integer
|
34
124
|
def self.validate_input(str)
|
35
125
|
raise ArgumentError, format(ERRORS[:invalid_type], str.class) unless str.is_a?(String)
|
36
126
|
raise ArgumentError, ERRORS[:empty_string] if str.empty?
|
@@ -43,9 +133,25 @@ module Feen
|
|
43
133
|
|
44
134
|
# Parses the structure recursively
|
45
135
|
#
|
136
|
+
# Determines the dimensionality of the board by analyzing separator patterns
|
137
|
+
# and recursively parses nested structures. Uses the longest separator sequence
|
138
|
+
# to determine the highest dimension level.
|
139
|
+
#
|
46
140
|
# @param str [String] FEEN piece placement string
|
47
|
-
# @return [Array] Parsed structure
|
48
|
-
|
141
|
+
# @return [Array] Parsed structure (1D array for ranks, nested arrays for higher dimensions)
|
142
|
+
#
|
143
|
+
# @example 1D structure (single rank)
|
144
|
+
# parse_structure("rnbq")
|
145
|
+
# # => ["r", "n", "b", "q"]
|
146
|
+
#
|
147
|
+
# @example 2D structure (multiple ranks)
|
148
|
+
# parse_structure("rn/pq")
|
149
|
+
# # => [["r", "n"], ["p", "q"]]
|
150
|
+
#
|
151
|
+
# @example 3D structure (multiple planes)
|
152
|
+
# parse_structure("r/p//R/P")
|
153
|
+
# # => [[["r"], ["p"]], [["R"], ["P"]]]
|
154
|
+
private_class_method def self.parse_structure(str)
|
49
155
|
# Handle trailing separators
|
50
156
|
raise ArgumentError, ERRORS[:invalid_format] if str.end_with?(DIMENSION_SEPARATOR)
|
51
157
|
|
@@ -64,10 +170,22 @@ module Feen
|
|
64
170
|
|
65
171
|
# Splits string by separator while preserving shorter separators
|
66
172
|
#
|
173
|
+
# Intelligently splits a string by a specific separator pattern while
|
174
|
+
# ensuring that shorter separator patterns within the string are preserved
|
175
|
+
# for recursive parsing of nested dimensions.
|
176
|
+
#
|
67
177
|
# @param str [String] String to split
|
68
|
-
# @param separator [String] Separator to split by
|
69
|
-
# @return [Array<String>] Split parts
|
70
|
-
|
178
|
+
# @param separator [String] Separator to split by (e.g., "/", "//", "///")
|
179
|
+
# @return [Array<String>] Split parts, with empty parts removed
|
180
|
+
#
|
181
|
+
# @example Split by single separator
|
182
|
+
# smart_split("a/b/c", "/")
|
183
|
+
# # => ["a", "b", "c"]
|
184
|
+
#
|
185
|
+
# @example Split by double separator, preserving single separators
|
186
|
+
# smart_split("a/b//c/d", "//")
|
187
|
+
# # => ["a/b", "c/d"]
|
188
|
+
private_class_method def self.smart_split(str, separator)
|
71
189
|
return [str] unless str.include?(separator)
|
72
190
|
|
73
191
|
parts = str.split(separator)
|
@@ -76,9 +194,29 @@ module Feen
|
|
76
194
|
|
77
195
|
# Parses a rank (sequence of cells)
|
78
196
|
#
|
197
|
+
# Processes a 1D sequence of cells, expanding numeric values to empty squares
|
198
|
+
# and extracting pieces with their PNN modifiers. Numbers represent consecutive
|
199
|
+
# empty squares, while letters (with optional modifiers) represent pieces.
|
200
|
+
#
|
79
201
|
# @param str [String] FEEN rank string
|
80
|
-
# @return [Array] Array of cells
|
81
|
-
|
202
|
+
# @return [Array<String>] Array of cells (empty strings for empty squares, piece strings for pieces)
|
203
|
+
#
|
204
|
+
# @example Simple pieces
|
205
|
+
# parse_rank("rnbq")
|
206
|
+
# # => ["r", "n", "b", "q"]
|
207
|
+
#
|
208
|
+
# @example Mixed pieces and empty squares
|
209
|
+
# parse_rank("r2k1r")
|
210
|
+
# # => ["r", "", "", "k", "", "r"]
|
211
|
+
#
|
212
|
+
# @example All empty squares
|
213
|
+
# parse_rank("8")
|
214
|
+
# # => ["", "", "", "", "", "", "", ""]
|
215
|
+
#
|
216
|
+
# @example Pieces with modifiers
|
217
|
+
# parse_rank("+P-R'")
|
218
|
+
# # => ["+P", "-R'"]
|
219
|
+
private_class_method def self.parse_rank(str)
|
82
220
|
return [] if str.empty?
|
83
221
|
|
84
222
|
cells = []
|
@@ -114,10 +252,32 @@ module Feen
|
|
114
252
|
|
115
253
|
# Extracts a piece starting at given position
|
116
254
|
#
|
255
|
+
# Parses a piece identifier with optional PNN modifiers starting at the specified
|
256
|
+
# position in the string. Handles prefix modifiers (+, -), the required letter,
|
257
|
+
# and suffix modifiers (').
|
258
|
+
#
|
117
259
|
# @param str [String] String to parse
|
118
|
-
# @param start_index [Integer] Starting position
|
260
|
+
# @param start_index [Integer] Starting position in the string
|
119
261
|
# @return [Hash] Hash with :piece and :next_index keys
|
120
|
-
|
262
|
+
# - :piece [String] The complete piece identifier with modifiers
|
263
|
+
# - :next_index [Integer] Position after the piece in the string
|
264
|
+
#
|
265
|
+
# @example Extract simple piece
|
266
|
+
# extract_piece("Kqr", 0)
|
267
|
+
# # => { piece: "K", next_index: 1 }
|
268
|
+
#
|
269
|
+
# @example Extract piece with prefix modifier
|
270
|
+
# extract_piece("+Pqr", 0)
|
271
|
+
# # => { piece: "+P", next_index: 2 }
|
272
|
+
#
|
273
|
+
# @example Extract piece with suffix modifier
|
274
|
+
# extract_piece("K'qr", 0)
|
275
|
+
# # => { piece: "K'", next_index: 2 }
|
276
|
+
#
|
277
|
+
# @example Extract piece with both prefix and suffix modifiers
|
278
|
+
# extract_piece("+P'qr", 0)
|
279
|
+
# # => { piece: "+P'", next_index: 3 }
|
280
|
+
private_class_method def self.extract_piece(str, start_index)
|
121
281
|
piece = ""
|
122
282
|
i = start_index
|
123
283
|
|
@@ -9,10 +9,11 @@ module Feen
|
|
9
9
|
module PiecesInHand
|
10
10
|
# Error messages for validation
|
11
11
|
Errors = {
|
12
|
-
invalid_type:
|
13
|
-
empty_string:
|
14
|
-
invalid_format:
|
15
|
-
missing_separator:
|
12
|
+
invalid_type: "Pieces in hand must be a string, got %s",
|
13
|
+
empty_string: "Pieces in hand string cannot be empty",
|
14
|
+
invalid_format: "Invalid pieces in hand format: %s",
|
15
|
+
missing_separator: "Pieces in hand format must contain exactly one '/' separator. Got: %s",
|
16
|
+
modifiers_not_allowed: 'Pieces in hand cannot contain modifiers: "%s"'
|
16
17
|
}.freeze
|
17
18
|
|
18
19
|
# Base piece pattern: single letter only (no modifiers allowed in hand)
|
@@ -22,16 +23,16 @@ module Feen
|
|
22
23
|
VALID_COUNT_PATTERN = /\A(?:[2-9]|[1-9]\d+)\z/
|
23
24
|
|
24
25
|
# Pattern for piece with optional count in pieces in hand
|
25
|
-
PIECE_WITH_COUNT_PATTERN = /(?:([2-9]|[1-9]\d+))?([a-zA-Z])/
|
26
|
+
PIECE_WITH_COUNT_PATTERN = /(?:([2-9]|[1-9]\d+))?([-+]?[a-zA-Z]'?)/
|
26
27
|
|
27
28
|
# Complete validation pattern for pieces in hand string
|
28
29
|
VALID_FORMAT_PATTERN = %r{\A
|
29
|
-
(?:
|
30
|
-
(?:(?:[2-9]|[1-9]\d+)?[A-Z])*
|
30
|
+
(?: # Uppercase section (optional)
|
31
|
+
(?:(?:[2-9]|[1-9]\d+)?[-+]?[A-Z]'?)* # Zero or more uppercase pieces with optional counts and modifiers
|
31
32
|
)
|
32
|
-
/
|
33
|
-
(?:
|
34
|
-
(?:(?:[2-9]|[1-9]\d+)?[a-z])*
|
33
|
+
/ # Mandatory separator
|
34
|
+
(?: # Lowercase section (optional)
|
35
|
+
(?:(?:[2-9]|[1-9]\d+)?[-+]?[a-z]'?)* # Zero or more lowercase pieces with optional counts and modifiers
|
35
36
|
)
|
36
37
|
\z}x
|
37
38
|
|
@@ -94,7 +95,7 @@ module Feen
|
|
94
95
|
parts_count = str.count("/")
|
95
96
|
raise ::ArgumentError, format(Errors[:missing_separator], parts_count) unless parts_count == 1
|
96
97
|
|
97
|
-
# Must match the overall pattern
|
98
|
+
# Must match the overall pattern (including potential modifiers for detection)
|
98
99
|
raise ::ArgumentError, format(Errors[:invalid_format], str) unless str.match?(VALID_FORMAT_PATTERN)
|
99
100
|
|
100
101
|
# Additional validation: check for any modifiers (forbidden in hand)
|
@@ -111,9 +112,9 @@ module Feen
|
|
111
112
|
return unless str.match?(/[+\-']/)
|
112
113
|
|
113
114
|
# Find the specific invalid piece to provide a better error message
|
114
|
-
invalid_pieces = str.scan(/(?:[2-9]|[1-9]\d+)?[
|
115
|
+
invalid_pieces = str.scan(/(?:[2-9]|[1-9]\d+)?[-+]?[a-zA-Z]'?/).grep(/[+\-']/)
|
115
116
|
|
116
|
-
raise ::ArgumentError,
|
117
|
+
raise ::ArgumentError, format(Errors[:modifiers_not_allowed], invalid_pieces.first)
|
117
118
|
end
|
118
119
|
|
119
120
|
# Parses a specific section (uppercase or lowercase) and returns expanded pieces
|
@@ -145,12 +146,15 @@ module Feen
|
|
145
146
|
match = section[position..].match(PIECE_WITH_COUNT_PATTERN)
|
146
147
|
break unless match
|
147
148
|
|
148
|
-
count_str,
|
149
|
+
count_str, piece_with_modifiers = match.captures
|
149
150
|
count = count_str ? count_str.to_i : 1
|
150
151
|
|
152
|
+
# Extract just the base piece (remove any modifiers)
|
153
|
+
base_piece = extract_base_piece(piece_with_modifiers)
|
154
|
+
|
151
155
|
# Validate piece is base form only (single letter)
|
152
|
-
unless
|
153
|
-
raise ::ArgumentError, "Pieces in hand must be base form only: '#{
|
156
|
+
unless base_piece.match?(BASE_PIECE_PATTERN)
|
157
|
+
raise ::ArgumentError, "Pieces in hand must be base form only: '#{base_piece}'"
|
154
158
|
end
|
155
159
|
|
156
160
|
# Validate count format
|
@@ -159,14 +163,14 @@ module Feen
|
|
159
163
|
end
|
160
164
|
|
161
165
|
# Validate that the piece matches the expected case
|
162
|
-
piece_case =
|
166
|
+
piece_case = base_piece.match?(/[A-Z]/) ? :uppercase : :lowercase
|
163
167
|
unless piece_case == case_type
|
164
168
|
case_name = case_type == :uppercase ? "uppercase" : "lowercase"
|
165
|
-
raise ::ArgumentError, "Piece '#{
|
169
|
+
raise ::ArgumentError, "Piece '#{base_piece}' has wrong case for #{case_name} section"
|
166
170
|
end
|
167
171
|
|
168
172
|
# Add to our result with piece type and count
|
169
|
-
result << { piece:
|
173
|
+
result << { piece: base_piece, count: count }
|
170
174
|
|
171
175
|
# Move position forward
|
172
176
|
position += match[0].length
|
@@ -175,6 +179,18 @@ module Feen
|
|
175
179
|
result
|
176
180
|
end
|
177
181
|
|
182
|
+
# Extracts the base piece from a piece string that may contain modifiers
|
183
|
+
#
|
184
|
+
# @param piece_str [String] Piece string potentially with modifiers
|
185
|
+
# @return [String] Base piece without modifiers
|
186
|
+
private_class_method def self.extract_base_piece(piece_str)
|
187
|
+
# Remove prefix modifiers (+ or -)
|
188
|
+
without_prefix = piece_str.gsub(/^[-+]/, "")
|
189
|
+
|
190
|
+
# Remove suffix modifiers (')
|
191
|
+
without_prefix.gsub(/'$/, "")
|
192
|
+
end
|
193
|
+
|
178
194
|
# Expands the pieces based on their counts into an array.
|
179
195
|
#
|
180
196
|
# @param pieces_with_counts [Array<Hash>] Array of pieces with their counts
|
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.
|
4
|
+
version: 5.0.0.beta9
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Cyril Kato
|
@@ -39,6 +39,8 @@ metadata:
|
|
39
39
|
source_code_uri: https://github.com/sashite/feen.rb
|
40
40
|
specification_uri: https://sashite.dev/documents/feen/1.0.0/
|
41
41
|
rubygems_mfa_required: 'true'
|
42
|
+
keywords: board, board-games, chess, deserialization, feen, fen, game, makruk, notation,
|
43
|
+
serialization, shogi, xiangqi"
|
42
44
|
rdoc_options: []
|
43
45
|
require_paths:
|
44
46
|
- lib
|