sashite-pan 2.0.0 → 4.0.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 +507 -171
- data/lib/sashite/pan/action/capture.rb +253 -0
- data/lib/sashite/pan/action/drop.rb +261 -0
- data/lib/sashite/pan/action/drop_capture.rb +261 -0
- data/lib/sashite/pan/action/modify.rb +221 -0
- data/lib/sashite/pan/action/move.rb +253 -0
- data/lib/sashite/pan/action/pass.rb +189 -0
- data/lib/sashite/pan/action/special.rb +255 -0
- data/lib/sashite/pan/action/static_capture.rb +207 -0
- data/lib/sashite/pan/action.rb +180 -0
- data/lib/sashite/pan.rb +30 -47
- data/lib/sashite-pan.rb +10 -3
- metadata +47 -10
- data/lib/sashite/pan/dumper/error.rb +0 -11
- data/lib/sashite/pan/dumper.rb +0 -81
- data/lib/sashite/pan/parser/error.rb +0 -11
- data/lib/sashite/pan/parser.rb +0 -90
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "sashite/cell"
|
|
4
|
+
require "sashite/epin"
|
|
5
|
+
|
|
6
|
+
module Sashite
|
|
7
|
+
module Pan
|
|
8
|
+
module Action
|
|
9
|
+
# Move action class
|
|
10
|
+
#
|
|
11
|
+
# Handles move actions to empty squares, with optional transformation.
|
|
12
|
+
#
|
|
13
|
+
# Format: <source>-<destination>[=<piece>]
|
|
14
|
+
# Examples: "e2-e4", "e7-e8=Q", "a7-a8=+R"
|
|
15
|
+
class Move
|
|
16
|
+
# Action type
|
|
17
|
+
TYPE = :move
|
|
18
|
+
|
|
19
|
+
# Operator constant
|
|
20
|
+
OPERATOR = "-"
|
|
21
|
+
|
|
22
|
+
# Transformation separator
|
|
23
|
+
TRANSFORMATION_SEPARATOR = "="
|
|
24
|
+
|
|
25
|
+
# Error messages
|
|
26
|
+
ERROR_INVALID_MOVE = "Invalid move notation: %s"
|
|
27
|
+
ERROR_INVALID_SOURCE = "Invalid source coordinate: %s"
|
|
28
|
+
ERROR_INVALID_DESTINATION = "Invalid destination coordinate: %s"
|
|
29
|
+
ERROR_INVALID_TRANSFORMATION = "Invalid transformation piece: %s"
|
|
30
|
+
|
|
31
|
+
# @return [String] source CELL coordinate
|
|
32
|
+
attr_reader :source
|
|
33
|
+
|
|
34
|
+
# @return [String] destination CELL coordinate
|
|
35
|
+
attr_reader :destination
|
|
36
|
+
|
|
37
|
+
# @return [String, nil] optional EPIN transformation
|
|
38
|
+
attr_reader :transformation
|
|
39
|
+
|
|
40
|
+
# Check if a string represents a valid move action
|
|
41
|
+
#
|
|
42
|
+
# @param pan_string [String] the string to validate
|
|
43
|
+
# @return [Boolean] true if valid move notation
|
|
44
|
+
#
|
|
45
|
+
# @example
|
|
46
|
+
# Move.valid?("e2-e4") # => true
|
|
47
|
+
# Move.valid?("e7-e8=Q") # => true
|
|
48
|
+
# Move.valid?("e2+e4") # => false
|
|
49
|
+
def self.valid?(pan_string)
|
|
50
|
+
return false unless pan_string.is_a?(::String)
|
|
51
|
+
return false unless pan_string.include?(OPERATOR)
|
|
52
|
+
|
|
53
|
+
parts = pan_string.split(OPERATOR, 2)
|
|
54
|
+
return false if parts.size != 2
|
|
55
|
+
|
|
56
|
+
source_part = parts[0]
|
|
57
|
+
dest_and_transform = parts[1]
|
|
58
|
+
|
|
59
|
+
return false unless ::Sashite::Cell.valid?(source_part)
|
|
60
|
+
|
|
61
|
+
# Check if there's a transformation
|
|
62
|
+
if dest_and_transform.include?(TRANSFORMATION_SEPARATOR)
|
|
63
|
+
dest_parts = dest_and_transform.split(TRANSFORMATION_SEPARATOR, 2)
|
|
64
|
+
return false if dest_parts.size != 2
|
|
65
|
+
|
|
66
|
+
destination_part = dest_parts[0]
|
|
67
|
+
transformation_part = dest_parts[1]
|
|
68
|
+
|
|
69
|
+
return false unless ::Sashite::Cell.valid?(destination_part)
|
|
70
|
+
return false unless ::Sashite::Epin.valid?(transformation_part)
|
|
71
|
+
else
|
|
72
|
+
return false unless ::Sashite::Cell.valid?(dest_and_transform)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
true
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Parse a move notation string into a Move instance
|
|
79
|
+
#
|
|
80
|
+
# @param pan_string [String] move notation string
|
|
81
|
+
# @return [Move] move action instance
|
|
82
|
+
# @raise [ArgumentError] if the string is not valid move notation
|
|
83
|
+
#
|
|
84
|
+
# @example
|
|
85
|
+
# Move.parse("e2-e4") # => #<Move source="e2" destination="e4">
|
|
86
|
+
# Move.parse("e7-e8=Q") # => #<Move source="e7" destination="e8" transformation="Q">
|
|
87
|
+
def self.parse(pan_string)
|
|
88
|
+
raise ::ArgumentError, format(ERROR_INVALID_MOVE, pan_string) unless valid?(pan_string)
|
|
89
|
+
|
|
90
|
+
parts = pan_string.split(OPERATOR, 2)
|
|
91
|
+
source = parts[0]
|
|
92
|
+
dest_and_transform = parts[1]
|
|
93
|
+
|
|
94
|
+
if dest_and_transform.include?(TRANSFORMATION_SEPARATOR)
|
|
95
|
+
dest_parts = dest_and_transform.split(TRANSFORMATION_SEPARATOR, 2)
|
|
96
|
+
destination = dest_parts[0]
|
|
97
|
+
transformation = dest_parts[1]
|
|
98
|
+
else
|
|
99
|
+
destination = dest_and_transform
|
|
100
|
+
transformation = nil
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
new(source, destination, transformation: transformation)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Create a new move action instance
|
|
107
|
+
#
|
|
108
|
+
# @param source [String] source CELL coordinate
|
|
109
|
+
# @param destination [String] destination CELL coordinate
|
|
110
|
+
# @param transformation [String, nil] optional EPIN transformation
|
|
111
|
+
# @raise [ArgumentError] if coordinates or transformation are invalid
|
|
112
|
+
#
|
|
113
|
+
# @example
|
|
114
|
+
# Move.new("e2", "e4") # => #<Move ...>
|
|
115
|
+
# Move.new("e7", "e8", transformation: "Q") # => #<Move ...>
|
|
116
|
+
def initialize(source, destination, transformation: nil)
|
|
117
|
+
raise ::ArgumentError, format(ERROR_INVALID_SOURCE, source) unless ::Sashite::Cell.valid?(source)
|
|
118
|
+
unless ::Sashite::Cell.valid?(destination)
|
|
119
|
+
raise ::ArgumentError, format(ERROR_INVALID_DESTINATION, destination)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
if transformation && !::Sashite::Epin.valid?(transformation)
|
|
123
|
+
raise ::ArgumentError, format(ERROR_INVALID_TRANSFORMATION, transformation)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
@source = source
|
|
127
|
+
@destination = destination
|
|
128
|
+
@transformation = transformation
|
|
129
|
+
|
|
130
|
+
freeze
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Get the action type
|
|
134
|
+
#
|
|
135
|
+
# @return [Symbol] :move
|
|
136
|
+
def type
|
|
137
|
+
TYPE
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Get the piece identifier
|
|
141
|
+
#
|
|
142
|
+
# @return [nil] move actions have no piece identifier
|
|
143
|
+
def piece
|
|
144
|
+
nil
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Convert the action to its PAN string representation
|
|
148
|
+
#
|
|
149
|
+
# @return [String] move notation
|
|
150
|
+
#
|
|
151
|
+
# @example
|
|
152
|
+
# action.to_s # => "e2-e4" or "e7-e8=Q"
|
|
153
|
+
def to_s
|
|
154
|
+
result = "#{source}#{OPERATOR}#{destination}"
|
|
155
|
+
result += "#{TRANSFORMATION_SEPARATOR}#{transformation}" if transformation
|
|
156
|
+
result
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Check if this is a pass action
|
|
160
|
+
#
|
|
161
|
+
# @return [Boolean] false
|
|
162
|
+
def pass?
|
|
163
|
+
false
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Check if this is a move action
|
|
167
|
+
#
|
|
168
|
+
# @return [Boolean] true
|
|
169
|
+
def move?
|
|
170
|
+
true
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Check if this is a capture action
|
|
174
|
+
#
|
|
175
|
+
# @return [Boolean] false
|
|
176
|
+
def capture?
|
|
177
|
+
false
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Check if this is a special action
|
|
181
|
+
#
|
|
182
|
+
# @return [Boolean] false
|
|
183
|
+
def special?
|
|
184
|
+
false
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Check if this is a static capture action
|
|
188
|
+
#
|
|
189
|
+
# @return [Boolean] false
|
|
190
|
+
def static_capture?
|
|
191
|
+
false
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Check if this is a drop action
|
|
195
|
+
#
|
|
196
|
+
# @return [Boolean] false
|
|
197
|
+
def drop?
|
|
198
|
+
false
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Check if this is a drop capture action
|
|
202
|
+
#
|
|
203
|
+
# @return [Boolean] false
|
|
204
|
+
def drop_capture?
|
|
205
|
+
false
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Check if this is a modify action
|
|
209
|
+
#
|
|
210
|
+
# @return [Boolean] false
|
|
211
|
+
def modify?
|
|
212
|
+
false
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Check if this is a movement action
|
|
216
|
+
#
|
|
217
|
+
# @return [Boolean] true
|
|
218
|
+
def movement?
|
|
219
|
+
true
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Check if this is a drop action (drop or drop_capture)
|
|
223
|
+
#
|
|
224
|
+
# @return [Boolean] false
|
|
225
|
+
def drop_action?
|
|
226
|
+
false
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Custom equality comparison
|
|
230
|
+
#
|
|
231
|
+
# @param other [Object] object to compare with
|
|
232
|
+
# @return [Boolean] true if actions are equal
|
|
233
|
+
def ==(other)
|
|
234
|
+
return false unless other.is_a?(self.class)
|
|
235
|
+
|
|
236
|
+
source == other.source &&
|
|
237
|
+
destination == other.destination &&
|
|
238
|
+
transformation == other.transformation
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Alias for == to ensure Set functionality works correctly
|
|
242
|
+
alias eql? ==
|
|
243
|
+
|
|
244
|
+
# Custom hash implementation for use in collections
|
|
245
|
+
#
|
|
246
|
+
# @return [Integer] hash value
|
|
247
|
+
def hash
|
|
248
|
+
[self.class, source, destination, transformation].hash
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "singleton"
|
|
4
|
+
|
|
5
|
+
module Sashite
|
|
6
|
+
module Pan
|
|
7
|
+
module Action
|
|
8
|
+
# Pass action class
|
|
9
|
+
#
|
|
10
|
+
# Represents a pass action where the active player voluntarily ends their turn
|
|
11
|
+
# without performing any movement or transformation.
|
|
12
|
+
#
|
|
13
|
+
# This class is implemented as a singleton since all pass actions are identical.
|
|
14
|
+
#
|
|
15
|
+
# Format: "..."
|
|
16
|
+
class Pass
|
|
17
|
+
include ::Singleton
|
|
18
|
+
|
|
19
|
+
# Action type
|
|
20
|
+
TYPE = :pass
|
|
21
|
+
|
|
22
|
+
# Pass notation constant
|
|
23
|
+
NOTATION = "..."
|
|
24
|
+
|
|
25
|
+
# Error messages
|
|
26
|
+
ERROR_INVALID_PASS = "Invalid pass notation: %s"
|
|
27
|
+
|
|
28
|
+
# Check if a string represents a valid pass action
|
|
29
|
+
#
|
|
30
|
+
# @param pan_string [String] the string to validate
|
|
31
|
+
# @return [Boolean] true if valid pass notation
|
|
32
|
+
#
|
|
33
|
+
# @example
|
|
34
|
+
# Pass.valid?("...") # => true
|
|
35
|
+
# Pass.valid?("e2-e4") # => false
|
|
36
|
+
def self.valid?(pan_string)
|
|
37
|
+
pan_string == NOTATION
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Parse a pass notation string into a Pass instance
|
|
41
|
+
#
|
|
42
|
+
# @param pan_string [String] pass notation string
|
|
43
|
+
# @return [Pass] the singleton pass action instance
|
|
44
|
+
# @raise [ArgumentError] if the string is not a valid pass notation
|
|
45
|
+
#
|
|
46
|
+
# @example
|
|
47
|
+
# Pass.parse("...") # => #<Pass>
|
|
48
|
+
def self.parse(pan_string)
|
|
49
|
+
raise ::ArgumentError, format(ERROR_INVALID_PASS, pan_string) unless valid?(pan_string)
|
|
50
|
+
|
|
51
|
+
instance
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Get the action type
|
|
55
|
+
#
|
|
56
|
+
# @return [Symbol] :pass
|
|
57
|
+
def type
|
|
58
|
+
TYPE
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Get the source coordinate
|
|
62
|
+
#
|
|
63
|
+
# @return [nil] pass actions have no source
|
|
64
|
+
def source
|
|
65
|
+
nil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Get the destination coordinate
|
|
69
|
+
#
|
|
70
|
+
# @return [nil] pass actions have no destination
|
|
71
|
+
def destination
|
|
72
|
+
nil
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Get the piece identifier
|
|
76
|
+
#
|
|
77
|
+
# @return [nil] pass actions have no piece
|
|
78
|
+
def piece
|
|
79
|
+
nil
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Get the transformation piece
|
|
83
|
+
#
|
|
84
|
+
# @return [nil] pass actions have no transformation
|
|
85
|
+
def transformation
|
|
86
|
+
nil
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Convert the action to its PAN string representation
|
|
90
|
+
#
|
|
91
|
+
# @return [String] pass notation "..."
|
|
92
|
+
#
|
|
93
|
+
# @example
|
|
94
|
+
# action.to_s # => "..."
|
|
95
|
+
def to_s
|
|
96
|
+
NOTATION
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Check if this is a pass action
|
|
100
|
+
#
|
|
101
|
+
# @return [Boolean] true
|
|
102
|
+
def pass?
|
|
103
|
+
true
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Check if this is a move action
|
|
107
|
+
#
|
|
108
|
+
# @return [Boolean] false
|
|
109
|
+
def move?
|
|
110
|
+
false
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Check if this is a capture action
|
|
114
|
+
#
|
|
115
|
+
# @return [Boolean] false
|
|
116
|
+
def capture?
|
|
117
|
+
false
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Check if this is a special action
|
|
121
|
+
#
|
|
122
|
+
# @return [Boolean] false
|
|
123
|
+
def special?
|
|
124
|
+
false
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Check if this is a static capture action
|
|
128
|
+
#
|
|
129
|
+
# @return [Boolean] false
|
|
130
|
+
def static_capture?
|
|
131
|
+
false
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Check if this is a drop action
|
|
135
|
+
#
|
|
136
|
+
# @return [Boolean] false
|
|
137
|
+
def drop?
|
|
138
|
+
false
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Check if this is a drop capture action
|
|
142
|
+
#
|
|
143
|
+
# @return [Boolean] false
|
|
144
|
+
def drop_capture?
|
|
145
|
+
false
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Check if this is a modify action
|
|
149
|
+
#
|
|
150
|
+
# @return [Boolean] false
|
|
151
|
+
def modify?
|
|
152
|
+
false
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Check if this is a movement action
|
|
156
|
+
#
|
|
157
|
+
# @return [Boolean] false
|
|
158
|
+
def movement?
|
|
159
|
+
false
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Check if this is a drop action (drop or drop_capture)
|
|
163
|
+
#
|
|
164
|
+
# @return [Boolean] false
|
|
165
|
+
def drop_action?
|
|
166
|
+
false
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Custom equality comparison
|
|
170
|
+
#
|
|
171
|
+
# @param other [Object] object to compare with
|
|
172
|
+
# @return [Boolean] true if other is also the Pass singleton
|
|
173
|
+
def ==(other)
|
|
174
|
+
other.equal?(self)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Alias for == to ensure Set functionality works correctly
|
|
178
|
+
alias eql? ==
|
|
179
|
+
|
|
180
|
+
# Custom hash implementation for use in collections
|
|
181
|
+
#
|
|
182
|
+
# @return [Integer] hash value
|
|
183
|
+
def hash
|
|
184
|
+
[self.class, TYPE].hash
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "sashite/cell"
|
|
4
|
+
require "sashite/epin"
|
|
5
|
+
|
|
6
|
+
module Sashite
|
|
7
|
+
module Pan
|
|
8
|
+
module Action
|
|
9
|
+
# Special action class
|
|
10
|
+
#
|
|
11
|
+
# Handles special move actions with implicit side effects.
|
|
12
|
+
# Used for moves like castling or en passant where additional
|
|
13
|
+
# transformations occur beyond the primary movement.
|
|
14
|
+
#
|
|
15
|
+
# Format: <source>~<destination>[=<piece>]
|
|
16
|
+
# Examples: "e1~g1" (castling), "e5~f6" (en passant)
|
|
17
|
+
class Special
|
|
18
|
+
# Action type
|
|
19
|
+
TYPE = :special
|
|
20
|
+
|
|
21
|
+
# Operator constant
|
|
22
|
+
OPERATOR = "~"
|
|
23
|
+
|
|
24
|
+
# Transformation separator
|
|
25
|
+
TRANSFORMATION_SEPARATOR = "="
|
|
26
|
+
|
|
27
|
+
# Error messages
|
|
28
|
+
ERROR_INVALID_SPECIAL = "Invalid special notation: %s"
|
|
29
|
+
ERROR_INVALID_SOURCE = "Invalid source coordinate: %s"
|
|
30
|
+
ERROR_INVALID_DESTINATION = "Invalid destination coordinate: %s"
|
|
31
|
+
ERROR_INVALID_TRANSFORMATION = "Invalid transformation piece: %s"
|
|
32
|
+
|
|
33
|
+
# @return [String] source CELL coordinate
|
|
34
|
+
attr_reader :source
|
|
35
|
+
|
|
36
|
+
# @return [String] destination CELL coordinate
|
|
37
|
+
attr_reader :destination
|
|
38
|
+
|
|
39
|
+
# @return [String, nil] optional EPIN transformation
|
|
40
|
+
attr_reader :transformation
|
|
41
|
+
|
|
42
|
+
# Check if a string represents a valid special action
|
|
43
|
+
#
|
|
44
|
+
# @param pan_string [String] the string to validate
|
|
45
|
+
# @return [Boolean] true if valid special notation
|
|
46
|
+
#
|
|
47
|
+
# @example
|
|
48
|
+
# Special.valid?("e1~g1") # => true
|
|
49
|
+
# Special.valid?("e5~f6") # => true
|
|
50
|
+
# Special.valid?("e1-g1") # => false
|
|
51
|
+
def self.valid?(pan_string)
|
|
52
|
+
return false unless pan_string.is_a?(::String)
|
|
53
|
+
return false unless pan_string.include?(OPERATOR)
|
|
54
|
+
|
|
55
|
+
parts = pan_string.split(OPERATOR, 2)
|
|
56
|
+
return false if parts.size != 2
|
|
57
|
+
|
|
58
|
+
source_part = parts[0]
|
|
59
|
+
dest_and_transform = parts[1]
|
|
60
|
+
|
|
61
|
+
return false unless ::Sashite::Cell.valid?(source_part)
|
|
62
|
+
|
|
63
|
+
# Check if there's a transformation
|
|
64
|
+
if dest_and_transform.include?(TRANSFORMATION_SEPARATOR)
|
|
65
|
+
dest_parts = dest_and_transform.split(TRANSFORMATION_SEPARATOR, 2)
|
|
66
|
+
return false if dest_parts.size != 2
|
|
67
|
+
|
|
68
|
+
destination_part = dest_parts[0]
|
|
69
|
+
transformation_part = dest_parts[1]
|
|
70
|
+
|
|
71
|
+
return false unless ::Sashite::Cell.valid?(destination_part)
|
|
72
|
+
return false unless ::Sashite::Epin.valid?(transformation_part)
|
|
73
|
+
else
|
|
74
|
+
return false unless ::Sashite::Cell.valid?(dest_and_transform)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
true
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Parse a special notation string into a Special instance
|
|
81
|
+
#
|
|
82
|
+
# @param pan_string [String] special notation string
|
|
83
|
+
# @return [Special] special action instance
|
|
84
|
+
# @raise [ArgumentError] if the string is not valid special notation
|
|
85
|
+
#
|
|
86
|
+
# @example
|
|
87
|
+
# Special.parse("e1~g1") # => #<Special source="e1" destination="g1">
|
|
88
|
+
# Special.parse("e5~f6") # => #<Special source="e5" destination="f6">
|
|
89
|
+
def self.parse(pan_string)
|
|
90
|
+
raise ::ArgumentError, format(ERROR_INVALID_SPECIAL, pan_string) unless valid?(pan_string)
|
|
91
|
+
|
|
92
|
+
parts = pan_string.split(OPERATOR, 2)
|
|
93
|
+
source = parts[0]
|
|
94
|
+
dest_and_transform = parts[1]
|
|
95
|
+
|
|
96
|
+
if dest_and_transform.include?(TRANSFORMATION_SEPARATOR)
|
|
97
|
+
dest_parts = dest_and_transform.split(TRANSFORMATION_SEPARATOR, 2)
|
|
98
|
+
destination = dest_parts[0]
|
|
99
|
+
transformation = dest_parts[1]
|
|
100
|
+
else
|
|
101
|
+
destination = dest_and_transform
|
|
102
|
+
transformation = nil
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
new(source, destination, transformation: transformation)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Create a new special action instance
|
|
109
|
+
#
|
|
110
|
+
# @param source [String] source CELL coordinate
|
|
111
|
+
# @param destination [String] destination CELL coordinate
|
|
112
|
+
# @param transformation [String, nil] optional EPIN transformation
|
|
113
|
+
# @raise [ArgumentError] if coordinates or transformation are invalid
|
|
114
|
+
#
|
|
115
|
+
# @example
|
|
116
|
+
# Special.new("e1", "g1") # => #<Special ...>
|
|
117
|
+
# Special.new("e5", "f6", transformation: nil) # => #<Special ...>
|
|
118
|
+
def initialize(source, destination, transformation: nil)
|
|
119
|
+
raise ::ArgumentError, format(ERROR_INVALID_SOURCE, source) unless ::Sashite::Cell.valid?(source)
|
|
120
|
+
unless ::Sashite::Cell.valid?(destination)
|
|
121
|
+
raise ::ArgumentError, format(ERROR_INVALID_DESTINATION, destination)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
if transformation && !::Sashite::Epin.valid?(transformation)
|
|
125
|
+
raise ::ArgumentError, format(ERROR_INVALID_TRANSFORMATION, transformation)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
@source = source
|
|
129
|
+
@destination = destination
|
|
130
|
+
@transformation = transformation
|
|
131
|
+
|
|
132
|
+
freeze
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Get the action type
|
|
136
|
+
#
|
|
137
|
+
# @return [Symbol] :special
|
|
138
|
+
def type
|
|
139
|
+
TYPE
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Get the piece identifier
|
|
143
|
+
#
|
|
144
|
+
# @return [nil] special actions have no piece identifier
|
|
145
|
+
def piece
|
|
146
|
+
nil
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Convert the action to its PAN string representation
|
|
150
|
+
#
|
|
151
|
+
# @return [String] special notation
|
|
152
|
+
#
|
|
153
|
+
# @example
|
|
154
|
+
# action.to_s # => "e1~g1" or "e5~f6"
|
|
155
|
+
def to_s
|
|
156
|
+
result = "#{source}#{OPERATOR}#{destination}"
|
|
157
|
+
result += "#{TRANSFORMATION_SEPARATOR}#{transformation}" if transformation
|
|
158
|
+
result
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Check if this is a pass action
|
|
162
|
+
#
|
|
163
|
+
# @return [Boolean] false
|
|
164
|
+
def pass?
|
|
165
|
+
false
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Check if this is a move action
|
|
169
|
+
#
|
|
170
|
+
# @return [Boolean] false
|
|
171
|
+
def move?
|
|
172
|
+
false
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Check if this is a capture action
|
|
176
|
+
#
|
|
177
|
+
# @return [Boolean] false
|
|
178
|
+
def capture?
|
|
179
|
+
false
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Check if this is a special action
|
|
183
|
+
#
|
|
184
|
+
# @return [Boolean] true
|
|
185
|
+
def special?
|
|
186
|
+
true
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Check if this is a static capture action
|
|
190
|
+
#
|
|
191
|
+
# @return [Boolean] false
|
|
192
|
+
def static_capture?
|
|
193
|
+
false
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Check if this is a drop action
|
|
197
|
+
#
|
|
198
|
+
# @return [Boolean] false
|
|
199
|
+
def drop?
|
|
200
|
+
false
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Check if this is a drop capture action
|
|
204
|
+
#
|
|
205
|
+
# @return [Boolean] false
|
|
206
|
+
def drop_capture?
|
|
207
|
+
false
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Check if this is a modify action
|
|
211
|
+
#
|
|
212
|
+
# @return [Boolean] false
|
|
213
|
+
def modify?
|
|
214
|
+
false
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Check if this is a movement action
|
|
218
|
+
#
|
|
219
|
+
# @return [Boolean] true
|
|
220
|
+
def movement?
|
|
221
|
+
true
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Check if this is a drop action (drop or drop_capture)
|
|
225
|
+
#
|
|
226
|
+
# @return [Boolean] false
|
|
227
|
+
def drop_action?
|
|
228
|
+
false
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Custom equality comparison
|
|
232
|
+
#
|
|
233
|
+
# @param other [Object] object to compare with
|
|
234
|
+
# @return [Boolean] true if actions are equal
|
|
235
|
+
def ==(other)
|
|
236
|
+
return false unless other.is_a?(self.class)
|
|
237
|
+
|
|
238
|
+
source == other.source &&
|
|
239
|
+
destination == other.destination &&
|
|
240
|
+
transformation == other.transformation
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Alias for == to ensure Set functionality works correctly
|
|
244
|
+
alias eql? ==
|
|
245
|
+
|
|
246
|
+
# Custom hash implementation for use in collections
|
|
247
|
+
#
|
|
248
|
+
# @return [Integer] hash value
|
|
249
|
+
def hash
|
|
250
|
+
[self.class, source, destination, transformation].hash
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|