sashite-pan 1.3.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/LICENSE.md +1 -1
- data/README.md +228 -47
- data/lib/sashite/pan/dumper.rb +105 -20
- data/lib/sashite/pan/parser.rb +92 -18
- data/lib/sashite/pan.rb +156 -8
- data/lib/sashite-pan.rb +11 -2
- metadata +18 -154
- 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: 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/LICENSE.md
CHANGED
data/README.md
CHANGED
@@ -1,102 +1,283 @@
|
|
1
|
-
#
|
1
|
+
# Pan.rb
|
2
2
|
|
3
|
-
[](https://github.com/sashite/pan.rb/
|
3
|
+
[](https://github.com/sashite/pan.rb/tags)
|
4
4
|
[](https://rubydoc.info/github/sashite/pan.rb/main)
|
5
|
-
|
6
|
-
[](https://github.com/sashite/pan.rb/actions?query=workflow%3Arubocop+branch%3Amain)
|
5
|
+

|
7
6
|
[](https://github.com/sashite/pan.rb/raw/main/LICENSE.md)
|
8
7
|
|
9
|
-
|
8
|
+
> **PAN** (Portable Action Notation) support for the Ruby language.
|
10
9
|
|
11
|
-
##
|
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 played on coordinate-based boards. PAN provides a human-readable and space-efficient notation for expressing move actions in a rule-agnostic manner.
|
13
|
+
|
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
|
+
|
16
|
+
This gem implements the [PAN Specification v1.0.0](https://sashite.dev/documents/pan/1.0.0/), providing a Ruby interface for:
|
12
17
|
|
13
|
-
|
18
|
+
- Parsing PAN strings into structured move data
|
19
|
+
- Validating PAN strings according to the specification
|
20
|
+
- Converting between PAN and other move representations
|
21
|
+
|
22
|
+
## Installation
|
14
23
|
|
15
24
|
```ruby
|
25
|
+
# In your Gemfile
|
16
26
|
gem "sashite-pan"
|
17
27
|
```
|
18
28
|
|
19
|
-
|
29
|
+
Or install manually:
|
20
30
|
|
21
31
|
```sh
|
22
|
-
|
32
|
+
gem install sashite-pan
|
23
33
|
```
|
24
34
|
|
25
|
-
|
35
|
+
## PAN Format
|
26
36
|
|
27
|
-
|
28
|
-
|
37
|
+
PAN uses three fundamental move types with intuitive operators:
|
38
|
+
|
39
|
+
### Simple Move (Non-capture)
|
40
|
+
```
|
41
|
+
<source>-<destination>
|
42
|
+
```
|
43
|
+
**Example**: `e2-e4` - Moves a piece from e2 to e4
|
44
|
+
|
45
|
+
### Capture Move
|
46
|
+
```
|
47
|
+
<source>x<destination>
|
48
|
+
```
|
49
|
+
**Example**: `e4xd5` - Moves a piece from e4 to d5, capturing the piece at d5
|
50
|
+
|
51
|
+
### Drop/Placement
|
29
52
|
```
|
53
|
+
*<destination>
|
54
|
+
```
|
55
|
+
**Example**: `*e4` - Places a piece at e4 from off-board (hand, reserve, etc.)
|
56
|
+
|
57
|
+
### Coordinate System
|
58
|
+
|
59
|
+
PAN uses algebraic coordinates consisting of:
|
60
|
+
- **File**: A single lowercase letter (`a-z`)
|
61
|
+
- **Rank**: A single digit (`0-9`)
|
62
|
+
|
63
|
+
Examples: `e4`, `a1`, `h8`, `d5`
|
30
64
|
|
31
|
-
## Usage
|
65
|
+
## Basic Usage
|
32
66
|
|
33
|
-
|
67
|
+
### Parsing PAN Strings
|
68
|
+
|
69
|
+
Convert a PAN string into structured move data:
|
34
70
|
|
35
71
|
```ruby
|
36
|
-
require "sashite
|
72
|
+
require "sashite-pan"
|
73
|
+
|
74
|
+
# Simple move
|
75
|
+
result = Sashite::Pan.parse("e2-e4")
|
76
|
+
# => {type: :move, source: "e2", destination: "e4"}
|
77
|
+
|
78
|
+
# Capture
|
79
|
+
result = Sashite::Pan.parse("e4xd5")
|
80
|
+
# => {type: :capture, source: "e4", destination: "d5"}
|
81
|
+
|
82
|
+
# Drop from hand
|
83
|
+
result = Sashite::Pan.parse("*e4")
|
84
|
+
# => {type: :drop, destination: "e4"}
|
85
|
+
```
|
37
86
|
|
38
|
-
|
87
|
+
### Safe Parsing
|
39
88
|
|
40
|
-
|
41
|
-
[52, 36, "♙"]
|
42
|
-
]
|
89
|
+
Parse a PAN string without raising exceptions:
|
43
90
|
|
44
|
-
|
91
|
+
```ruby
|
92
|
+
require "sashite-pan"
|
45
93
|
|
46
|
-
#
|
94
|
+
# Valid PAN string
|
95
|
+
result = Sashite::Pan.safe_parse("e2-e4")
|
96
|
+
# => {type: :move, source: "e2", destination: "e4"}
|
47
97
|
|
48
|
-
|
98
|
+
# Invalid PAN string
|
99
|
+
result = Sashite::Pan.safe_parse("invalid")
|
100
|
+
# => nil
|
49
101
|
```
|
50
102
|
|
51
|
-
|
103
|
+
### Validation
|
52
104
|
|
53
|
-
|
105
|
+
Check if a string is valid PAN notation:
|
54
106
|
|
55
107
|
```ruby
|
56
|
-
|
57
|
-
|
108
|
+
require "sashite-pan"
|
109
|
+
|
110
|
+
Sashite::Pan.valid?("e2-e4") # => true
|
111
|
+
Sashite::Pan.valid?("*e4") # => true
|
112
|
+
Sashite::Pan.valid?("e4xd5") # => true
|
113
|
+
|
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)
|
58
118
|
```
|
59
119
|
|
60
|
-
|
120
|
+
## Examples
|
121
|
+
|
122
|
+
### Chess Examples
|
61
123
|
|
62
124
|
```ruby
|
63
|
-
|
64
|
-
|
125
|
+
require "sashite-pan"
|
126
|
+
|
127
|
+
# Pawn advance
|
128
|
+
Sashite::Pan.parse("e2-e4")
|
129
|
+
# => {type: :move, source: "e2", destination: "e4"}
|
130
|
+
|
131
|
+
# Capture
|
132
|
+
Sashite::Pan.parse("exd5")
|
133
|
+
# => {type: :capture, source: "e4", destination: "d5"}
|
134
|
+
|
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
|
65
139
|
```
|
66
140
|
|
67
|
-
###
|
141
|
+
### Shogi Examples
|
68
142
|
|
69
143
|
```ruby
|
70
|
-
|
71
|
-
|
144
|
+
require "sashite-pan"
|
145
|
+
|
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"}
|
157
|
+
|
158
|
+
# Note: PAN cannot specify which piece type is being dropped
|
159
|
+
# or whether a piece is promoted
|
72
160
|
```
|
73
161
|
|
74
|
-
|
162
|
+
## Limitations and Context Dependency
|
75
163
|
|
76
|
-
|
77
|
-
Western chess, more than one action could be consider like a move, and joined
|
78
|
-
thanks to the [`portable_move_notation`](https://rubygems.org/gems/portable_move_notation) gem.
|
164
|
+
**Important**: PAN is intentionally minimal and rule-agnostic. It has several important limitations:
|
79
165
|
|
80
|
-
###
|
166
|
+
### What PAN Cannot Represent
|
167
|
+
|
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
|
173
|
+
|
174
|
+
### Examples of Ambiguity
|
81
175
|
|
82
176
|
```ruby
|
83
|
-
|
84
|
-
|
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
|
85
183
|
```
|
86
184
|
|
87
|
-
###
|
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:
|
88
195
|
|
89
196
|
```ruby
|
90
|
-
|
91
|
-
|
197
|
+
require "sashite-pan"
|
198
|
+
|
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
|
92
216
|
```
|
93
217
|
|
94
|
-
##
|
218
|
+
## Regular Expression Pattern
|
219
|
+
|
220
|
+
PAN strings can be validated using this pattern:
|
221
|
+
|
222
|
+
```ruby
|
223
|
+
PAN_PATTERN = /\A(\*|[a-z][0-9][-x])([a-z][0-9])\z/
|
224
|
+
|
225
|
+
def valid_pan?(string)
|
226
|
+
return false unless string.match?(PAN_PATTERN)
|
227
|
+
|
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
|
95
234
|
|
96
|
-
|
235
|
+
true
|
236
|
+
end
|
237
|
+
```
|
238
|
+
|
239
|
+
## Use Cases
|
240
|
+
|
241
|
+
### Optimal for PAN
|
242
|
+
|
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
|
247
|
+
|
248
|
+
### Consider Alternatives When
|
249
|
+
|
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
|
254
|
+
|
255
|
+
## Integration Considerations
|
256
|
+
|
257
|
+
When using PAN in your applications:
|
258
|
+
|
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
|
263
|
+
|
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
|
271
|
+
|
272
|
+
## Documentation
|
273
|
+
|
274
|
+
- [Official PAN Specification](https://sashite.dev/documents/pan/1.0.0/)
|
275
|
+
- [API Documentation](https://rubydoc.info/github/sashite/pan.rb/main)
|
276
|
+
|
277
|
+
## License
|
97
278
|
|
98
|
-
|
279
|
+
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).
|
99
280
|
|
100
|
-
|
281
|
+
## About Sashité
|
101
282
|
|
102
|
-
|
283
|
+
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,35 +1,120 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "action"
|
4
|
-
|
5
3
|
module Sashite
|
6
|
-
module
|
7
|
-
# Dumper
|
8
|
-
|
9
|
-
|
10
|
-
actions.map { |action_items| new(*action_items).call }
|
11
|
-
.join(separator)
|
4
|
+
module Pan
|
5
|
+
# Dumper for converting structured move data to PAN strings
|
6
|
+
module Dumper
|
7
|
+
class Error < ::StandardError
|
12
8
|
end
|
13
9
|
|
14
|
-
|
15
|
-
|
10
|
+
# Convert structured move data to PAN string
|
11
|
+
#
|
12
|
+
# @param move_data [Hash] Move data with type, source, destination
|
13
|
+
# @return [String] PAN string representation
|
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)
|
16
18
|
|
17
|
-
|
18
|
-
@dst_square = Integer(dst_square)
|
19
|
-
@piece_name = piece_name.to_s
|
20
|
-
@piece_hand = piece_hand&.to_s
|
21
|
-
end
|
19
|
+
validate_move_data(move_data)
|
22
20
|
|
23
|
-
|
24
|
-
|
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
|
25
31
|
end
|
26
32
|
|
27
33
|
private
|
28
34
|
|
29
|
-
|
30
|
-
|
35
|
+
# Validate the structure of move data
|
36
|
+
#
|
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
|
43
|
+
|
44
|
+
unless move_data.key?(:destination)
|
45
|
+
raise Dumper::Error, "Move data must have :destination key"
|
46
|
+
end
|
47
|
+
|
48
|
+
validate_coordinate(move_data[:destination], "destination")
|
49
|
+
|
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
|
64
|
+
end
|
65
|
+
|
66
|
+
# Validate a coordinate follows PAN format
|
67
|
+
#
|
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
|
83
|
+
end
|
84
|
+
|
85
|
+
# Validate that source and destination are different
|
86
|
+
#
|
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
|
94
|
+
end
|
95
|
+
|
96
|
+
# Generate PAN string for simple move
|
97
|
+
#
|
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
|
31
111
|
|
32
|
-
|
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]}"
|
33
118
|
end
|
34
119
|
end
|
35
120
|
end
|
data/lib/sashite/pan/parser.rb
CHANGED
@@ -1,29 +1,103 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "action"
|
4
|
-
|
5
3
|
module Sashite
|
6
|
-
module
|
7
|
-
# Parser
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
4
|
+
module Pan
|
5
|
+
# Parser for Portable Action Notation (PAN) strings
|
6
|
+
module Parser
|
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
|
14
|
+
#
|
15
|
+
# @param pan_string [String] The PAN string to parse
|
16
|
+
# @return [Hash] Structured move data with type, source, and destination
|
17
|
+
# @raise [Parser::Error] If the PAN string is invalid
|
18
|
+
def self.call(pan_string)
|
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)
|
21
|
+
raise Parser::Error, "PAN string cannot be empty" if pan_string.empty?
|
22
|
+
|
23
|
+
validate_format(pan_string)
|
24
|
+
parse_move(pan_string)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
# Validate the basic format of a PAN string
|
30
|
+
#
|
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
|
37
|
+
|
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
|
12
46
|
end
|
13
47
|
|
14
|
-
|
15
|
-
|
48
|
+
# Parse the move based on its type
|
49
|
+
#
|
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)
|
60
|
+
else
|
61
|
+
# This should never happen due to earlier validation
|
62
|
+
raise Parser::Error, "Unexpected PAN format: #{pan_string}"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Parse a simple move (non-capture)
|
67
|
+
#
|
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
|
+
}
|
77
|
+
end
|
16
78
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
79
|
+
# Parse a capture move
|
80
|
+
#
|
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
|
+
}
|
23
90
|
end
|
24
91
|
|
25
|
-
|
26
|
-
|
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
|
+
}
|
27
101
|
end
|
28
102
|
end
|
29
103
|
end
|
data/lib/sashite/pan.rb
CHANGED
@@ -1,17 +1,165 @@
|
|
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 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"}
|
20
|
+
#
|
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
|
31
|
+
#
|
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
|
+
# @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)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Validate a PAN string without raising exceptions
|
49
|
+
#
|
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
|
67
|
+
#
|
68
|
+
# @param pan_string [String] The PAN string to parse
|
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
|
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/)
|
8
110
|
end
|
9
111
|
|
10
|
-
|
11
|
-
|
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
|
12
163
|
end
|
13
164
|
end
|
14
165
|
end
|
15
|
-
|
16
|
-
require_relative "pan/dumper"
|
17
|
-
require_relative "pan/parser"
|
data/lib/sashite-pan.rb
CHANGED
@@ -1,6 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
#
|
4
|
-
module Sashite
|
3
|
+
# Sashité namespace for board game notation libraries
|
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
|
13
|
+
end
|
5
14
|
|
6
15
|
require_relative "sashite/pan"
|
metadata
CHANGED
@@ -1,157 +1,20 @@
|
|
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
|
8
|
-
autorequire:
|
9
8
|
bindir: bin
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
12
|
-
dependencies:
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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-md
|
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-performance
|
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: rubocop-rake
|
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: rubocop-thread_safety
|
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
|
-
- !ruby/object:Gem::Dependency
|
126
|
-
name: simplecov
|
127
|
-
requirement: !ruby/object:Gem::Requirement
|
128
|
-
requirements:
|
129
|
-
- - ">="
|
130
|
-
- !ruby/object:Gem::Version
|
131
|
-
version: '0'
|
132
|
-
type: :development
|
133
|
-
prerelease: false
|
134
|
-
version_requirements: !ruby/object:Gem::Requirement
|
135
|
-
requirements:
|
136
|
-
- - ">="
|
137
|
-
- !ruby/object:Gem::Version
|
138
|
-
version: '0'
|
139
|
-
- !ruby/object:Gem::Dependency
|
140
|
-
name: yard
|
141
|
-
requirement: !ruby/object:Gem::Requirement
|
142
|
-
requirements:
|
143
|
-
- - ">="
|
144
|
-
- !ruby/object:Gem::Version
|
145
|
-
version: '0'
|
146
|
-
type: :development
|
147
|
-
prerelease: false
|
148
|
-
version_requirements: !ruby/object:Gem::Requirement
|
149
|
-
requirements:
|
150
|
-
- - ">="
|
151
|
-
- !ruby/object:Gem::Version
|
152
|
-
version: '0'
|
153
|
-
description: A Ruby interface for data serialization in PAN (Portable Action Notation)
|
154
|
-
format.
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
|
+
dependencies: []
|
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.
|
155
18
|
email: contact@cyril.email
|
156
19
|
executables: []
|
157
20
|
extensions: []
|
@@ -161,17 +24,18 @@ files:
|
|
161
24
|
- README.md
|
162
25
|
- lib/sashite-pan.rb
|
163
26
|
- lib/sashite/pan.rb
|
164
|
-
- lib/sashite/pan/action.rb
|
165
27
|
- lib/sashite/pan/dumper.rb
|
166
28
|
- lib/sashite/pan/parser.rb
|
167
|
-
homepage: https://
|
29
|
+
homepage: https://github.com/sashite/pan.rb
|
168
30
|
licenses:
|
169
31
|
- MIT
|
170
32
|
metadata:
|
171
33
|
bug_tracker_uri: https://github.com/sashite/pan.rb/issues
|
172
|
-
documentation_uri: https://rubydoc.info/
|
34
|
+
documentation_uri: https://rubydoc.info/github/sashite/pan.rb/main
|
35
|
+
homepage_uri: https://github.com/sashite/pan.rb
|
173
36
|
source_code_uri: https://github.com/sashite/pan.rb
|
174
|
-
|
37
|
+
specification_uri: https://sashite.dev/documents/pan/1.0.0/
|
38
|
+
rubygems_mfa_required: 'true'
|
175
39
|
rdoc_options: []
|
176
40
|
require_paths:
|
177
41
|
- lib
|
@@ -179,15 +43,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
179
43
|
requirements:
|
180
44
|
- - ">="
|
181
45
|
- !ruby/object:Gem::Version
|
182
|
-
version: 2.
|
46
|
+
version: 3.2.0
|
183
47
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
184
48
|
requirements:
|
185
49
|
- - ">="
|
186
50
|
- !ruby/object:Gem::Version
|
187
51
|
version: '0'
|
188
52
|
requirements: []
|
189
|
-
rubygems_version: 3.
|
190
|
-
signing_key:
|
53
|
+
rubygems_version: 3.6.9
|
191
54
|
specification_version: 4
|
192
|
-
summary:
|
55
|
+
summary: Compact notation for board game moves - parse chess, shogi, and strategy
|
56
|
+
game actions
|
193
57
|
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
|