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,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,61 +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 PMN format
13
- #
14
- # @param pan_string [String] The PAN string to parse
15
- # @return [Array<Hash>] Array of PMN action objects
16
- # @raise [Parser::Error] If the PAN string is invalid
17
- def parse(pan_string)
18
- Parser.call(pan_string)
19
- end
20
-
21
- # Convert PMN actions to PAN string
14
+ # Check if a string represents a valid PAN action
22
15
  #
23
- # @param pmn_actions [Array<Hash>] Array of PMN action objects
24
- # @return [String] PAN string representation
25
- # @raise [Dumper::Error] If the PMN data is invalid
26
- def dump(pmn_actions)
27
- Dumper.call(pmn_actions)
28
- end
29
-
30
- # Validate a PAN string without raising exceptions
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 pan_string [String] The PAN string to validate
33
- # @return [Boolean] True if valid, false otherwise
34
- def valid?(pan_string)
35
- parse(pan_string)
36
- true
37
- rescue Parser::Error
38
- false
19
+ # @example
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)
39
27
  end
40
28
 
41
- # Parse a PAN string without raising exceptions
29
+ # Parse a PAN string into an Action object
42
30
  #
43
- # @param pan_string [String] The PAN string to parse
44
- # @return [Array<Hash>, nil] Array of PMN actions or nil if invalid
45
- def safe_parse(pan_string)
46
- parse(pan_string)
47
- rescue Parser::Error
48
- nil
49
- end
50
-
51
- # Convert PMN actions to 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
52
34
  #
53
- # @param pmn_actions [Array<Hash>] Array of PMN action objects
54
- # @return [String, nil] PAN string or nil if invalid
55
- def safe_dump(pmn_actions)
56
- dump(pmn_actions)
57
- rescue Dumper::Error
58
- nil
35
+ # @example
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)
59
42
  end
60
43
  end
61
44
  end
data/lib/sashite-pan.rb CHANGED
@@ -1,7 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Sashité namespace
3
+ require_relative "sashite/pan"
4
+
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
14
  end
6
-
7
- require_relative "sashite/pan"
metadata CHANGED
@@ -1,15 +1,46 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sashite-pan
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 4.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cyril Kato
8
8
  bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
- dependencies: []
12
- description: A Ruby implementation of the Portable Action Notation (PAN) specification.
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: sashite-cell
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '2.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '2.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: sashite-epin
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '1.1'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.1'
40
+ description: |
41
+ Parse and generate Portable Action Notation (PAN) strings for representing atomic actions in abstract strategy board games including chess, shogi, xiangqi, and others. PAN provides an intuitive operator-based syntax with six core operators: "-" (move to empty square), "+" (capture), "~" (special moves with side effects), "*" (drop to board), "." (drop with capture), and "=" (in-place transformation), plus "..." (pass turn).
42
+ Supports coordinates via CELL specification and piece identifiers via EPIN specification. Handles transformations ("e7-e8=Q"), enhanced/diminished states ("+R", "-P"), and style derivation markers ("K'"). Provides comprehensive validation, immutable action objects, and functional API design.
43
+ Examples: "e2-e4" (move), "d1+f3" (capture), "e1~g1" (castling), "P*e5" (drop), "e7-e8=Q" (promotion), "..." (pass), "+d4" (static capture), "e4=+P" (modify).
13
44
  email: contact@cyril.email
14
45
  executables: []
15
46
  extensions: []
@@ -19,10 +50,15 @@ files:
19
50
  - README.md
20
51
  - lib/sashite-pan.rb
21
52
  - lib/sashite/pan.rb
22
- - lib/sashite/pan/dumper.rb
23
- - lib/sashite/pan/dumper/error.rb
24
- - lib/sashite/pan/parser.rb
25
- - lib/sashite/pan/parser/error.rb
53
+ - lib/sashite/pan/action.rb
54
+ - lib/sashite/pan/action/capture.rb
55
+ - lib/sashite/pan/action/drop.rb
56
+ - lib/sashite/pan/action/drop_capture.rb
57
+ - lib/sashite/pan/action/modify.rb
58
+ - lib/sashite/pan/action/move.rb
59
+ - lib/sashite/pan/action/pass.rb
60
+ - lib/sashite/pan/action/special.rb
61
+ - lib/sashite/pan/action/static_capture.rb
26
62
  homepage: https://github.com/sashite/pan.rb
27
63
  licenses:
28
64
  - MIT
@@ -31,7 +67,7 @@ metadata:
31
67
  documentation_uri: https://rubydoc.info/github/sashite/pan.rb/main
