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.
@@ -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
- # This implementation supports full PNN notation including prefixes and suffixes.
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 full PNN format,
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 Parse complex pieces with counts and modifiers
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"]
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[:invalid_format], str) unless parts_count == 1
95
+ raise ::ArgumentError, format(Errors[:missing_separator], parts_count) unless parts_count == 1
74
96
 
75
- uppercase_section, lowercase_section = str.split("/", 2)
97
+ # Must match the overall pattern
98
+ raise ::ArgumentError, format(Errors[:invalid_format], str) unless str.match?(VALID_FORMAT_PATTERN)
76
99
 
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?
100
+ # Additional validation: check for any modifiers (forbidden in hand)
101
+ validate_no_modifiers(str)
80
102
  end
81
103
 
82
- # Validates the format of a specific section (uppercase or lowercase)
104
+ # Validates that no modifiers are present in the pieces in hand string
83
105
  #
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)
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.validate_individual_pieces_in_section(section, case_type)
115
- position = 0
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
- # Validate count format (no "0" or "1" prefixes allowed)
139
- if count_str && !count_str.match?(PnnPatterns::VALID_COUNT_PATTERN)
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
- position += match[0].length
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, _case_type)
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 (no canonical order validation needed)
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
- private_class_method def self.extract_pieces_with_counts_from_section(section)
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(PnnPatterns::PIECE_WITH_COUNT_PATTERN)
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 - CHESS/chess"
29
+ # feen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR / CHESS/chess"
30
30
  # result = Feen::Parser.parse(feen)
31
31
  # # => {
32
32
  # # piece_placement: [
@@ -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 - CHESS/chess"
79
+ # feen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR / CHESS/chess"
80
80
  # result = Feen::Parser.safe_parse(feen)
81
81
  # # => {piece_placement: [...], pieces_in_hand: [...], games_turn: [...]}
82
82
  #
data/lib/feen.rb CHANGED
@@ -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 - CHESS/chess"
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 - CHESS/chess"
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 - CHESS/chess")
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?("lnsgk3l/5g3/p1ppB2pp/9/8B/2P6/P2PPPPPP/3K3R1/5rSNL 2g2s5PNln SHOGI/shogi") # => true
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 not in lexicographic order)
113
- # Feen.valid?("lnsgk3l/5g3/p1ppB2pp/9/8B/2P6/P2PPPPPP/3K3R1/5rSNL N5P2gn2sl SHOGI/shogi") # => false
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.beta7
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