sashite-feen 0.3.0 → 0.5.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 +243 -418
- data/lib/sashite/feen/dumper/hands.rb +105 -0
- data/lib/sashite/feen/dumper/piece_placement.rb +75 -143
- data/lib/sashite/feen/dumper/style_turn.rb +15 -30
- data/lib/sashite/feen/dumper.rb +12 -42
- data/lib/sashite/feen/error.rb +5 -73
- data/lib/sashite/feen/errors/cardinality_error.rb +18 -0
- data/lib/sashite/feen/errors/hands_error.rb +20 -0
- data/lib/sashite/feen/errors/parse_error.rb +20 -0
- data/lib/sashite/feen/errors/piece_placement_error.rb +25 -0
- data/lib/sashite/feen/errors/style_turn_error.rb +18 -0
- data/lib/sashite/feen/parser/hand.rb +243 -0
- data/lib/sashite/feen/parser/hands.rb +102 -0
- data/lib/sashite/feen/parser/piece_placement.rb +422 -311
- data/lib/sashite/feen/parser/style_turn.rb +80 -76
- data/lib/sashite/feen/parser.rb +187 -56
- data/lib/sashite/feen/shared/ascii.rb +52 -0
- data/lib/sashite/feen/shared/limits.rb +24 -0
- data/lib/sashite/feen/shared/separators.rb +18 -0
- data/lib/sashite/feen.rb +23 -50
- data/lib/sashite-feen.rb +0 -11
- metadata +22 -38
- data/LICENSE.md +0 -21
- data/lib/sashite/feen/dumper/pieces_in_hand.rb +0 -149
- data/lib/sashite/feen/hands.rb +0 -80
- data/lib/sashite/feen/parser/pieces_in_hand.rb +0 -243
- data/lib/sashite/feen/placement.rb +0 -307
- data/lib/sashite/feen/position.rb +0 -77
- data/lib/sashite/feen/styles.rb +0 -72
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../shared/ascii"
|
|
4
|
+
|
|
5
|
+
module Sashite
|
|
6
|
+
module Feen
|
|
7
|
+
module Dumper
|
|
8
|
+
# Serializer for the FEEN Hands field (Field 2).
|
|
9
|
+
#
|
|
10
|
+
# Converts hand maps (piece → count) into the canonical FEEN string.
|
|
11
|
+
#
|
|
12
|
+
# @api private
|
|
13
|
+
module Hands
|
|
14
|
+
# Serializes hands to a FEEN Hands field string.
|
|
15
|
+
#
|
|
16
|
+
# @param first_player_hand [Hash{String => Integer}] First player's pieces
|
|
17
|
+
# @param second_player_hand [Hash{String => Integer}] Second player's pieces
|
|
18
|
+
# @return [String] Canonical Hands field string
|
|
19
|
+
def self.dump(first_player_hand, second_player_hand)
|
|
20
|
+
"#{dump_hand(first_player_hand)}/#{dump_hand(second_player_hand)}"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class << self
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def dump_hand(hand)
|
|
27
|
+
return "" if hand.empty?
|
|
28
|
+
|
|
29
|
+
items = hand.to_a
|
|
30
|
+
|
|
31
|
+
items.sort! do |a, b|
|
|
32
|
+
# a = [piece, count], b = [piece, count]
|
|
33
|
+
cmp = b[1] <=> a[1]
|
|
34
|
+
cmp == 0 ? compare_pieces(a[0], b[0]) : cmp
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
result = String.new
|
|
38
|
+
items.each do |piece, count|
|
|
39
|
+
result << count.to_s if count >= 2
|
|
40
|
+
result << piece
|
|
41
|
+
end
|
|
42
|
+
result
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Compares two EPIN strings according to canonical ordering.
|
|
46
|
+
def compare_pieces(str_a, str_b)
|
|
47
|
+
pos_a = 0
|
|
48
|
+
byte_a = str_a.getbyte(pos_a)
|
|
49
|
+
|
|
50
|
+
if byte_a == Ascii::MINUS
|
|
51
|
+
state_a = 0; pos_a += 1; byte_a = str_a.getbyte(pos_a)
|
|
52
|
+
elsif byte_a == Ascii::PLUS
|
|
53
|
+
state_a = 1; pos_a += 1; byte_a = str_a.getbyte(pos_a)
|
|
54
|
+
else
|
|
55
|
+
state_a = 2
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
pos_b = 0
|
|
59
|
+
byte_b = str_b.getbyte(pos_b)
|
|
60
|
+
|
|
61
|
+
if byte_b == Ascii::MINUS
|
|
62
|
+
state_b = 0; pos_b += 1; byte_b = str_b.getbyte(pos_b)
|
|
63
|
+
elsif byte_b == Ascii::PLUS
|
|
64
|
+
state_b = 1; pos_b += 1; byte_b = str_b.getbyte(pos_b)
|
|
65
|
+
else
|
|
66
|
+
state_b = 2
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# 2. By base letter -- case-insensitive
|
|
70
|
+
cmp = (byte_a | 0x20) <=> (byte_b | 0x20)
|
|
71
|
+
return cmp unless cmp == 0
|
|
72
|
+
|
|
73
|
+
# 3. By letter case -- uppercase before lowercase
|
|
74
|
+
cmp = (byte_a < Ascii::LOWER_A ? 0 : 1) <=> (byte_b < Ascii::LOWER_A ? 0 : 1)
|
|
75
|
+
return cmp unless cmp == 0
|
|
76
|
+
|
|
77
|
+
# 4. By state modifier
|
|
78
|
+
cmp = state_a <=> state_b
|
|
79
|
+
return cmp unless cmp == 0
|
|
80
|
+
|
|
81
|
+
pos_a += 1; byte_a = str_a.getbyte(pos_a)
|
|
82
|
+
pos_b += 1; byte_b = str_b.getbyte(pos_b)
|
|
83
|
+
|
|
84
|
+
# 5. By terminal marker
|
|
85
|
+
ta = byte_a == Ascii::CARET ? 1 : 0
|
|
86
|
+
tb = byte_b == Ascii::CARET ? 1 : 0
|
|
87
|
+
cmp = ta <=> tb
|
|
88
|
+
return cmp unless cmp == 0
|
|
89
|
+
|
|
90
|
+
if ta == 1; byte_a = str_a.getbyte(pos_a + 1); end
|
|
91
|
+
if tb == 1; byte_b = str_b.getbyte(pos_b + 1); end
|
|
92
|
+
|
|
93
|
+
# 6. By derivation marker
|
|
94
|
+
(byte_a == Ascii::APOSTROPHE ? 1 : 0) <=> (byte_b == Ascii::APOSTROPHE ? 1 : 0)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private_class_method :dump_hand,
|
|
99
|
+
:compare_pieces
|
|
100
|
+
|
|
101
|
+
freeze
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -3,163 +3,95 @@
|
|
|
3
3
|
module Sashite
|
|
4
4
|
module Feen
|
|
5
5
|
module Dumper
|
|
6
|
-
#
|
|
6
|
+
# Serializer for the FEEN Piece Placement field (Field 1).
|
|
7
7
|
#
|
|
8
|
-
# Converts a
|
|
9
|
-
#
|
|
10
|
-
# - Empty square compression (consecutive nils → numbers)
|
|
11
|
-
# - Exact separator preservation (from Placement.separators)
|
|
12
|
-
# - Support for any irregular board structure
|
|
8
|
+
# Converts a flat board + shape into the canonical FEEN string
|
|
9
|
+
# with run-length encoding and dimensional separators.
|
|
13
10
|
#
|
|
14
|
-
#
|
|
15
|
-
# round-trip conversion (dump → parse → dump).
|
|
16
|
-
#
|
|
17
|
-
# @see https://sashite.dev/specs/feen/1.0.0/
|
|
11
|
+
# @api private
|
|
18
12
|
module PiecePlacement
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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)
|
|
13
|
+
# Serializes a flat board to a FEEN Piece Placement field string.
|
|
14
|
+
#
|
|
15
|
+
# @param board [Array<String, nil>] Flat board (row-major order)
|
|
16
|
+
# @param shape [Array<Integer>] Board dimensions
|
|
17
|
+
# @return [String] Canonical Piece Placement string
|
|
18
|
+
def self.dump(board, shape)
|
|
19
|
+
case shape.size
|
|
20
|
+
when 1
|
|
21
|
+
result = String.new
|
|
22
|
+
dump_rank_into(result, board, 0, shape[0])
|
|
23
|
+
result
|
|
24
|
+
when 2
|
|
25
|
+
dump_2d(board, shape[0], shape[1])
|
|
26
|
+
when 3
|
|
27
|
+
dump_3d(board, shape[0], shape[1], shape[2])
|
|
28
|
+
end
|
|
55
29
|
end
|
|
56
30
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
31
|
+
class << self
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def dump_2d(board, ranks, files)
|
|
35
|
+
result = String.new
|
|
36
|
+
offset = 0
|
|
37
|
+
|
|
38
|
+
ranks.times do |i|
|
|
39
|
+
result << "/" if i > 0
|
|
40
|
+
dump_rank_into(result, board, offset, files)
|
|
41
|
+
offset += files
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
result
|
|
81
45
|
end
|
|
82
46
|
|
|
83
|
-
|
|
84
|
-
|
|
47
|
+
def dump_3d(board, layers, ranks, files)
|
|
48
|
+
result = String.new
|
|
49
|
+
offset = 0
|
|
85
50
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
|
51
|
+
layers.times do |li|
|
|
52
|
+
result << "//" if li > 0
|
|
53
|
+
|
|
54
|
+
ranks.times do |ri|
|
|
55
|
+
result << "/" if ri > 0
|
|
56
|
+
dump_rank_into(result, board, offset, files)
|
|
57
|
+
offset += files
|
|
58
|
+
end
|
|
133
59
|
end
|
|
60
|
+
|
|
61
|
+
result
|
|
134
62
|
end
|
|
135
63
|
|
|
136
|
-
#
|
|
137
|
-
|
|
64
|
+
# Appends a run-length-encoded rank into a buffer.
|
|
65
|
+
def dump_rank_into(result, board, offset, length)
|
|
66
|
+
empty_count = 0
|
|
67
|
+
stop = offset + length
|
|
138
68
|
|
|
139
|
-
|
|
140
|
-
|
|
69
|
+
while offset < stop
|
|
70
|
+
square = board[offset]
|
|
141
71
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
|
72
|
+
if square.nil?
|
|
73
|
+
empty_count += 1
|
|
74
|
+
else
|
|
75
|
+
if empty_count > 0
|
|
76
|
+
result << empty_count.to_s
|
|
77
|
+
empty_count = 0
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
result << square
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
offset += 1
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
result << empty_count.to_s if empty_count > 0
|
|
87
|
+
end
|
|
162
88
|
end
|
|
89
|
+
|
|
90
|
+
private_class_method :dump_2d,
|
|
91
|
+
:dump_3d,
|
|
92
|
+
:dump_rank_into
|
|
93
|
+
|
|
94
|
+
freeze
|
|
163
95
|
end
|
|
164
96
|
end
|
|
165
97
|
end
|
|
@@ -3,40 +3,25 @@
|
|
|
3
3
|
module Sashite
|
|
4
4
|
module Feen
|
|
5
5
|
module Dumper
|
|
6
|
-
#
|
|
6
|
+
# Serializer for the FEEN Style-Turn field (Field 3).
|
|
7
7
|
#
|
|
8
|
-
#
|
|
9
|
-
# encoding game styles and indicating the active player.
|
|
10
|
-
#
|
|
11
|
-
# @see https://sashite.dev/specs/feen/1.0.0/
|
|
8
|
+
# @api private
|
|
12
9
|
module StyleTurn
|
|
13
|
-
#
|
|
14
|
-
STYLE_SEPARATOR = "/"
|
|
15
|
-
|
|
16
|
-
# Dump a Styles object into its FEEN style-turn string.
|
|
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).
|
|
22
|
-
#
|
|
23
|
-
# @param styles [Styles] The styles object with active and inactive styles
|
|
24
|
-
# @return [String] FEEN style-turn field string
|
|
10
|
+
# Serializes styles and turn to a FEEN Style-Turn field string.
|
|
25
11
|
#
|
|
26
|
-
# @
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
# # => "C/m"
|
|
37
|
-
def self.dump(styles)
|
|
38
|
-
"#{styles.active}#{STYLE_SEPARATOR}#{styles.inactive}"
|
|
12
|
+
# @param first_player_style [String] SIN token for the first player
|
|
13
|
+
# @param second_player_style [String] SIN token for the second player
|
|
14
|
+
# @param turn [Symbol] :first or :second
|
|
15
|
+
# @return [String] Canonical Style-Turn field string
|
|
16
|
+
def self.dump(first_player_style, second_player_style, turn)
|
|
17
|
+
if turn == :first
|
|
18
|
+
"#{first_player_style}/#{second_player_style}"
|
|
19
|
+
else
|
|
20
|
+
"#{second_player_style}/#{first_player_style}"
|
|
21
|
+
end
|
|
39
22
|
end
|
|
23
|
+
|
|
24
|
+
freeze
|
|
40
25
|
end
|
|
41
26
|
end
|
|
42
27
|
end
|
data/lib/sashite/feen/dumper.rb
CHANGED
|
@@ -1,59 +1,29 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "dumper/piece_placement"
|
|
4
|
-
require_relative "dumper/
|
|
4
|
+
require_relative "dumper/hands"
|
|
5
5
|
require_relative "dumper/style_turn"
|
|
6
6
|
|
|
7
7
|
module Sashite
|
|
8
8
|
module Feen
|
|
9
|
-
#
|
|
9
|
+
# Serializer for FEEN strings. Converts a Qi position into a
|
|
10
|
+
# canonical FEEN string with three space-separated fields.
|
|
10
11
|
#
|
|
11
|
-
#
|
|
12
|
-
# by delegating serialization to specialized dumpers for each component.
|
|
13
|
-
#
|
|
14
|
-
# @see https://sashite.dev/specs/feen/1.0.0/
|
|
12
|
+
# @api private
|
|
15
13
|
module Dumper
|
|
16
|
-
#
|
|
17
|
-
FIELD_SEPARATOR = " "
|
|
18
|
-
|
|
19
|
-
# Number of fields in a FEEN string.
|
|
20
|
-
FIELD_COUNT = 3
|
|
21
|
-
|
|
22
|
-
# Dump a Position object into its canonical FEEN string representation.
|
|
23
|
-
#
|
|
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
|
|
14
|
+
# Serializes a Qi position to a canonical FEEN string.
|
|
29
15
|
#
|
|
30
|
-
# @
|
|
31
|
-
#
|
|
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"
|
|
16
|
+
# @param position [Qi] The position to serialize
|
|
17
|
+
# @return [String] Canonical FEEN string
|
|
33
18
|
def self.dump(position)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
Dumper::StyleTurn.dump(position.styles)
|
|
38
|
-
]
|
|
19
|
+
pp = PiecePlacement.dump(position.board, position.shape)
|
|
20
|
+
hands = Hands.dump(position.first_player_hand, position.second_player_hand)
|
|
21
|
+
st = StyleTurn.dump(position.first_player_style, position.second_player_style, position.turn)
|
|
39
22
|
|
|
40
|
-
|
|
23
|
+
"#{pp} #{hands} #{st}"
|
|
41
24
|
end
|
|
42
25
|
|
|
43
|
-
|
|
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)
|
|
56
|
-
end
|
|
26
|
+
freeze
|
|
57
27
|
end
|
|
58
28
|
end
|
|
59
29
|
end
|
data/lib/sashite/feen/error.rb
CHANGED
|
@@ -4,80 +4,12 @@ module Sashite
|
|
|
4
4
|
module Feen
|
|
5
5
|
# Base error class for all FEEN-related errors.
|
|
6
6
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
7
|
+
# Inherits from ArgumentError for compatibility with standard Ruby
|
|
8
|
+
# error handling patterns.
|
|
9
9
|
#
|
|
10
|
-
# @
|
|
11
|
-
class Error <
|
|
12
|
-
|
|
13
|
-
#
|
|
14
|
-
# Indicates problems with the overall FEEN format, such as:
|
|
15
|
-
# - Missing or incorrect number of fields
|
|
16
|
-
# - Missing required separators
|
|
17
|
-
# - Empty fields where content is required
|
|
18
|
-
# - Invalid field structure
|
|
19
|
-
#
|
|
20
|
-
# @example Missing field separator
|
|
21
|
-
# raise Error::Syntax, "FEEN must have exactly 3 space-separated fields"
|
|
22
|
-
#
|
|
23
|
-
# @example Empty required field
|
|
24
|
-
# raise Error::Syntax, "active style cannot be empty"
|
|
25
|
-
class Syntax < Error; end
|
|
26
|
-
|
|
27
|
-
# Error raised when EPIN (Extended Piece Identifier Notation) is invalid.
|
|
28
|
-
#
|
|
29
|
-
# Indicates problems with piece notation, such as:
|
|
30
|
-
# - Invalid EPIN format
|
|
31
|
-
# - Unrecognized piece characters
|
|
32
|
-
# - Malformed state modifiers or derivation suffixes
|
|
33
|
-
# - EPIN parsing failures
|
|
34
|
-
#
|
|
35
|
-
# @example Invalid EPIN format
|
|
36
|
-
# raise Error::Piece, "invalid EPIN notation: K#"
|
|
37
|
-
#
|
|
38
|
-
# @example Failed EPIN parsing
|
|
39
|
-
# raise Error::Piece, "failed to parse EPIN 'X': unknown piece type"
|
|
40
|
-
class Piece < Error; end
|
|
41
|
-
|
|
42
|
-
# Error raised when SIN (Style Identifier Notation) is invalid.
|
|
43
|
-
#
|
|
44
|
-
# Indicates problems with style notation, such as:
|
|
45
|
-
# - Invalid SIN format
|
|
46
|
-
# - Non-letter characters in style identifier
|
|
47
|
-
# - Multi-character style identifiers
|
|
48
|
-
# - SIN parsing failures
|
|
49
|
-
#
|
|
50
|
-
# @example Invalid SIN format
|
|
51
|
-
# raise Error::Style, "invalid SIN notation: '1' (must be a single letter)"
|
|
52
|
-
#
|
|
53
|
-
# @example Failed SIN parsing
|
|
54
|
-
# raise Error::Style, "failed to parse SIN 'XY': too long"
|
|
55
|
-
class Style < Error; end
|
|
56
|
-
|
|
57
|
-
# Error raised when piece counts are invalid.
|
|
58
|
-
#
|
|
59
|
-
# Indicates problems with piece quantity specifications, such as:
|
|
60
|
-
# - Count less than 1
|
|
61
|
-
# - Count exceeding reasonable limits
|
|
62
|
-
# - Invalid count format
|
|
63
|
-
#
|
|
64
|
-
# @example Count too small
|
|
65
|
-
# raise Error::Count, "piece count must be at least 1, got 0"
|
|
66
|
-
#
|
|
67
|
-
# @example Count too large
|
|
68
|
-
# raise Error::Count, "piece count too large: 9999"
|
|
69
|
-
class Count < Error; end
|
|
70
|
-
|
|
71
|
-
# Error raised for other semantic validation failures.
|
|
72
|
-
#
|
|
73
|
-
# Indicates problems that don't fit other error categories, such as:
|
|
74
|
-
# - Inconsistent position state
|
|
75
|
-
# - Rule violations
|
|
76
|
-
# - Other semantic constraints
|
|
77
|
-
#
|
|
78
|
-
# @example Semantic constraint violation
|
|
79
|
-
# raise Error::Validation, "position violates conservation principle"
|
|
80
|
-
class Validation < Error; end
|
|
10
|
+
# @api public
|
|
11
|
+
class Error < ::ArgumentError
|
|
12
|
+
freeze
|
|
81
13
|
end
|
|
82
14
|
end
|
|
83
15
|
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "parse_error"
|
|
4
|
+
|
|
5
|
+
module Sashite
|
|
6
|
+
module Feen
|
|
7
|
+
# Error raised when cardinality constraints are violated.
|
|
8
|
+
#
|
|
9
|
+
# Total pieces (board + hands) must not exceed total squares.
|
|
10
|
+
#
|
|
11
|
+
# @api public
|
|
12
|
+
class CardinalityError < ParseError
|
|
13
|
+
TOO_MANY_PIECES = "too many pieces for board size"
|
|
14
|
+
|
|
15
|
+
freeze
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "parse_error"
|
|
4
|
+
|
|
5
|
+
module Sashite
|
|
6
|
+
module Feen
|
|
7
|
+
# Error raised when Hands field (Field 2) parsing fails.
|
|
8
|
+
#
|
|
9
|
+
# @api public
|
|
10
|
+
class HandsError < ParseError
|
|
11
|
+
INVALID_DELIMITER = "invalid hands delimiter"
|
|
12
|
+
INVALID_COUNT = "invalid hand count"
|
|
13
|
+
INVALID_PIECE_TOKEN = "invalid piece token"
|
|
14
|
+
NOT_AGGREGATED = "hand items not aggregated"
|
|
15
|
+
NOT_CANONICAL = "hand items not in canonical order"
|
|
16
|
+
|
|
17
|
+
freeze
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../error"
|
|
4
|
+
|
|
5
|
+
module Sashite
|
|
6
|
+
module Feen
|
|
7
|
+
# Error raised when FEEN parsing fails at the top level.
|
|
8
|
+
#
|
|
9
|
+
# Covers general failures (input length, field count). Field-specific
|
|
10
|
+
# errors have their own subclasses.
|
|
11
|
+
#
|
|
12
|
+
# @api public
|
|
13
|
+
class ParseError < Error
|
|
14
|
+
INPUT_TOO_LONG = "input exceeds maximum length"
|
|
15
|
+
INVALID_FIELD_COUNT = "invalid field count"
|
|
16
|
+
|
|
17
|
+
freeze
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "parse_error"
|
|
4
|
+
|
|
5
|
+
module Sashite
|
|
6
|
+
module Feen
|
|
7
|
+
# Error raised when Piece Placement field (Field 1) parsing fails.
|
|
8
|
+
#
|
|
9
|
+
# @api public
|
|
10
|
+
class PiecePlacementError < ParseError
|
|
11
|
+
EMPTY = "piece placement is empty"
|
|
12
|
+
STARTS_WITH_SEPARATOR = "piece placement starts with separator"
|
|
13
|
+
ENDS_WITH_SEPARATOR = "piece placement ends with separator"
|
|
14
|
+
EMPTY_SEGMENT = "empty segment"
|
|
15
|
+
INVALID_EMPTY_COUNT = "invalid empty count"
|
|
16
|
+
INVALID_PIECE_TOKEN = "invalid piece token"
|
|
17
|
+
CONSECUTIVE_EMPTY_COUNTS = "consecutive empty counts must be merged"
|
|
18
|
+
DIMENSIONAL_COHERENCE = "dimensional coherence violation"
|
|
19
|
+
EXCEEDS_MAX_DIMENSIONS = "exceeds maximum dimensions"
|
|
20
|
+
DIMENSION_SIZE_EXCEEDED = "dimension size exceeded"
|
|
21
|
+
|
|
22
|
+
freeze
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "parse_error"
|
|
4
|
+
|
|
5
|
+
module Sashite
|
|
6
|
+
module Feen
|
|
7
|
+
# Error raised when Style-Turn field (Field 3) parsing fails.
|
|
8
|
+
#
|
|
9
|
+
# @api public
|
|
10
|
+
class StyleTurnError < ParseError
|
|
11
|
+
INVALID_DELIMITER = "invalid style-turn delimiter"
|
|
12
|
+
INVALID_STYLE_TOKEN = "invalid style token"
|
|
13
|
+
SAME_CASE = "style tokens must have opposite case"
|
|
14
|
+
|
|
15
|
+
freeze
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|