sashite-pan 1.2.0 → 2.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.
- checksums.yaml +4 -4
- data/LICENSE.md +1 -1
- data/README.md +256 -38
- data/lib/sashite/pan/dumper/error.rb +11 -0
- data/lib/sashite/pan/dumper.rb +65 -18
- data/lib/sashite/pan/parser/error.rb +11 -0
- data/lib/sashite/pan/parser.rb +78 -16
- data/lib/sashite/pan.rb +52 -8
- data/lib/sashite-pan.rb +4 -3
- metadata +14 -126
- data/lib/sashite/pan/action.rb +0 -24
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: eec6ed9599bc268e1ef620b16fb261435b718bd6f95be8b74d82d31047caaf78
|
4
|
+
data.tar.gz: 57b17727d5e48a9a749af2962caa97e7e646ea91130fa9a44bb8eedc9690f7be
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1f0b7ff44689f303b3555b3aad7e64b4960a3f1f9a51ed7830312a2110536e9ff834f448c3145271a2206d60669374cab1b1a0637317bc7d391e8a93de3c3dba
|
7
|
+
data.tar.gz: c14bcb71d0703847b7c073c30cb66ea2c785502daf21ecf08cc384576849795944f06df6142d11dde9c09fa1efca612078445a17de255af2cdd4975e076f5e2f
|
data/LICENSE.md
CHANGED
data/README.md
CHANGED
@@ -1,78 +1,296 @@
|
|
1
|
-
#
|
1
|
+
# Pan.rb
|
2
2
|
|
3
|
-
|
3
|
+
[](https://github.com/sashite/pan.rb/tags)
|
4
|
+
[](https://rubydoc.info/github/sashite/pan.rb/main)
|
5
|
+

|
6
|
+
[](https://github.com/sashite/pan.rb/raw/main/LICENSE.md)
|
4
7
|
|
5
|
-
|
8
|
+
> **PAN** (Portable Action Notation) support for the Ruby language.
|
9
|
+
|
10
|
+
## What is PAN?
|
11
|
+
|
12
|
+
PAN (Portable Action Notation) is a compact, string-based format for representing **executed moves** in abstract strategy board games. PAN serves as a human-readable and space-efficient alternative to PMN (Portable Move Notation), expressing the same semantic information in a condensed textual format.
|
13
|
+
|
14
|
+
While PMN uses JSON arrays to describe move sequences, PAN encodes the same information using a delimited string format that is easier to read, write, and transmit in contexts where JSON overhead is undesirable.
|
6
15
|
|
7
|
-
|
16
|
+
This gem implements the [PAN Specification v1.0.0](https://sashite.dev/documents/pan/1.0.0/), providing a Ruby interface for:
|
17
|
+
|
18
|
+
- Converting between PAN strings and PMN format
|
19
|
+
- Parsing PAN strings into structured move data
|
20
|
+
- Creating PAN strings from move components
|
21
|
+
- Validating PAN strings according to the specification
|
22
|
+
|
23
|
+
## Installation
|
8
24
|
|
9
25
|
```ruby
|
10
|
-
|
26
|
+
# In your Gemfile
|
27
|
+
gem "sashite-pan"
|
28
|
+
```
|
29
|
+
|
30
|
+
Or install manually:
|
31
|
+
|
32
|
+
```sh
|
33
|
+
gem install sashite-pan
|
34
|
+
```
|
35
|
+
|
36
|
+
## PAN Format
|
37
|
+
|
38
|
+
A PAN string represents one or more **actions** that constitute a complete move in a game. The format structure is:
|
39
|
+
|
40
|
+
### Single Action
|
41
|
+
|
11
42
|
```
|
43
|
+
<source>,<destination>,<piece>[,<hand_piece>]
|
44
|
+
```
|
45
|
+
|
46
|
+
### Multiple Actions
|
12
47
|
|
13
|
-
|
48
|
+
```
|
49
|
+
<action1>;<action2>[;<action3>...]
|
50
|
+
```
|
14
51
|
|
15
|
-
|
52
|
+
Where:
|
16
53
|
|
17
|
-
|
54
|
+
- **source**: Origin square label, or `*` for drops from hand
|
55
|
+
- **destination**: Target square label
|
56
|
+
- **piece**: Piece being moved (PNN format with optional modifiers)
|
57
|
+
- **hand_piece**: Optional piece added to mover's hand (captures, promotions)
|
18
58
|
|
19
|
-
|
59
|
+
## Basic Usage
|
20
60
|
|
21
|
-
|
61
|
+
### Parsing PAN Strings
|
22
62
|
|
23
|
-
|
63
|
+
Convert a PAN string into PMN format (array of action hashes):
|
24
64
|
|
25
65
|
```ruby
|
26
|
-
require
|
66
|
+
require "sashite-pan"
|
67
|
+
|
68
|
+
# Simple move
|
69
|
+
result = Sashite::Pan.parse("27,18,+P")
|
70
|
+
# => [{"src_square"=>"27", "dst_square"=>"18", "piece_name"=>"+P"}]
|
71
|
+
|
72
|
+
# Capture with hand piece
|
73
|
+
result = Sashite::Pan.parse("36,27,B,P")
|
74
|
+
# => [{"src_square"=>"36", "dst_square"=>"27", "piece_name"=>"B", "piece_hand"=>"P"}]
|
75
|
+
|
76
|
+
# Drop from hand
|
77
|
+
result = Sashite::Pan.parse("*,27,p")
|
78
|
+
# => [{"src_square"=>nil, "dst_square"=>"27", "piece_name"=>"p"}]
|
79
|
+
|
80
|
+
# Multiple actions (castling)
|
81
|
+
result = Sashite::Pan.parse("e1,g1,K;h1,f1,R")
|
82
|
+
# => [
|
83
|
+
# {"src_square"=>"e1", "dst_square"=>"g1", "piece_name"=>"K"},
|
84
|
+
# {"src_square"=>"h1", "dst_square"=>"f1", "piece_name"=>"R"}
|
85
|
+
# ]
|
86
|
+
```
|
87
|
+
|
88
|
+
### Safe Parsing
|
27
89
|
|
28
|
-
|
90
|
+
Parse a PAN string without raising exceptions:
|
91
|
+
|
92
|
+
```ruby
|
93
|
+
require "sashite-pan"
|
29
94
|
|
30
|
-
|
31
|
-
|
95
|
+
# Valid PAN string
|
96
|
+
result = Sashite::Pan.safe_parse("e2,e4,P'")
|
97
|
+
# => [{"src_square"=>"e2", "dst_square"=>"e4", "piece_name"=>"P'"}]
|
98
|
+
|
99
|
+
# Invalid PAN string
|
100
|
+
result = Sashite::Pan.safe_parse("invalid pan string")
|
101
|
+
# => nil
|
102
|
+
```
|
103
|
+
|
104
|
+
### Creating PAN Strings
|
105
|
+
|
106
|
+
Convert PMN actions (array of hashes) into a PAN string:
|
107
|
+
|
108
|
+
```ruby
|
109
|
+
require "sashite-pan"
|
110
|
+
|
111
|
+
# Simple move
|
112
|
+
pmn_actions = [{"src_square" => "27", "dst_square" => "18", "piece_name" => "+P"}]
|
113
|
+
pan_string = Sashite::Pan.dump(pmn_actions)
|
114
|
+
# => "27,18,+P"
|
115
|
+
|
116
|
+
# Capture with hand piece
|
117
|
+
pmn_actions = [{"src_square" => "36", "dst_square" => "27", "piece_name" => "B", "piece_hand" => "P"}]
|
118
|
+
pan_string = Sashite::Pan.dump(pmn_actions)
|
119
|
+
# => "36,27,B,P"
|
120
|
+
|
121
|
+
# Drop from hand
|
122
|
+
pmn_actions = [{"src_square" => nil, "dst_square" => "27", "piece_name" => "p"}]
|
123
|
+
pan_string = Sashite::Pan.dump(pmn_actions)
|
124
|
+
# => "*,27,p"
|
125
|
+
|
126
|
+
# Multiple actions (castling)
|
127
|
+
pmn_actions = [
|
128
|
+
{"src_square" => "e1", "dst_square" => "g1", "piece_name" => "K"},
|
129
|
+
{"src_square" => "h1", "dst_square" => "f1", "piece_name" => "R"}
|
32
130
|
]
|
131
|
+
pan_string = Sashite::Pan.dump(pmn_actions)
|
132
|
+
# => "e1,g1,K;h1,f1,R"
|
133
|
+
```
|
33
134
|
|
34
|
-
|
135
|
+
### Safe Dumping
|
35
136
|
|
36
|
-
|
137
|
+
Create PAN strings without raising exceptions:
|
37
138
|
|
38
|
-
|
139
|
+
```ruby
|
140
|
+
require "sashite-pan"
|
141
|
+
|
142
|
+
# Valid PMN data
|
143
|
+
pmn_actions = [{"src_square" => "e2", "dst_square" => "e4", "piece_name" => "P"}]
|
144
|
+
result = Sashite::Pan.safe_dump(pmn_actions)
|
145
|
+
# => "e2,e4,P"
|
146
|
+
|
147
|
+
# Invalid PMN data
|
148
|
+
invalid_data = [{"invalid" => "data"}]
|
149
|
+
result = Sashite::Pan.safe_dump(invalid_data)
|
150
|
+
# => nil
|
151
|
+
```
|
152
|
+
|
153
|
+
### Validation
|
154
|
+
|
155
|
+
Check if a string is valid PAN notation:
|
156
|
+
|
157
|
+
```ruby
|
158
|
+
require "sashite-pan"
|
159
|
+
|
160
|
+
Sashite::Pan.valid?("27,18,+P") # => true
|
161
|
+
Sashite::Pan.valid?("*,27,p") # => true
|
162
|
+
Sashite::Pan.valid?("e1,g1,K;h1,f1,R") # => true
|
163
|
+
|
164
|
+
Sashite::Pan.valid?("") # => false
|
165
|
+
Sashite::Pan.valid?("invalid") # => false
|
166
|
+
Sashite::Pan.valid?("27,18") # => false (missing piece)
|
39
167
|
```
|
40
168
|
|
41
169
|
## Examples
|
42
170
|
|
171
|
+
### Shogi Examples
|
172
|
+
|
43
173
|
```ruby
|
44
|
-
|
174
|
+
require "sashite-pan"
|
45
175
|
|
46
|
-
|
47
|
-
Sashite::
|
176
|
+
# Pawn promotion
|
177
|
+
Sashite::Pan.parse("27,18,+P")
|
178
|
+
# => [{"src_square"=>"27", "dst_square"=>"18", "piece_name"=>"+P"}]
|
48
179
|
|
49
|
-
#
|
180
|
+
# Bishop captures promoted pawn
|
181
|
+
Sashite::Pan.parse("36,27,B,P")
|
182
|
+
# => [{"src_square"=>"36", "dst_square"=>"27", "piece_name"=>"B", "piece_hand"=>"P"}]
|
50
183
|
|
51
|
-
|
52
|
-
Sashite::
|
184
|
+
# Drop pawn from hand
|
185
|
+
Sashite::Pan.parse("*,27,p")
|
186
|
+
# => [{"src_square"=>nil, "dst_square"=>"27", "piece_name"=>"p"}]
|
187
|
+
```
|
53
188
|
|
54
|
-
|
189
|
+
### Chess Examples
|
55
190
|
|
56
|
-
|
57
|
-
|
191
|
+
```ruby
|
192
|
+
require "sashite-pan"
|
193
|
+
|
194
|
+
# Kingside castling
|
195
|
+
Sashite::Pan.parse("e1,g1,K;h1,f1,R")
|
196
|
+
# => [
|
197
|
+
# {"src_square"=>"e1", "dst_square"=>"g1", "piece_name"=>"K"},
|
198
|
+
# {"src_square"=>"h1", "dst_square"=>"f1", "piece_name"=>"R"}
|
199
|
+
# ]
|
200
|
+
|
201
|
+
# Pawn with state modifier (can be captured en passant)
|
202
|
+
Sashite::Pan.parse("e2,e4,P'")
|
203
|
+
# => [{"src_square"=>"e2", "dst_square"=>"e4", "piece_name"=>"P'"}]
|
204
|
+
|
205
|
+
# En passant capture (multi-step)
|
206
|
+
Sashite::Pan.parse("d4,e3,p;e3,e4,p")
|
207
|
+
# => [
|
208
|
+
# {"src_square"=>"d4", "dst_square"=>"e3", "piece_name"=>"p"},
|
209
|
+
# {"src_square"=>"e3", "dst_square"=>"e4", "piece_name"=>"p"}
|
210
|
+
# ]
|
211
|
+
```
|
58
212
|
|
59
|
-
|
213
|
+
## Integration with PMN
|
60
214
|
|
61
|
-
|
62
|
-
Sashite::PAN.parse('*,42,P') # => [[nil, 42, 'P', nil]]
|
215
|
+
PAN is designed to work seamlessly with PMN (Portable Move Notation). You can easily convert between the two formats:
|
63
216
|
|
64
|
-
|
217
|
+
```ruby
|
218
|
+
require "sashite-pan"
|
219
|
+
require "portable_move_notation"
|
220
|
+
|
221
|
+
# Start with a PAN string
|
222
|
+
pan_string = "e2,e4,P';d7,d5,p"
|
223
|
+
|
224
|
+
# Convert to PMN format
|
225
|
+
pmn_actions = Sashite::Pan.parse(pan_string)
|
226
|
+
# => [
|
227
|
+
# {"src_square"=>"e2", "dst_square"=>"e4", "piece_name"=>"P'"},
|
228
|
+
# {"src_square"=>"d7", "dst_square"=>"d5", "piece_name"=>"p"}
|
229
|
+
# ]
|
230
|
+
|
231
|
+
# Use with PMN library
|
232
|
+
move = PortableMoveNotation::Move.new(*pmn_actions.map { |action|
|
233
|
+
PortableMoveNotation::Action.new(**action.transform_keys(&:to_sym))
|
234
|
+
})
|
235
|
+
|
236
|
+
# Convert back to PAN
|
237
|
+
new_pan_string = Sashite::Pan.dump(pmn_actions)
|
238
|
+
# => "e2,e4,P';d7,d5,p"
|
239
|
+
```
|
240
|
+
|
241
|
+
## Use Cases
|
242
|
+
|
243
|
+
PAN is optimal for:
|
244
|
+
|
245
|
+
- **Move logging and game records**: Compact storage of game moves
|
246
|
+
- **Network transmission**: Efficient move data transmission
|
247
|
+
- **Command-line interfaces**: Human-readable move input/output
|
248
|
+
- **Quick manual entry**: Easy to type and edit move sequences
|
249
|
+
- **Storage optimization**: Space-efficient alternative to JSON
|
250
|
+
|
251
|
+
PMN is optimal for:
|
252
|
+
|
253
|
+
- **Programmatic analysis**: Complex move processing and validation
|
254
|
+
- **JSON-based systems**: Direct integration with JSON APIs
|
255
|
+
- **Structured data processing**: Schema validation and type checking
|
256
|
+
|
257
|
+
## Properties of PAN
|
65
258
|
|
66
|
-
|
67
|
-
|
259
|
+
- **Rule-agnostic**: PAN does not encode legality, validity, or game-specific conditions
|
260
|
+
- **Space-efficient**: Significantly more compact than equivalent JSON representation
|
261
|
+
- **Human-readable**: Easy to read, write, and understand
|
262
|
+
- **Lossless conversion**: Perfect bidirectional conversion with PMN format
|
263
|
+
|
264
|
+
## Error Handling
|
265
|
+
|
266
|
+
The library provides detailed error messages for invalid input:
|
267
|
+
|
268
|
+
```ruby
|
269
|
+
require "sashite-pan"
|
270
|
+
|
271
|
+
begin
|
272
|
+
Sashite::Pan.parse("invalid,pan") # Missing piece component
|
273
|
+
rescue Sashite::Pan::Parser::Error => e
|
274
|
+
puts e.message # => "Action must have at least 3 components (source, destination, piece)"
|
275
|
+
end
|
276
|
+
|
277
|
+
begin
|
278
|
+
Sashite::Pan.dump([{"invalid" => "data"}]) # Missing required fields
|
279
|
+
rescue Sashite::Pan::Dumper::Error => e
|
280
|
+
puts e.message # => "Action must have dst_square"
|
281
|
+
end
|
68
282
|
```
|
69
283
|
|
70
|
-
##
|
284
|
+
## Documentation
|
285
|
+
|
286
|
+
- [Official PAN Specification](https://sashite.dev/documents/pan/1.0.0/)
|
287
|
+
- [API Documentation](https://rubydoc.info/github/sashite/pan.rb/main)
|
288
|
+
- [PMN Specification](https://sashite.dev/documents/pmn/1.0.0/)
|
71
289
|
|
72
|
-
|
290
|
+
## License
|
73
291
|
|
74
|
-
|
292
|
+
The [gem](https://rubygems.org/gems/sashite-pan) is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
75
293
|
|
76
|
-
|
294
|
+
## About Sashité
|
77
295
|
|
78
|
-
|
296
|
+
This project is maintained by [Sashité](https://sashite.com/) — promoting chess variants and sharing the beauty of Chinese, Japanese, and Western chess cultures.
|
data/lib/sashite/pan/dumper.rb
CHANGED
@@ -1,33 +1,80 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative
|
3
|
+
require_relative "dumper/error"
|
4
4
|
|
5
5
|
module Sashite
|
6
|
-
module
|
7
|
-
# Dumper
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
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(";")
|
12
20
|
end
|
13
21
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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(",")
|
19
41
|
end
|
20
42
|
|
21
|
-
|
22
|
-
|
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"]
|
23
57
|
end
|
24
58
|
|
25
|
-
|
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
|
26
66
|
|
27
|
-
|
28
|
-
|
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?
|
29
73
|
|
30
|
-
|
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
|
31
78
|
end
|
32
79
|
end
|
33
80
|
end
|
data/lib/sashite/pan/parser.rb
CHANGED
@@ -1,27 +1,89 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative
|
3
|
+
require_relative "parser/error"
|
4
4
|
|
5
5
|
module Sashite
|
6
|
-
module
|
7
|
-
# Parser
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
6
|
+
module Pan
|
7
|
+
# Parser for Portable Action Notation (PAN) strings
|
8
|
+
module Parser
|
9
|
+
# Parse a PAN string into PMN format
|
10
|
+
#
|
11
|
+
# @param pan_string [String] The PAN string to parse
|
12
|
+
# @return [Array<Hash>] Array of PMN action objects
|
13
|
+
# @raise [Parser::Error] If the PAN string is invalid
|
14
|
+
def self.call(pan_string)
|
15
|
+
raise Parser::Error, "PAN string cannot be nil" if pan_string.nil?
|
16
|
+
raise Parser::Error, "PAN string cannot be empty" if pan_string.empty?
|
17
|
+
|
18
|
+
actions = pan_string.split(";").map(&:strip)
|
19
|
+
raise Parser::Error, "No actions found" if actions.empty?
|
20
|
+
|
21
|
+
actions.map { |action| parse_action(action) }
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
# Parse a single action string into a PMN action hash
|
27
|
+
#
|
28
|
+
# @param action_string [String] Single action in PAN format
|
29
|
+
# @return [Hash] PMN action object
|
30
|
+
# @raise [Parser::Error] If the action is invalid
|
31
|
+
def self.parse_action(action_string)
|
32
|
+
components = action_string.split(",").map(&:strip)
|
33
|
+
|
34
|
+
validate_action_components(components)
|
35
|
+
|
36
|
+
{
|
37
|
+
"src_square" => parse_source_square(components[0]),
|
38
|
+
"dst_square" => components[1],
|
39
|
+
"piece_name" => components[2],
|
40
|
+
"piece_hand" => components[3] || nil
|
41
|
+
}.compact
|
42
|
+
end
|
43
|
+
|
44
|
+
# Validate action components structure
|
45
|
+
#
|
46
|
+
# @param components [Array<String>] Components of the action
|
47
|
+
# @raise [Parser::Error] If components are invalid
|
48
|
+
def self.validate_action_components(components)
|
49
|
+
case components.length
|
50
|
+
when 0, 1, 2
|
51
|
+
raise Parser::Error, "Action must have at least 3 components (source, destination, piece)"
|
52
|
+
when 3, 4
|
53
|
+
# Valid number of components
|
54
|
+
else
|
55
|
+
raise Parser::Error, "Action cannot have more than 4 components"
|
56
|
+
end
|
57
|
+
|
58
|
+
components.each_with_index do |component, index|
|
59
|
+
if component.nil? || component.empty?
|
60
|
+
raise Parser::Error, "Component #{index} cannot be empty"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
validate_piece_identifier(components[2])
|
65
|
+
validate_piece_identifier(components[3]) if components[3]
|
12
66
|
end
|
13
67
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
@piece_hand = action_args.fetch(3, nil)
|
68
|
+
# Parse source square, handling drop notation
|
69
|
+
#
|
70
|
+
# @param source [String] Source square or "*" for drop
|
71
|
+
# @return [String, nil] Square identifier or nil for drops
|
72
|
+
def self.parse_source_square(source)
|
73
|
+
source == "*" ? nil : source
|
21
74
|
end
|
22
75
|
|
23
|
-
|
24
|
-
|
76
|
+
# Validate piece identifier follows PNN specification
|
77
|
+
#
|
78
|
+
# @param piece [String] Piece identifier to validate
|
79
|
+
# @raise [Parser::Error] If piece identifier is invalid
|
80
|
+
def self.validate_piece_identifier(piece)
|
81
|
+
return if piece.nil?
|
82
|
+
|
83
|
+
# PNN pattern: optional prefix (+/-), letter (a-z/A-Z), optional suffix (')
|
84
|
+
unless piece.match?(/\A[-+]?[a-zA-Z][']?\z/)
|
85
|
+
raise Parser::Error, "Invalid piece identifier: #{piece}"
|
86
|
+
end
|
25
87
|
end
|
26
88
|
end
|
27
89
|
end
|
data/lib/sashite/pan.rb
CHANGED
@@ -1,17 +1,61 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "pan/dumper"
|
4
|
+
require_relative "pan/parser"
|
5
|
+
|
3
6
|
module Sashite
|
4
7
|
# The PAN (Portable Action Notation) module
|
5
|
-
module
|
6
|
-
|
7
|
-
|
8
|
+
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)
|
8
19
|
end
|
9
20
|
|
10
|
-
|
11
|
-
|
21
|
+
# Convert PMN actions to PAN string
|
22
|
+
#
|
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
|
31
|
+
#
|
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
|
39
|
+
end
|
40
|
+
|
41
|
+
# Parse a PAN string without raising exceptions
|
42
|
+
#
|
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
|
52
|
+
#
|
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
|
12
59
|
end
|
13
60
|
end
|
14
61
|
end
|
15
|
-
|
16
|
-
require_relative 'pan/dumper'
|
17
|
-
require_relative 'pan/parser'
|
data/lib/sashite-pan.rb
CHANGED
metadata
CHANGED
@@ -1,129 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sashite-pan
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Cyril Kato
|
8
|
-
autorequire:
|
9
8
|
bindir: bin
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
12
|
-
dependencies:
|
13
|
-
|
14
|
-
name: awesome_print
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
16
|
-
requirements:
|
17
|
-
- - ">="
|
18
|
-
- !ruby/object:Gem::Version
|
19
|
-
version: '0'
|
20
|
-
type: :development
|
21
|
-
prerelease: false
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
23
|
-
requirements:
|
24
|
-
- - ">="
|
25
|
-
- !ruby/object:Gem::Version
|
26
|
-
version: '0'
|
27
|
-
- !ruby/object:Gem::Dependency
|
28
|
-
name: bundler
|
29
|
-
requirement: !ruby/object:Gem::Requirement
|
30
|
-
requirements:
|
31
|
-
- - ">="
|
32
|
-
- !ruby/object:Gem::Version
|
33
|
-
version: '0'
|
34
|
-
type: :development
|
35
|
-
prerelease: false
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
37
|
-
requirements:
|
38
|
-
- - ">="
|
39
|
-
- !ruby/object:Gem::Version
|
40
|
-
version: '0'
|
41
|
-
- !ruby/object:Gem::Dependency
|
42
|
-
name: byebug
|
43
|
-
requirement: !ruby/object:Gem::Requirement
|
44
|
-
requirements:
|
45
|
-
- - ">="
|
46
|
-
- !ruby/object:Gem::Version
|
47
|
-
version: '0'
|
48
|
-
type: :development
|
49
|
-
prerelease: false
|
50
|
-
version_requirements: !ruby/object:Gem::Requirement
|
51
|
-
requirements:
|
52
|
-
- - ">="
|
53
|
-
- !ruby/object:Gem::Version
|
54
|
-
version: '0'
|
55
|
-
- !ruby/object:Gem::Dependency
|
56
|
-
name: rake
|
57
|
-
requirement: !ruby/object:Gem::Requirement
|
58
|
-
requirements:
|
59
|
-
- - ">="
|
60
|
-
- !ruby/object:Gem::Version
|
61
|
-
version: '0'
|
62
|
-
type: :development
|
63
|
-
prerelease: false
|
64
|
-
version_requirements: !ruby/object:Gem::Requirement
|
65
|
-
requirements:
|
66
|
-
- - ">="
|
67
|
-
- !ruby/object:Gem::Version
|
68
|
-
version: '0'
|
69
|
-
- !ruby/object:Gem::Dependency
|
70
|
-
name: rubocop-performance
|
71
|
-
requirement: !ruby/object:Gem::Requirement
|
72
|
-
requirements:
|
73
|
-
- - ">="
|
74
|
-
- !ruby/object:Gem::Version
|
75
|
-
version: '0'
|
76
|
-
type: :development
|
77
|
-
prerelease: false
|
78
|
-
version_requirements: !ruby/object:Gem::Requirement
|
79
|
-
requirements:
|
80
|
-
- - ">="
|
81
|
-
- !ruby/object:Gem::Version
|
82
|
-
version: '0'
|
83
|
-
- !ruby/object:Gem::Dependency
|
84
|
-
name: rubocop-thread_safety
|
85
|
-
requirement: !ruby/object:Gem::Requirement
|
86
|
-
requirements:
|
87
|
-
- - ">="
|
88
|
-
- !ruby/object:Gem::Version
|
89
|
-
version: '0'
|
90
|
-
type: :development
|
91
|
-
prerelease: false
|
92
|
-
version_requirements: !ruby/object:Gem::Requirement
|
93
|
-
requirements:
|
94
|
-
- - ">="
|
95
|
-
- !ruby/object:Gem::Version
|
96
|
-
version: '0'
|
97
|
-
- !ruby/object:Gem::Dependency
|
98
|
-
name: simplecov
|
99
|
-
requirement: !ruby/object:Gem::Requirement
|
100
|
-
requirements:
|
101
|
-
- - ">="
|
102
|
-
- !ruby/object:Gem::Version
|
103
|
-
version: '0'
|
104
|
-
type: :development
|
105
|
-
prerelease: false
|
106
|
-
version_requirements: !ruby/object:Gem::Requirement
|
107
|
-
requirements:
|
108
|
-
- - ">="
|
109
|
-
- !ruby/object:Gem::Version
|
110
|
-
version: '0'
|
111
|
-
- !ruby/object:Gem::Dependency
|
112
|
-
name: yard
|
113
|
-
requirement: !ruby/object:Gem::Requirement
|
114
|
-
requirements:
|
115
|
-
- - ">="
|
116
|
-
- !ruby/object:Gem::Version
|
117
|
-
version: '0'
|
118
|
-
type: :development
|
119
|
-
prerelease: false
|
120
|
-
version_requirements: !ruby/object:Gem::Requirement
|
121
|
-
requirements:
|
122
|
-
- - ">="
|
123
|
-
- !ruby/object:Gem::Version
|
124
|
-
version: '0'
|
125
|
-
description: A Ruby interface for data serialization in PAN (Portable Action Notation)
|
126
|
-
format.
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
|
+
dependencies: []
|
12
|
+
description: A Ruby implementation of the Portable Action Notation (PAN) specification.
|
127
13
|
email: contact@cyril.email
|
128
14
|
executables: []
|
129
15
|
extensions: []
|
@@ -133,17 +19,20 @@ files:
|
|
133
19
|
- README.md
|
134
20
|
- lib/sashite-pan.rb
|
135
21
|
- lib/sashite/pan.rb
|
136
|
-
- lib/sashite/pan/action.rb
|
137
22
|
- lib/sashite/pan/dumper.rb
|
23
|
+
- lib/sashite/pan/dumper/error.rb
|
138
24
|
- lib/sashite/pan/parser.rb
|
139
|
-
|
25
|
+
- lib/sashite/pan/parser/error.rb
|
26
|
+
homepage: https://github.com/sashite/pan.rb
|
140
27
|
licenses:
|
141
28
|
- MIT
|
142
29
|
metadata:
|
143
30
|
bug_tracker_uri: https://github.com/sashite/pan.rb/issues
|
144
|
-
documentation_uri: https://rubydoc.info/
|
31
|
+
documentation_uri: https://rubydoc.info/github/sashite/pan.rb/main
|
32
|
+
homepage_uri: https://github.com/sashite/pan.rb
|
145
33
|
source_code_uri: https://github.com/sashite/pan.rb
|
146
|
-
|
34
|
+
specification_uri: https://sashite.dev/documents/pan/1.0.0/
|
35
|
+
rubygems_mfa_required: 'true'
|
147
36
|
rdoc_options: []
|
148
37
|
require_paths:
|
149
38
|
- lib
|
@@ -151,15 +40,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
151
40
|
requirements:
|
152
41
|
- - ">="
|
153
42
|
- !ruby/object:Gem::Version
|
154
|
-
version:
|
43
|
+
version: 3.2.0
|
155
44
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
156
45
|
requirements:
|
157
46
|
- - ">="
|
158
47
|
- !ruby/object:Gem::Version
|
159
48
|
version: '0'
|
160
49
|
requirements: []
|
161
|
-
rubygems_version: 3.
|
162
|
-
signing_key:
|
50
|
+
rubygems_version: 3.6.9
|
163
51
|
specification_version: 4
|
164
|
-
summary:
|
52
|
+
summary: Portable Action Notation (PAN) parser and validator for Ruby
|
165
53
|
test_files: []
|
data/lib/sashite/pan/action.rb
DELETED
@@ -1,24 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Sashite
|
4
|
-
module PAN
|
5
|
-
# Action class
|
6
|
-
class Action
|
7
|
-
attr_reader :src_square, :dst_square, :piece_name, :piece_hand
|
8
|
-
|
9
|
-
private_class_method def self.separator
|
10
|
-
';'
|
11
|
-
end
|
12
|
-
|
13
|
-
private
|
14
|
-
|
15
|
-
def separator
|
16
|
-
','
|
17
|
-
end
|
18
|
-
|
19
|
-
def drop_char
|
20
|
-
'*'
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|