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,207 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sashite/cell"
4
+
5
+ module Sashite
6
+ module Pan
7
+ module Action
8
+ # Static capture action class
9
+ #
10
+ # Handles capture actions without movement - removing a piece from the board
11
+ # without the capturing piece moving.
12
+ #
13
+ # Format: +<square>
14
+ # Examples: "+d4", "+e5"
15
+ class StaticCapture
16
+ # Action type
17
+ TYPE = :static_capture
18
+
19
+ # Operator constant
20
+ OPERATOR = "+"
21
+
22
+ # Error messages
23
+ ERROR_INVALID_STATIC_CAPTURE = "Invalid static capture notation: %s"
24
+ ERROR_INVALID_SQUARE = "Invalid square coordinate: %s"
25
+
26
+ # @return [String] destination CELL coordinate (square where piece is captured)
27
+ attr_reader :destination
28
+
29
+ # Check if a string represents a valid static capture action
30
+ #
31
+ # @param pan_string [String] the string to validate
32
+ # @return [Boolean] true if valid static capture notation
33
+ #
34
+ # @example
35
+ # StaticCapture.valid?("+d4") # => true
36
+ # StaticCapture.valid?("+e5") # => true
37
+ # StaticCapture.valid?("d4") # => false
38
+ def self.valid?(pan_string)
39
+ return false unless pan_string.is_a?(::String)
40
+ return false unless pan_string.start_with?(OPERATOR)
41
+ return false if pan_string.length < 2
42
+
43
+ square = pan_string[1..]
44
+ ::Sashite::Cell.valid?(square)
45
+ end
46
+
47
+ # Parse a static capture notation string into a StaticCapture instance
48
+ #
49
+ # @param pan_string [String] static capture notation string
50
+ # @return [StaticCapture] static capture action instance
51
+ # @raise [ArgumentError] if the string is not valid static capture notation
52
+ #
53
+ # @example
54
+ # StaticCapture.parse("+d4") # => #<StaticCapture destination="d4">
55
+ def self.parse(pan_string)
56
+ raise ::ArgumentError, format(ERROR_INVALID_STATIC_CAPTURE, pan_string) unless valid?(pan_string)
57
+
58
+ square = pan_string[1..]
59
+ new(square)
60
+ end
61
+
62
+ # Create a new static capture action instance
63
+ #
64
+ # @param square [String] CELL coordinate of piece to capture
65
+ # @raise [ArgumentError] if coordinate is invalid
66
+ #
67
+ # @example
68
+ # StaticCapture.new("d4") # => #<StaticCapture ...>
69
+ def initialize(square)
70
+ raise ::ArgumentError, format(ERROR_INVALID_SQUARE, square) unless ::Sashite::Cell.valid?(square)
71
+
72
+ @destination = square
73
+
74
+ freeze
75
+ end
76
+
77
+ # Get the action type
78
+ #
79
+ # @return [Symbol] :static_capture
80
+ def type
81
+ TYPE
82
+ end
83
+
84
+ # Get the source coordinate
85
+ #
86
+ # @return [nil] static capture actions have no source
87
+ def source
88
+ nil
89
+ end
90
+
91
+ # Get the piece identifier
92
+ #
93
+ # @return [nil] static capture actions have no piece identifier
94
+ def piece
95
+ nil
96
+ end
97
+
98
+ # Get the transformation piece
99
+ #
100
+ # @return [nil] static capture actions have no transformation
101
+ def transformation
102
+ nil
103
+ end
104
+
105
+ # Convert the action to its PAN string representation
106
+ #
107
+ # @return [String] static capture notation
108
+ #
109
+ # @example
110
+ # action.to_s # => "+d4"
111
+ def to_s
112
+ "#{OPERATOR}#{destination}"
113
+ end
114
+
115
+ # Check if this is a pass action
116
+ #
117
+ # @return [Boolean] false
118
+ def pass?
119
+ false
120
+ end
121
+
122
+ # Check if this is a move action
123
+ #
124
+ # @return [Boolean] false
125
+ def move?
126
+ false
127
+ end
128
+
129
+ # Check if this is a capture action
130
+ #
131
+ # @return [Boolean] false
132
+ def capture?
133
+ false
134
+ end
135
+
136
+ # Check if this is a special action
137
+ #
138
+ # @return [Boolean] false
139
+ def special?
140
+ false
141
+ end
142
+
143
+ # Check if this is a static capture action
144
+ #
145
+ # @return [Boolean] true
146
+ def static_capture?
147
+ true
148
+ end
149
+
150
+ # Check if this is a drop action
151
+ #
152
+ # @return [Boolean] false
153
+ def drop?
154
+ false
155
+ end
156
+
157
+ # Check if this is a drop capture action
158
+ #
159
+ # @return [Boolean] false
160
+ def drop_capture?
161
+ false
162
+ end
163
+
164
+ # Check if this is a modify action
165
+ #
166
+ # @return [Boolean] false
167
+ def modify?
168
+ false
169
+ end
170
+
171
+ # Check if this is a movement action
172
+ #
173
+ # @return [Boolean] false
174
+ def movement?
175
+ false
176
+ end
177
+
178
+ # Check if this is a drop action (drop or drop_capture)
179
+ #
180
+ # @return [Boolean] false
181
+ def drop_action?
182
+ false
183
+ end
184
+
185
+ # Custom equality comparison
186
+ #
187
+ # @param other [Object] object to compare with
188
+ # @return [Boolean] true if actions are equal
189
+ def ==(other)
190
+ return false unless other.is_a?(self.class)
191
+
192
+ destination == other.destination
193
+ end
194
+
195
+ # Alias for == to ensure Set functionality works correctly
196
+ alias eql? ==
197
+
198
+ # Custom hash implementation for use in collections
199
+ #
200
+ # @return [Integer] hash value
201
+ def hash
202
+ [self.class, destination].hash
203
+ end
204
+ end
205
+ end
206
+ end
207
+ end
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "action/pass"
4
+ require_relative "action/move"
5
+ require_relative "action/capture"
6
+ require_relative "action/special"
7
+ require_relative "action/static_capture"
8
+ require_relative "action/drop"
9
+ require_relative "action/drop_capture"
10
+ require_relative "action/modify"
11
+
12
+ module Sashite
13
+ module Pan
14
+ # Action module
15
+ #
16
+ # Orchestrates all action types in PAN (Portable Action Notation) format.
17
+ # Each action type is implemented as a separate, autonomous class.
18
+ #
19
+ # This module provides a unified interface for validation, parsing,
20
+ # and factory methods that delegate to the appropriate action class.
21
+ module Action
22
+ # Error messages
23
+ ERROR_INVALID_PAN = "Invalid PAN string: %s"
24
+
25
+ # Check if a string represents a valid PAN action
26
+ #
27
+ # @param pan_string [String] the string to validate
28
+ # @return [Boolean] true if the string is a valid PAN action
29
+ #
30
+ # @example
31
+ # Action.valid?("e2-e4") # => true
32
+ # Action.valid?("P*e5") # => true
33
+ # Action.valid?("...") # => true
34
+ # Action.valid?("invalid") # => false
35
+ def self.valid?(pan_string)
36
+ return false unless pan_string.is_a?(::String)
37
+ return false if pan_string.empty?
38
+
39
+ # Try each action type's validation
40
+ Pass.valid?(pan_string) ||
41
+ Move.valid?(pan_string) ||
42
+ Capture.valid?(pan_string) ||
43
+ Special.valid?(pan_string) ||
44
+ StaticCapture.valid?(pan_string) ||
45
+ Drop.valid?(pan_string) ||
46
+ DropCapture.valid?(pan_string) ||
47
+ Modify.valid?(pan_string)
48
+ end
49
+
50
+ # Parse a PAN string into an action object
51
+ #
52
+ # @param pan_string [String] PAN notation string
53
+ # @return [Pass, Move, Capture, Special, StaticCapture, Drop, DropCapture, Modify] immutable action instance
54
+ # @raise [ArgumentError] if the PAN string is invalid
55
+ #
56
+ # @example
57
+ # Action.parse("e2-e4") # => #<Move ...>
58
+ # Action.parse("d1+f3") # => #<Capture ...>
59
+ # Action.parse("...") # => #<Pass ...>
60
+ def self.parse(pan_string)
61
+ string_value = String(pan_string)
62
+
63
+ # Try each action type's parser in order of specificity
64
+ return Pass.parse(string_value) if Pass.valid?(string_value)
65
+ return Move.parse(string_value) if Move.valid?(string_value)
66
+ return Capture.parse(string_value) if Capture.valid?(string_value)
67
+ return Special.parse(string_value) if Special.valid?(string_value)
68
+ return StaticCapture.parse(string_value) if StaticCapture.valid?(string_value)
69
+ return Drop.parse(string_value) if Drop.valid?(string_value)
70
+ return DropCapture.parse(string_value) if DropCapture.valid?(string_value)
71
+ return Modify.parse(string_value) if Modify.valid?(string_value)
72
+
73
+ raise ::ArgumentError, format(ERROR_INVALID_PAN, string_value)
74
+ end
75
+
76
+ # Create a pass action
77
+ #
78
+ # @return [Pass] pass action instance
79
+ #
80
+ # @example
81
+ # Action.pass # => #<Pass>
82
+ def self.pass
83
+ Pass.instance
84
+ end
85
+
86
+ # Create a move action to an empty square
87
+ #
88
+ # @param source [String] source CELL coordinate
89
+ # @param destination [String] destination CELL coordinate
90
+ # @param transformation [String, nil] optional EPIN transformation
91
+ # @return [Move] move action instance
92
+ #
93
+ # @example
94
+ # Action.move("e2", "e4") # => #<Move ...>
95
+ # Action.move("e7", "e8", transformation: "Q") # => #<Move ...>
96
+ def self.move(source, destination, transformation: nil)
97
+ Move.new(source, destination, transformation: transformation)
98
+ end
99
+
100
+ # Create a capture action at destination
101
+ #
102
+ # @param source [String] source CELL coordinate
103
+ # @param destination [String] destination CELL coordinate
104
+ # @param transformation [String, nil] optional EPIN transformation
105
+ # @return [Capture] capture action instance
106
+ #
107
+ # @example
108
+ # Action.capture("d1", "f3") # => #<Capture ...>
109
+ # Action.capture("b7", "a8", transformation: "R") # => #<Capture ...>
110
+ def self.capture(source, destination, transformation: nil)
111
+ Capture.new(source, destination, transformation: transformation)
112
+ end
113
+
114
+ # Create a special move action with implicit side effects
115
+ #
116
+ # @param source [String] source CELL coordinate
117
+ # @param destination [String] destination CELL coordinate
118
+ # @param transformation [String, nil] optional EPIN transformation
119
+ # @return [Special] special action instance
120
+ #
121
+ # @example
122
+ # Action.special("e1", "g1") # => #<Special ...>
123
+ def self.special(source, destination, transformation: nil)
124
+ Special.new(source, destination, transformation: transformation)
125
+ end
126
+
127
+ # Create a static capture action (remove piece without movement)
128
+ #
129
+ # @param square [String] CELL coordinate of piece to capture
130
+ # @return [StaticCapture] static capture action instance
131
+ #
132
+ # @example
133
+ # Action.static_capture("d4") # => #<StaticCapture ...>
134
+ def self.static_capture(square)
135
+ StaticCapture.new(square)
136
+ end
137
+
138
+ # Create a drop action to empty square
139
+ #
140
+ # @param destination [String] destination CELL coordinate
141
+ # @param piece [String, nil] optional EPIN piece identifier
142
+ # @param transformation [String, nil] optional EPIN transformation
143
+ # @return [Drop] drop action instance
144
+ #
145
+ # @example
146
+ # Action.drop("e5", piece: "P") # => #<Drop ...>
147
+ # Action.drop("d4") # => #<Drop ...>
148
+ # Action.drop("c3", piece: "S", transformation: "+S") # => #<Drop ...>
149
+ def self.drop(destination, piece: nil, transformation: nil)
150
+ Drop.new(destination, piece: piece, transformation: transformation)
151
+ end
152
+
153
+ # Create a drop action with capture
154
+ #
155
+ # @param destination [String] destination CELL coordinate
156
+ # @param piece [String, nil] optional EPIN piece identifier
157
+ # @param transformation [String, nil] optional EPIN transformation
158
+ # @return [DropCapture] drop capture action instance
159
+ #
160
+ # @example
161
+ # Action.drop_capture("b4", piece: "L") # => #<DropCapture ...>
162
+ def self.drop_capture(destination, piece: nil, transformation: nil)
163
+ DropCapture.new(destination, piece: piece, transformation: transformation)
164
+ end
165
+
166
+ # Create an in-place transformation action
167
+ #
168
+ # @param square [String] CELL coordinate
169
+ # @param piece [String] EPIN piece identifier (final state)
170
+ # @return [Modify] modification action instance
171
+ #
172
+ # @example
173
+ # Action.modify("e4", "+P") # => #<Modify ...>
174
+ # Action.modify("c3", "k'") # => #<Modify ...>
175
+ def self.modify(square, piece)
176
+ Modify.new(square, piece)
177
+ end
178
+ end
179
+ end
180
+ end
data/lib/sashite/pan.rb CHANGED
@@ -1,165 +1,44 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "pan/dumper"
4
- require_relative "pan/parser"
3
+ require_relative "pan/action"
5
4
 
