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.
@@ -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
+ # Capture action class
10
+ #
11
+ # Handles capture actions at destination, with optional transformation.
12
+ #
13
+ # Format: <source>+<destination>[=<piece>]
14
+ # Examples: "d1+f3", "b7+a8=R"
15
+ class Capture
16
+ # Action type
17
+ TYPE = :capture
18
+
19
+ # Operator constant
20
+ OPERATOR = "+"
21
+
22
+ # Transformation separator
23
+ TRANSFORMATION_SEPARATOR = "="
24
+
25
+ # Error messages
26
+ ERROR_INVALID_CAPTURE = "Invalid capture 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 capture action
41
+ #
42
+ # @param pan_string [String] the string to validate
43
+ # @return [Boolean] true if valid capture notation
44
+ #
45
+ # @example
46
+ # Capture.valid?("d1+f3") # => true
47
+ # Capture.valid?("b7+a8=R") # => true
48
+ # Capture.valid?("d1-f3") # => 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 capture notation string into a Capture instance
79
+ #
80
+ # @param pan_string [String] capture notation string
81
+ # @return [Capture] capture action instance
82
+ # @raise [ArgumentError] if the string is not valid capture notation
83
+ #
84
+ # @example
85
+ # Capture.parse("d1+f3") # => #<Capture source="d1" destination="f3">
86
+ # Capture.parse("b7+a8=R") # => #<Capture source="b7" destination="a8" transformation="R">
87
+ def self.parse(pan_string)
88
+ raise ::ArgumentError, format(ERROR_INVALID_CAPTURE, 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 capture 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
+ # Capture.new("d1", "f3") # => #<Capture ...>
115
+ # Capture.new("b7", "a8", transformation: "R") # => #<Capture ...>
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] :capture
136
+ def type
137
+ TYPE
138
+ end
139
+
140
+ # Get the piece identifier
141
+ #
142
+ # @return [nil] capture 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] capture notation
150
+ #
151
+ # @example
152
+ # action.to_s # => "d1+f3" or "b7+a8=R"
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] false
169
+ def move?
170
+ false
171
+ end
172
+
173
+ # Check if this is a capture action
174
+ #
175
+ # @return [Boolean] true
176
+ def capture?
177
+ true
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,261 @@
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
+ # Drop action class
10
+ #
11
+ # Handles drop actions to empty squares, with optional piece identifier
12
+ # and transformation.
13
+ #
14
+ # Format: [<piece>]*<destination>[=<piece>]
15
+ # Examples: "P*e5", "*d4", "S*c3=+S"
16
+ class Drop
17
+ # Action type
18
+ TYPE = :drop
19
+
20
+ # Operator constant
21
+ OPERATOR = "*"
22
+
23
+ # Transformation separator
24
+ TRANSFORMATION_SEPARATOR = "="
25
+
26
+ # Error messages
27
+ ERROR_INVALID_DROP = "Invalid drop notation: %s"
28
+ ERROR_INVALID_DESTINATION = "Invalid destination coordinate: %s"
29
+ ERROR_INVALID_PIECE = "Invalid piece identifier: %s"
30
+ ERROR_INVALID_TRANSFORMATION = "Invalid transformation piece: %s"
31
+
32
+ # @return [String] destination CELL coordinate
33
+ attr_reader :destination
34
+
35
+ # @return [String, nil] optional EPIN piece identifier
36
+ attr_reader :piece
37
+
38
+ # @return [String, nil] optional EPIN transformation
39
+ attr_reader :transformation
40
+
41
+ # Check if a string represents a valid drop action
42
+ #
43
+ # @param pan_string [String] the string to validate
44
+ # @return [Boolean] true if valid drop notation
45
+ #
46
+ # @example
47
+ # Drop.valid?("P*e5") # => true
48
+ # Drop.valid?("*d4") # => true
49
+ # Drop.valid?("S*c3=+S") # => true
50
+ def self.valid?(pan_string)
51
+ return false unless pan_string.is_a?(::String)
52
+ return false unless pan_string.include?(OPERATOR)
53
+
54
+ parts = pan_string.split(OPERATOR, 2)
55
+ return false if parts.size != 2
56
+
57
+ piece_part = parts[0]
58
+ dest_and_transform = parts[1]
59
+
60
+ # Piece part is optional, but if present must be valid EPIN
61
+ return false if !piece_part.empty? && !::Sashite::Epin.valid?(piece_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 drop notation string into a Drop instance
81
+ #
82
+ # @param pan_string [String] drop notation string
83
+ # @return [Drop] drop action instance
84
+ # @raise [ArgumentError] if the string is not valid drop notation
85
+ #
86
+ # @example
87
+ # Drop.parse("P*e5") # => #<Drop piece="P" destination="e5">
88
+ # Drop.parse("*d4") # => #<Drop destination="d4">
89
+ # Drop.parse("S*c3=+S") # => #<Drop piece="S" destination="c3" transformation="+S">
90
+ def self.parse(pan_string)
91
+ raise ::ArgumentError, format(ERROR_INVALID_DROP, pan_string) unless valid?(pan_string)
92
+
93
+ parts = pan_string.split(OPERATOR, 2)
94
+ piece = parts[0].empty? ? nil : parts[0]
95
+ dest_and_transform = parts[1]
96
+
97
+ if dest_and_transform.include?(TRANSFORMATION_SEPARATOR)
98
+ dest_parts = dest_and_transform.split(TRANSFORMATION_SEPARATOR, 2)
99
+ destination = dest_parts[0]
100
+ transformation = dest_parts[1]
101
+ else
102
+ destination = dest_and_transform
103
+ transformation = nil
104
+ end
105
+
106
+ new(destination, piece: piece, transformation: transformation)
107
+ end
108
+
109
+ # Create a new drop action instance
110
+ #
111
+ # @param destination [String] destination CELL coordinate
112
+ # @param piece [String, nil] optional EPIN piece identifier
113
+ # @param transformation [String, nil] optional EPIN transformation
114
+ # @raise [ArgumentError] if coordinates, piece, or transformation are invalid
115
+ #
116
+ # @example
117
+ # Drop.new("e5", piece: "P") # => #<Drop ...>
118
+ # Drop.new("d4") # => #<Drop ...>
119
+ # Drop.new("c3", piece: "S", transformation: "+S") # => #<Drop ...>
120
+ def initialize(destination, piece: nil, transformation: nil)
121
+ unless ::Sashite::Cell.valid?(destination)
122
+ raise ::ArgumentError, format(ERROR_INVALID_DESTINATION, destination)
123
+ end
124
+
125
+ raise ::ArgumentError, format(ERROR_INVALID_PIECE, piece) if piece && !::Sashite::Epin.valid?(piece)
126
+
127
+ if transformation && !::Sashite::Epin.valid?(transformation)
128
+ raise ::ArgumentError, format(ERROR_INVALID_TRANSFORMATION, transformation)
129
+ end
130
+
131
+ @destination = destination
132
+ @piece = piece
133
+ @transformation = transformation
134
+
135
+ freeze
136
+ end
137
+
138
+ # Get the action type
139
+ #
140
+ # @return [Symbol] :drop
141
+ def type
142
+ TYPE
143
+ end
144
+
145
+ # Get the source coordinate
146
+ #
147
+ # @return [nil] drop actions have no source
148
+ def source
149
+ nil
150
+ end
151
+
152
+ # Convert the action to its PAN string representation
153
+ #
154
+ # @return [String] drop notation
155
+ #
156
+ # @example
157
+ # action.to_s # => "P*e5" or "*d4" or "S*c3=+S"
158
+ def to_s
159
+ result = +""
160
+ result << piece if piece
161
+ result << OPERATOR
162
+ result << destination
163
+ result << TRANSFORMATION_SEPARATOR << transformation if transformation
164
+ result
165
+ end
166
+
167
+ # Check if this is a pass action
168
+ #
169
+ # @return [Boolean] false
170
+ def pass?
171
+ false
172
+ end
173
+
174
+ # Check if this is a move action
175
+ #
176
+ # @return [Boolean] false
177
+ def move?
178
+ false
179
+ end
180
+
181
+ # Check if this is a capture action
182
+ #
183
+ # @return [Boolean] false
184
+ def capture?
185
+ false
186
+ end
187
+
188
+ # Check if this is a special action
189
+ #
190
+ # @return [Boolean] false
191
+ def special?
192
+ false
193
+ end
194
+
195
+ # Check if this is a static capture action
196
+ #
197
+ # @return [Boolean] false
198
+ def static_capture?
199
+ false
200
+ end
201
+
202
+ # Check if this is a drop action
203
+ #
204
+ # @return [Boolean] true
205
+ def drop?
206
+ true
207
+ end
208
+
209
+ # Check if this is a drop capture action
210
+ #
211
+ # @return [Boolean] false
212
+ def drop_capture?
213
+ false
214
+ end
215
+
216
+ # Check if this is a modify action
217
+ #
218
+ # @return [Boolean] false
219
+ def modify?
220
+ false
221
+ end
222
+
223
+ # Check if this is a movement action
224
+ #
225
+ # @return [Boolean] false
226
+ def movement?
227
+ false
228
+ end
229
+
230
+ # Check if this is a drop action (drop or drop_capture)
231
+ #
232
+ # @return [Boolean] true
233
+ def drop_action?
234
+ true
235
+ end
236
+
237
+ # Custom equality comparison
238
+ #
239
+ # @param other [Object] object to compare with
240
+ # @return [Boolean] true if actions are equal
241
+ def ==(other)
242
+ return false unless other.is_a?(self.class)
243
+
244
+ destination == other.destination &&
245
+ piece == other.piece &&
246
+ transformation == other.transformation
247
+ end
248
+
249
+ # Alias for == to ensure Set functionality works correctly
250
+ alias eql? ==
251
+
252
+ # Custom hash implementation for use in collections
253
+ #
254
+ # @return [Integer] hash value
255
+ def hash
256
+ [self.class, destination, piece, transformation].hash
257
+ end
258
+ end
259
+ end
260
+ end
261
+ end