32
68
  homepage_uri: https://github.com/sashite/pan.rb
33
69
  source_code_uri: https://github.com/sashite/pan.rb
34
- specification_uri: https://sashite.dev/documents/pan/1.0.0/
70
+ specification_uri: https://sashite.dev/specs/pan/1.0.0/
35
71
  rubygems_mfa_required: 'true'
36
72
  rdoc_options: []
37
73
  require_paths:
@@ -47,7 +83,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
47
83
  - !ruby/object:Gem::Version
48
84
  version: '0'
49
85
  requirements: []
50
- rubygems_version: 3.6.9
86
+ rubygems_version: 3.7.1
51
87
  specification_version: 4
52
- summary: Portable Action Notation (PAN) parser and validator for Ruby
88
+ summary: Portable Action Notation (PAN) - operator-based notation for abstract strategy
89
+ game actions
53
90
  test_files: []
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Sashite
4
- module Pan
5
- module Dumper
6
- # Error raised when PAN dumping fails
7
- class Error < ::StandardError
8
- end
9
- end
10
- end
11
- end
@@ -1,81 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "dumper/error"
4
-
5
- module Sashite
6
- module Pan
7
- # Dumper for converting PMN format to PAN strings
8
- module Dumper
9
- # Convert PMN actions to PAN string
10
- #
11
- # @param pmn_actions [Array<Hash>] Array of PMN action objects
12
- # @return [String] PAN string representation
13
- # @raise [Dumper::Error] If the PMN data is invalid
14
- def self.call(pmn_actions)
15
- raise Dumper::Error, "PMN actions cannot be nil" if pmn_actions.nil?
16
- raise Dumper::Error, "PMN actions cannot be empty" if pmn_actions.empty?
17
- raise Dumper::Error, "PMN actions must be an array" unless pmn_actions.is_a?(::Array)
18
-
19
- pmn_actions.map { |action| dump_action(action) }.join(";")
20
- end
21
-
22
- private
23
-
24
- # Convert a single PMN action to PAN format
25
- #
26
- # @param action [Hash] PMN action object
27
- # @return [String] PAN action string
28
- # @raise [Dumper::Error] If the action is invalid
29
- def self.dump_action(action)
30
- validate_pmn_action(action)
31
-
32
- components = [
33
- dump_source_square(action["src_square"]),
34
- action["dst_square"],
35
- action["piece_name"]
36
- ]
37
-
38
- components << action["piece_hand"] if action["piece_hand"]
39
-
40
- components.join(",")
41
- end
42
-
43
- # Validate PMN action structure
44
- #
45
- # @param action [Hash] PMN action to validate
46
- # @raise [Dumper::Error] If action is invalid
47
- def self.validate_pmn_action(action)
48
- raise Dumper::Error, "Action must be a Hash" unless action.is_a?(::Hash)
49
- raise Dumper::Error, "Action must have dst_square" unless action.key?("dst_square")
50
- raise Dumper::Error, "Action must have piece_name" unless action.key?("piece_name")
51
-
52
- raise Dumper::Error, "dst_square cannot be nil or empty" if action["dst_square"].nil? || action["dst_square"].empty?
53
- raise Dumper::Error, "piece_name cannot be nil or empty" if action["piece_name"].nil? || action["piece_name"].empty?
54
-
55
- validate_piece_identifier(action["piece_name"])
56
- validate_piece_identifier(action["piece_hand"]) if action["piece_hand"]
57
- end
58
-
59
- # Convert source square, handling drops
60
- #
61
- # @param src_square [String, nil] Source square or nil for drop
62
- # @return [String] "*" for drops, otherwise the square identifier
63
- def self.dump_source_square(src_square)
64
- src_square.nil? ? "*" : src_square
65
- end
66
-
67
- # Validate piece identifier follows PNN specification
68
- #
69
- # @param piece [String] Piece identifier to validate
70
- # @raise [Dumper::Error] If piece identifier is invalid
71
- def self.validate_piece_identifier(piece)
72
- return if piece.nil?
73
-
74
- # PNN pattern: optional prefix (+/-), letter (a-z/A-Z), optional suffix (')
75
- unless piece.match?(/\A[-+]?[a-zA-Z][']?\z/)
76
- raise Dumper::Error, "Invalid piece identifier: #{piece}"
77
- end
78
- end
79
- end
80
- end
81
- end
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Sashite
4
- module Pan
5
- module Parser
6
- # Error raised when PAN parsing fails
7
- class Error < ::StandardError
8
- end
9
- end
10
- end
11
- end