sashite-feen 0.2.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 +436 -42
- data/lib/sashite/feen/dumper/piece_placement.rb +107 -50
- data/lib/sashite/feen/dumper/pieces_in_hand.rb +1 -0
- data/lib/sashite/feen/hands.rb +2 -2
- data/lib/sashite/feen/parser/piece_placement.rb +190 -176
- data/lib/sashite/feen/parser/pieces_in_hand.rb +7 -13
- data/lib/sashite/feen/parser.rb +11 -2
- data/lib/sashite/feen/placement.rb +260 -30
- metadata +1 -1
|
@@ -6,19 +6,23 @@ module Sashite
|
|
|
6
6
|
# Dumper for the piece placement field (first field of FEEN).
|
|
7
7
|
#
|
|
8
8
|
# Converts a Placement object into its FEEN string representation,
|
|
9
|
-
# encoding board configuration using EPIN notation with
|
|
10
|
-
# compression
|
|
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).
|
|
11
16
|
#
|
|
12
17
|
# @see https://sashite.dev/specs/feen/1.0.0/
|
|
13
18
|
module PiecePlacement
|
|
14
|
-
# Rank separator for 2D boards.
|
|
15
|
-
RANK_SEPARATOR = "/"
|
|
16
|
-
|
|
17
19
|
# Dump a Placement object into its FEEN piece placement string.
|
|
18
20
|
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
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
|
|
22
26
|
#
|
|
23
27
|
# @param placement [Placement] The board placement object
|
|
24
28
|
# @return [String] FEEN piece placement field string
|
|
@@ -30,78 +34,131 @@ module Sashite
|
|
|
30
34
|
# @example Empty 8x8 board
|
|
31
35
|
# dump(placement)
|
|
32
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"
|
|
33
49
|
def self.dump(placement)
|
|
34
|
-
|
|
35
|
-
|
|
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)
|
|
55
|
+
end
|
|
56
|
+
|
|
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
|
|
82
|
+
|
|
83
|
+
result.join
|
|
36
84
|
end
|
|
37
85
|
|
|
38
86
|
# Dump a single rank into its FEEN representation.
|
|
39
87
|
#
|
|
40
88
|
# Converts a rank (array of pieces and nils) into FEEN notation by:
|
|
41
|
-
# 1. Converting pieces to EPIN strings
|
|
42
|
-
# 2. Compressing consecutive nils into
|
|
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)
|
|
43
97
|
#
|
|
44
98
|
# @param rank [Array] Array of piece objects and nils
|
|
45
99
|
# @return [String] FEEN rank string
|
|
46
100
|
#
|
|
47
|
-
# @example Rank with pieces
|
|
48
|
-
# dump_rank([
|
|
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])
|
|
49
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"
|
|
50
120
|
private_class_method def self.dump_rank(rank)
|
|
51
121
|
result = []
|
|
52
122
|
empty_count = 0
|
|
53
123
|
|
|
54
124
|
rank.each do |square|
|
|
55
125
|
if square.nil?
|
|
126
|
+
# Empty square: increment counter
|
|
56
127
|
empty_count += 1
|
|
57
128
|
else
|
|
58
|
-
|
|
129
|
+
# Piece: flush empty count, add piece
|
|
130
|
+
flush_empty_count!(result, empty_count)
|
|
59
131
|
result << square.to_s
|
|
60
132
|
empty_count = 0
|
|
61
133
|
end
|
|
62
134
|
end
|
|
63
135
|
|
|
64
|
-
|
|
136
|
+
# Flush final empty count
|
|
137
|
+
flush_empty_count!(result, empty_count)
|
|
138
|
+
|
|
65
139
|
result.join
|
|
66
140
|
end
|
|
67
141
|
|
|
68
|
-
#
|
|
142
|
+
# Flush accumulated empty count to result array.
|
|
69
143
|
#
|
|
70
|
-
#
|
|
144
|
+
# If empty_count > 0, appends the number as a string.
|
|
145
|
+
# This enables compression of consecutive empty squares.
|
|
71
146
|
#
|
|
72
|
-
# @param
|
|
73
|
-
# @param
|
|
74
|
-
# @
|
|
75
|
-
# @return [String] Complete piece placement string
|
|
147
|
+
# @param result [Array<String>] Result array being built
|
|
148
|
+
# @param empty_count [Integer] Number of consecutive empty squares
|
|
149
|
+
# @return [void]
|
|
76
150
|
#
|
|
77
|
-
# @example
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
#
|
|
81
|
-
#
|
|
82
|
-
#
|
|
83
|
-
#
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
ranks.join(separator)
|
|
89
|
-
else
|
|
90
|
-
# Multi-dimensional with section info
|
|
91
|
-
rank_separator = RANK_SEPARATOR
|
|
92
|
-
section_separator = RANK_SEPARATOR * (dimension - 1)
|
|
93
|
-
|
|
94
|
-
# Group ranks by sections
|
|
95
|
-
result = []
|
|
96
|
-
offset = 0
|
|
97
|
-
sections.each do |section_size|
|
|
98
|
-
section_ranks = ranks[offset, section_size]
|
|
99
|
-
result << section_ranks.join(rank_separator)
|
|
100
|
-
offset += section_size
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
result.join(section_separator)
|
|
104
|
-
end
|
|
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
|
|
105
162
|
end
|
|
106
163
|
end
|
|
107
164
|
end
|
data/lib/sashite/feen/hands.rb
CHANGED
|
@@ -30,8 +30,8 @@ module Sashite
|
|
|
30
30
|
# @example Both players have captured pieces
|
|
31
31
|
# hands = Hands.new([rook, bishop], [pawn1, pawn2, knight])
|
|
32
32
|
def initialize(first_player, second_player)
|
|
33
|
-
@first_player = first_player.freeze
|
|
34
|
-
@second_player = second_player.freeze
|
|
33
|
+
@first_player = first_player.sort_by(&:to_s).freeze
|
|
34
|
+
@second_player = second_player.sort_by(&:to_s).freeze
|
|
35
35
|
|
|
36
36
|
freeze
|
|
37
37
|
end
|