feen 5.0.0.beta2 → 5.0.0.beta3
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 +107 -86
- 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 +14 -0
- data/lib/feen/parser/pieces_in_hand/no_pieces.rb +10 -0
- data/lib/feen/parser/pieces_in_hand/valid_format_pattern.rb +15 -0
- data/lib/feen/parser/pieces_in_hand.rb +53 -44
- data/lib/feen/parser.rb +67 -30
- data/lib/feen.rb +42 -76
- metadata +9 -6
- 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
@@ -2,120 +2,133 @@
|
|
2
2
|
|
3
3
|
module Feen
|
4
4
|
module Dumper
|
5
|
-
# Handles conversion of piece placement data structure to FEEN notation string
|
6
5
|
module PiecePlacement
|
7
|
-
#
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
#
|
13
|
-
|
6
|
+
# Converts a piece placement structure to a FEEN-compliant string
|
7
|
+
#
|
8
|
+
# @param piece_placement [Array] Hierarchical array representing the board where:
|
9
|
+
# - Empty squares are represented by empty strings ("")
|
10
|
+
# - Pieces are represented by strings (e.g., "r", "K=", "+P")
|
11
|
+
# - Dimensions are represented by nested arrays
|
12
|
+
# @return [String] FEEN piece placement string
|
13
|
+
# @raise [ArgumentError] If the piece placement structure is invalid
|
14
|
+
def self.dump(piece_placement)
|
15
|
+
# Détecter la forme du tableau directement à partir de la structure
|
16
|
+
detect_shape(piece_placement)
|
14
17
|
|
15
|
-
|
16
|
-
|
18
|
+
# Formater directement la structure en chaîne FEEN
|
19
|
+
format_placement(piece_placement)
|
20
|
+
end
|
17
21
|
|
18
|
-
#
|
22
|
+
# Detects the shape of the board based on the piece_placement structure
|
19
23
|
#
|
20
24
|
# @param piece_placement [Array] Hierarchical array structure representing the board
|
21
|
-
# @return [
|
22
|
-
# @
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
25
|
+
# @return [Array<Integer>] Array of dimension sizes
|
26
|
+
# @raise [ArgumentError] If the piece_placement structure is invalid
|
27
|
+
def self.detect_shape(piece_placement)
|
28
|
+
return [] if piece_placement.empty?
|
29
|
+
|
30
|
+
dimensions = []
|
31
|
+
current = piece_placement
|
32
|
+
|
33
|
+
# Traverse the structure to determine shape
|
34
|
+
while current.is_a?(Array) && !current.empty?
|
35
|
+
dimensions << current.size
|
36
|
+
|
37
|
+
# Check if all elements at this level have the same structure
|
38
|
+
validate_dimension_uniformity(current)
|
28
39
|
|
29
|
-
|
30
|
-
|
40
|
+
# Check if we've reached the leaf level (array of strings)
|
41
|
+
break if current.first.is_a?(String) ||
|
42
|
+
(current.first.is_a?(Array) && current.first.empty?)
|
31
43
|
|
32
|
-
|
33
|
-
|
44
|
+
current = current.first
|
45
|
+
end
|
46
|
+
|
47
|
+
dimensions
|
34
48
|
end
|
35
49
|
|
36
|
-
#
|
50
|
+
# Validates that all elements in a dimension have the same structure
|
37
51
|
#
|
38
|
-
# @param
|
39
|
-
# @
|
40
|
-
def self.
|
41
|
-
return
|
52
|
+
# @param dimension [Array] Array of elements at a particular dimension level
|
53
|
+
# @raise [ArgumentError] If elements have inconsistent structure
|
54
|
+
def self.validate_dimension_uniformity(dimension)
|
55
|
+
return if dimension.empty?
|
42
56
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
57
|
+
first_type = dimension.first.class
|
58
|
+
first_size = dimension.first.is_a?(Array) ? dimension.first.size : nil
|
59
|
+
|
60
|
+
dimension.each do |element|
|
61
|
+
unless element.class == first_type
|
62
|
+
raise ArgumentError, "Inconsistent element types in dimension: #{first_type} vs #{element.class}"
|
63
|
+
end
|
64
|
+
|
65
|
+
if element.is_a?(Array) && element.size != first_size
|
66
|
+
raise ArgumentError, "Inconsistent dimension sizes: expected #{first_size}, got #{element.size}"
|
67
|
+
end
|
47
68
|
end
|
48
69
|
end
|
49
70
|
|
50
|
-
#
|
71
|
+
# Formats the piece placement structure into a FEEN string
|
51
72
|
#
|
52
|
-
# @param
|
53
|
-
# @
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
if data.first.is_a?(Array)
|
60
|
-
parts = data.map { |subdata| dump_recursive(subdata, max_depth, current_depth + 1) }
|
61
|
-
|
62
|
-
# The number of separators depends on the current depth
|
63
|
-
# The lowest level uses a single '/', then increases progressively
|
64
|
-
separator_count = max_depth - current_depth
|
65
|
-
separator = DIMENSION_SEPARATOR * separator_count
|
66
|
-
|
67
|
-
parts.join(separator)
|
68
|
-
else
|
69
|
-
# This is a simple row (rank)
|
70
|
-
dump_rank(data)
|
73
|
+
# @param placement [Array] Piece placement structure
|
74
|
+
# @return [String] FEEN piece placement string
|
75
|
+
def self.format_placement(placement)
|
76
|
+
# For 1D arrays (ranks), format directly
|
77
|
+
if !placement.is_a?(Array) ||
|
78
|
+
(placement.is_a?(Array) && (placement.empty? || !placement.first.is_a?(Array)))
|
79
|
+
return format_rank(placement)
|
71
80
|
end
|
81
|
+
|
82
|
+
# For 2D+ arrays, format each sub-element and join with appropriate separator
|
83
|
+
depth = calculate_depth(placement) - 1
|
84
|
+
separator = "/" * depth
|
85
|
+
|
86
|
+
# Important: Ne pas inverser le tableau - nous voulons maintenir l'ordre original
|
87
|
+
elements = placement
|
88
|
+
elements.map { |element| format_placement(element) }.join(separator)
|
72
89
|
end
|
73
90
|
|
74
|
-
#
|
91
|
+
# Formats a rank (1D array of cells)
|
75
92
|
#
|
76
|
-
# @param
|
77
|
-
# @return [String] FEEN
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
if chunk.first.nil?
|
88
|
-
# Group of empty cells -> count
|
89
|
-
chunk.size.to_s
|
93
|
+
# @param rank [Array] 1D array of cells
|
94
|
+
# @return [String] FEEN rank string
|
95
|
+
def self.format_rank(rank)
|
96
|
+
return "" if !rank.is_a?(Array) || rank.empty?
|
97
|
+
|
98
|
+
result = ""
|
99
|
+
empty_count = 0
|
100
|
+
|
101
|
+
rank.each do |cell|
|
102
|
+
if cell.empty?
|
103
|
+
empty_count += 1
|
90
104
|
else
|
91
|
-
#
|
92
|
-
|
105
|
+
# Add accumulated empty squares
|
106
|
+
result += empty_count.to_s if empty_count > 0
|
107
|
+
empty_count = 0
|
108
|
+
|
109
|
+
# Add the piece
|
110
|
+
result += cell
|
93
111
|
end
|
94
|
-
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Add any trailing empty squares
|
115
|
+
result += empty_count.to_s if empty_count > 0
|
116
|
+
|
117
|
+
result
|
95
118
|
end
|
96
119
|
|
97
|
-
#
|
120
|
+
# Calculates the depth of a nested structure
|
98
121
|
#
|
99
|
-
# @param
|
100
|
-
# @return [
|
101
|
-
|
102
|
-
|
103
|
-
# PiecePlacement.dump_cell(cell)
|
104
|
-
# # => "P="
|
105
|
-
def self.dump_cell(cell)
|
106
|
-
return EMPTY_STRING if cell.nil?
|
107
|
-
|
108
|
-
# Combine prefix (if any) + piece identifier + suffix (if any)
|
109
|
-
[
|
110
|
-
cell[:prefix],
|
111
|
-
cell[:id],
|
112
|
-
cell[:suffix]
|
113
|
-
].compact.join(EMPTY_STRING)
|
114
|
-
end
|
122
|
+
# @param structure [Array] Structure to analyze
|
123
|
+
# @return [Integer] Depth of the structure
|
124
|
+
def self.calculate_depth(structure)
|
125
|
+
return 0 unless structure.is_a?(Array) && !structure.empty?
|
115
126
|
|
116
|
-
|
117
|
-
|
118
|
-
|
127
|
+
if structure.first.is_a?(Array)
|
128
|
+
1 + calculate_depth(structure.first)
|
129
|
+
else
|
130
|
+
1
|
131
|
+
end
|
119
132
|
end
|
120
133
|
end
|
121
134
|
end
|
@@ -0,0 +1,12 @@
|
|
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 format: '%<value>s'"
|
9
|
+
}.freeze
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -1,56 +1,72 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative File.join("pieces_in_hand", "no_pieces")
|
4
|
+
require_relative File.join("pieces_in_hand", "errors")
|
5
|
+
|
3
6
|
module Feen
|
4
7
|
module Dumper
|
5
|
-
# Handles conversion of pieces in hand data
|
8
|
+
# Handles conversion of pieces in hand data to FEEN notation string
|
6
9
|
module PiecesInHand
|
7
|
-
|
8
|
-
ERRORS = {
|
9
|
-
invalid_piece: "Invalid piece at index %d: must be a Hash with an :id key",
|
10
|
-
invalid_id: "Invalid piece ID at index %d: must be a single alphabetic character",
|
11
|
-
prefix_not_allowed: "Prefix is not allowed for pieces in hand at index %d",
|
12
|
-
suffix_not_allowed: "Suffix is not allowed for pieces in hand at index %d"
|
13
|
-
}.freeze
|
14
|
-
|
15
|
-
# Converts the internal pieces in hand representation to a FEEN string
|
10
|
+
# Converts an array of piece identifiers to a FEEN-formatted pieces in hand string
|
16
11
|
#
|
17
|
-
# @param
|
12
|
+
# @param piece_chars [Array<String>] Array of single-character piece identifiers
|
18
13
|
# @return [String] FEEN-formatted pieces in hand string
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
14
|
+
# @raise [ArgumentError] If any piece identifier is invalid
|
15
|
+
# @example
|
16
|
+
# PiecesInHand.dump("P", "p", "B")
|
17
|
+
# # => "BPp"
|
18
|
+
#
|
19
|
+
# PiecesInHand.dump
|
20
|
+
# # => "-"
|
21
|
+
def self.dump(*piece_chars)
|
22
|
+
# If no pieces in hand, return the standardized empty indicator
|
23
|
+
return NoPieces if piece_chars.empty?
|
24
|
+
|
25
|
+
# Validate each piece character according to the FEEN specification
|
26
|
+
validated_chars = validate_piece_chars(piece_chars)
|
27
|
+
|
28
|
+
# Sort pieces in ASCII lexicographic order and join them
|
29
|
+
validated_chars.sort.join
|
31
30
|
end
|
32
31
|
|
33
|
-
# Validates
|
32
|
+
# Validates all piece characters according to FEEN specification
|
34
33
|
#
|
35
|
-
# @param
|
36
|
-
# @
|
37
|
-
# @
|
38
|
-
def self.
|
39
|
-
|
40
|
-
|
41
|
-
raise ArgumentError, format(ERRORS[:invalid_piece], index) unless piece.is_a?(Hash) && piece.key?(:id)
|
42
|
-
|
43
|
-
# Validate piece ID
|
44
|
-
unless piece[:id].is_a?(String) && piece[:id].match?(/\A[a-zA-Z]\z/)
|
45
|
-
raise ArgumentError, format(ERRORS[:invalid_id], index)
|
46
|
-
end
|
47
|
-
|
48
|
-
# Ensure no prefix or suffix is present
|
49
|
-
raise ArgumentError, format(ERRORS[:prefix_not_allowed], index) if piece.key?(:prefix) && !piece[:prefix].nil?
|
50
|
-
|
51
|
-
raise ArgumentError, format(ERRORS[:suffix_not_allowed], index) if piece.key?(:suffix) && !piece[:suffix].nil?
|
34
|
+
# @param piece_chars [Array<Object>] Array of piece character candidates
|
35
|
+
# @return [Array<String>] Array of validated piece characters
|
36
|
+
# @raise [ArgumentError] If any piece character is invalid
|
37
|
+
private_class_method def self.validate_piece_chars(piece_chars)
|
38
|
+
piece_chars.each_with_index.map do |char, index|
|
39
|
+
validate_piece_char(char, index)
|
52
40
|
end
|
53
41
|
end
|
42
|
+
|
43
|
+
# Validates a single piece character according to FEEN specification
|
44
|
+
#
|
45
|
+
# @param char [Object] Piece character candidate
|
46
|
+
# @param index [Integer] Index of the character in the original array
|
47
|
+
# @return [String] Validated piece character
|
48
|
+
# @raise [ArgumentError] If the piece character is invalid
|
49
|
+
private_class_method def self.validate_piece_char(char, index)
|
50
|
+
# Validate type
|
51
|
+
unless char.is_a?(::String)
|
52
|
+
raise ::ArgumentError, format(
|
53
|
+
Errors[:invalid_type],
|
54
|
+
index: index,
|
55
|
+
type: char.class
|
56
|
+
)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Validate format (single alphabetic character)
|
60
|
+
unless char.match?(/\A[a-zA-Z]\z/)
|
61
|
+
raise ::ArgumentError, format(
|
62
|
+
Errors[:invalid_format],
|
63
|
+
index: index,
|
64
|
+
value: char
|
65
|
+
)
|
66
|
+
end
|
67
|
+
|
68
|
+
char
|
69
|
+
end
|
54
70
|
end
|
55
71
|
end
|
56
72
|
end
|
data/lib/feen/dumper.rb
CHANGED
@@ -5,51 +5,82 @@ require_relative File.join("dumper", "piece_placement")
|
|
5
5
|
require_relative File.join("dumper", "pieces_in_hand")
|
6
6
|
|
7
7
|
module Feen
|
8
|
-
# Module responsible for converting internal data structures to FEEN notation strings
|
8
|
+
# Module responsible for converting internal data structures to FEEN notation strings.
|
9
|
+
# This implements the serialization part of the FEEN (Format for Encounter & Entertainment Notation) format.
|
9
10
|
module Dumper
|
10
|
-
#
|
11
|
+
# Field separator used between the three main components of FEEN notation
|
12
|
+
FIELD_SEPARATOR = " "
|
13
|
+
|
14
|
+
# Error messages for validation
|
15
|
+
ERRORS = {
|
16
|
+
invalid_piece_placement_type: "Piece placement must be an Array, got %s",
|
17
|
+
invalid_games_turn_type: "Games turn must be an Array with exactly two elements, got %s",
|
18
|
+
invalid_pieces_in_hand_type: "Pieces in hand must be an Array, got %s"
|
19
|
+
}.freeze
|
20
|
+
|
21
|
+
# Converts position components to a complete FEEN string.
|
22
|
+
#
|
23
|
+
# @example Creating a FEEN string for chess initial position
|
24
|
+
# Feen::Dumper.dump(
|
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
|
+
# pieces_in_hand: [],
|
36
|
+
# games_turn: ["CHESS", "chess"]
|
37
|
+
# )
|
38
|
+
# # => "rnbqk=bnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK=BNR - CHESS/chess"
|
11
39
|
#
|
12
|
-
# @param
|
13
|
-
#
|
14
|
-
#
|
15
|
-
# @
|
16
|
-
#
|
17
|
-
|
18
|
-
|
40
|
+
# @param piece_placement [Array] Board position data structure representing the spatial
|
41
|
+
# distribution of pieces across the board, where each cell
|
42
|
+
# is represented by a String (or empty string for empty cells)
|
43
|
+
# @param pieces_in_hand [Array<String>] Pieces available for dropping onto the board,
|
44
|
+
# each represented as a single character string
|
45
|
+
# @param games_turn [Array<String>] A two-element array where the first element is the
|
46
|
+
# active player's variant and the second is the inactive player's variant
|
47
|
+
# @return [String] Complete FEEN string representation compliant with the specification
|
48
|
+
# @raise [ArgumentError] If any input parameter is invalid
|
49
|
+
# @see https://sashite.dev/documents/feen/1.0.0/ FEEN Specification v1.0.0
|
50
|
+
def self.dump(piece_placement:, pieces_in_hand:, games_turn:)
|
51
|
+
# Validate input types
|
52
|
+
validate_inputs(piece_placement, games_turn, pieces_in_hand)
|
19
53
|
|
54
|
+
# Process each component with appropriate submodule and combine into final FEEN string
|
20
55
|
[
|
21
|
-
PiecePlacement.dump(
|
22
|
-
|
23
|
-
|
24
|
-
].join(
|
56
|
+
PiecePlacement.dump(piece_placement),
|
57
|
+
PiecesInHand.dump(*pieces_in_hand),
|
58
|
+
GamesTurn.dump(*games_turn)
|
59
|
+
].join(FIELD_SEPARATOR)
|
25
60
|
end
|
26
61
|
|
27
|
-
# Validates the
|
62
|
+
# Validates the input parameters for type and structure
|
28
63
|
#
|
29
|
-
# @param
|
30
|
-
# @
|
64
|
+
# @param piece_placement [Object] Piece placement parameter to validate
|
65
|
+
# @param games_turn [Object] Games turn parameter to validate
|
66
|
+
# @param pieces_in_hand [Object] Pieces in hand parameter to validate
|
67
|
+
# @raise [ArgumentError] If any parameter is invalid
|
31
68
|
# @return [void]
|
32
|
-
def self.
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
required_keys = %i[piece_placement games_turn pieces_in_hand]
|
37
|
-
missing_keys = required_keys - position.keys
|
38
|
-
|
39
|
-
raise ArgumentError, "Missing required keys in position: #{missing_keys.join(', ')}" unless missing_keys.empty?
|
40
|
-
|
41
|
-
# Validate types of values
|
42
|
-
unless position[:piece_placement].is_a?(Array)
|
43
|
-
raise ArgumentError, "piece_placement must be an Array, got #{position[:piece_placement].class}"
|
69
|
+
private_class_method def self.validate_inputs(piece_placement, games_turn, pieces_in_hand)
|
70
|
+
# Validate piece_placement is an Array
|
71
|
+
unless piece_placement.is_a?(Array)
|
72
|
+
raise ArgumentError, format(ERRORS[:invalid_piece_placement_type], piece_placement.class)
|
44
73
|
end
|
45
74
|
|
46
|
-
|
47
|
-
|
75
|
+
# Validate games_turn is an Array with exactly 2 elements
|
76
|
+
unless games_turn.is_a?(Array) && games_turn.size == 2
|
77
|
+
raise ArgumentError, format(ERRORS[:invalid_games_turn_type], games_turn.inspect)
|
48
78
|
end
|
49
79
|
|
50
|
-
|
80
|
+
# Validate pieces_in_hand is an Array
|
81
|
+
return if pieces_in_hand.is_a?(Array)
|
51
82
|
|
52
|
-
raise ArgumentError,
|
83
|
+
raise ArgumentError, format(ERRORS[:invalid_pieces_in_hand_type], pieces_in_hand.class)
|
53
84
|
end
|
54
85
|
end
|
55
86
|
end
|
@@ -0,0 +1,14 @@
|
|
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
|
@@ -0,0 +1,24 @@
|
|
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
|