sashite-feen 0.1.0 → 0.3.0
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 +412 -137
- data/lib/sashite/feen/dumper/piece_placement.rb +144 -64
- data/lib/sashite/feen/dumper/pieces_in_hand.rb +124 -34
- data/lib/sashite/feen/dumper/style_turn.rb +29 -45
- data/lib/sashite/feen/dumper.rb +40 -30
- data/lib/sashite/feen/error.rb +72 -29
- data/lib/sashite/feen/hands.rb +62 -20
- data/lib/sashite/feen/parser/piece_placement.rb +324 -118
- data/lib/sashite/feen/parser/pieces_in_hand.rb +210 -44
- data/lib/sashite/feen/parser/style_turn.rb +81 -41
- data/lib/sashite/feen/parser.rb +60 -15
- data/lib/sashite/feen/placement.rb +295 -19
- data/lib/sashite/feen/position.rb +64 -13
- data/lib/sashite/feen/styles.rb +54 -57
- data/lib/sashite/feen.rb +57 -96
- metadata +1 -2
- data/lib/sashite/feen/ordering.rb +0 -16
|
@@ -3,83 +3,163 @@
|
|
|
3
3
|
module Sashite
|
|
4
4
|
module Feen
|
|
5
5
|
module Dumper
|
|
6
|
+
# Dumper for the piece placement field (first field of FEEN).
|
|
7
|
+
#
|
|
8
|
+
# Converts a Placement object into its FEEN string representation,
|
|
9
|
+
# encoding board configuration using EPIN notation with:
|
|
10
|
+
# - Empty square compression (consecutive nils → numbers)
|
|
11
|
+
# - Exact separator preservation (from Placement.separators)
|
|
12
|
+
# - Support for any irregular board structure
|
|
13
|
+
#
|
|
14
|
+
# The dumper produces canonical FEEN strings that enable perfect
|
|
15
|
+
# round-trip conversion (dump → parse → dump).
|
|
16
|
+
#
|
|
17
|
+
# @see https://sashite.dev/specs/feen/1.0.0/
|
|
6
18
|
module PiecePlacement
|
|
7
|
-
#
|
|
8
|
-
RANK_SEPARATOR = "/"
|
|
9
|
-
|
|
10
|
-
module_function
|
|
11
|
-
|
|
12
|
-
# Dump a Placement grid to FEEN ranks (e.g., "rnbqkbnr/pppppppp/8/...")
|
|
19
|
+
# Dump a Placement object into its FEEN piece placement string.
|
|
13
20
|
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
21
|
+
# Process:
|
|
22
|
+
# 1. For 1D boards: dump single rank directly
|
|
23
|
+
# 2. For multi-D boards: interleave ranks with their separators
|
|
24
|
+
# 3. Compress consecutive empty squares into numbers
|
|
25
|
+
# 4. Convert pieces to EPIN strings
|
|
26
|
+
#
|
|
27
|
+
# @param placement [Placement] The board placement object
|
|
28
|
+
# @return [String] FEEN piece placement field string
|
|
29
|
+
#
|
|
30
|
+
# @example Chess starting position
|
|
31
|
+
# dump(placement)
|
|
32
|
+
# # => "+rnbq+kbn+r/+p+p+p+p+p+p+p+p/8/8/8/8/+P+P+P+P+P+P+P+P/+RNBQ+KBN+R"
|
|
33
|
+
#
|
|
34
|
+
# @example Empty 8x8 board
|
|
35
|
+
# dump(placement)
|
|
36
|
+
# # => "8/8/8/8/8/8/8/8"
|
|
37
|
+
#
|
|
38
|
+
# @example 1D board
|
|
39
|
+
# dump(placement)
|
|
40
|
+
# # => "K2P3k"
|
|
41
|
+
#
|
|
42
|
+
# @example Irregular 3D board
|
|
43
|
+
# dump(placement)
|
|
44
|
+
# # => "5/5//5/5/5"
|
|
45
|
+
#
|
|
46
|
+
# @example Very large board
|
|
47
|
+
# dump(placement)
|
|
48
|
+
# # => "100/100/100"
|
|
49
|
+
def self.dump(placement)
|
|
50
|
+
# Special case: 1D board (no separators)
|
|
51
|
+
return dump_rank(placement.ranks[0]) if placement.one_dimensional?
|
|
52
|
+
|
|
53
|
+
# Multi-dimensional: interleave ranks and separators
|
|
54
|
+
dump_multi_dimensional(placement)
|
|
39
55
|
end
|
|
40
56
|
|
|
41
|
-
#
|
|
57
|
+
# Dump multi-dimensional placement.
|
|
58
|
+
#
|
|
59
|
+
# Alternates between ranks and separators:
|
|
60
|
+
# rank[0] + sep[0] + rank[1] + sep[1] + ... + rank[n]
|
|
61
|
+
#
|
|
62
|
+
# @param placement [Placement] Placement object
|
|
63
|
+
# @return [String] FEEN string with separators
|
|
64
|
+
#
|
|
65
|
+
# @example 2D board
|
|
66
|
+
# dump_multi_dimensional(placement)
|
|
67
|
+
# # => "r1/r2/r3"
|
|
68
|
+
#
|
|
69
|
+
# @example 3D board with mixed separators
|
|
70
|
+
# dump_multi_dimensional(placement)
|
|
71
|
+
# # => "r1/r2//r3"
|
|
72
|
+
private_class_method def self.dump_multi_dimensional(placement)
|
|
73
|
+
result = []
|
|
74
|
+
|
|
75
|
+
placement.ranks.each_with_index do |rank, idx|
|
|
76
|
+
# Dump the rank
|
|
77
|
+
result << dump_rank(rank)
|
|
78
|
+
|
|
79
|
+
# Add separator if not last rank
|
|
80
|
+
result << placement.separators[idx] if idx < placement.separators.size
|
|
81
|
+
end
|
|
42
82
|
|
|
43
|
-
|
|
44
|
-
def _empty_cell?(cell)
|
|
45
|
-
cell.nil? || cell == ""
|
|
83
|
+
result.join
|
|
46
84
|
end
|
|
47
|
-
private_class_method :_empty_cell?
|
|
48
|
-
|
|
49
|
-
def _dump_row(row, r_idx)
|
|
50
|
-
out = +""
|
|
51
|
-
empty_run = 0
|
|
52
|
-
|
|
53
|
-
row.each_with_index do |cell, c_idx|
|
|
54
|
-
if _empty_cell?(cell)
|
|
55
|
-
empty_run += 1
|
|
56
|
-
next
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
if empty_run.positive?
|
|
60
|
-
out << empty_run.to_s
|
|
61
|
-
empty_run = 0
|
|
62
|
-
end
|
|
63
85
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
86
|
+
# Dump a single rank into its FEEN representation.
|
|
87
|
+
#
|
|
88
|
+
# Converts a rank (array of pieces and nils) into FEEN notation by:
|
|
89
|
+
# 1. Converting pieces to EPIN strings (via piece.to_s)
|
|
90
|
+
# 2. Compressing consecutive nils into number strings
|
|
91
|
+
#
|
|
92
|
+
# Algorithm:
|
|
93
|
+
# - Iterate through rank squares
|
|
94
|
+
# - Count consecutive nils (empty_count)
|
|
95
|
+
# - When hitting a piece: flush count (if > 0), add piece
|
|
96
|
+
# - At end: flush final count (if > 0)
|
|
97
|
+
#
|
|
98
|
+
# @param rank [Array] Array of piece objects and nils
|
|
99
|
+
# @return [String] FEEN rank string
|
|
100
|
+
#
|
|
101
|
+
# @example Rank with pieces only
|
|
102
|
+
# dump_rank([K, Q, R, B])
|
|
103
|
+
# # => "KQRB"
|
|
104
|
+
#
|
|
105
|
+
# @example Rank with empty squares
|
|
106
|
+
# dump_rank([K, nil, nil, Q])
|
|
107
|
+
# # => "K2Q"
|
|
108
|
+
#
|
|
109
|
+
# @example Rank all empty
|
|
110
|
+
# dump_rank([nil, nil, nil, nil, nil, nil, nil, nil])
|
|
111
|
+
# # => "8"
|
|
112
|
+
#
|
|
113
|
+
# @example Very large empty count
|
|
114
|
+
# dump_rank(Array.new(100, nil))
|
|
115
|
+
# # => "100"
|
|
116
|
+
#
|
|
117
|
+
# @example Complex rank
|
|
118
|
+
# dump_rank([+K, nil, nil, -p', nil, R])
|
|
119
|
+
# # => "+K2-p'1R"
|
|
120
|
+
private_class_method def self.dump_rank(rank)
|
|
121
|
+
result = []
|
|
122
|
+
empty_count = 0
|
|
123
|
+
|
|
124
|
+
rank.each do |square|
|
|
125
|
+
if square.nil?
|
|
126
|
+
# Empty square: increment counter
|
|
127
|
+
empty_count += 1
|
|
128
|
+
else
|
|
129
|
+
# Piece: flush empty count, add piece
|
|
130
|
+
flush_empty_count!(result, empty_count)
|
|
131
|
+
result << square.to_s
|
|
132
|
+
empty_count = 0
|
|
69
133
|
end
|
|
70
134
|
end
|
|
71
135
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
end
|
|
75
|
-
private_class_method :_dump_row
|
|
136
|
+
# Flush final empty count
|
|
137
|
+
flush_empty_count!(result, empty_count)
|
|
76
138
|
|
|
77
|
-
|
|
78
|
-
|
|
139
|
+
result.join
|
|
140
|
+
end
|
|
79
141
|
|
|
80
|
-
|
|
142
|
+
# Flush accumulated empty count to result array.
|
|
143
|
+
#
|
|
144
|
+
# If empty_count > 0, appends the number as a string.
|
|
145
|
+
# This enables compression of consecutive empty squares.
|
|
146
|
+
#
|
|
147
|
+
# @param result [Array<String>] Result array being built
|
|
148
|
+
# @param empty_count [Integer] Number of consecutive empty squares
|
|
149
|
+
# @return [void]
|
|
150
|
+
#
|
|
151
|
+
# @example Flush count
|
|
152
|
+
# result = ["K"]
|
|
153
|
+
# flush_empty_count!(result, 5)
|
|
154
|
+
# result # => ["K", "5"]
|
|
155
|
+
#
|
|
156
|
+
# @example No flush (zero count)
|
|
157
|
+
# result = ["K"]
|
|
158
|
+
# flush_empty_count!(result, 0)
|
|
159
|
+
# result # => ["K"]
|
|
160
|
+
private_class_method def self.flush_empty_count!(result, empty_count)
|
|
161
|
+
result << empty_count.to_s if empty_count > 0
|
|
81
162
|
end
|
|
82
|
-
private_class_method :_coerce_placement
|
|
83
163
|
end
|
|
84
164
|
end
|
|
85
165
|
end
|
|
@@ -3,56 +3,146 @@
|
|
|
3
3
|
module Sashite
|
|
4
4
|
module Feen
|
|
5
5
|
module Dumper
|
|
6
|
+
# Dumper for the pieces-in-hand field (second field of FEEN).
|
|
7
|
+
#
|
|
8
|
+
# Converts a Hands object into its FEEN string representation,
|
|
9
|
+
# encoding captured pieces held by each player in canonical sorted order.
|
|
10
|
+
#
|
|
11
|
+
# @see https://sashite.dev/specs/feen/1.0.0/
|
|
6
12
|
module PiecesInHand
|
|
7
|
-
#
|
|
8
|
-
|
|
13
|
+
# Player separator in pieces-in-hand field.
|
|
14
|
+
PLAYER_SEPARATOR = "/"
|
|
9
15
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
#
|
|
16
|
+
# Dump a Hands object into its FEEN pieces-in-hand string.
|
|
17
|
+
#
|
|
18
|
+
# Generates canonical representation with pieces sorted according to
|
|
19
|
+
# FEEN ordering rules: by quantity (descending), base letter (ascending),
|
|
20
|
+
# case (uppercase first), prefix (-, +, none), and suffix (none, ').
|
|
21
|
+
#
|
|
22
|
+
# @param hands [Hands] The hands object containing pieces for both players
|
|
23
|
+
# @return [String] FEEN pieces-in-hand field string
|
|
24
|
+
#
|
|
25
|
+
# @example No pieces in hand
|
|
26
|
+
# dump(hands)
|
|
27
|
+
# # => "/"
|
|
13
28
|
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
29
|
+
# @example First player has pieces
|
|
30
|
+
# dump(hands)
|
|
31
|
+
# # => "2P/p"
|
|
17
32
|
#
|
|
18
|
-
# @
|
|
19
|
-
#
|
|
20
|
-
|
|
21
|
-
|
|
33
|
+
# @example Both players have pieces
|
|
34
|
+
# dump(hands)
|
|
35
|
+
# # => "RBN/2p"
|
|
36
|
+
def self.dump(hands)
|
|
37
|
+
first_player_str = dump_player_pieces(hands.first_player)
|
|
38
|
+
second_player_str = dump_player_pieces(hands.second_player)
|
|
22
39
|
|
|
23
|
-
|
|
24
|
-
|
|
40
|
+
"#{first_player_str}#{PLAYER_SEPARATOR}#{second_player_str}"
|
|
41
|
+
end
|
|
25
42
|
|
|
26
|
-
|
|
43
|
+
# Dump pieces for a single player.
|
|
44
|
+
#
|
|
45
|
+
# Groups identical pieces, counts them, sorts canonically, and formats
|
|
46
|
+
# with count prefix when needed (e.g., "3P" for three pawns).
|
|
47
|
+
#
|
|
48
|
+
# @param pieces [Array] Array of piece objects for one player
|
|
49
|
+
# @return [String] Formatted piece string (empty if no pieces)
|
|
50
|
+
#
|
|
51
|
+
# @example Single piece types
|
|
52
|
+
# dump_player_pieces([pawn1, pawn2, pawn3, rook1])
|
|
53
|
+
# # => "3PR"
|
|
54
|
+
#
|
|
55
|
+
# @example Empty hand
|
|
56
|
+
# dump_player_pieces([])
|
|
57
|
+
# # => ""
|
|
58
|
+
private_class_method def self.dump_player_pieces(pieces)
|
|
59
|
+
return "" if pieces.empty?
|
|
27
60
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
61
|
+
grouped = group_pieces(pieces)
|
|
62
|
+
sorted = sort_grouped_pieces(grouped)
|
|
63
|
+
format_pieces(sorted)
|
|
64
|
+
end
|
|
31
65
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
66
|
+
# Group identical pieces and count occurrences.
|
|
67
|
+
#
|
|
68
|
+
# @param pieces [Array] Array of piece objects
|
|
69
|
+
# @return [Hash] Hash mapping piece strings to counts
|
|
70
|
+
#
|
|
71
|
+
# @example
|
|
72
|
+
# group_pieces([pawn1, pawn2, rook1])
|
|
73
|
+
# # => {"P" => 2, "R" => 1}
|
|
74
|
+
private_class_method def self.group_pieces(pieces)
|
|
75
|
+
pieces.group_by(&:to_s).transform_values(&:size)
|
|
76
|
+
end
|
|
37
77
|
|
|
38
|
-
|
|
78
|
+
# Sort grouped pieces according to FEEN canonical ordering.
|
|
79
|
+
#
|
|
80
|
+
# Sorting rules (in order of precedence):
|
|
81
|
+
# 1. By quantity (descending) - most pieces first
|
|
82
|
+
# 2. By base letter (ascending, case-insensitive)
|
|
83
|
+
# 3. By case - uppercase before lowercase
|
|
84
|
+
# 4. By prefix - "-", "+", then none
|
|
85
|
+
# 5. By suffix - none, then "'"
|
|
86
|
+
#
|
|
87
|
+
# @param grouped [Hash] Hash of piece strings to counts
|
|
88
|
+
# @return [Array<Array>] Sorted array of [piece_string, count] pairs
|
|
89
|
+
#
|
|
90
|
+
# @example
|
|
91
|
+
# sort_grouped_pieces({"p" => 2, "P" => 3, "R" => 1, "+K" => 1, "K'" => 1})
|
|
92
|
+
# # => [["+K", 1], ["K'", 1], ["P", 3], ["p", 2], ["R", 1]]
|
|
93
|
+
private_class_method def self.sort_grouped_pieces(grouped)
|
|
94
|
+
grouped.sort_by do |piece_str, count|
|
|
95
|
+
[
|
|
96
|
+
-count, # Quantity (descending)
|
|
97
|
+
extract_base_letter(piece_str), # Base letter (ascending)
|
|
98
|
+
piece_str.match?(/[A-Z]/) ? 0 : 1, # Case (uppercase first)
|
|
99
|
+
prefix_order(piece_str), # Prefix order
|
|
100
|
+
piece_str.end_with?("'") ? 1 : 0 # Suffix order (none first)
|
|
101
|
+
]
|
|
39
102
|
end
|
|
103
|
+
end
|
|
40
104
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
105
|
+
# Extract base letter from piece string (without modifiers).
|
|
106
|
+
#
|
|
107
|
+
# @param piece_str [String] EPIN piece string
|
|
108
|
+
# @return [String] Uppercase base letter
|
|
109
|
+
#
|
|
110
|
+
# @example
|
|
111
|
+
# extract_base_letter("+K'") # => "K"
|
|
112
|
+
# extract_base_letter("-p") # => "P"
|
|
113
|
+
private_class_method def self.extract_base_letter(piece_str)
|
|
114
|
+
piece_str.gsub(/[+\-']/, "").upcase
|
|
46
115
|
end
|
|
47
116
|
|
|
48
|
-
#
|
|
117
|
+
# Determine prefix sorting order.
|
|
118
|
+
#
|
|
119
|
+
# @param piece_str [String] EPIN piece string
|
|
120
|
+
# @return [Integer] Sort order (0 for "-", 1 for "+", 2 for none)
|
|
121
|
+
#
|
|
122
|
+
# @example
|
|
123
|
+
# prefix_order("-K") # => 0
|
|
124
|
+
# prefix_order("+K") # => 1
|
|
125
|
+
# prefix_order("K") # => 2
|
|
126
|
+
private_class_method def self.prefix_order(piece_str)
|
|
127
|
+
return 0 if piece_str.start_with?("-")
|
|
128
|
+
return 1 if piece_str.start_with?("+")
|
|
49
129
|
|
|
50
|
-
|
|
51
|
-
|
|
130
|
+
2
|
|
131
|
+
end
|
|
52
132
|
|
|
53
|
-
|
|
133
|
+
# Format sorted pieces with count prefixes.
|
|
134
|
+
#
|
|
135
|
+
# @param sorted [Array<Array>] Sorted array of [piece_string, count] pairs
|
|
136
|
+
# @return [String] Formatted piece string
|
|
137
|
+
#
|
|
138
|
+
# @example
|
|
139
|
+
# format_pieces([["P", 3], ["R", 1], ["p", 2]])
|
|
140
|
+
# # => "3PR2p"
|
|
141
|
+
private_class_method def self.format_pieces(sorted)
|
|
142
|
+
sorted.map do |piece_str, count|
|
|
143
|
+
count > 1 ? "#{count}#{piece_str}" : piece_str
|
|
144
|
+
end.join
|
|
54
145
|
end
|
|
55
|
-
private_class_method :_coerce_hands
|
|
56
146
|
end
|
|
57
147
|
end
|
|
58
148
|
end
|
|
@@ -3,56 +3,40 @@
|
|
|
3
3
|
module Sashite
|
|
4
4
|
module Feen
|
|
5
5
|
module Dumper
|
|
6
|
+
# Dumper for the style-turn field (third field of FEEN).
|
|
7
|
+
#
|
|
8
|
+
# Converts a Styles object into its FEEN string representation,
|
|
9
|
+
# encoding game styles and indicating the active player.
|
|
10
|
+
#
|
|
11
|
+
# @see https://sashite.dev/specs/feen/1.0.0/
|
|
6
12
|
module StyleTurn
|
|
7
|
-
#
|
|
8
|
-
|
|
9
|
-
# Separator between multiple style tokens
|
|
10
|
-
STYLES_SEPARATOR = ","
|
|
13
|
+
# Style separator in style-turn field.
|
|
14
|
+
STYLE_SEPARATOR = "/"
|
|
11
15
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
# Dump the style/turn field (e.g., "w", "b;rule1,variantX")
|
|
16
|
+
# Dump a Styles object into its FEEN style-turn string.
|
|
15
17
|
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
+
# Formats the active and inactive player styles with the active
|
|
19
|
+
# player's style appearing first. The case of each style identifier
|
|
20
|
+
# indicates which player uses it (uppercase = first player,
|
|
21
|
+
# lowercase = second player).
|
|
18
22
|
#
|
|
19
|
-
# @param styles [
|
|
20
|
-
# @return [String]
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"#{turn_str}#{TURN_STYLES_SEPARATOR}#{tokens.join(STYLES_SEPARATOR)}"
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# -- internals ---------------------------------------------------------
|
|
39
|
-
|
|
40
|
-
def _dump_turn(turn)
|
|
41
|
-
case turn
|
|
42
|
-
when :first then "w"
|
|
43
|
-
when :second then "b"
|
|
44
|
-
else
|
|
45
|
-
raise Error::Style, "invalid turn symbol #{turn.inspect}"
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
private_class_method :_dump_turn
|
|
49
|
-
|
|
50
|
-
def _coerce_styles(obj)
|
|
51
|
-
return obj if obj.is_a?(Styles)
|
|
52
|
-
|
|
53
|
-
raise TypeError, "expected Sashite::Feen::Styles, got #{obj.class}"
|
|
23
|
+
# @param styles [Styles] The styles object with active and inactive styles
|
|
24
|
+
# @return [String] FEEN style-turn field string
|
|
25
|
+
#
|
|
26
|
+
# @example Chess game, white to move
|
|
27
|
+
# dump(styles)
|
|
28
|
+
# # => "C/c"
|
|
29
|
+
#
|
|
30
|
+
# @example Chess game, black to move
|
|
31
|
+
# dump(styles)
|
|
32
|
+
# # => "c/C"
|
|
33
|
+
#
|
|
34
|
+
# @example Cross-style game, first player to move
|
|
35
|
+
# dump(styles)
|
|
36
|
+
# # => "C/m"
|
|
37
|
+
def self.dump(styles)
|
|
38
|
+
"#{styles.active}#{STYLE_SEPARATOR}#{styles.inactive}"
|
|
54
39
|
end
|
|
55
|
-
private_class_method :_coerce_styles
|
|
56
40
|
end
|
|
57
41
|
end
|
|
58
42
|
end
|
data/lib/sashite/feen/dumper.rb
CHANGED
|
@@ -1,49 +1,59 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# FEEN Dumper (entry point)
|
|
4
|
-
# -------------------------
|
|
5
|
-
# Serializes a Position object into its canonical FEEN string by delegating
|
|
6
|
-
# each field to its dedicated sub-dumper.
|
|
7
|
-
#
|
|
8
|
-
# Sub-dumpers:
|
|
9
|
-
# dumper/piece_placement.rb
|
|
10
|
-
# dumper/pieces_in_hand.rb
|
|
11
|
-
# dumper/style_turn.rb
|
|
12
|
-
|
|
13
3
|
require_relative "dumper/piece_placement"
|
|
14
4
|
require_relative "dumper/pieces_in_hand"
|
|
15
5
|
require_relative "dumper/style_turn"
|
|
16
6
|
|
|
17
7
|
module Sashite
|
|
18
8
|
module Feen
|
|
9
|
+
# Dumper for FEEN (Forsyth–Edwards Enhanced Notation) positions.
|
|
10
|
+
#
|
|
11
|
+
# Converts a Position object into its canonical FEEN string representation
|
|
12
|
+
# by delegating serialization to specialized dumpers for each component.
|
|
13
|
+
#
|
|
14
|
+
# @see https://sashite.dev/specs/feen/1.0.0/
|
|
19
15
|
module Dumper
|
|
20
|
-
#
|
|
16
|
+
# Field separator in FEEN notation.
|
|
21
17
|
FIELD_SEPARATOR = " "
|
|
22
18
|
|
|
23
|
-
|
|
19
|
+
# Number of fields in a FEEN string.
|
|
20
|
+
FIELD_COUNT = 3
|
|
24
21
|
|
|
25
|
-
# Dump a Position into
|
|
22
|
+
# Dump a Position object into its canonical FEEN string representation.
|
|
26
23
|
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
24
|
+
# Generates a deterministic FEEN string from a position object. The same
|
|
25
|
+
# position will always produce the same canonical string.
|
|
26
|
+
#
|
|
27
|
+
# @param position [Position] A position object with placement, hands, and styles
|
|
28
|
+
# @return [String] Canonical FEEN notation string
|
|
29
|
+
#
|
|
30
|
+
# @example Dump a position to FEEN
|
|
31
|
+
# feen_string = Dumper.dump(position)
|
|
32
|
+
# # => "+rnbq+kbn+r/+p+p+p+p+p+p+p+p/8/8/8/8/+P+P+P+P+P+P+P+P/+RNBQ+KBN+R / C/c"
|
|
33
|
+
def self.dump(position)
|
|
34
|
+
fields = [
|
|
35
|
+
Dumper::PiecePlacement.dump(position.placement),
|
|
36
|
+
Dumper::PiecesInHand.dump(position.hands),
|
|
37
|
+
Dumper::StyleTurn.dump(position.styles)
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
join_fields(fields)
|
|
37
41
|
end
|
|
38
42
|
|
|
39
|
-
#
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
# Join the three FEEN fields into a single string.
|
|
44
|
+
#
|
|
45
|
+
# Combines the piece placement, pieces in hand, and style-turn fields
|
|
46
|
+
# with the field separator.
|
|
47
|
+
#
|
|
48
|
+
# @param fields [Array<String>] Array of three field strings
|
|
49
|
+
# @return [String] Complete FEEN string
|
|
50
|
+
#
|
|
51
|
+
# @example Join three fields
|
|
52
|
+
# join_fields(["rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR", "/", "C/c"])
|
|
53
|
+
# # => "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR / C/c"
|
|
54
|
+
private_class_method def self.join_fields(fields)
|
|
55
|
+
fields.join(FIELD_SEPARATOR)
|
|
45
56
|
end
|
|
46
|
-
private_class_method :_coerce_position
|
|
47
57
|
end
|
|
48
58
|
end
|
|
49
59
|
end
|