6
5
  module Sashite
7
- # The PAN (Portable Action Notation) module
6
+ # PAN (Portable Action Notation) implementation for Ruby
7
+ #
8
+ # Provides functionality for working with atomic actions in abstract strategy board games
9
+ # using a human-readable string format with intuitive operator-based syntax.
10
+ #
11
+ # This implementation is strictly compliant with PAN Specification v1.0.0
12
+ # @see https://sashite.dev/specs/pan/1.0.0/ PAN Specification v1.0.0
8
13
  module Pan
9
- # Main interface for PAN operations
10
- module_function
11
-
12
- # Parse a PAN string into structured move data
13
- #
14
- # @param pan_string [String] The PAN string to parse
15
- # @return [Hash] Structured move data with type, source, and destination
16
- # @raise [Parser::Error] If the PAN string is invalid
17
- # @example
18
- # Sashite::Pan.parse("e2-e4")
19
- # # => {type: :move, source: "e2", destination: "e4"}
14
+ # Check if a string represents a valid PAN action
20
15
  #
21
- # Sashite::Pan.parse("e4xd5")
22
- # # => {type: :capture, source: "e4", destination: "d5"}
23
- #
24
- # Sashite::Pan.parse("*e4")
25
- # # => {type: :drop, destination: "e4"}
26
- def parse(pan_string)
27
- Parser.call(pan_string)
28
- end
29
-
30
- # Convert structured move data to PAN string
16
+ # @param pan_string [String] the string to validate
17
+ # @return [Boolean] true if the string is a valid PAN action
31
18
  #
