feen 5.0.0.beta7 → 5.0.0.beta8
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 +381 -245
- data/lib/feen/dumper/games_turn.rb +32 -29
- data/lib/feen/dumper/piece_placement.rb +85 -63
- data/lib/feen/dumper/pieces_in_hand.rb +32 -54
- data/lib/feen/dumper.rb +2 -2
- data/lib/feen/parser/games_turn.rb +36 -16
- data/lib/feen/parser/piece_placement.rb +78 -564
- data/lib/feen/parser/pieces_in_hand.rb +75 -101
- data/lib/feen/parser.rb +3 -3
- data/lib/feen.rb +20 -9
- metadata +1 -6
- data/lib/feen/dumper/pieces_in_hand/errors.rb +0 -12
- data/lib/feen/parser/games_turn/errors.rb +0 -14
- data/lib/feen/parser/games_turn/valid_games_turn_pattern.rb +0 -24
- data/lib/feen/parser/pieces_in_hand/errors.rb +0 -20
- data/lib/feen/parser/pieces_in_hand/pnn_patterns.rb +0 -89
@@ -1,22 +1,47 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative File.join("pieces_in_hand", "errors")
|
4
|
-
require_relative File.join("pieces_in_hand", "pnn_patterns")
|
5
|
-
|
6
3
|
module Feen
|
7
4
|
module Parser
|
8
5
|
# Handles parsing of the pieces in hand section of a FEEN string.
|
9
6
|
# Pieces in hand represent pieces available for dropping onto the board.
|
10
|
-
#
|
7
|
+
# According to FEEN specification, pieces in hand MUST be in base form only (no modifiers).
|
11
8
|
# Format: "UPPERCASE_PIECES/LOWERCASE_PIECES"
|
12
9
|
module PiecesInHand
|
10
|
+
# Error messages for validation
|
11
|
+
Errors = {
|
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
|
+
}.freeze
|
17
|
+
|
18
|
+
# Base piece pattern: single letter only (no modifiers allowed in hand)
|
19
|
+
BASE_PIECE_PATTERN = /\A[a-zA-Z]\z/
|
20
|
+
|
21
|
+
# Valid count pattern: 2-9 or any number with 2+ digits (no 0, 1, or leading zeros)
|
22
|
+
VALID_COUNT_PATTERN = /\A(?:[2-9]|[1-9]\d+)\z/
|
23
|
+
|
24
|
+
# Pattern for piece with optional count in pieces in hand
|
25
|
+
PIECE_WITH_COUNT_PATTERN = /(?:([2-9]|[1-9]\d+))?([a-zA-Z])/
|
26
|
+
|
27
|
+
# Complete validation pattern for pieces in hand string
|
28
|
+
VALID_FORMAT_PATTERN = %r{\A
|
29
|
+
(?: # Uppercase section (optional)
|
30
|
+
(?:(?:[2-9]|[1-9]\d+)?[A-Z])* # Zero or more uppercase pieces with optional counts
|
31
|
+
)
|
32
|
+
/ # Mandatory separator
|
33
|
+
(?: # Lowercase section (optional)
|
34
|
+
(?:(?:[2-9]|[1-9]\d+)?[a-z])* # Zero or more lowercase pieces with optional counts
|
35
|
+
)
|
36
|
+
\z}x
|
37
|
+
|
13
38
|
# Parses the pieces in hand section of a FEEN string.
|
14
39
|
#
|
15
40
|
# @param pieces_in_hand_str [String] FEEN pieces in hand string in format "UPPERCASE/lowercase"
|
16
|
-
# @return [Array<String>] Array of piece identifiers in
|
41
|
+
# @return [Array<String>] Array of piece identifiers in base form only,
|
17
42
|
# expanded based on their counts and sorted alphabetically.
|
18
43
|
# Empty array if no pieces are in hand.
|
19
|
-
# @raise [ArgumentError] If the input string is invalid
|
44
|
+
# @raise [ArgumentError] If the input string is invalid or contains modifiers
|
20
45
|
#
|
21
46
|
# @example Parse no pieces in hand
|
22
47
|
# PiecesInHand.parse("/")
|
@@ -26,13 +51,10 @@ module Feen
|
|
26
51
|
# PiecesInHand.parse("3P2B/p")
|
27
52
|
# # => ["B", "B", "P", "P", "P", "p"]
|
28
53
|
#
|
29
|
-
# @example
|
30
|
-
# PiecesInHand.parse("
|
31
|
-
# # =>
|
32
|
-
# # "P", "P", "P", "P", "P", "P", "P", "P", "P", "P",
|
33
|
-
# # "b", "p'", "p'", "q"]
|
54
|
+
# @example Invalid - modifiers not allowed in hand
|
55
|
+
# PiecesInHand.parse("+P/p")
|
56
|
+
# # => ArgumentError: Pieces in hand cannot contain modifiers: '+P'
|
34
57
|
def self.parse(pieces_in_hand_str)
|
35
|
-
# Validate input
|
36
58
|
validate_input_type(pieces_in_hand_str)
|
37
59
|
validate_format(pieces_in_hand_str)
|
38
60
|
|
@@ -42,7 +64,7 @@ module Feen
|
|
42
64
|
# Split by the separator to get uppercase and lowercase sections
|
43
65
|
uppercase_section, lowercase_section = pieces_in_hand_str.split("/", 2)
|
44
66
|
|
45
|
-
# Parse each section separately
|
67
|
+
# Parse each section separately and validate no modifiers
|
46
68
|
uppercase_pieces = parse_pieces_section(uppercase_section || "", :uppercase)
|
47
69
|
lowercase_pieces = parse_pieces_section(lowercase_section || "", :lowercase)
|
48
70
|
|
@@ -62,103 +84,36 @@ module Feen
|
|
62
84
|
end
|
63
85
|
|
64
86
|
# Validates that the input string matches the expected format according to FEEN specification.
|
65
|
-
# Format must be: "UPPERCASE_PIECES/LOWERCASE_PIECES"
|
87
|
+
# Format must be: "UPPERCASE_PIECES/LOWERCASE_PIECES" with base pieces only (no modifiers).
|
66
88
|
#
|
67
89
|
# @param str [String] Input string to validate
|
68
|
-
# @raise [ArgumentError] If format is invalid
|
90
|
+
# @raise [ArgumentError] If format is invalid or contains modifiers
|
69
91
|
# @return [void]
|
70
92
|
private_class_method def self.validate_format(str)
|
71
93
|
# Must contain exactly one "/" separator
|
72
94
|
parts_count = str.count("/")
|
73
|
-
raise ::ArgumentError, format(Errors[:
|
95
|
+
raise ::ArgumentError, format(Errors[:missing_separator], parts_count) unless parts_count == 1
|
74
96
|
|
75
|
-
|
97
|
+
# Must match the overall pattern
|
98
|
+
raise ::ArgumentError, format(Errors[:invalid_format], str) unless str.match?(VALID_FORMAT_PATTERN)
|
76
99
|
|
77
|
-
#
|
78
|
-
|
79
|
-
validate_section_format(lowercase_section, :lowercase) unless lowercase_section.empty?
|
100
|
+
# Additional validation: check for any modifiers (forbidden in hand)
|
101
|
+
validate_no_modifiers(str)
|
80
102
|
end
|
81
103
|
|
82
|
-
# Validates
|
104
|
+
# Validates that no modifiers are present in the pieces in hand string
|
83
105
|
#
|
84
|
-
# @param
|
85
|
-
# @
|
86
|
-
# @raise [ArgumentError] If the section format is invalid
|
87
|
-
# @return [void]
|
88
|
-
private_class_method def self.validate_section_format(section, case_type)
|
89
|
-
return if section.empty?
|
90
|
-
|
91
|
-
# Build the appropriate pattern based on case type
|
92
|
-
case_pattern = case case_type
|
93
|
-
when :uppercase
|
94
|
-
PnnPatterns::UPPERCASE_SECTION_PATTERN
|
95
|
-
when :lowercase
|
96
|
-
PnnPatterns::LOWERCASE_SECTION_PATTERN
|
97
|
-
else
|
98
|
-
raise ArgumentError, "Invalid case type: #{case_type}"
|
99
|
-
end
|
100
|
-
|
101
|
-
# Validate overall section pattern
|
102
|
-
raise ::ArgumentError, format(Errors[:invalid_format], section) unless section.match?(case_pattern)
|
103
|
-
|
104
|
-
# Validate individual pieces in the section
|
105
|
-
validate_individual_pieces_in_section(section, case_type)
|
106
|
-
end
|
107
|
-
|
108
|
-
# Validates each individual piece in a section for PNN compliance
|
109
|
-
#
|
110
|
-
# @param section [String] FEEN pieces section string
|
111
|
-
# @param case_type [Symbol] Either :uppercase or :lowercase
|
112
|
-
# @raise [ArgumentError] If any piece is invalid PNN format
|
106
|
+
# @param str [String] Input string to validate
|
107
|
+
# @raise [ArgumentError] If modifiers are found
|
113
108
|
# @return [void]
|
114
|
-
private_class_method def self.
|
115
|
-
|
116
|
-
|
117
|
-
while position < section.length
|
118
|
-
match = section[position..].match(PnnPatterns::PIECE_WITH_COUNT_PATTERN)
|
119
|
-
|
120
|
-
unless match
|
121
|
-
remaining = section[position..]
|
122
|
-
raise ::ArgumentError, format(Errors[:invalid_format], remaining)
|
123
|
-
end
|
124
|
-
|
125
|
-
count_str, piece = match.captures
|
126
|
-
|
127
|
-
# Skip empty matches (shouldn't happen with our pattern, but safety check)
|
128
|
-
if piece.nil? || piece.empty?
|
129
|
-
position += 1
|
130
|
-
next
|
131
|
-
end
|
132
|
-
|
133
|
-
# Validate the piece follows PNN specification
|
134
|
-
unless piece.match?(PnnPatterns::PNN_PIECE_PATTERN)
|
135
|
-
raise ::ArgumentError, format(Errors[:invalid_pnn_piece], piece)
|
136
|
-
end
|
109
|
+
private_class_method def self.validate_no_modifiers(str)
|
110
|
+
# Check for any modifier characters that are forbidden in pieces in hand
|
111
|
+
return unless str.match?(/[+\-']/)
|
137
112
|
|
138
|
-
|
139
|
-
|
140
|
-
raise ::ArgumentError, format(Errors[:invalid_count], count_str)
|
141
|
-
end
|
142
|
-
|
143
|
-
# Validate that the piece matches the expected case
|
144
|
-
piece_case = piece_is_uppercase?(piece) ? :uppercase : :lowercase
|
145
|
-
unless piece_case == case_type
|
146
|
-
case_name = case_type == :uppercase ? "uppercase" : "lowercase"
|
147
|
-
raise ::ArgumentError, "#{case_name.capitalize} section contains #{piece_case} piece: '#{piece}'"
|
148
|
-
end
|
113
|
+
# Find the specific invalid piece to provide a better error message
|
114
|
+
invalid_pieces = str.scan(/(?:[2-9]|[1-9]\d+)?[+\-']?[a-zA-Z]'?/).grep(/[+\-']/)
|
149
115
|
|
150
|
-
|
151
|
-
end
|
152
|
-
end
|
153
|
-
|
154
|
-
# Determines if a piece belongs to the uppercase group
|
155
|
-
#
|
156
|
-
# @param piece [String] Piece identifier (e.g., "P", "+P", "P'", "+P'")
|
157
|
-
# @return [Boolean] True if the piece's main letter is uppercase
|
158
|
-
private_class_method def self.piece_is_uppercase?(piece)
|
159
|
-
# Extract the main letter (skip prefixes like + or -)
|
160
|
-
main_letter = piece.gsub(/\A[+-]/, "").gsub(/'\z/, "")
|
161
|
-
main_letter.match?(/[A-Z]/)
|
116
|
+
raise ::ArgumentError, "Pieces in hand cannot contain modifiers: '#{invalid_pieces.first}'"
|
162
117
|
end
|
163
118
|
|
164
119
|
# Parses a specific section (uppercase or lowercase) and returns expanded pieces
|
@@ -166,31 +121,50 @@ module Feen
|
|
166
121
|
# @param section [String] The section string to parse
|
167
122
|
# @param case_type [Symbol] Either :uppercase or :lowercase (for validation)
|
168
123
|
# @return [Array<String>] Array of expanded pieces from this section
|
169
|
-
private_class_method def self.parse_pieces_section(section,
|
124
|
+
private_class_method def self.parse_pieces_section(section, case_type)
|
170
125
|
return [] if section.empty?
|
171
126
|
|
172
127
|
# Extract pieces with their counts
|
173
|
-
pieces_with_counts = extract_pieces_with_counts_from_section(section)
|
128
|
+
pieces_with_counts = extract_pieces_with_counts_from_section(section, case_type)
|
174
129
|
|
175
|
-
# Expand the pieces into an array
|
130
|
+
# Expand the pieces into an array
|
176
131
|
expand_pieces(pieces_with_counts)
|
177
132
|
end
|
178
133
|
|
179
134
|
# Extracts pieces with their counts from a section string.
|
180
135
|
#
|
181
136
|
# @param section [String] FEEN pieces section string
|
137
|
+
# @param case_type [Symbol] Either :uppercase or :lowercase
|
182
138
|
# @return [Array<Hash>] Array of hashes with :piece and :count keys
|
183
|
-
|
139
|
+
# @raise [ArgumentError] If pieces don't match the expected case or contain modifiers
|
140
|
+
private_class_method def self.extract_pieces_with_counts_from_section(section, case_type)
|
184
141
|
result = []
|
185
142
|
position = 0
|
186
143
|
|
187
144
|
while position < section.length
|
188
|
-
match = section[position..].match(
|
145
|
+
match = section[position..].match(PIECE_WITH_COUNT_PATTERN)
|
189
146
|
break unless match
|
190
147
|
|
191
148
|
count_str, piece = match.captures
|
192
149
|
count = count_str ? count_str.to_i : 1
|
193
150
|
|
151
|
+
# Validate piece is base form only (single letter)
|
152
|
+
unless piece.match?(BASE_PIECE_PATTERN)
|
153
|
+
raise ::ArgumentError, "Pieces in hand must be base form only: '#{piece}'"
|
154
|
+
end
|
155
|
+
|
156
|
+
# Validate count format
|
157
|
+
if count_str && !count_str.match?(VALID_COUNT_PATTERN)
|
158
|
+
raise ::ArgumentError, "Invalid count format: '#{count_str}'. Count cannot be '0' or '1', use the piece without count instead"
|
159
|
+
end
|
160
|
+
|
161
|
+
# Validate that the piece matches the expected case
|
162
|
+
piece_case = piece.match?(/[A-Z]/) ? :uppercase : :lowercase
|
163
|
+
unless piece_case == case_type
|
164
|
+
case_name = case_type == :uppercase ? "uppercase" : "lowercase"
|
165
|
+
raise ::ArgumentError, "Piece '#{piece}' has wrong case for #{case_name} section"
|
166
|
+
end
|
167
|
+
|
194
168
|
# Add to our result with piece type and count
|
195
169
|
result << { piece: piece, count: count }
|
196
170
|
|
data/lib/feen/parser.rb
CHANGED
@@ -21,12 +21,12 @@ module Feen
|
|
21
21
|
# @param feen_string [String] Complete FEEN notation string
|
22
22
|
# @return [Hash] Hash containing the parsed position data with the following keys:
|
23
23
|
# - :piece_placement [Array] - Hierarchical array structure representing the board
|
24
|
-
# - :pieces_in_hand [Array<String>] - Pieces available for dropping onto the board
|
24
|
+
# - :pieces_in_hand [Array<String>] - Pieces available for dropping onto the board (base form only)
|
25
25
|
# - :games_turn [Array<String>] - A two-element array with [active_variant, inactive_variant]
|
26
26
|
# @raise [ArgumentError] If the FEEN string is invalid
|
27
27
|
#
|
28
28
|
# @example Parsing a standard chess initial position
|
29
|
-
# feen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR
|
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: [
|
@@ -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 = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR
|
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
@@ -16,11 +16,12 @@ module Feen
|
|
16
16
|
#
|
17
17
|
# @param piece_placement [Array] Board position data structure representing the spatial
|
18
18
|
# distribution of pieces across the board
|
19
|
-
# @param pieces_in_hand [Array<String>] Pieces available for dropping onto the board
|
19
|
+
# @param pieces_in_hand [Array<String>] Pieces available for dropping onto the board.
|
20
|
+
# MUST be in base form only (no modifiers allowed)
|
20
21
|
# @param games_turn [Array<String>] A two-element array where the first element is the
|
21
22
|
# active player's variant and the second is the inactive player's variant
|
22
23
|
# @return [String] FEEN notation string
|
23
|
-
# @raise [ArgumentError] If any parameter is invalid
|
24
|
+
# @raise [ArgumentError] If any parameter is invalid or pieces_in_hand contains modifiers
|
24
25
|
# @example
|
25
26
|
# piece_placement = [
|
26
27
|
# ["r", "n", "b", "q", "k", "b", "n", "r"],
|
@@ -37,7 +38,7 @@ module Feen
|
|
37
38
|
# pieces_in_hand: [],
|
38
39
|
# games_turn: ["CHESS", "chess"]
|
39
40
|
# )
|
40
|
-
# # => "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR
|
41
|
+
# # => "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR / CHESS/chess"
|
41
42
|
def self.dump(piece_placement:, pieces_in_hand:, games_turn:)
|
42
43
|
Dumper.dump(piece_placement:, pieces_in_hand:, games_turn:)
|
43
44
|
end
|
@@ -47,11 +48,11 @@ module Feen
|
|
47
48
|
# @param feen_string [String] Complete FEEN notation string
|
48
49
|
# @return [Hash] Hash containing the parsed position data with the following keys:
|
49
50
|
# - :piece_placement [Array] - Hierarchical array structure representing the board
|
50
|
-
# - :pieces_in_hand [Array<String>] - Pieces available for dropping onto the board
|
51
|
+
# - :pieces_in_hand [Array<String>] - Pieces available for dropping onto the board (base form only)
|
51
52
|
# - :games_turn [Array<String>] - A two-element array with [active_variant, inactive_variant]
|
52
53
|
# @raise [ArgumentError] If the FEEN string is invalid
|
53
54
|
# @example
|
54
|
-
# feen_string = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR
|
55
|
+
# feen_string = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR / CHESS/chess"
|
55
56
|
# Feen.parse(feen_string)
|
56
57
|
# # => {
|
57
58
|
# # piece_placement: [
|
@@ -80,7 +81,7 @@ module Feen
|
|
80
81
|
# @return [Hash, nil] Hash containing the parsed position data or nil if parsing fails
|
81
82
|
# @example
|
82
83
|
# # Valid FEEN string
|
83
|
-
# Feen.safe_parse("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR
|
84
|
+
# Feen.safe_parse("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR / CHESS/chess")
|
84
85
|
# # => {piece_placement: [...], pieces_in_hand: [...], games_turn: [...]}
|
85
86
|
#
|
86
87
|
# # Invalid FEEN string
|
@@ -100,17 +101,27 @@ module Feen
|
|
100
101
|
# This approach guarantees that the string not only follows FEEN syntax
|
101
102
|
# but is also in its most compact, canonical representation.
|
102
103
|
#
|
104
|
+
# According to FEEN specification:
|
105
|
+
# - Pieces in hand must be in base form only (no modifiers like +, -, ')
|
106
|
+
# - Pieces in hand must be sorted canonically within each case section:
|
107
|
+
# 1. By quantity (descending)
|
108
|
+
# 2. By piece letter (alphabetically ascending)
|
109
|
+
# - Case separation is enforced (uppercase/lowercase)
|
110
|
+
#
|
103
111
|
# @param feen_string [String] FEEN string to validate
|
104
112
|
# @return [Boolean] True if the string is a valid and canonical FEEN string
|
105
113
|
# @example
|
106
114
|
# # Canonical form
|
107
|
-
# Feen.valid?("
|
115
|
+
# Feen.valid?("lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL / SHOGI/shogi") # => true
|
108
116
|
#
|
109
117
|
# # Invalid syntax
|
110
118
|
# Feen.valid?("invalid feen string") # => false
|
111
119
|
#
|
112
|
-
# # Valid syntax but non-canonical form (pieces in hand
|
113
|
-
# Feen.valid?("
|
120
|
+
# # Valid syntax but non-canonical form (pieces in hand with modifiers)
|
121
|
+
# Feen.valid?("8/8/8/8/8/8/8/8 +P/ FOO/bar") # => false
|
122
|
+
#
|
123
|
+
# # Valid syntax but non-canonical form (wrong ordering in pieces in hand)
|
124
|
+
# Feen.valid?("8/8/8/8/8/8/8/8 P3K/ FOO/bar") # => false
|
114
125
|
def self.valid?(feen_string)
|
115
126
|
# First check: Basic syntax validation
|
116
127
|
begin
|
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.beta8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Cyril Kato
|
@@ -25,15 +25,10 @@ files:
|
|
25
25
|
- lib/feen/dumper/games_turn.rb
|
26
26
|
- lib/feen/dumper/piece_placement.rb
|
27
27
|
- lib/feen/dumper/pieces_in_hand.rb
|
28
|
-
- lib/feen/dumper/pieces_in_hand/errors.rb
|
29
28
|
- lib/feen/parser.rb
|
30
29
|
- lib/feen/parser/games_turn.rb
|
31
|
-
- lib/feen/parser/games_turn/errors.rb
|
32
|
-
- lib/feen/parser/games_turn/valid_games_turn_pattern.rb
|
33
30
|
- lib/feen/parser/piece_placement.rb
|
34
31
|
- lib/feen/parser/pieces_in_hand.rb
|
35
|
-
- lib/feen/parser/pieces_in_hand/errors.rb
|
36
|
-
- lib/feen/parser/pieces_in_hand/pnn_patterns.rb
|
37
32
|
homepage: https://github.com/sashite/feen.rb
|
38
33
|
licenses:
|
39
34
|
- MIT
|
@@ -1,12 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Feen
|
4
|
-
module Dumper
|
5
|
-
module PiecesInHand
|
6
|
-
Errors = {
|
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 PNN format: '%<value>s'. Expected format: [prefix]letter[suffix] where prefix is + or -, suffix is ', and letter is a-z or A-Z"
|
9
|
-
}.freeze
|
10
|
-
end
|
11
|
-
end
|
12
|
-
end
|
@@ -1,14 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Feen
|
4
|
-
module Parser
|
5
|
-
module GamesTurn
|
6
|
-
# Error messages for games turn parsing
|
7
|
-
Errors = {
|
8
|
-
invalid_type: "Games turn must be a string, got %s",
|
9
|
-
empty_string: "Games turn string cannot be empty",
|
10
|
-
invalid_format: "Invalid games turn format. Expected format: UPPERCASE/lowercase or lowercase/UPPERCASE"
|
11
|
-
}.freeze
|
12
|
-
end
|
13
|
-
end
|
14
|
-
end
|
@@ -1,24 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Feen
|
4
|
-
module Parser
|
5
|
-
module GamesTurn
|
6
|
-
# Complete pattern matching the BNF specification with named groups
|
7
|
-
# <games-turn> ::= <game-id-uppercase> "/" <game-id-lowercase>
|
8
|
-
# | <game-id-lowercase> "/" <game-id-uppercase>
|
9
|
-
ValidGamesTurnPattern = %r{
|
10
|
-
\A # Start of string
|
11
|
-
(?: # Non-capturing group for alternatives
|
12
|
-
(?<uppercase_first>[A-Z]+) # Named group: uppercase identifier first
|
13
|
-
/ # Separator
|
14
|
-
(?<lowercase_second>[a-z]+) # Named group: lowercase identifier second
|
15
|
-
| # OR
|
16
|
-
(?<lowercase_first>[a-z]+) # Named group: lowercase identifier first
|
17
|
-
/ # Separator
|
18
|
-
(?<uppercase_second>[A-Z]+) # Named group: uppercase identifier second
|
19
|
-
)
|
20
|
-
\z # End of string
|
21
|
-
}x
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
@@ -1,20 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Feen
|
4
|
-
module Parser
|
5
|
-
module PiecesInHand
|
6
|
-
# Error messages for validation
|
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
|
-
invalid_pnn_piece: "Invalid PNN piece format: '%s'. Expected format: [prefix]letter[suffix] where prefix is + or -, suffix is ', and letter is a-z or A-Z",
|
12
|
-
invalid_count: "Invalid count format: '%s'. Count cannot be '0' or '1', use the piece without count instead",
|
13
|
-
canonical_order_violation: "Pieces in hand must be in canonical order (by quantity descending, then alphabetically). Got: '%<actual>s', expected: '%<expected>s'",
|
14
|
-
missing_separator: "Pieces in hand format must contain exactly one '/' separator. Got: %s",
|
15
|
-
wrong_case_in_section: "Piece '%<piece>s' has wrong case for %<section>s section",
|
16
|
-
invalid_section_format: "Invalid format in %<section>s section: %<content>s"
|
17
|
-
}.freeze
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|
@@ -1,89 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Feen
|
4
|
-
module Parser
|
5
|
-
module PiecesInHand
|
6
|
-
# Patterns for PNN (Piece Name Notation) validation and parsing
|
7
|
-
module PnnPatterns
|
8
|
-
# Basic PNN piece pattern following the specification:
|
9
|
-
# <piece> ::= <letter> | <prefix> <letter> | <letter> <suffix> | <prefix> <letter> <suffix>
|
10
|
-
# <prefix> ::= "+" | "-"
|
11
|
-
# <suffix> ::= "'"
|
12
|
-
# <letter> ::= [a-zA-Z]
|
13
|
-
PNN_PIECE_PATTERN = /\A[-+]?[a-zA-Z]'?\z/
|
14
|
-
|
15
|
-
# Pattern for valid count prefixes according to FEEN specification:
|
16
|
-
# - Cannot be "0" or "1" (use piece without prefix instead)
|
17
|
-
# - Can be 2-9 or any number with 2+ digits
|
18
|
-
VALID_COUNT_PATTERN = /\A(?:[2-9]|\d{2,})\z/
|
19
|
-
|
20
|
-
# Pattern to extract piece with optional count from pieces in hand string
|
21
|
-
# Matches: optional count followed by complete PNN piece
|
22
|
-
# Groups: (count_str, piece_str)
|
23
|
-
# Note: We need to handle the full PNN piece including modifiers
|
24
|
-
PIECE_WITH_COUNT_PATTERN = /(?:([2-9]|\d{2,}))?([-+]?[a-zA-Z]'?)/
|
25
|
-
|
26
|
-
# Pattern for uppercase pieces only (used for uppercase section validation)
|
27
|
-
UPPERCASE_PIECE_PATTERN = /[-+]?[A-Z]'?/
|
28
|
-
|
29
|
-
# Pattern for lowercase pieces only (used for lowercase section validation)
|
30
|
-
LOWERCASE_PIECE_PATTERN = /[-+]?[a-z]'?/
|
31
|
-
|
32
|
-
# Pattern for uppercase section: sequence of uppercase pieces with optional counts
|
33
|
-
# Format: [count]piece[count]piece... where pieces are uppercase
|
34
|
-
UPPERCASE_SECTION_PATTERN = /\A
|
35
|
-
(?:
|
36
|
-
(?:[2-9]|\d{2,})? # Optional count (2-9 or 10+)
|
37
|
-
[-+]? # Optional single prefix (+ or -)
|
38
|
-
[A-Z] # Required uppercase letter
|
39
|
-
'? # Optional single suffix (')
|
40
|
-
)+ # One or more uppercase pieces
|
41
|
-
\z/x
|
42
|
-
|
43
|
-
# Pattern for lowercase section: sequence of lowercase pieces with optional counts
|
44
|
-
# Format: [count]piece[count]piece... where pieces are lowercase
|
45
|
-
LOWERCASE_SECTION_PATTERN = /\A
|
46
|
-
(?:
|
47
|
-
(?:[2-9]|\d{2,})? # Optional count (2-9 or 10+)
|
48
|
-
[-+]? # Optional single prefix (+ or -)
|
49
|
-
[a-z] # Required lowercase letter
|
50
|
-
'? # Optional single suffix (')
|
51
|
-
)+ # One or more lowercase pieces
|
52
|
-
\z/x
|
53
|
-
|
54
|
-
# Complete validation pattern for pieces in hand string with case separation
|
55
|
-
# Based on the FEEN BNF specification with PNN support
|
56
|
-
# Format: "UPPERCASE_PIECES/LOWERCASE_PIECES"
|
57
|
-
# Either section can be empty, but the "/" separator is mandatory
|
58
|
-
VALID_FORMAT_PATTERN = %r{\A
|
59
|
-
(?:
|
60
|
-
(?: # Uppercase section (optional)
|
61
|
-
(?:[2-9]|\d{2,})? # Optional count (2-9 or 10+)
|
62
|
-
[-+]? # Optional single prefix (+ or -)
|
63
|
-
[A-Z] # Required uppercase letter
|
64
|
-
'? # Optional single suffix (')
|
65
|
-
)* # Zero or more uppercase pieces
|
66
|
-
)
|
67
|
-
/ # Mandatory separator
|
68
|
-
(?:
|
69
|
-
(?: # Lowercase section (optional)
|
70
|
-
(?:[2-9]|\d{2,})? # Optional count (2-9 or 10+)
|
71
|
-
[-+]? # Optional single prefix (+ or -)
|
72
|
-
[a-z] # Required lowercase letter
|
73
|
-
'? # Optional single suffix (')
|
74
|
-
)* # Zero or more lowercase pieces
|
75
|
-
)
|
76
|
-
\z}x
|
77
|
-
|
78
|
-
# Pattern for extracting all pieces globally (used for comprehensive validation)
|
79
|
-
GLOBAL_PIECE_EXTRACTION_PATTERN = /(?:([2-9]|\d{2,}))?([-+]?[a-zA-Z]'?)/
|
80
|
-
|
81
|
-
# Pattern specifically for uppercase pieces with counts (for section parsing)
|
82
|
-
UPPERCASE_PIECE_WITH_COUNT_PATTERN = /(?:([2-9]|\d{2,}))?([-+]?[A-Z]'?)/
|
83
|
-
|
84
|
-
# Pattern specifically for lowercase pieces with counts (for section parsing)
|
85
|
-
LOWERCASE_PIECE_WITH_COUNT_PATTERN = /(?:([2-9]|\d{2,}))?([-+]?[a-z]'?)/
|
86
|
-
end
|
87
|
-
end
|
88
|
-
end
|
89
|
-
end
|