feen 5.0.0.beta2 → 5.0.0.beta4
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 +135 -87
- data/lib/feen/dumper/games_turn.rb +40 -65
- data/lib/feen/dumper/piece_placement.rb +102 -89
- data/lib/feen/dumper/pieces_in_hand/errors.rb +12 -0
- data/lib/feen/dumper/pieces_in_hand/no_pieces.rb +10 -0
- data/lib/feen/dumper/pieces_in_hand.rb +57 -41
- data/lib/feen/dumper.rb +63 -32
- data/lib/feen/parser/games_turn/errors.rb +14 -0
- data/lib/feen/parser/games_turn/valid_games_turn_pattern.rb +24 -0
- data/lib/feen/parser/games_turn.rb +32 -110
- data/lib/feen/parser/piece_placement.rb +490 -77
- data/lib/feen/parser/pieces_in_hand/errors.rb +15 -0
- data/lib/feen/parser/pieces_in_hand/no_pieces.rb +10 -0
- data/lib/feen/parser/pieces_in_hand/piece_count_pattern.rb +13 -0
- data/lib/feen/parser/pieces_in_hand/valid_format_pattern.rb +29 -0
- data/lib/feen/parser/pieces_in_hand.rb +90 -45
- data/lib/feen/parser.rb +67 -26
- data/lib/feen.rb +95 -76
- metadata +21 -7
- data/lib/feen/converter/from_fen.rb +0 -170
- data/lib/feen/converter/to_fen.rb +0 -153
- data/lib/feen/converter.rb +0 -16
- data/lib/feen/sanitizer.rb +0 -119
@@ -0,0 +1,15 @@
|
|
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
|
+
sorting_error: "Pieces in hand must be in ASCII lexicographic order"
|
12
|
+
}.freeze
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,13 @@
|
|
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
|
@@ -0,0 +1,29 @@
|
|
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
|
@@ -1,74 +1,119 @@
|
|
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", "no_pieces")
|
5
|
+
require_relative File.join("pieces_in_hand", "piece_count_pattern")
|
6
|
+
require_relative File.join("pieces_in_hand", "valid_format_pattern")
|
7
|
+
|
3
8
|
module Feen
|
4
9
|
module Parser
|
5
|
-
# Handles parsing of the pieces in hand section of a FEEN string
|
10
|
+
# Handles parsing of the pieces in hand section of a FEEN string.
|
11
|
+
# Pieces in hand represent pieces available for dropping onto the board.
|
6
12
|
module PiecesInHand
|
7
|
-
|
8
|
-
ERRORS = {
|
9
|
-
invalid_type: "Pieces in hand must be a string, got %s",
|
10
|
-
empty_string: "Pieces in hand string cannot be empty",
|
11
|
-
invalid_chars: "Invalid characters in pieces in hand: %s",
|
12
|
-
invalid_identifier: "Invalid piece identifier at position %d"
|
13
|
-
}.freeze
|
14
|
-
|
15
|
-
# Parses the pieces in hand section of a FEEN string
|
13
|
+
# Parses the pieces in hand section of a FEEN string.
|
16
14
|
#
|
17
15
|
# @param pieces_in_hand_str [String] FEEN pieces in hand string
|
18
|
-
# @return [Array<
|
16
|
+
# @return [Array<String>] Array of single-character piece identifiers in the
|
17
|
+
# format specified in the FEEN string (no prefixes or suffixes), expanded
|
18
|
+
# based on their counts and sorted in ASCII lexicographic order.
|
19
|
+
# Empty array if no pieces are in hand.
|
19
20
|
# @raise [ArgumentError] If the input string is invalid
|
21
|
+
#
|
22
|
+
# @example Parse no pieces in hand
|
23
|
+
# PiecesInHand.parse("-")
|
24
|
+
# # => []
|
25
|
+
#
|
26
|
+
# @example Parse multiple pieces in hand
|
27
|
+
# PiecesInHand.parse("BN2Pb")
|
28
|
+
# # => ["B", "N", "P", "P", "b"]
|
29
|
+
#
|
30
|
+
# @example Parse pieces with counts
|
31
|
+
# PiecesInHand.parse("N5P2b")
|
32
|
+
# # => ["N", "P", "P", "P", "P", "P", "b", "b"]
|
20
33
|
def self.parse(pieces_in_hand_str)
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
return [] if pieces_in_hand_str == NO_PIECES
|
34
|
+
# Validate input
|
35
|
+
validate_input_type(pieces_in_hand_str)
|
36
|
+
validate_format(pieces_in_hand_str)
|
25
37
|
|
26
|
-
pieces
|
27
|
-
|
38
|
+
# Handle the no-pieces case early
|
39
|
+
return [] if pieces_in_hand_str == NoPieces
|
28
40
|
|
29
|
-
|
30
|
-
|
31
|
-
|
41
|
+
# Extract pieces with their counts and validate the order
|
42
|
+
pieces_with_counts = extract_pieces_with_counts(pieces_in_hand_str)
|
43
|
+
validate_lexicographic_order(pieces_with_counts)
|
32
44
|
|
33
|
-
|
34
|
-
|
45
|
+
# Expand the pieces into an array and maintain lexicographic ordering
|
46
|
+
expand_pieces(pieces_with_counts)
|
47
|
+
end
|
35
48
|
|
36
|
-
|
49
|
+
# Validates that the input is a non-empty string.
|
50
|
+
#
|
51
|
+
# @param str [String] Input string to validate
|
52
|
+
# @raise [ArgumentError] If input is not a string or is empty
|
53
|
+
# @return [void]
|
54
|
+
private_class_method def self.validate_input_type(str)
|
55
|
+
raise ::ArgumentError, format(Errors[:invalid_type], str.class) unless str.is_a?(::String)
|
56
|
+
raise ::ArgumentError, Errors[:empty_string] if str.empty?
|
57
|
+
end
|
37
58
|
|
38
|
-
|
39
|
-
|
59
|
+
# Validates that the input string matches the expected format according to FEEN specification.
|
60
|
+
#
|
61
|
+
# @param str [String] Input string to validate
|
62
|
+
# @raise [ArgumentError] If format is invalid
|
63
|
+
# @return [void]
|
64
|
+
private_class_method def self.validate_format(str)
|
65
|
+
return if str == NoPieces || str.match?(ValidFormatPattern)
|
40
66
|
|
41
|
-
|
67
|
+
raise ::ArgumentError, format(Errors[:invalid_format], str)
|
42
68
|
end
|
43
69
|
|
44
|
-
#
|
70
|
+
# Extracts pieces with their counts from the FEEN string.
|
45
71
|
#
|
46
72
|
# @param str [String] FEEN pieces in hand string
|
47
|
-
# @
|
48
|
-
|
49
|
-
|
50
|
-
|
73
|
+
# @return [Array<Hash>] Array of hashes with :piece and :count keys
|
74
|
+
private_class_method def self.extract_pieces_with_counts(str)
|
75
|
+
result = []
|
76
|
+
position = 0
|
77
|
+
|
78
|
+
while position < str.length
|
79
|
+
match = str[position..].match(PieceCountPattern)
|
80
|
+
break unless match
|
51
81
|
|
52
|
-
|
82
|
+
count_str, piece = match.captures
|
83
|
+
count = count_str ? count_str.to_i : 1
|
53
84
|
|
54
|
-
|
55
|
-
|
85
|
+
# Add to our result with piece type and count
|
86
|
+
result << { piece: piece, count: count }
|
56
87
|
|
57
|
-
|
58
|
-
|
59
|
-
|
88
|
+
# Move position forward
|
89
|
+
position += match[0].length
|
90
|
+
end
|
60
91
|
|
61
|
-
|
62
|
-
raise ArgumentError, format(ERRORS[:invalid_chars], invalid_chars)
|
92
|
+
result
|
63
93
|
end
|
64
94
|
|
65
|
-
#
|
95
|
+
# Validates that pieces are in lexicographic ASCII order.
|
66
96
|
#
|
67
|
-
# @param
|
68
|
-
# @
|
69
|
-
|
70
|
-
|
71
|
-
|
97
|
+
# @param pieces_with_counts [Array<Hash>] Array of pieces with their counts
|
98
|
+
# @raise [ArgumentError] If pieces are not in lexicographic order
|
99
|
+
# @return [void]
|
100
|
+
private_class_method def self.validate_lexicographic_order(pieces_with_counts)
|
101
|
+
pieces = pieces_with_counts.map { |item| item[:piece] }
|
102
|
+
|
103
|
+
# Verify the array is sorted lexicographically
|
104
|
+
return if pieces == pieces.sort
|
105
|
+
|
106
|
+
raise ::ArgumentError, Errors[:sorting_error]
|
107
|
+
end
|
108
|
+
|
109
|
+
# Expands the pieces based on their counts into an array.
|
110
|
+
#
|
111
|
+
# @param pieces_with_counts [Array<Hash>] Array of pieces with their counts
|
112
|
+
# @return [Array<String>] Array of expanded pieces
|
113
|
+
private_class_method def self.expand_pieces(pieces_with_counts)
|
114
|
+
pieces_with_counts.flat_map do |item|
|
115
|
+
Array.new(item[:count], item[:piece])
|
116
|
+
end
|
72
117
|
end
|
73
118
|
end
|
74
119
|
end
|
data/lib/feen/parser.rb
CHANGED
@@ -5,48 +5,89 @@ require_relative File.join("parser", "piece_placement")
|
|
5
5
|
require_relative File.join("parser", "pieces_in_hand")
|
6
6
|
|
7
7
|
module Feen
|
8
|
-
# Module responsible for parsing FEEN notation strings into internal data structures
|
8
|
+
# Module responsible for parsing FEEN notation strings into internal data structures.
|
9
|
+
# FEEN (Forsyth–Edwards Enhanced Notation) is a compact, canonical, and rule-agnostic
|
10
|
+
# textual format for representing static board positions in two-player piece-placement games.
|
9
11
|
module Parser
|
12
|
+
# Regular expression pattern for matching a valid FEEN string structure
|
13
|
+
# Captures exactly three non-space components separated by single spaces
|
14
|
+
FEEN_PATTERN = /\A([^\s]+)\s([^\s]+)\s([^\s]+)\z/
|
15
|
+
|
16
|
+
# Error message for invalid FEEN format
|
17
|
+
INVALID_FORMAT_ERROR = "Invalid FEEN format: expected exactly 3 fields separated by single spaces"
|
18
|
+
|
10
19
|
# Parses a complete FEEN string into a structured representation
|
11
20
|
#
|
12
21
|
# @param feen_string [String] Complete FEEN notation string
|
13
|
-
# @return [Hash] Hash containing the parsed position data
|
22
|
+
# @return [Hash] Hash containing the parsed position data with the following keys:
|
23
|
+
# - :piece_placement [Array] - Hierarchical array structure representing the board
|
24
|
+
# - :pieces_in_hand [Array<String>] - Pieces available for dropping onto the board
|
25
|
+
# - :games_turn [Array<String>] - A two-element array with [active_variant, inactive_variant]
|
14
26
|
# @raise [ArgumentError] If the FEEN string is invalid
|
27
|
+
#
|
28
|
+
# @example Parsing a standard chess initial position
|
29
|
+
# feen = "rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR - CHESS/chess"
|
30
|
+
# result = Feen::Parser.parse(feen)
|
31
|
+
# # => {
|
32
|
+
# # piece_placement: [
|
33
|
+
# # ["r", "n", "b", "q", "k=", "b", "n", "r"],
|
34
|
+
# # ["p", "p", "p", "p", "p", "p", "p", "p"],
|
35
|
+
# # ["", "", "", "", "", "", "", ""],
|
36
|
+
# # ["", "", "", "", "", "", "", ""],
|
37
|
+
# # ["", "", "", "", "", "", "", ""],
|
38
|
+
# # ["", "", "", "", "", "", "", ""],
|
39
|
+
# # ["P", "P", "P", "P", "P", "P", "P", "P"],
|
40
|
+
# # ["R", "N", "B", "Q", "K=", "B", "N", "R"]
|
41
|
+
# # ],
|
42
|
+
# # pieces_in_hand: [],
|
43
|
+
# # games_turn: ["CHESS", "chess"]
|
44
|
+
# # }
|
15
45
|
def self.parse(feen_string)
|
16
|
-
|
46
|
+
feen_string = String(feen_string)
|
17
47
|
|
18
|
-
#
|
19
|
-
|
48
|
+
# Match the FEEN string against the expected pattern
|
49
|
+
match = FEEN_PATTERN.match(feen_string)
|
20
50
|
|
21
|
-
|
51
|
+
# Raise an error if the format doesn't match the expected pattern
|
52
|
+
raise ::ArgumentError, INVALID_FORMAT_ERROR unless match
|
53
|
+
|
54
|
+
# Capture the three distinct parts
|
55
|
+
piece_placement_string, pieces_in_hand_string, games_turn_string = match.captures
|
22
56
|
|
23
57
|
# Parse each field using the appropriate submodule
|
24
|
-
piece_placement = PiecePlacement.parse(
|
25
|
-
|
26
|
-
|
58
|
+
piece_placement = PiecePlacement.parse(piece_placement_string)
|
59
|
+
pieces_in_hand = PiecesInHand.parse(pieces_in_hand_string)
|
60
|
+
games_turn = GamesTurn.parse(games_turn_string)
|
27
61
|
|
28
|
-
#
|
62
|
+
# Create a structured representation of the position
|
29
63
|
{
|
30
|
-
piece_placement
|
31
|
-
|
32
|
-
|
64
|
+
piece_placement:,
|
65
|
+
pieces_in_hand:,
|
66
|
+
games_turn:
|
33
67
|
}
|
34
68
|
end
|
35
69
|
|
36
|
-
#
|
70
|
+
# Safely parses a complete FEEN string into a structured representation without raising exceptions
|
37
71
|
#
|
38
|
-
#
|
39
|
-
#
|
40
|
-
#
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
72
|
+
# This method works like `parse` but returns nil instead of raising an exception
|
73
|
+
# if the FEEN string is invalid.
|
74
|
+
#
|
75
|
+
# @param feen_string [String] Complete FEEN notation string
|
76
|
+
# @return [Hash, nil] Hash containing the parsed position data or nil if parsing fails
|
77
|
+
#
|
78
|
+
# @example Parsing a valid FEEN string
|
79
|
+
# feen = "rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR - CHESS/chess"
|
80
|
+
# result = Feen::Parser.safe_parse(feen)
|
81
|
+
# # => {piece_placement: [...], pieces_in_hand: [...], games_turn: [...]}
|
82
|
+
#
|
83
|
+
# @example Parsing an invalid FEEN string
|
84
|
+
# feen = "invalid feen string"
|
85
|
+
# result = Feen::Parser.safe_parse(feen)
|
86
|
+
# # => nil
|
87
|
+
def self.safe_parse(feen_string)
|
88
|
+
parse(feen_string)
|
89
|
+
rescue ::ArgumentError
|
90
|
+
nil
|
50
91
|
end
|
51
92
|
end
|
52
93
|
end
|
data/lib/feen.rb
CHANGED
@@ -1,110 +1,129 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative File.join("feen", "converter")
|
4
3
|
require_relative File.join("feen", "dumper")
|
5
4
|
require_relative File.join("feen", "parser")
|
6
5
|
|
7
6
|
# This module provides a Ruby interface for data serialization and
|
8
7
|
# deserialization in FEEN format.
|
9
8
|
#
|
9
|
+
# FEEN (Forsyth–Edwards Enhanced Notation) is a compact, canonical, and
|
10
|
+
# rule-agnostic textual format for representing static board positions
|
11
|
+
# in two-player piece-placement games.
|
12
|
+
#
|
10
13
|
# @see https://sashite.dev/documents/feen/1.0.0/
|
11
14
|
module Feen
|
12
|
-
# Dumps position
|
15
|
+
# Dumps position components into a FEEN string.
|
13
16
|
#
|
14
|
-
# @param
|
15
|
-
#
|
16
|
-
# @
|
17
|
-
# @
|
17
|
+
# @param piece_placement [Array] Board position data structure representing the spatial
|
18
|
+
# distribution of pieces across the board
|
19
|
+
# @param pieces_in_hand [Array<String>] Pieces available for dropping onto the board
|
20
|
+
# @param games_turn [Array<String>] A two-element array where the first element is the
|
21
|
+
# active player's variant and the second is the inactive player's variant
|
18
22
|
# @return [String] FEEN notation string
|
19
|
-
# @raise [ArgumentError] If
|
23
|
+
# @raise [ArgumentError] If any parameter is invalid
|
20
24
|
# @example
|
21
|
-
#
|
22
|
-
#
|
23
|
-
#
|
24
|
-
#
|
25
|
-
#
|
26
|
-
#
|
27
|
-
#
|
28
|
-
#
|
29
|
-
#
|
30
|
-
#
|
31
|
-
#
|
32
|
-
#
|
33
|
-
#
|
34
|
-
#
|
35
|
-
#
|
36
|
-
#
|
37
|
-
|
38
|
-
|
39
|
-
def self.dump(position)
|
40
|
-
Dumper.dump(position)
|
25
|
+
# piece_placement = [
|
26
|
+
# ["r", "n", "b", "q", "k=", "b", "n", "r"],
|
27
|
+
# ["p", "p", "p", "p", "p", "p", "p", "p"],
|
28
|
+
# ["", "", "", "", "", "", "", ""],
|
29
|
+
# ["", "", "", "", "", "", "", ""],
|
30
|
+
# ["", "", "", "", "", "", "", ""],
|
31
|
+
# ["", "", "", "", "", "", "", ""],
|
32
|
+
# ["P", "P", "P", "P", "P", "P", "P", "P"],
|
33
|
+
# ["R", "N", "B", "Q", "K=", "B", "N", "R"]
|
34
|
+
# ]
|
35
|
+
# Feen.dump(
|
36
|
+
# piece_placement: piece_placement,
|
37
|
+
# pieces_in_hand: [],
|
38
|
+
# games_turn: ["CHESS", "chess"]
|
39
|
+
# )
|
40
|
+
# # => "rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR - CHESS/chess"
|
41
|
+
def self.dump(piece_placement:, pieces_in_hand:, games_turn:)
|
42
|
+
Dumper.dump(piece_placement:, pieces_in_hand:, games_turn:)
|
41
43
|
end
|
42
44
|
|
43
|
-
# Parses a FEEN string into position
|
45
|
+
# Parses a FEEN string into position components.
|
44
46
|
#
|
45
|
-
# @param feen_string [String] FEEN notation string
|
46
|
-
# @return [Hash] Hash containing the parsed position data
|
47
|
+
# @param feen_string [String] Complete FEEN notation string
|
48
|
+
# @return [Hash] Hash containing the parsed position data with the following keys:
|
49
|
+
# - :piece_placement [Array] - Hierarchical array structure representing the board
|
50
|
+
# - :pieces_in_hand [Array<String>] - Pieces available for dropping onto the board
|
51
|
+
# - :games_turn [Array<String>] - A two-element array with [active_variant, inactive_variant]
|
47
52
|
# @raise [ArgumentError] If the FEEN string is invalid
|
48
53
|
# @example
|
49
|
-
# feen_string = "rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR CHESS/chess
|
54
|
+
# feen_string = "rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR - CHESS/chess"
|
50
55
|
# Feen.parse(feen_string)
|
51
56
|
# # => {
|
52
|
-
# # piece_placement: [
|
53
|
-
# #
|
54
|
-
# #
|
55
|
-
# #
|
56
|
-
# #
|
57
|
-
# #
|
58
|
-
# #
|
59
|
-
# #
|
60
|
-
# #
|
61
|
-
# #
|
62
|
-
# #
|
63
|
-
# #
|
64
|
-
# # lowercase_game: 'chess',
|
65
|
-
# # active_player_casing: :uppercase
|
66
|
-
# # },
|
67
|
-
# # pieces_in_hand: []
|
57
|
+
# # piece_placement: [
|
58
|
+
# # ["r", "n", "b", "q", "k=", "b", "n", "r"],
|
59
|
+
# # ["p", "p", "p", "p", "p", "p", "p", "p"],
|
60
|
+
# # ["", "", "", "", "", "", "", ""],
|
61
|
+
# # ["", "", "", "", "", "", "", ""],
|
62
|
+
# # ["", "", "", "", "", "", "", ""],
|
63
|
+
# # ["", "", "", "", "", "", "", ""],
|
64
|
+
# # ["P", "P", "P", "P", "P", "P", "P", "P"],
|
65
|
+
# # ["R", "N", "B", "Q", "K=", "B", "N", "R"]
|
66
|
+
# # ],
|
67
|
+
# # pieces_in_hand: [],
|
68
|
+
# # games_turn: ["CHESS", "chess"]
|
68
69
|
# # }
|
69
70
|
def self.parse(feen_string)
|
70
71
|
Parser.parse(feen_string)
|
71
72
|
end
|
72
73
|
|
73
|
-
#
|
74
|
+
# Safely parses a FEEN string into position components without raising exceptions.
|
74
75
|
#
|
75
|
-
#
|
76
|
-
#
|
77
|
-
# @example
|
78
|
-
# Feen.valid?("rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR CHESS/chess -") # => true
|
79
|
-
# Feen.valid?("invalid feen string") # => false
|
80
|
-
def self.valid?(feen_string)
|
81
|
-
parse(feen_string)
|
82
|
-
true
|
83
|
-
rescue ::ArgumentError
|
84
|
-
false
|
85
|
-
end
|
86
|
-
|
87
|
-
# Converts a FEN string to a FEEN string for chess positions
|
76
|
+
# This method works like `parse` but returns nil instead of raising an exception
|
77
|
+
# if the FEEN string is invalid.
|
88
78
|
#
|
89
|
-
# @param
|
90
|
-
# @return [
|
91
|
-
# @raise [ArgumentError] If the FEN string is invalid
|
79
|
+
# @param feen_string [String] Complete FEEN notation string
|
80
|
+
# @return [Hash, nil] Hash containing the parsed position data or nil if parsing fails
|
92
81
|
# @example
|
93
|
-
#
|
94
|
-
#
|
95
|
-
|
96
|
-
|
82
|
+
# # Valid FEEN string
|
83
|
+
# Feen.safe_parse("rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR - CHESS/chess")
|
84
|
+
# # => {piece_placement: [...], pieces_in_hand: [...], games_turn: [...]}
|
85
|
+
#
|
86
|
+
# # Invalid FEEN string
|
87
|
+
# Feen.safe_parse("invalid feen string")
|
88
|
+
# # => nil
|
89
|
+
def self.safe_parse(feen_string)
|
90
|
+
Parser.safe_parse(feen_string)
|
97
91
|
end
|
98
92
|
|
99
|
-
#
|
93
|
+
# Validates if the given string is a valid and canonical FEEN string
|
100
94
|
#
|
101
|
-
#
|
102
|
-
#
|
103
|
-
#
|
95
|
+
# This method performs a complete validation in two steps:
|
96
|
+
# 1. Syntax check: Verifies the string can be parsed as FEEN
|
97
|
+
# 2. Canonicity check: Ensures the string is in canonical form by comparing
|
98
|
+
# it with a freshly generated FEEN string created from its parsed components
|
99
|
+
#
|
100
|
+
# This approach guarantees that the string not only follows FEEN syntax
|
101
|
+
# but is also in its most compact, canonical representation.
|
102
|
+
#
|
103
|
+
# @param feen_string [String] FEEN string to validate
|
104
|
+
# @return [Boolean] True if the string is a valid and canonical FEEN string
|
104
105
|
# @example
|
105
|
-
#
|
106
|
-
#
|
107
|
-
|
108
|
-
|
106
|
+
# # Canonical form
|
107
|
+
# Feen.valid?("lnsgk3l/5g3/p1ppB2pp/9/8B/2P6/P2PPPPPP/3K3R1/5rSNL N5P2gln2s SHOGI/shogi") # => true
|
108
|
+
#
|
109
|
+
# # Invalid syntax
|
110
|
+
# Feen.valid?("invalid feen string") # => false
|
111
|
+
#
|
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
|
114
|
+
def self.valid?(feen_string)
|
115
|
+
# First check: Basic syntax validation
|
116
|
+
begin
|
117
|
+
parsed_data = parse(feen_string)
|
118
|
+
rescue ::ArgumentError
|
119
|
+
return false
|
120
|
+
end
|
121
|
+
|
122
|
+
# Second check: Canonicity validation through round-trip conversion
|
123
|
+
# Generate a fresh FEEN string from the parsed data
|
124
|
+
canonical_feen = dump(**parsed_data)
|
125
|
+
|
126
|
+
# Compare the original string with the canonical form
|
127
|
+
feen_string == canonical_feen
|
109
128
|
end
|
110
129
|
end
|
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.beta4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Cyril Kato
|
@@ -10,6 +10,9 @@ cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
11
11
|
dependencies: []
|
12
12
|
description: A Ruby interface for data serialization and deserialization in FEEN format.
|
13
|
+
FEEN is a compact, canonical, and rule-agnostic textual format for representing
|
14
|
+
static board positions in two-player piece-placement games like Chess, Shogi, Xiangqi,
|
15
|
+
and others.
|
13
16
|
email: contact@cyril.email
|
14
17
|
executables: []
|
15
18
|
extensions: []
|
@@ -18,23 +21,34 @@ files:
|
|
18
21
|
- LICENSE.md
|
19
22
|
- README.md
|
20
23
|
- lib/feen.rb
|
21
|
-
- lib/feen/converter.rb
|
22
|
-
- lib/feen/converter/from_fen.rb
|
23
|
-
- lib/feen/converter/to_fen.rb
|
24
24
|
- lib/feen/dumper.rb
|
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
|
+
- lib/feen/dumper/pieces_in_hand/no_pieces.rb
|
28
30
|
- lib/feen/parser.rb
|
29
31
|
- lib/feen/parser/games_turn.rb
|
32
|
+
- lib/feen/parser/games_turn/errors.rb
|
33
|
+
- lib/feen/parser/games_turn/valid_games_turn_pattern.rb
|
30
34
|
- lib/feen/parser/piece_placement.rb
|
31
35
|
- lib/feen/parser/pieces_in_hand.rb
|
32
|
-
- lib/feen/
|
36
|
+
- lib/feen/parser/pieces_in_hand/errors.rb
|
37
|
+
- lib/feen/parser/pieces_in_hand/no_pieces.rb
|
38
|
+
- lib/feen/parser/pieces_in_hand/piece_count_pattern.rb
|
39
|
+
- lib/feen/parser/pieces_in_hand/valid_format_pattern.rb
|
33
40
|
homepage: https://github.com/sashite/feen.rb
|
34
41
|
licenses:
|
35
42
|
- MIT
|
36
43
|
metadata:
|
44
|
+
bug_tracker_uri: https://github.com/sashite/feen.rb/issues
|
45
|
+
documentation_uri: https://rubydoc.info/github/sashite/feen.rb/main
|
46
|
+
homepage_uri: https://github.com/sashite/feen.rb
|
47
|
+
source_code_uri: https://github.com/sashite/feen.rb
|
48
|
+
specification_uri: https://sashite.dev/documents/feen/1.0.0/
|
49
|
+
funding_uri: https://github.com/sponsors/cyril
|
37
50
|
rubygems_mfa_required: 'true'
|
51
|
+
article_uri: https://blog.cyril.email/posts/2025-05-01/introducing-feen-notation.html
|
38
52
|
rdoc_options: []
|
39
53
|
require_paths:
|
40
54
|
- lib
|
@@ -42,7 +56,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
42
56
|
requirements:
|
43
57
|
- - ">="
|
44
58
|
- !ruby/object:Gem::Version
|
45
|
-
version: 3.
|
59
|
+
version: 3.2.0
|
46
60
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
47
61
|
requirements:
|
48
62
|
- - ">="
|
@@ -51,5 +65,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
51
65
|
requirements: []
|
52
66
|
rubygems_version: 3.6.7
|
53
67
|
specification_version: 4
|
54
|
-
summary: FEEN support for the Ruby language.
|
68
|
+
summary: FEEN (Forsyth–Edwards Enhanced Notation) support for the Ruby language.
|
55
69
|
test_files: []
|