32
- # @param move_data [Hash] Structured move data with type, source, and destination
33
- # @return [String] PAN string representation
34
- # @raise [Dumper::Error] If the move data is invalid
35
19
  # @example
36
- # Sashite::Pan.dump({type: :move, source: "e2", destination: "e4"})
37
- # # => "e2-e4"
38
- #
39
- # Sashite::Pan.dump({type: :capture, source: "e4", destination: "d5"})
40
- # # => "e4xd5"
41
- #
42
- # Sashite::Pan.dump({type: :drop, destination: "e4"})
43
- # # => "*e4"
44
- def dump(move_data)
45
- Dumper.call(move_data)
20
+ # Sashite::Pan.valid?("e2-e4") # => true
21
+ # Sashite::Pan.valid?("d1+f3") # => true
22
+ # Sashite::Pan.valid?("...") # => true
23
+ # Sashite::Pan.valid?("P*e5") # => true
24
+ # Sashite::Pan.valid?("invalid") # => false
25
+ def self.valid?(pan_string)
26
+ Action.valid?(pan_string)
46
27
  end
47
28
 
48
- # Validate a PAN string without raising exceptions
29
+ # Parse a PAN string into an Action object
49
30
  #
50
- # @param pan_string [String] The PAN string to validate
51
- # @return [Boolean] True if valid, false otherwise
52
- # @example
53
- # Sashite::Pan.valid?("e2-e4") # => true
54
- # Sashite::Pan.valid?("*e4") # => true
55
- # Sashite::Pan.valid?("e4xd5") # => true
56
- # Sashite::Pan.valid?("") # => false
57
- # Sashite::Pan.valid?("e2-e2") # => false
58
- # Sashite::Pan.valid?("E2-e4") # => false
59
- def valid?(pan_string)
60
- parse(pan_string)
61
- true
62
- rescue Parser::Error
63
- false
64
- end
65
-
66
- # Parse a PAN string without raising exceptions
31
+ # @param pan_string [String] PAN notation string
32
+ # @return [Pan::Action] immutable action instance
33
+ # @raise [ArgumentError] if the PAN string is invalid
67
34
  #
