sashite-pan 3.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,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 capture action class
10
+ #
11
+ # Handles drop actions with capture - placing a piece from reserve
12
+ # onto an occupied square, capturing the piece there.
13
+ #
14
+ # Format: [<piece>].<destination>[=<piece>]
15
+ # Examples: "L.b4", ".c3", "P.e5=+P"
16
+ class DropCapture
17
+ # Action type
18
+ TYPE = :drop_capture
19
+
20
+ # Operator constant
21
+ OPERATOR = "."
22
+
23
+ # Transformation separator
24
+ TRANSFORMATION_SEPARATOR = "="
25
+
26
+ # Error messages
27
+ ERROR_INVALID_DROP_CAPTURE = "Invalid drop capture 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 capture action
42
+ #
43
+ # @param pan_string [String] the string to validate
44
+ # @return [Boolean] true if valid drop capture notation
45
+ #
46
+ # @example
47
+ # DropCapture.valid?("L.b4") # => true
48
+ # DropCapture.valid?(".c3") # => true
49
+ # DropCapture.valid?("P.e5=+P") # => 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 capture notation string into a DropCapture instance
81
+ #
82
+ # @param pan_string [String] drop capture notation string
83
+ # @return [DropCapture] drop capture action instance
84
+ # @raise [ArgumentError] if the string is not valid drop capture notation
85
+ #
86
+ # @example
87
+ # DropCapture.parse("L.b4") # => #<DropCapture piece="L" destination="b4">
88
+ # DropCapture.parse(".c3") # => #<DropCapture destination="c3">
89
+ # DropCapture.parse("P.e5=+P") # => #<DropCapture piece="P" destination="e5" transformation="+P">
90
+ def self.parse(pan_string)
91
+ raise ::ArgumentError, format(ERROR_INVALID_DROP_CAPTURE, 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 capture 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
+ # DropCapture.new("b4", piece: "L") # => #<DropCapture ...>
118
+ # DropCapture.new("c3") # => #<DropCapture ...>
119
+ # DropCapture.new("e5", piece: "P", transformation: "+P") # => #<DropCapture ...>
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_capture
141
+ def type
142
+ TYPE
143
+ end
144
+
145
+ # Get the source coordinate
146
+ #
147
+ # @return [nil] drop capture 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 capture notation
155
+ #
156
+ # @example
157
+ # action.to_s # => "L.b4" or ".c3" or "P.e5=+P"
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] false
205
+ def drop?
206
+ false
207
+ end
208
+
209
+ # Check if this is a drop capture action
210
+ #
211
+ # @return [Boolean] true
212
+ def drop_capture?
213
+ true
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
@@ -0,0 +1,221 @@
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
+ # Modify action class
10
+ #
11
+ # Handles in-place transformation actions where a piece changes
12
+ # its attributes without moving.
13
+ #
14
+ # Format: <square>=<piece>
15
+ # Examples: "e4=+P", "c3=k'"
16
+ class Modify
17
+ # Action type
18
+ TYPE = :modify
19
+
20
+ # Operator constant
21
+ OPERATOR = "="
22
+
23
+ # Error messages
24
+ ERROR_INVALID_MODIFY = "Invalid modify notation: %s"
25
+ ERROR_INVALID_SQUARE = "Invalid square coordinate: %s"
26
+ ERROR_INVALID_PIECE = "Invalid piece identifier: %s"
27
+
28
+ # @return [String] destination CELL coordinate (square being modified)
29
+ attr_reader :destination
30
+
31
+ # @return [String] EPIN piece identifier (final state)
32
+ attr_reader :piece
33
+
34
+ # Check if a string represents a valid modify action
35
+ #
36
+ # @param pan_string [String] the string to validate
37
+ # @return [Boolean] true if valid modify notation
38
+ #
39
+ # @example
40
+ # Modify.valid?("e4=+P") # => true
41
+ # Modify.valid?("c3=k'") # => true
42
+ # Modify.valid?("e4") # => false
43
+ def self.valid?(pan_string)
44
+ return false unless pan_string.is_a?(::String)
45
+ return false unless pan_string.include?(OPERATOR)
46
+
47
+ parts = pan_string.split(OPERATOR, 2)
48
+ return false if parts.size != 2
49
+
50
+ square_part = parts[0]
51
+ piece_part = parts[1]
52
+
53
+ return false unless ::Sashite::Cell.valid?(square_part)
54
+ return false unless ::Sashite::Epin.valid?(piece_part)
55
+
56
+ true
57
+ end
58
+
59
+ # Parse a modify notation string into a Modify instance
60
+ #
61
+ # @param pan_string [String] modify notation string
62
+ # @return [Modify] modify action instance
63
+ # @raise [ArgumentError] if the string is not valid modify notation
64
+ #
65
+ # @example
66
+ # Modify.parse("e4=+P") # => #<Modify destination="e4" piece="+P">
67
+ # Modify.parse("c3=k'") # => #<Modify destination="c3" piece="k'">
68
+ def self.parse(pan_string)
69
+ raise ::ArgumentError, format(ERROR_INVALID_MODIFY, pan_string) unless valid?(pan_string)
70
+
71
+ parts = pan_string.split(OPERATOR, 2)
72
+ square = parts[0]
73
+ piece = parts[1]
74
+
75
+ new(square, piece)
76
+ end
77
+
78
+ # Create a new modify action instance
79
+ #
80
+ # @param square [String] CELL coordinate
81
+ # @param piece [String] EPIN piece identifier (final state)
82
+ # @raise [ArgumentError] if coordinate or piece is invalid
83
+ #
84
+ # @example
85
+ # Modify.new("e4", "+P") # => #<Modify ...>
86
+ # Modify.new("c3", "k'") # => #<Modify ...>
87
+ def initialize(square, piece)
88
+ raise ::ArgumentError, format(ERROR_INVALID_SQUARE, square) unless ::Sashite::Cell.valid?(square)
89
+ raise ::ArgumentError, format(ERROR_INVALID_PIECE, piece) unless ::Sashite::Epin.valid?(piece)
90
+
91
+ @destination = square
92
+ @piece = piece
93
+
94
+ freeze
95
+ end
96
+
97
+ # Get the action type
98
+ #
99
+ # @return [Symbol] :modify
100
+ def type
101
+ TYPE
102
+ end
103
+
104
+ # Get the source coordinate
105
+ #
106
+ # @return [nil] modify actions have no source
107
+ def source
108
+ nil
109
+ end
110
+
111
+ # Get the transformation piece
112
+ #
113
+ # @return [nil] modify actions have no separate transformation (piece represents final state)
114
+ def transformation
115
+ nil
116
+ end
117
+
118
+ # Convert the action to its PAN string representation
119
+ #
120
+ # @return [String] modify notation
121
+ #
122
+ # @example
123
+ # action.to_s # => "e4=+P" or "c3=k'"
124
+ def to_s
125
+ "#{destination}#{OPERATOR}#{piece}"
126
+ end
127
+
128
+ # Check if this is a pass action
129
+ #
130
+ # @return [Boolean] false
131
+ def pass?
132
+ false
133
+ end
134
+
135
+ # Check if this is a move action
136
+ #
137
+ # @return [Boolean] false
138
+ def move?
139
+ false
140
+ end
141
+
142
+ # Check if this is a capture action
143
+ #
144
+ # @return [Boolean] false
145
+ def capture?
146
+ false
147
+ end
148
+
149
+ # Check if this is a special action
150
+ #
151
+ # @return [Boolean] false
152
+ def special?
153
+ false
154
+ end
155
+
156
+ # Check if this is a static capture action
157
+ #
158
+ # @return [Boolean] false
159
+ def static_capture?
160
+ false
161
+ end
162
+
163
+ # Check if this is a drop action
164
+ #
165
+ # @return [Boolean] false
166
+ def drop?
167
+ false
168
+ end
169
+
170
+ # Check if this is a drop capture action
171
+ #
172
+ # @return [Boolean] false
173
+ def drop_capture?
174
+ false
175
+ end
176
+
177
+ # Check if this is a modify action
178
+ #
179
+ # @return [Boolean] true
180
+ def modify?
181
+ true
182
+ end
183
+
184
+ # Check if this is a movement action
185
+ #
186
+ # @return [Boolean] false
187
+ def movement?
188
+ false
189
+ end
190
+
191
+ # Check if this is a drop action (drop or drop_capture)
192
+ #
193
+ # @return [Boolean] false
194
+ def drop_action?
195
+ false
196
+ end
197
+
198
+ # Custom equality comparison
199
+ #
200
+ # @param other [Object] object to compare with
201
+ # @return [Boolean] true if actions are equal
202
+ def ==(other)
203
+ return false unless other.is_a?(self.class)
204
+
205
+ destination == other.destination &&
206
+ piece == other.piece
207
+ end
208
+
209
+ # Alias for == to ensure Set functionality works correctly
210
+ alias eql? ==
211
+
212
+ # Custom hash implementation for use in collections
213
+ #
214
+ # @return [Integer] hash value
215
+ def hash
216
+ [self.class, destination, piece].hash
217
+ end
218
+ end
219
+ end
220
+ end
221
+ end