sashite-pan 2.0.0 → 3.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/README.md +149 -162
- data/lib/sashite/pan/dumper.rb +92 -52
- data/lib/sashite/pan/parser.rb +69 -55
- data/lib/sashite/pan.rb +116 -12
- data/lib/sashite-pan.rb +9 -1
- metadata +9 -5
- data/lib/sashite/pan/dumper/error.rb +0 -11
- data/lib/sashite/pan/parser/error.rb +0 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2e35bec227c5721965c2355d4534738acc1cc54807a7e3358a944c0c60cf3fd3
|
4
|
+
data.tar.gz: 30ba0f48da191edf2b971e8eb2fb3d94ff03a7f39580bfb25d334b15cb692a5f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 44bb851835eeb4327ddd27aa82068332460571083f02af9100d4bcb37aa8944dea0e700a606496eeb1ae295f377a0e8dda2f737b00d50d191c9489083a21e629
|
7
|
+
data.tar.gz: ad48f345973e0ccf6582446ab35cf02828eab72e0399297fef89dd713e43173f47780ea6214880d6924da73965037ac95175bf79d5cfc40da0b65d518314cced
|
data/README.md
CHANGED
@@ -9,16 +9,15 @@
|
|
9
9
|
|
10
10
|
## What is PAN?
|
11
11
|
|
12
|
-
PAN (Portable Action Notation) is a compact, string-based format for representing **executed moves** in abstract strategy board games. PAN
|
12
|
+
PAN (Portable Action Notation) is a compact, string-based format for representing **executed moves** in abstract strategy board games played on coordinate-based boards. PAN provides a human-readable and space-efficient notation for expressing move actions in a rule-agnostic manner.
|
13
13
|
|
14
|
-
|
14
|
+
PAN focuses on representing the spatial aspects of moves: where pieces move from and to, and whether the move involves capture or placement. The notation is designed to be intuitive and compatible with standard algebraic coordinate systems.
|
15
15
|
|
16
16
|
This gem implements the [PAN Specification v1.0.0](https://sashite.dev/documents/pan/1.0.0/), providing a Ruby interface for:
|
17
17
|
|
18
|
-
- Converting between PAN strings and PMN format
|
19
18
|
- Parsing PAN strings into structured move data
|
20
|
-
- Creating PAN strings from move components
|
21
19
|
- Validating PAN strings according to the specification
|
20
|
+
- Converting between PAN and other move representations
|
22
21
|
|
23
22
|
## Installation
|
24
23
|
|
@@ -35,54 +34,54 @@ gem install sashite-pan
|
|
35
34
|
|
36
35
|
## PAN Format
|
37
36
|
|
38
|
-
|
39
|
-
|
40
|
-
### Single Action
|
37
|
+
PAN uses three fundamental move types with intuitive operators:
|
41
38
|
|
39
|
+
### Simple Move (Non-capture)
|
42
40
|
```
|
43
|
-
<source
|
41
|
+
<source>-<destination>
|
44
42
|
```
|
43
|
+
**Example**: `e2-e4` - Moves a piece from e2 to e4
|
45
44
|
|
46
|
-
###
|
45
|
+
### Capture Move
|
46
|
+
```
|
47
|
+
<source>x<destination>
|
48
|
+
```
|
49
|
+
**Example**: `e4xd5` - Moves a piece from e4 to d5, capturing the piece at d5
|
47
50
|
|
51
|
+
### Drop/Placement
|
48
52
|
```
|
49
|
-
|
53
|
+
*<destination>
|
50
54
|
```
|
55
|
+
**Example**: `*e4` - Places a piece at e4 from off-board (hand, reserve, etc.)
|
56
|
+
|
57
|
+
### Coordinate System
|
51
58
|
|
52
|
-
|
59
|
+
PAN uses algebraic coordinates consisting of:
|
60
|
+
- **File**: A single lowercase letter (`a-z`)
|
61
|
+
- **Rank**: A single digit (`0-9`)
|
53
62
|
|
54
|
-
|
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)
|
63
|
+
Examples: `e4`, `a1`, `h8`, `d5`
|
58
64
|
|
59
65
|
## Basic Usage
|
60
66
|
|
61
67
|
### Parsing PAN Strings
|
62
68
|
|
63
|
-
Convert a PAN string into
|
69
|
+
Convert a PAN string into structured move data:
|
64
70
|
|
65
71
|
```ruby
|
66
72
|
require "sashite-pan"
|
67
73
|
|
68
74
|
# Simple move
|
69
|
-
result = Sashite::Pan.parse("
|
70
|
-
# =>
|
75
|
+
result = Sashite::Pan.parse("e2-e4")
|
76
|
+
# => {type: :move, source: "e2", destination: "e4"}
|
71
77
|
|
72
|
-
# Capture
|
73
|
-
result = Sashite::Pan.parse("
|
74
|
-
# =>
|
78
|
+
# Capture
|
79
|
+
result = Sashite::Pan.parse("e4xd5")
|
80
|
+
# => {type: :capture, source: "e4", destination: "d5"}
|
75
81
|
|
76
82
|
# Drop from hand
|
77
|
-
result = Sashite::Pan.parse("
|
78
|
-
# =>
|
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
|
-
# ]
|
83
|
+
result = Sashite::Pan.parse("*e4")
|
84
|
+
# => {type: :drop, destination: "e4"}
|
86
85
|
```
|
87
86
|
|
88
87
|
### Safe Parsing
|
@@ -93,199 +92,187 @@ Parse a PAN string without raising exceptions:
|
|
93
92
|
require "sashite-pan"
|
94
93
|
|
95
94
|
# Valid PAN string
|
96
|
-
result = Sashite::Pan.safe_parse("e2
|
97
|
-
# =>
|
95
|
+
result = Sashite::Pan.safe_parse("e2-e4")
|
96
|
+
# => {type: :move, source: "e2", destination: "e4"}
|
98
97
|
|
99
98
|
# Invalid PAN string
|
100
|
-
result = Sashite::Pan.safe_parse("invalid
|
99
|
+
result = Sashite::Pan.safe_parse("invalid")
|
101
100
|
# => nil
|
102
101
|
```
|
103
102
|
|
104
|
-
###
|
103
|
+
### Validation
|
105
104
|
|
106
|
-
|
105
|
+
Check if a string is valid PAN notation:
|
107
106
|
|
108
107
|
```ruby
|
109
108
|
require "sashite-pan"
|
110
109
|
|
111
|
-
#
|
112
|
-
|
113
|
-
|
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"
|
110
|
+
Sashite::Pan.valid?("e2-e4") # => true
|
111
|
+
Sashite::Pan.valid?("*e4") # => true
|
112
|
+
Sashite::Pan.valid?("e4xd5") # => true
|
120
113
|
|
121
|
-
#
|
122
|
-
|
123
|
-
|
124
|
-
# =>
|
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"}
|
130
|
-
]
|
131
|
-
pan_string = Sashite::Pan.dump(pmn_actions)
|
132
|
-
# => "e1,g1,K;h1,f1,R"
|
114
|
+
Sashite::Pan.valid?("") # => false
|
115
|
+
Sashite::Pan.valid?("e2-e2") # => false (source equals destination)
|
116
|
+
Sashite::Pan.valid?("E2-e4") # => false (uppercase file)
|
117
|
+
Sashite::Pan.valid?("e2 - e4") # => false (spaces not allowed)
|
133
118
|
```
|
134
119
|
|
135
|
-
|
120
|
+
## Examples
|
136
121
|
|
137
|
-
|
122
|
+
### Chess Examples
|
138
123
|
|
139
124
|
```ruby
|
140
125
|
require "sashite-pan"
|
141
126
|
|
142
|
-
#
|
143
|
-
|
144
|
-
|
145
|
-
# => "e2,e4,P"
|
127
|
+
# Pawn advance
|
128
|
+
Sashite::Pan.parse("e2-e4")
|
129
|
+
# => {type: :move, source: "e2", destination: "e4"}
|
146
130
|
|
147
|
-
#
|
148
|
-
|
149
|
-
|
150
|
-
# => nil
|
151
|
-
```
|
131
|
+
# Capture
|
132
|
+
Sashite::Pan.parse("exd5")
|
133
|
+
# => {type: :capture, source: "e4", destination: "d5"}
|
152
134
|
|
153
|
-
|
135
|
+
# Note: PAN cannot distinguish piece types or promotion choices
|
136
|
+
# These moves require game context for complete interpretation:
|
137
|
+
Sashite::Pan.parse("e7-e8") # Could be pawn promotion to any piece
|
138
|
+
Sashite::Pan.parse("a1-a8") # Could be rook, queen, or promoted piece
|
139
|
+
```
|
154
140
|
|
155
|
-
|
141
|
+
### Shogi Examples
|
156
142
|
|
157
143
|
```ruby
|
158
144
|
require "sashite-pan"
|
159
145
|
|
160
|
-
|
161
|
-
Sashite::Pan.
|
162
|
-
|
146
|
+
# Piece movement
|
147
|
+
Sashite::Pan.parse("g7-f7")
|
148
|
+
# => {type: :move, source: "g7", destination: "f7"}
|
149
|
+
|
150
|
+
# Drop from hand
|
151
|
+
Sashite::Pan.parse("*e5")
|
152
|
+
# => {type: :drop, destination: "e5"}
|
153
|
+
|
154
|
+
# Capture (captured piece goes to hand in Shogi)
|
155
|
+
Sashite::Pan.parse("h2xg2")
|
156
|
+
# => {type: :capture, source: "h2", destination: "g2"}
|
163
157
|
|
164
|
-
|
165
|
-
|
166
|
-
Sashite::Pan.valid?("27,18") # => false (missing piece)
|
158
|
+
# Note: PAN cannot specify which piece type is being dropped
|
159
|
+
# or whether a piece is promoted
|
167
160
|
```
|
168
161
|
|
169
|
-
##
|
162
|
+
## Limitations and Context Dependency
|
170
163
|
|
171
|
-
|
164
|
+
**Important**: PAN is intentionally minimal and rule-agnostic. It has several important limitations:
|
172
165
|
|
173
|
-
|
174
|
-
require "sashite-pan"
|
166
|
+
### What PAN Cannot Represent
|
175
167
|
|
176
|
-
|
177
|
-
|
178
|
-
|
168
|
+
- **Piece types**: Cannot distinguish between different pieces making the same move
|
169
|
+
- **Promotion choices**: Cannot specify what piece a pawn promotes to
|
170
|
+
- **Game state**: No encoding of check, checkmate, or game conditions
|
171
|
+
- **Complex moves**: Castling requires external representation
|
172
|
+
- **Piece identity**: Multiple pieces of the same type making similar moves
|
179
173
|
|
180
|
-
|
181
|
-
Sashite::Pan.parse("36,27,B,P")
|
182
|
-
# => [{"src_square"=>"36", "dst_square"=>"27", "piece_name"=>"B", "piece_hand"=>"P"}]
|
174
|
+
### Examples of Ambiguity
|
183
175
|
|
184
|
-
|
185
|
-
|
186
|
-
|
176
|
+
```ruby
|
177
|
+
# These PAN strings are syntactically valid but may be ambiguous:
|
178
|
+
|
179
|
+
"e7-e8" # Pawn promotion - but to what piece?
|
180
|
+
"*g4" # Drop - but which piece from hand?
|
181
|
+
"a1-a8" # Movement - but which piece type?
|
182
|
+
"e1-g1" # Could be castling, but rook movement not shown
|
187
183
|
```
|
188
184
|
|
189
|
-
###
|
185
|
+
### When PAN is Insufficient
|
186
|
+
|
187
|
+
- Games where multiple pieces can make the same spatial move
|
188
|
+
- Games requiring promotion choice specification
|
189
|
+
- Analysis requiring piece type identification
|
190
|
+
- Self-contained game records without context
|
191
|
+
|
192
|
+
## Error Handling
|
193
|
+
|
194
|
+
The library provides detailed error messages for invalid input:
|
190
195
|
|
191
196
|
```ruby
|
192
197
|
require "sashite-pan"
|
193
198
|
|
194
|
-
|
195
|
-
Sashite::Pan.parse("
|
196
|
-
|
197
|
-
#
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
#
|
202
|
-
Sashite::Pan
|
203
|
-
# =>
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
#
|
208
|
-
|
209
|
-
#
|
210
|
-
|
199
|
+
begin
|
200
|
+
Sashite::Pan.parse("e2-e2") # Source equals destination
|
201
|
+
rescue Sashite::Pan::Parser::Error => e
|
202
|
+
puts e.message # => "Source and destination cannot be identical"
|
203
|
+
end
|
204
|
+
|
205
|
+
begin
|
206
|
+
Sashite::Pan.parse("E2-e4") # Invalid uppercase file
|
207
|
+
rescue Sashite::Pan::Parser::Error => e
|
208
|
+
puts e.message # => "Invalid PAN format: E2-e4"
|
209
|
+
end
|
210
|
+
|
211
|
+
begin
|
212
|
+
Sashite::Pan.parse("") # Empty string
|
213
|
+
rescue Sashite::Pan::Parser::Error => e
|
214
|
+
puts e.message # => "PAN string cannot be empty"
|
215
|
+
end
|
211
216
|
```
|
212
217
|
|
213
|
-
##
|
218
|
+
## Regular Expression Pattern
|
214
219
|
|
215
|
-
PAN
|
220
|
+
PAN strings can be validated using this pattern:
|
216
221
|
|
217
222
|
```ruby
|
218
|
-
|
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
|
-
```
|
223
|
+
PAN_PATTERN = /\A(\*|[a-z][0-9][-x])([a-z][0-9])\z/
|
240
224
|
|
241
|
-
|
225
|
+
def valid_pan?(string)
|
226
|
+
return false unless string.match?(PAN_PATTERN)
|
242
227
|
|
243
|
-
|
228
|
+
# Additional validation for source != destination
|
229
|
+
if string.include?('-') || string.include?('x')
|
230
|
+
source = string[0..1]
|
231
|
+
destination = string[-2..-1]
|
232
|
+
return source != destination
|
233
|
+
end
|
244
234
|
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
- **Quick manual entry**: Easy to type and edit move sequences
|
249
|
-
- **Storage optimization**: Space-efficient alternative to JSON
|
235
|
+
true
|
236
|
+
end
|
237
|
+
```
|
250
238
|
|
251
|
-
|
239
|
+
## Use Cases
|
252
240
|
|
253
|
-
|
254
|
-
- **JSON-based systems**: Direct integration with JSON APIs
|
255
|
-
- **Structured data processing**: Schema validation and type checking
|
241
|
+
### Optimal for PAN
|
256
242
|
|
257
|
-
|
243
|
+
- **Move logs**: Simple game records where context is available
|
244
|
+
- **User interfaces**: Command input for move entry
|
245
|
+
- **Network protocols**: Compact move transmission
|
246
|
+
- **Quick notation**: Manual notation for simple games
|
258
247
|
|
259
|
-
|
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
|
248
|
+
### Consider Alternatives When
|
263
249
|
|
264
|
-
|
250
|
+
- **Ambiguous games**: Multiple pieces can make the same spatial move
|
251
|
+
- **Complex promotions**: Games with multiple promotion choices
|
252
|
+
- **Analysis tools**: When piece identity is crucial
|
253
|
+
- **Self-contained records**: When context is not available
|
265
254
|
|
266
|
-
|
255
|
+
## Integration Considerations
|
267
256
|
|
268
|
-
|
269
|
-
require "sashite-pan"
|
257
|
+
When using PAN in your applications:
|
270
258
|
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
end
|
259
|
+
1. **Always pair with context**: Store board state alongside PAN moves
|
260
|
+
2. **Document assumptions**: Clearly specify how ambiguities are resolved
|
261
|
+
3. **Validate rigorously**: Check both syntax and semantic validity
|
262
|
+
4. **Handle edge cases**: Plan for promotion and drop ambiguities
|
276
263
|
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
264
|
+
## Properties of PAN
|
265
|
+
|
266
|
+
- **Rule-agnostic**: Does not encode piece types, legality, or game-specific conditions
|
267
|
+
- **Compact**: Minimal character overhead (3-5 characters per move)
|
268
|
+
- **Human-readable**: Intuitive algebraic notation
|
269
|
+
- **Space-efficient**: Excellent for large game databases
|
270
|
+
- **Context-dependent**: Requires external game state for complete interpretation
|
283
271
|
|
284
272
|
## Documentation
|
285
273
|
|
286
274
|
- [Official PAN Specification](https://sashite.dev/documents/pan/1.0.0/)
|
287
275
|
- [API Documentation](https://rubydoc.info/github/sashite/pan.rb/main)
|
288
|
-
- [PMN Specification](https://sashite.dev/documents/pmn/1.0.0/)
|
289
276
|
|
290
277
|
## License
|
291
278
|
|
data/lib/sashite/pan/dumper.rb
CHANGED
@@ -1,80 +1,120 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "dumper/error"
|
4
|
-
|
5
3
|
module Sashite
|
6
4
|
module Pan
|
7
|
-
# Dumper for converting
|
5
|
+
# Dumper for converting structured move data to PAN strings
|
8
6
|
module Dumper
|
9
|
-
|
7
|
+
class Error < ::StandardError
|
8
|
+
end
|
9
|
+
|
10
|
+
# Convert structured move data to PAN string
|
10
11
|
#
|
11
|
-
# @param
|
12
|
+
# @param move_data [Hash] Move data with type, source, destination
|
12
13
|
# @return [String] PAN string representation
|
13
|
-
# @raise [Dumper::Error] If the
|
14
|
-
def self.call(
|
15
|
-
raise Dumper::Error, "
|
16
|
-
raise Dumper::Error, "
|
17
|
-
raise Dumper::Error, "PMN actions must be an array" unless pmn_actions.is_a?(::Array)
|
14
|
+
# @raise [Dumper::Error] If the move data is invalid
|
15
|
+
def self.call(move_data)
|
16
|
+
raise Dumper::Error, "Move data cannot be nil" if move_data.nil?
|
17
|
+
raise Dumper::Error, "Move data must be a Hash" unless move_data.is_a?(::Hash)
|
18
18
|
|
19
|
-
|
19
|
+
validate_move_data(move_data)
|
20
|
+
|
21
|
+
case move_data[:type]
|
22
|
+
when :move
|
23
|
+
dump_simple_move(move_data)
|
24
|
+
when :capture
|
25
|
+
dump_capture_move(move_data)
|
26
|
+
when :drop
|
27
|
+
dump_drop_move(move_data)
|
28
|
+
else
|
29
|
+
raise Dumper::Error, "Invalid move type: #{move_data[:type]}"
|
30
|
+
end
|
20
31
|
end
|
21
32
|
|
22
33
|
private
|
23
34
|
|
24
|
-
#
|
35
|
+
# Validate the structure of move data
|
25
36
|
#
|
26
|
-
# @param
|
27
|
-
# @
|
28
|
-
|
29
|
-
|
30
|
-
|
37
|
+
# @param move_data [Hash] Move data to validate
|
38
|
+
# @raise [Dumper::Error] If move data is invalid
|
39
|
+
def self.validate_move_data(move_data)
|
40
|
+
unless move_data.key?(:type)
|
41
|
+
raise Dumper::Error, "Move data must have :type key"
|
42
|
+
end
|
31
43
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
action["piece_name"]
|
36
|
-
]
|
44
|
+
unless move_data.key?(:destination)
|
45
|
+
raise Dumper::Error, "Move data must have :destination key"
|
46
|
+
end
|
37
47
|
|
38
|
-
|
48
|
+
validate_coordinate(move_data[:destination], "destination")
|
39
49
|
|
40
|
-
|
50
|
+
case move_data[:type]
|
51
|
+
when :move, :capture
|
52
|
+
unless move_data.key?(:source)
|
53
|
+
raise Dumper::Error, "Move and capture types must have :source key"
|
54
|
+
end
|
55
|
+
validate_coordinate(move_data[:source], "source")
|
56
|
+
validate_different_coordinates(move_data[:source], move_data[:destination])
|
57
|
+
when :drop
|
58
|
+
if move_data.key?(:source)
|
59
|
+
raise Dumper::Error, "Drop type cannot have :source key"
|
60
|
+
end
|
61
|
+
else
|
62
|
+
raise Dumper::Error, "Invalid move type: #{move_data[:type]}"
|
63
|
+
end
|
41
64
|
end
|
42
65
|
|
43
|
-
# Validate
|
66
|
+
# Validate a coordinate follows PAN format
|
44
67
|
#
|
45
|
-
# @param
|
46
|
-
# @
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
68
|
+
# @param coordinate [String] Coordinate to validate
|
69
|
+
# @param field_name [String] Name of the field for error messages
|
70
|
+
# @raise [Dumper::Error] If coordinate is invalid
|
71
|
+
def self.validate_coordinate(coordinate, field_name)
|
72
|
+
if coordinate.nil? || coordinate.empty?
|
73
|
+
raise Dumper::Error, "#{field_name.capitalize} coordinate cannot be nil or empty"
|
74
|
+
end
|
75
|
+
|
76
|
+
unless coordinate.is_a?(::String)
|
77
|
+
raise Dumper::Error, "#{field_name.capitalize} coordinate must be a String"
|
78
|
+
end
|
79
|
+
|
80
|
+
unless coordinate.match?(/\A[a-z][0-9]\z/)
|
81
|
+
raise Dumper::Error, "Invalid #{field_name} coordinate format: #{coordinate}. Must be lowercase letter followed by digit (e.g., 'e4')"
|
82
|
+
end
|
57
83
|
end
|
58
84
|
|
59
|
-
#
|
85
|
+
# Validate that source and destination are different
|
60
86
|
#
|
61
|
-
# @param
|
62
|
-
# @
|
63
|
-
|
64
|
-
|
87
|
+
# @param source [String] Source coordinate
|
88
|
+
# @param destination [String] Destination coordinate
|
89
|
+
# @raise [Dumper::Error] If coordinates are the same
|
90
|
+
def self.validate_different_coordinates(source, destination)
|
91
|
+
if source == destination
|
92
|
+
raise Dumper::Error, "Source and destination coordinates cannot be identical: #{source}"
|
93
|
+
end
|
65
94
|
end
|
66
95
|
|
67
|
-
#
|
96
|
+
# Generate PAN string for simple move
|
68
97
|
#
|
69
|
-
# @param
|
70
|
-
# @
|
71
|
-
def self.
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
98
|
+
# @param move_data [Hash] Move data with :source and :destination
|
99
|
+
# @return [String] PAN string in format "source-destination"
|
100
|
+
def self.dump_simple_move(move_data)
|
101
|
+
"#{move_data[:source]}-#{move_data[:destination]}"
|
102
|
+
end
|
103
|
+
|
104
|
+
# Generate PAN string for capture move
|
105
|
+
#
|
106
|
+
# @param move_data [Hash] Move data with :source and :destination
|
107
|
+
# @return [String] PAN string in format "sourcexdestination"
|
108
|
+
def self.dump_capture_move(move_data)
|
109
|
+
"#{move_data[:source]}x#{move_data[:destination]}"
|
110
|
+
end
|
111
|
+
|
112
|
+
# Generate PAN string for drop move
|
113
|
+
#
|
114
|
+
# @param move_data [Hash] Move data with :destination
|
115
|
+
# @return [String] PAN string in format "*destination"
|
116
|
+
def self.dump_drop_move(move_data)
|
117
|
+
"*#{move_data[:destination]}"
|
78
118
|
end
|
79
119
|
end
|
80
120
|
end
|
data/lib/sashite/pan/parser.rb
CHANGED
@@ -1,89 +1,103 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "parser/error"
|
4
|
-
|
5
3
|
module Sashite
|
6
4
|
module Pan
|
7
5
|
# Parser for Portable Action Notation (PAN) strings
|
8
6
|
module Parser
|
9
|
-
|
7
|
+
class Error < ::StandardError
|
8
|
+
end
|
9
|
+
|
10
|
+
# Regular expression pattern for validating PAN strings
|
11
|
+
PAN_PATTERN = /\A(\*|[a-z][0-9][-x])([a-z][0-9])\z/
|
12
|
+
|
13
|
+
# Parse a PAN string into structured move data
|
10
14
|
#
|
11
15
|
# @param pan_string [String] The PAN string to parse
|
12
|
-
# @return [
|
16
|
+
# @return [Hash] Structured move data with type, source, and destination
|
13
17
|
# @raise [Parser::Error] If the PAN string is invalid
|
14
18
|
def self.call(pan_string)
|
15
19
|
raise Parser::Error, "PAN string cannot be nil" if pan_string.nil?
|
20
|
+
raise Parser::Error, "PAN string must be a String" unless pan_string.is_a?(::String)
|
16
21
|
raise Parser::Error, "PAN string cannot be empty" if pan_string.empty?
|
17
22
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
actions.map { |action| parse_action(action) }
|
23
|
+
validate_format(pan_string)
|
24
|
+
parse_move(pan_string)
|
22
25
|
end
|
23
26
|
|
24
27
|
private
|
25
28
|
|
26
|
-
#
|
29
|
+
# Validate the basic format of a PAN string
|
27
30
|
#
|
28
|
-
# @param
|
29
|
-
# @
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
validate_action_components(components)
|
31
|
+
# @param pan_string [String] The PAN string to validate
|
32
|
+
# @raise [Parser::Error] If format is invalid
|
33
|
+
def self.validate_format(pan_string)
|
34
|
+
unless pan_string.match?(PAN_PATTERN)
|
35
|
+
raise Parser::Error, "Invalid PAN format: #{pan_string}"
|
36
|
+
end
|
35
37
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
38
|
+
# Additional validation for source != destination in moves and captures
|
39
|
+
if pan_string.include?('-') || pan_string.include?('x')
|
40
|
+
source = pan_string[0..1]
|
41
|
+
destination = pan_string[-2..-1]
|
42
|
+
if source == destination
|
43
|
+
raise Parser::Error, "Source and destination cannot be identical: #{source}"
|
44
|
+
end
|
45
|
+
end
|
42
46
|
end
|
43
47
|
|
44
|
-
#
|
48
|
+
# Parse the move based on its type
|
45
49
|
#
|
46
|
-
# @param
|
47
|
-
# @
|
48
|
-
def self.
|
49
|
-
case
|
50
|
-
when 0
|
51
|
-
|
52
|
-
when
|
53
|
-
|
50
|
+
# @param pan_string [String] The validated PAN string
|
51
|
+
# @return [Hash] Structured move data
|
52
|
+
def self.parse_move(pan_string)
|
53
|
+
case pan_string
|
54
|
+
when /\A([a-z][0-9])-([a-z][0-9])\z/
|
55
|
+
parse_simple_move($1, $2)
|
56
|
+
when /\A([a-z][0-9])x([a-z][0-9])\z/
|
57
|
+
parse_capture_move($1, $2)
|
58
|
+
when /\A\*([a-z][0-9])\z/
|
59
|
+
parse_drop_move($1)
|
54
60
|
else
|
55
|
-
|
61
|
+
# This should never happen due to earlier validation
|
62
|
+
raise Parser::Error, "Unexpected PAN format: #{pan_string}"
|
56
63
|
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]
|
66
64
|
end
|
67
65
|
|
68
|
-
# Parse
|
66
|
+
# Parse a simple move (non-capture)
|
69
67
|
#
|
70
|
-
# @param source [String] Source
|
71
|
-
# @
|
72
|
-
|
73
|
-
|
68
|
+
# @param source [String] Source coordinate
|
69
|
+
# @param destination [String] Destination coordinate
|
70
|
+
# @return [Hash] Move data for simple move
|
71
|
+
def self.parse_simple_move(source, destination)
|
72
|
+
{
|
73
|
+
type: :move,
|
74
|
+
source: source,
|
75
|
+
destination: destination
|
76
|
+
}
|
74
77
|
end
|
75
78
|
|
76
|
-
#
|
79
|
+
# Parse a capture move
|
77
80
|
#
|
78
|
-
# @param
|
79
|
-
# @
|
80
|
-
|
81
|
-
|
81
|
+
# @param source [String] Source coordinate
|
82
|
+
# @param destination [String] Destination coordinate
|
83
|
+
# @return [Hash] Move data for capture move
|
84
|
+
def self.parse_capture_move(source, destination)
|
85
|
+
{
|
86
|
+
type: :capture,
|
87
|
+
source: source,
|
88
|
+
destination: destination
|
89
|
+
}
|
90
|
+
end
|
82
91
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
92
|
+
# Parse a drop/placement move
|
93
|
+
#
|
94
|
+
# @param destination [String] Destination coordinate
|
95
|
+
# @return [Hash] Move data for drop move
|
96
|
+
def self.parse_drop_move(destination)
|
97
|
+
{
|
98
|
+
type: :drop,
|
99
|
+
destination: destination
|
100
|
+
}
|
87
101
|
end
|
88
102
|
end
|
89
103
|
end
|
data/lib/sashite/pan.rb
CHANGED
@@ -9,28 +9,53 @@ module Sashite
|
|
9
9
|
# Main interface for PAN operations
|
10
10
|
module_function
|
11
11
|
|
12
|
-
# Parse a PAN string into
|
12
|
+
# Parse a PAN string into structured move data
|
13
13
|
#
|
14
14
|
# @param pan_string [String] The PAN string to parse
|
15
|
-
# @return [
|
15
|
+
# @return [Hash] Structured move data with type, source, and destination
|
16
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"}
|
20
|
+
#
|
21
|
+
# Sashite::Pan.parse("e4xd5")
|
22
|
+
# # => {type: :capture, source: "e4", destination: "d5"}
|
23
|
+
#
|
24
|
+
# Sashite::Pan.parse("*e4")
|
25
|
+
# # => {type: :drop, destination: "e4"}
|
17
26
|
def parse(pan_string)
|
18
27
|
Parser.call(pan_string)
|
19
28
|
end
|
20
29
|
|
21
|
-
# Convert
|
30
|
+
# Convert structured move data to PAN string
|
22
31
|
#
|
23
|
-
# @param
|
32
|
+
# @param move_data [Hash] Structured move data with type, source, and destination
|
24
33
|
# @return [String] PAN string representation
|
25
|
-
# @raise [Dumper::Error] If the
|
26
|
-
|
27
|
-
|
34
|
+
# @raise [Dumper::Error] If the move data is invalid
|
35
|
+
# @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)
|
28
46
|
end
|
29
47
|
|
30
48
|
# Validate a PAN string without raising exceptions
|
31
49
|
#
|
32
50
|
# @param pan_string [String] The PAN string to validate
|
33
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
|
34
59
|
def valid?(pan_string)
|
35
60
|
parse(pan_string)
|
36
61
|
true
|
@@ -41,21 +66,100 @@ module Sashite
|
|
41
66
|
# Parse a PAN string without raising exceptions
|
42
67
|
#
|
43
68
|
# @param pan_string [String] The PAN string to parse
|
44
|
-
# @return [
|
69
|
+
# @return [Hash, nil] Structured move data or nil if invalid
|
70
|
+
# @example
|
71
|
+
# Sashite::Pan.safe_parse("e2-e4")
|
72
|
+
# # => {type: :move, source: "e2", destination: "e4"}
|
73
|
+
#
|
74
|
+
# Sashite::Pan.safe_parse("invalid")
|
75
|
+
# # => nil
|
45
76
|
def safe_parse(pan_string)
|
46
77
|
parse(pan_string)
|
47
78
|
rescue Parser::Error
|
48
79
|
nil
|
49
80
|
end
|
50
81
|
|
51
|
-
# Convert
|
82
|
+
# Convert structured move data to PAN string without raising exceptions
|
52
83
|
#
|
53
|
-
# @param
|
84
|
+
# @param move_data [Hash] Structured move data with type, source, and destination
|
54
85
|
# @return [String, nil] PAN string or nil if invalid
|
55
|
-
|
56
|
-
|
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)
|
57
94
|
rescue Dumper::Error
|
58
95
|
nil
|
59
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
|
163
|
+
end
|
60
164
|
end
|
61
165
|
end
|
data/lib/sashite-pan.rb
CHANGED
@@ -1,7 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# Sashité namespace
|
3
|
+
# Sashité namespace for board game notation libraries
|
4
4
|
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
|
5
13
|
end
|
6
14
|
|
7
15
|
require_relative "sashite/pan"
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sashite-pan
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 3.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Cyril Kato
|
@@ -9,7 +9,12 @@ bindir: bin
|
|
9
9
|
cert_chain: []
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
11
11
|
dependencies: []
|
12
|
-
description:
|
12
|
+
description: |
|
13
|
+
Parse and generate Portable Action Notation (PAN) strings for representing moves
|
14
|
+
in chess, shogi, and other strategy board games. PAN provides a compact,
|
15
|
+
human-readable format for move logging, game transmission, and database storage.
|
16
|
+
Supports simple moves (e2-e4), captures (exd5), and piece drops (*e4) with
|
17
|
+
comprehensive validation and error handling.
|
13
18
|
email: contact@cyril.email
|
14
19
|
executables: []
|
15
20
|
extensions: []
|
@@ -20,9 +25,7 @@ files:
|
|
20
25
|
- lib/sashite-pan.rb
|
21
26
|
- lib/sashite/pan.rb
|
22
27
|
- lib/sashite/pan/dumper.rb
|
23
|
-
- lib/sashite/pan/dumper/error.rb
|
24
28
|
- lib/sashite/pan/parser.rb
|
25
|
-
- lib/sashite/pan/parser/error.rb
|
26
29
|
homepage: https://github.com/sashite/pan.rb
|
27
30
|
licenses:
|
28
31
|
- MIT
|
@@ -49,5 +52,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
49
52
|
requirements: []
|
50
53
|
rubygems_version: 3.6.9
|
51
54
|
specification_version: 4
|
52
|
-
summary:
|
55
|
+
summary: Compact notation for board game moves - parse chess, shogi, and strategy
|
56
|
+
game actions
|
53
57
|
test_files: []
|