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
+ # 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