68
- # @param pan_string [String] The PAN string to parse
69
- # @return [Hash, nil] Structured move data or nil if invalid
70
35
  # @example
71
- # Sashite::Pan.safe_parse("e2-e4")
72
- # # => {type: :move, source: "e2", destination: "e4"}
73
- #
74
- # Sashite::Pan.safe_parse("invalid")
75
- # # => nil
76
- def safe_parse(pan_string)
77
- parse(pan_string)
78
- rescue Parser::Error
79
- nil
80
- end
81
-
82
- # Convert structured move data to PAN string without raising exceptions
83
- #
84
- # @param move_data [Hash] Structured move data with type, source, and destination
85
- # @return [String, nil] PAN string or nil if invalid
86
- # @example
87
- # Sashite::Pan.safe_dump({type: :move, source: "e2", destination: "e4"})
88
- # # => "e2-e4"
89
- #
90
- # Sashite::Pan.safe_dump({invalid: :data})
91
- # # => nil
92
- def safe_dump(move_data)
93
- dump(move_data)
94
- rescue Dumper::Error
95
- nil
96
- end
97
-
98
- # Check if a coordinate is valid according to PAN specification
99
- #
100
- # @param coordinate [String] The coordinate to validate
101
- # @return [Boolean] True if valid, false otherwise
102
- # @example
103
- # Sashite::Pan.valid_coordinate?("e4") # => true
104
- # Sashite::Pan.valid_coordinate?("a1") # => true
105
- # Sashite::Pan.valid_coordinate?("E4") # => false (uppercase)
106
- # Sashite::Pan.valid_coordinate?("e10") # => false (multi-digit rank)
107
- def valid_coordinate?(coordinate)
108
- return false unless coordinate.is_a?(::String)
109
- coordinate.match?(/\A[a-z][0-9]\z/)
110
- end
111
-
112
- # Get the regular expression pattern used for PAN validation
113
- #
114
- # @return [Regexp] The regex pattern for PAN strings
115
- # @example
116
- # pattern = Sashite::Pan.pattern
117
- # pattern.match?("e2-e4") # => true
118
- def pattern
119
- Parser::PAN_PATTERN
120
- end
121
-
122
- # Convert a PAN string to a human-readable description
123
- #
124
- # @param pan_string [String] The PAN string to describe
125
- # @return [String] Human-readable description
126
- # @raise [Parser::Error] If the PAN string is invalid
127
- # @example
128
- # Sashite::Pan.describe("e2-e4")
129
- # # => "Move from e2 to e4"
130
- #
131
- # Sashite::Pan.describe("e4xd5")
132
- # # => "Capture from e4 to d5"
133
- #
134
- # Sashite::Pan.describe("*e4")
135
- # # => "Drop to e4"
136
- def describe(pan_string)
137
- move_data = parse(pan_string)
138
-
139
- case move_data[:type]
140
- when :move
141
- "Move from #{move_data[:source]} to #{move_data[:destination]}"
142
- when :capture
143
- "Capture from #{move_data[:source]} to #{move_data[:destination]}"
144
- when :drop
145
- "Drop to #{move_data[:destination]}"
146
- end
147
- end
148
-
149
- # Convert a PAN string to a human-readable description without raising exceptions
150
- #
151
- # @param pan_string [String] The PAN string to describe
152
- # @return [String, nil] Human-readable description or nil if invalid
153
- # @example
154
- # Sashite::Pan.safe_describe("e2-e4")
155
- # # => "Move from e2 to e4"
156
- #
157
- # Sashite::Pan.safe_describe("invalid")
158
- # # => nil
159
- def safe_describe(pan_string)
160
- describe(pan_string)
161
- rescue Parser::Error
162
- nil
36
+ # Sashite::Pan.parse("e2-e4") # => #<Pan::Action type=:move ...>
37
+ # Sashite::Pan.parse("d1+f3") # => #<Pan::Action type=:capture ...>
38
+ # Sashite::Pan.parse("...") # => #<Pan::Action type=:pass>
39
+ # Sashite::Pan.parse("P*e5") # => #<Pan::Action type=:drop ...>
40
+ def self.parse(pan_string)
41
+ Action.parse(pan_string)
163
42
  end
164
43
  end
165
44
  end
data/lib/sashite-pan.rb CHANGED
@@ -1,15 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "sashite/pan"
4
+
3
5
  # Sashité namespace for board game notation libraries
6
+ #
7
+ # Sashité provides a collection of libraries for representing and manipulating
8
+ # board game concepts according to the Sashité Protocol specifications.
9
+ #
10
+ # @see https://sashite.dev/protocol/ Sashité Protocol
11
+ # @see https://sashite.dev/specs/ Sashité Specifications
12
+ # @author Sashité
4
13
  module Sashite
5
- # Portable Action Notation (PAN) implementation for Ruby
6
- #
7
- # PAN is a compact, string-based format for representing executed moves
8
- # in abstract strategy board games played on coordinate-based boards.
9
- #
10
- # @see https://sashite.dev/documents/pan/1.0.0/ PAN Specification v1.0.0
11
- # @author Sashité
12
- # @since 1.0.0
13
14
  end
14
-
15
- require_relative "sashite/pan"