sashite-pan 2.0.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +507 -171
- data/lib/sashite/pan/action/capture.rb +253 -0
- data/lib/sashite/pan/action/drop.rb +261 -0
- data/lib/sashite/pan/action/drop_capture.rb +261 -0
- data/lib/sashite/pan/action/modify.rb +221 -0
- data/lib/sashite/pan/action/move.rb +253 -0
- data/lib/sashite/pan/action/pass.rb +189 -0
- data/lib/sashite/pan/action/special.rb +255 -0
- data/lib/sashite/pan/action/static_capture.rb +207 -0
- data/lib/sashite/pan/action.rb +180 -0
- data/lib/sashite/pan.rb +30 -47
- data/lib/sashite-pan.rb +10 -3
- metadata +47 -10
- data/lib/sashite/pan/dumper/error.rb +0 -11
- data/lib/sashite/pan/dumper.rb +0 -81
- data/lib/sashite/pan/parser/error.rb +0 -11
- data/lib/sashite/pan/parser.rb +0 -90
data/README.md
CHANGED
|
@@ -5,20 +5,13 @@
|
|
|
5
5
|

|
|
6
6
|
[](https://github.com/sashite/pan.rb/raw/main/LICENSE.md)
|
|
7
7
|
|
|
8
|
-
> **PAN** (Portable Action Notation)
|
|
8
|
+
> **PAN** (Portable Action Notation) implementation for the Ruby language.
|
|
9
9
|
|
|
10
10
|
## What is PAN?
|
|
11
11
|
|
|
12
|
-
PAN (Portable Action Notation) is a
|
|
12
|
+
PAN (Portable Action Notation) is a human-readable string format for representing atomic actions in abstract strategy board games. PAN provides an intuitive operator-based syntax to describe how pieces move, capture, transform, and interact on game boards.
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
14
|
+
This gem implements the [PAN Specification v1.0.0](https://sashite.dev/specs/pan/1.0.0/), providing a pure functional Ruby interface with immutable action objects.
|
|
22
15
|
|
|
23
16
|
## Installation
|
|
24
17
|
|
|
@@ -33,264 +26,607 @@ Or install manually:
|
|
|
33
26
|
gem install sashite-pan
|
|
34
27
|
```
|
|
35
28
|
|
|
36
|
-
##
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
require "sashite/pan"
|
|
33
|
+
|
|
34
|
+
# Validate PAN strings
|
|
35
|
+
Sashite::Pan.valid?("e2-e4") # => true
|
|
36
|
+
Sashite::Pan.valid?("d1+f3") # => true
|
|
37
|
+
Sashite::Pan.valid?("...") # => true
|
|
38
|
+
Sashite::Pan.valid?("invalid") # => false
|
|
39
|
+
|
|
40
|
+
# Parse PAN strings into action objects
|
|
41
|
+
action = Sashite::Pan.parse("e2-e4")
|
|
42
|
+
action.type # => :move
|
|
43
|
+
action.source # => "e2"
|
|
44
|
+
action.destination # => "e4"
|
|
45
|
+
action.to_s # => "e2-e4"
|
|
46
|
+
|
|
47
|
+
# Create actions programmatically
|
|
48
|
+
action = Sashite::Pan::Action.move("e2", "e4")
|
|
49
|
+
action.to_s # => "e2-e4"
|
|
50
|
+
|
|
51
|
+
promotion = Sashite::Pan::Action.move("e7", "e8", transformation: "Q")
|
|
52
|
+
promotion.to_s # => "e7-e8=Q"
|
|
53
|
+
|
|
54
|
+
capture = Sashite::Pan::Action.capture("d1", "f3")
|
|
55
|
+
capture.to_s # => "d1+f3"
|
|
56
|
+
|
|
57
|
+
# Drop actions (shogi-style)
|
|
58
|
+
drop = Sashite::Pan::Action.drop("e5", piece: "P")
|
|
59
|
+
drop.to_s # => "P*e5"
|
|
60
|
+
|
|
61
|
+
# Pass action
|
|
62
|
+
pass = Sashite::Pan::Action.pass
|
|
63
|
+
pass.to_s # => "..."
|
|
64
|
+
|
|
65
|
+
# Query action properties
|
|
66
|
+
action.move? # => true
|
|
67
|
+
action.pass? # => false
|
|
68
|
+
capture.capture? # => true
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Format Overview
|
|
72
|
+
|
|
73
|
+
PAN uses six intuitive operators:
|
|
74
|
+
|
|
75
|
+
| Operator | Meaning | Example |
|
|
76
|
+
|----------|---------|---------|
|
|
77
|
+
| `-` | Move to empty square | `e2-e4` |
|
|
78
|
+
| `+` | Capture at destination | `d1+f3` |
|
|
79
|
+
| `~` | Special move with side effects | `e1~g1` (castling) |
|
|
80
|
+
| `*` | Drop to empty square | `P*e5` |
|
|
81
|
+
| `.` | Drop with capture | `L.b4` |
|
|
82
|
+
| `=` | Transform piece | `e4=+P` |
|
|
83
|
+
| `...` | Pass turn | `...` |
|
|
84
|
+
|
|
85
|
+
For complete format details, see the [PAN Specification](https://sashite.dev/specs/pan/1.0.0/).
|
|
86
|
+
|
|
87
|
+
## API Reference
|
|
88
|
+
|
|
89
|
+
### Module Methods
|
|
90
|
+
|
|
91
|
+
#### Validation
|
|
92
|
+
|
|
93
|
+
```ruby
|
|
94
|
+
Sashite::Pan.valid?(pan_string)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Check if a string represents a valid PAN action.
|
|
98
|
+
|
|
99
|
+
**Parameters:**
|
|
100
|
+
- `pan_string` [String] - The string to validate
|
|
101
|
+
|
|
102
|
+
**Returns:** [Boolean] - true if valid PAN, false otherwise
|
|
37
103
|
|
|
38
|
-
|
|
104
|
+
**Examples:**
|
|
105
|
+
```ruby
|
|
106
|
+
Sashite::Pan.valid?("e2-e4") # => true
|
|
107
|
+
Sashite::Pan.valid?("P*d4") # => true
|
|
108
|
+
Sashite::Pan.valid?("...") # => true
|
|
109
|
+
Sashite::Pan.valid?("invalid") # => false
|
|
110
|
+
```
|
|
39
111
|
|
|
40
|
-
|
|
112
|
+
#### Parsing
|
|
41
113
|
|
|
114
|
+
```ruby
|
|
115
|
+
Sashite::Pan.parse(pan_string)
|
|
42
116
|
```
|
|
43
|
-
|
|
117
|
+
|
|
118
|
+
Parse a PAN string into an Action object.
|
|
119
|
+
|
|
120
|
+
**Parameters:**
|
|
121
|
+
- `pan_string` [String] - PAN notation string
|
|
122
|
+
|
|
123
|
+
**Returns:** [Pan::Action] - Immutable action object
|
|
124
|
+
|
|
125
|
+
**Raises:** [ArgumentError] - If the PAN string is invalid
|
|
126
|
+
|
|
127
|
+
**Examples:**
|
|
128
|
+
```ruby
|
|
129
|
+
Sashite::Pan.parse("e2-e4") # => #<Pan::Action type=:move ...>
|
|
130
|
+
Sashite::Pan.parse("d1+f3") # => #<Pan::Action type=:capture ...>
|
|
131
|
+
Sashite::Pan.parse("...") # => #<Pan::Action type=:pass>
|
|
44
132
|
```
|
|
45
133
|
|
|
46
|
-
###
|
|
134
|
+
### Action Class
|
|
47
135
|
|
|
136
|
+
#### Creation Methods
|
|
137
|
+
|
|
138
|
+
All creation methods return immutable Action objects.
|
|
139
|
+
|
|
140
|
+
##### Pass Action
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
Sashite::Pan::Action.pass
|
|
48
144
|
```
|
|
49
|
-
|
|
145
|
+
|
|
146
|
+
Create a pass action (no move, turn ends).
|
|
147
|
+
|
|
148
|
+
**Returns:** [Action] - Pass action
|
|
149
|
+
|
|
150
|
+
**Example:**
|
|
151
|
+
```ruby
|
|
152
|
+
action = Sashite::Pan::Action.pass
|
|
153
|
+
action.to_s # => "..."
|
|
50
154
|
```
|
|
51
155
|
|
|
52
|
-
|
|
156
|
+
##### Movement Actions
|
|
53
157
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
- **hand_piece**: Optional piece added to mover's hand (captures, promotions)
|
|
158
|
+
```ruby
|
|
159
|
+
Sashite::Pan::Action.move(source, destination, transformation: nil)
|
|
160
|
+
```
|
|
58
161
|
|
|
59
|
-
|
|
162
|
+
Create a move action to an empty square.
|
|
60
163
|
|
|
61
|
-
|
|
164
|
+
**Parameters:**
|
|
165
|
+
- `source` [String] - Source CELL coordinate
|
|
166
|
+
- `destination` [String] - Destination CELL coordinate
|
|
167
|
+
- `transformation` [String, nil] - Optional EPIN transformation
|
|
62
168
|
|
|
63
|
-
|
|
169
|
+
**Returns:** [Action] - Move action
|
|
64
170
|
|
|
171
|
+
**Examples:**
|
|
65
172
|
```ruby
|
|
66
|
-
|
|
173
|
+
Sashite::Pan::Action.move("e2", "e4")
|
|
174
|
+
# => "e2-e4"
|
|
67
175
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
# => [{"src_square"=>"27", "dst_square"=>"18", "piece_name"=>"+P"}]
|
|
176
|
+
Sashite::Pan::Action.move("e7", "e8", transformation: "Q")
|
|
177
|
+
# => "e7-e8=Q"
|
|
71
178
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
179
|
+
Sashite::Pan::Action.move("a7", "a8", transformation: "+R")
|
|
180
|
+
# => "a7-a8=+R"
|
|
181
|
+
```
|
|
75
182
|
|
|
76
|
-
|
|
77
|
-
result = Sashite::Pan.parse("*,27,p")
|
|
78
|
-
# => [{"src_square"=>nil, "dst_square"=>"27", "piece_name"=>"p"}]
|
|
183
|
+
---
|
|
79
184
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
# => [
|
|
83
|
-
# {"src_square"=>"e1", "dst_square"=>"g1", "piece_name"=>"K"},
|
|
84
|
-
# {"src_square"=>"h1", "dst_square"=>"f1", "piece_name"=>"R"}
|
|
85
|
-
# ]
|
|
185
|
+
```ruby
|
|
186
|
+
Sashite::Pan::Action.capture(source, destination, transformation: nil)
|
|
86
187
|
```
|
|
87
188
|
|
|
88
|
-
|
|
189
|
+
Create a capture action at destination.
|
|
89
190
|
|
|
90
|
-
|
|
191
|
+
**Parameters:**
|
|
192
|
+
- `source` [String] - Source CELL coordinate
|
|
193
|
+
- `destination` [String] - Destination CELL coordinate (occupied square)
|
|
194
|
+
- `transformation` [String, nil] - Optional EPIN transformation
|
|
91
195
|
|
|
196
|
+
**Returns:** [Action] - Capture action
|
|
197
|
+
|
|
198
|
+
**Examples:**
|
|
92
199
|
```ruby
|
|
93
|
-
|
|
200
|
+
Sashite::Pan::Action.capture("d1", "f3")
|
|
201
|
+
# => "d1+f3"
|
|
94
202
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
203
|
+
Sashite::Pan::Action.capture("b7", "a8", transformation: "R")
|
|
204
|
+
# => "b7+a8=R"
|
|
205
|
+
```
|
|
98
206
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
```ruby
|
|
210
|
+
Sashite::Pan::Action.special(source, destination, transformation: nil)
|
|
102
211
|
```
|
|
103
212
|
|
|
104
|
-
|
|
213
|
+
Create a special move action with implicit side effects.
|
|
214
|
+
|
|
215
|
+
**Parameters:**
|
|
216
|
+
- `source` [String] - Source CELL coordinate
|
|
217
|
+
- `destination` [String] - Destination CELL coordinate
|
|
218
|
+
- `transformation` [String, nil] - Optional EPIN transformation
|
|
219
|
+
|
|
220
|
+
**Returns:** [Action] - Special action
|
|
105
221
|
|
|
106
|
-
|
|
222
|
+
**Examples:**
|
|
223
|
+
```ruby
|
|
224
|
+
Sashite::Pan::Action.special("e1", "g1")
|
|
225
|
+
# => "e1~g1" (castling)
|
|
226
|
+
|
|
227
|
+
Sashite::Pan::Action.special("e5", "f6")
|
|
228
|
+
# => "e5~f6" (en passant)
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
##### Static Capture
|
|
107
232
|
|
|
108
233
|
```ruby
|
|
109
|
-
|
|
234
|
+
Sashite::Pan::Action.static_capture(square)
|
|
235
|
+
```
|
|
110
236
|
|
|
111
|
-
|
|
112
|
-
pmn_actions = [{"src_square" => "27", "dst_square" => "18", "piece_name" => "+P"}]
|
|
113
|
-
pan_string = Sashite::Pan.dump(pmn_actions)
|
|
114
|
-
# => "27,18,+P"
|
|
237
|
+
Create a static capture action (remove piece without movement).
|
|
115
238
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
239
|
+
**Parameters:**
|
|
240
|
+
- `square` [String] - CELL coordinate of piece to capture
|
|
241
|
+
|
|
242
|
+
**Returns:** [Action] - Static capture action
|
|
243
|
+
|
|
244
|
+
**Example:**
|
|
245
|
+
```ruby
|
|
246
|
+
Sashite::Pan::Action.static_capture("d4")
|
|
247
|
+
# => "+d4"
|
|
248
|
+
```
|
|
120
249
|
|
|
121
|
-
|
|
122
|
-
pmn_actions = [{"src_square" => nil, "dst_square" => "27", "piece_name" => "p"}]
|
|
123
|
-
pan_string = Sashite::Pan.dump(pmn_actions)
|
|
124
|
-
# => "*,27,p"
|
|
250
|
+
##### Drop Actions
|
|
125
251
|
|
|
126
|
-
|
|
127
|
-
|
|
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"
|
|
252
|
+
```ruby
|
|
253
|
+
Sashite::Pan::Action.drop(destination, piece: nil, transformation: nil)
|
|
133
254
|
```
|
|
134
255
|
|
|
135
|
-
|
|
256
|
+
Create a drop action to empty square.
|
|
257
|
+
|
|
258
|
+
**Parameters:**
|
|
259
|
+
- `destination` [String] - Destination CELL coordinate (empty square)
|
|
260
|
+
- `piece` [String, nil] - Optional EPIN piece identifier
|
|
261
|
+
- `transformation` [String, nil] - Optional EPIN transformation
|
|
136
262
|
|
|
137
|
-
|
|
263
|
+
**Returns:** [Action] - Drop action
|
|
138
264
|
|
|
265
|
+
**Examples:**
|
|
139
266
|
```ruby
|
|
140
|
-
|
|
267
|
+
Sashite::Pan::Action.drop("e5", piece: "P")
|
|
268
|
+
# => "P*e5"
|
|
141
269
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
result = Sashite::Pan.safe_dump(pmn_actions)
|
|
145
|
-
# => "e2,e4,P"
|
|
270
|
+
Sashite::Pan::Action.drop("d4")
|
|
271
|
+
# => "*d4" (piece type inferred from context)
|
|
146
272
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
result = Sashite::Pan.safe_dump(invalid_data)
|
|
150
|
-
# => nil
|
|
273
|
+
Sashite::Pan::Action.drop("c3", piece: "S", transformation: "+S")
|
|
274
|
+
# => "S*c3=+S"
|
|
151
275
|
```
|
|
152
276
|
|
|
153
|
-
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
```ruby
|
|
280
|
+
Sashite::Pan::Action.drop_capture(destination, piece: nil, transformation: nil)
|
|
281
|
+
```
|
|
154
282
|
|
|
155
|
-
|
|
283
|
+
Create a drop action with capture.
|
|
156
284
|
|
|
285
|
+
**Parameters:**
|
|
286
|
+
- `destination` [String] - Destination CELL coordinate (occupied square)
|
|
287
|
+
- `piece` [String, nil] - Optional EPIN piece identifier
|
|
288
|
+
- `transformation` [String, nil] - Optional EPIN transformation
|
|
289
|
+
|
|
290
|
+
**Returns:** [Action] - Drop capture action
|
|
291
|
+
|
|
292
|
+
**Example:**
|
|
157
293
|
```ruby
|
|
158
|
-
|
|
294
|
+
Sashite::Pan::Action.drop_capture("b4", piece: "L")
|
|
295
|
+
# => "L.b4"
|
|
296
|
+
```
|
|
159
297
|
|
|
160
|
-
|
|
161
|
-
Sashite::Pan.valid?("*,27,p") # => true
|
|
162
|
-
Sashite::Pan.valid?("e1,g1,K;h1,f1,R") # => true
|
|
298
|
+
##### Modification Action
|
|
163
299
|
|
|
164
|
-
|
|
165
|
-
Sashite::Pan.
|
|
166
|
-
Sashite::Pan.valid?("27,18") # => false (missing piece)
|
|
300
|
+
```ruby
|
|
301
|
+
Sashite::Pan::Action.modify(square, piece)
|
|
167
302
|
```
|
|
168
303
|
|
|
169
|
-
|
|
304
|
+
Create an in-place transformation action.
|
|
170
305
|
|
|
171
|
-
|
|
306
|
+
**Parameters:**
|
|
307
|
+
- `square` [String] - CELL coordinate
|
|
308
|
+
- `piece` [String] - EPIN piece identifier (final state)
|
|
172
309
|
|
|
310
|
+
**Returns:** [Action] - Modification action
|
|
311
|
+
|
|
312
|
+
**Examples:**
|
|
173
313
|
```ruby
|
|
174
|
-
|
|
314
|
+
Sashite::Pan::Action.modify("e4", "+P")
|
|
315
|
+
# => "e4=+P"
|
|
316
|
+
|
|
317
|
+
Sashite::Pan::Action.modify("c3", "k'")
|
|
318
|
+
# => "c3=k'"
|
|
319
|
+
```
|
|
175
320
|
|
|
176
|
-
|
|
177
|
-
Sashite::Pan.parse("27,18,+P")
|
|
178
|
-
# => [{"src_square"=>"27", "dst_square"=>"18", "piece_name"=>"+P"}]
|
|
321
|
+
#### Instance Methods
|
|
179
322
|
|
|
180
|
-
|
|
181
|
-
Sashite::Pan.parse("36,27,B,P")
|
|
182
|
-
# => [{"src_square"=>"36", "dst_square"=>"27", "piece_name"=>"B", "piece_hand"=>"P"}]
|
|
323
|
+
##### Attribute Access
|
|
183
324
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
# => [{"src_square"=>nil, "dst_square"=>"27", "piece_name"=>"p"}]
|
|
325
|
+
```ruby
|
|
326
|
+
action.type
|
|
187
327
|
```
|
|
188
328
|
|
|
189
|
-
|
|
329
|
+
Get the action type.
|
|
330
|
+
|
|
331
|
+
**Returns:** [Symbol] - One of: `:pass`, `:move`, `:capture`, `:special`, `:static_capture`, `:drop`, `:drop_capture`, `:modify`
|
|
332
|
+
|
|
333
|
+
---
|
|
190
334
|
|
|
191
335
|
```ruby
|
|
192
|
-
|
|
336
|
+
action.source
|
|
337
|
+
```
|
|
193
338
|
|
|
194
|
-
|
|
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
|
-
# ]
|
|
339
|
+
Get the source coordinate (for movement actions).
|
|
200
340
|
|
|
201
|
-
|
|
202
|
-
Sashite::Pan.parse("e2,e4,P'")
|
|
203
|
-
# => [{"src_square"=>"e2", "dst_square"=>"e4", "piece_name"=>"P'"}]
|
|
341
|
+
**Returns:** [String, nil] - CELL coordinate or nil
|
|
204
342
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
# {"src_square"=>"e3", "dst_square"=>"e4", "piece_name"=>"p"}
|
|
210
|
-
# ]
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
```ruby
|
|
346
|
+
action.destination
|
|
211
347
|
```
|
|
212
348
|
|
|
213
|
-
|
|
349
|
+
Get the destination coordinate.
|
|
214
350
|
|
|
215
|
-
|
|
351
|
+
**Returns:** [String, nil] - CELL coordinate or nil
|
|
352
|
+
|
|
353
|
+
---
|
|
354
|
+
|
|
355
|
+
```ruby
|
|
356
|
+
action.piece
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
Get the piece identifier (for drop/modify actions).
|
|
360
|
+
|
|
361
|
+
**Returns:** [String, nil] - EPIN identifier or nil
|
|
362
|
+
|
|
363
|
+
---
|
|
364
|
+
|
|
365
|
+
```ruby
|
|
366
|
+
action.transformation
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
Get the transformation piece (for actions with `=<piece>`).
|
|
370
|
+
|
|
371
|
+
**Returns:** [String, nil] - EPIN identifier or nil
|
|
372
|
+
|
|
373
|
+
---
|
|
216
374
|
|
|
217
375
|
```ruby
|
|
218
|
-
|
|
219
|
-
|
|
376
|
+
action.to_s
|
|
377
|
+
```
|
|
220
378
|
|
|
221
|
-
|
|
222
|
-
pan_string = "e2,e4,P';d7,d5,p"
|
|
379
|
+
Convert action to PAN string representation.
|
|
223
380
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
#
|
|
381
|
+
**Returns:** [String] - PAN notation
|
|
382
|
+
|
|
383
|
+
**Examples:**
|
|
384
|
+
```ruby
|
|
385
|
+
Sashite::Pan::Action.move("e2", "e4").to_s
|
|
386
|
+
# => "e2-e4"
|
|
387
|
+
|
|
388
|
+
Sashite::Pan::Action.drop("e5", piece: "P").to_s
|
|
389
|
+
# => "P*e5"
|
|
390
|
+
```
|
|
230
391
|
|
|
231
|
-
|
|
232
|
-
move = PortableMoveNotation::Move.new(*pmn_actions.map { |action|
|
|
233
|
-
PortableMoveNotation::Action.new(**action.transform_keys(&:to_sym))
|
|
234
|
-
})
|
|
392
|
+
##### Type Queries
|
|
235
393
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
394
|
+
```ruby
|
|
395
|
+
action.pass?
|
|
396
|
+
action.move?
|
|
397
|
+
action.capture?
|
|
398
|
+
action.special?
|
|
399
|
+
action.static_capture?
|
|
400
|
+
action.drop?
|
|
401
|
+
action.drop_capture?
|
|
402
|
+
action.modify?
|
|
403
|
+
action.movement? # true for move, capture, or special
|
|
404
|
+
action.drop_action? # true for drop or drop_capture
|
|
239
405
|
```
|
|
240
406
|
|
|
241
|
-
|
|
407
|
+
Check action type.
|
|
408
|
+
|
|
409
|
+
**Returns:** [Boolean]
|
|
242
410
|
|
|
243
|
-
|
|
411
|
+
**Examples:**
|
|
412
|
+
```ruby
|
|
413
|
+
action = Sashite::Pan.parse("e2-e4")
|
|
414
|
+
action.move? # => true
|
|
415
|
+
action.movement? # => true
|
|
416
|
+
action.pass? # => false
|
|
244
417
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
418
|
+
pass = Sashite::Pan::Action.pass
|
|
419
|
+
pass.pass? # => true
|
|
420
|
+
|
|
421
|
+
drop = Sashite::Pan.parse("P*e5")
|
|
422
|
+
drop.drop? # => true
|
|
423
|
+
drop.drop_action? # => true
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
##### Comparison
|
|
427
|
+
|
|
428
|
+
```ruby
|
|
429
|
+
action == other
|
|
430
|
+
```
|
|
250
431
|
|
|
251
|
-
|
|
432
|
+
Check equality between actions.
|
|
252
433
|
|
|
253
|
-
|
|
254
|
-
-
|
|
255
|
-
- **Structured data processing**: Schema validation and type checking
|
|
434
|
+
**Parameters:**
|
|
435
|
+
- `other` [Action] - Action to compare with
|
|
256
436
|
|
|
257
|
-
|
|
437
|
+
**Returns:** [Boolean] - true if actions are identical
|
|
258
438
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
439
|
+
**Example:**
|
|
440
|
+
```ruby
|
|
441
|
+
action1 = Sashite::Pan.parse("e2-e4")
|
|
442
|
+
action2 = Sashite::Pan::Action.move("e2", "e4")
|
|
443
|
+
action1 == action2 # => true
|
|
444
|
+
```
|
|
263
445
|
|
|
264
|
-
##
|
|
446
|
+
## Advanced Usage
|
|
265
447
|
|
|
266
|
-
|
|
448
|
+
### Parsing Game Sequences
|
|
267
449
|
|
|
268
450
|
```ruby
|
|
269
|
-
|
|
451
|
+
# Parse a sequence of moves
|
|
452
|
+
moves = %w[e2-e4 e7-e5 g1-f3 b8-c6]
|
|
453
|
+
actions = moves.map { |move| Sashite::Pan.parse(move) }
|
|
454
|
+
|
|
455
|
+
# Analyze action types
|
|
456
|
+
actions.count(&:move?) # => 4
|
|
457
|
+
actions.all?(&:movement?) # => true
|
|
458
|
+
|
|
459
|
+
# Extract coordinates
|
|
460
|
+
sources = actions.map(&:source)
|
|
461
|
+
destinations = actions.map(&:destination)
|
|
462
|
+
```
|
|
270
463
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
464
|
+
### Action Type Detection
|
|
465
|
+
|
|
466
|
+
```ruby
|
|
467
|
+
def describe_action(pan_string)
|
|
468
|
+
action = Sashite::Pan.parse(pan_string)
|
|
469
|
+
|
|
470
|
+
case action.type
|
|
471
|
+
when :pass
|
|
472
|
+
"Player passes"
|
|
473
|
+
when :move
|
|
474
|
+
"Move from #{action.source} to #{action.destination}"
|
|
475
|
+
when :capture
|
|
476
|
+
"Capture at #{action.destination}"
|
|
477
|
+
when :special
|
|
478
|
+
"Special move: #{action.source} to #{action.destination}"
|
|
479
|
+
when :drop
|
|
480
|
+
piece_str = action.piece ? "#{action.piece} " : ""
|
|
481
|
+
"Drop #{piece_str}at #{action.destination}"
|
|
482
|
+
when :modify
|
|
483
|
+
"Transform piece at #{action.square} to #{action.piece}"
|
|
484
|
+
end
|
|
275
485
|
end
|
|
276
486
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
487
|
+
describe_action("e2-e4") # => "Move from e2 to e4"
|
|
488
|
+
describe_action("d1+f3") # => "Capture at f3"
|
|
489
|
+
describe_action("P*e5") # => "Drop P at e5"
|
|
490
|
+
describe_action("...") # => "Player passes"
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
### Transformation Detection
|
|
494
|
+
|
|
495
|
+
```ruby
|
|
496
|
+
def has_promotion?(pan_string)
|
|
497
|
+
action = Sashite::Pan.parse(pan_string)
|
|
498
|
+
!action.transformation.nil?
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
has_promotion?("e2-e4") # => false
|
|
502
|
+
has_promotion?("e7-e8=Q") # => true
|
|
503
|
+
has_promotion?("P*e5") # => false
|
|
504
|
+
has_promotion?("S*c3=+S") # => true
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
### Building Move Generators
|
|
508
|
+
|
|
509
|
+
```ruby
|
|
510
|
+
class MoveBuilder
|
|
511
|
+
def initialize(source)
|
|
512
|
+
@source = source
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
def to(destination)
|
|
516
|
+
Sashite::Pan::Action.move(@source, destination)
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
def captures(destination)
|
|
520
|
+
Sashite::Pan::Action.capture(@source, destination)
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
def to_promoting(destination, piece)
|
|
524
|
+
Sashite::Pan::Action.move(@source, destination, transformation: piece)
|
|
525
|
+
end
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
# Usage
|
|
529
|
+
builder = MoveBuilder.new("e7")
|
|
530
|
+
builder.to("e8").to_s # => "e7-e8"
|
|
531
|
+
builder.to_promoting("e8", "Q").to_s # => "e7-e8=Q"
|
|
532
|
+
builder.captures("d8").to_s # => "e7+d8"
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
### Validation Before Parsing
|
|
536
|
+
|
|
537
|
+
```ruby
|
|
538
|
+
def safe_parse(pan_string)
|
|
539
|
+
return nil unless Sashite::Pan.valid?(pan_string)
|
|
540
|
+
|
|
541
|
+
Sashite::Pan.parse(pan_string)
|
|
542
|
+
rescue ArgumentError
|
|
543
|
+
nil
|
|
281
544
|
end
|
|
545
|
+
|
|
546
|
+
safe_parse("e2-e4") # => #<Pan::Action ...>
|
|
547
|
+
safe_parse("invalid") # => nil
|
|
282
548
|
```
|
|
283
549
|
|
|
550
|
+
### Pattern Matching (Ruby 3.0+)
|
|
551
|
+
|
|
552
|
+
```ruby
|
|
553
|
+
def analyze(action)
|
|
554
|
+
case action
|
|
555
|
+
in { type: :move, source:, destination:, transformation: nil }
|
|
556
|
+
"Simple move: #{source} → #{destination}"
|
|
557
|
+
in { type: :move, transformation: piece }
|
|
558
|
+
"Promotion to #{piece}"
|
|
559
|
+
in { type: :capture, source:, destination: }
|
|
560
|
+
"Capture: #{source} takes #{destination}"
|
|
561
|
+
in { type: :drop, piece:, destination: }
|
|
562
|
+
"Drop #{piece} at #{destination}"
|
|
563
|
+
in { type: :pass }
|
|
564
|
+
"Pass"
|
|
565
|
+
else
|
|
566
|
+
"Other action"
|
|
567
|
+
end
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
action = Sashite::Pan.parse("e7-e8=Q")
|
|
571
|
+
analyze(action) # => "Promotion to Q"
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
## Properties
|
|
575
|
+
|
|
576
|
+
* **Operator-based**: Intuitive symbols for different action types
|
|
577
|
+
* **Compact notation**: Minimal character usage while maintaining readability
|
|
578
|
+
* **Game-agnostic**: Works across chess, shōgi, xiangqi, and other abstract strategy games
|
|
579
|
+
* **CELL integration**: Uses CELL coordinates for board positions
|
|
580
|
+
* **EPIN integration**: Uses EPIN identifiers for piece representation
|
|
581
|
+
* **Immutable**: All action objects are frozen
|
|
582
|
+
* **Functional**: Pure functions with no side effects
|
|
583
|
+
* **Type-safe**: Strong validation and error handling
|
|
584
|
+
|
|
585
|
+
## Related Specifications
|
|
586
|
+
|
|
587
|
+
- [PAN Specification v1.0.0](https://sashite.dev/specs/pan/1.0.0/) - Complete format specification
|
|
588
|
+
- [PAN Examples](https://sashite.dev/specs/pan/1.0.0/examples/) - Usage examples across different games
|
|
589
|
+
- [CELL](https://sashite.dev/specs/cell/) - Coordinate encoding for board positions
|
|
590
|
+
- [EPIN](https://sashite.dev/specs/epin/) - Extended piece identifiers
|
|
591
|
+
- [Game Protocol](https://sashite.dev/protocol/) - Conceptual foundation
|
|
592
|
+
|
|
284
593
|
## Documentation
|
|
285
594
|
|
|
286
|
-
- [Official PAN Specification](https://sashite.dev/
|
|
595
|
+
- [Official PAN Specification v1.0.0](https://sashite.dev/specs/pan/1.0.0/)
|
|
287
596
|
- [API Documentation](https://rubydoc.info/github/sashite/pan.rb/main)
|
|
288
|
-
- [
|
|
597
|
+
- [PAN Examples](https://sashite.dev/specs/pan/1.0.0/examples/)
|
|
598
|
+
|
|
599
|
+
## Development
|
|
600
|
+
|
|
601
|
+
```sh
|
|
602
|
+
# Clone the repository
|
|
603
|
+
git clone https://github.com/sashite/pan.rb.git
|
|
604
|
+
cd pan.rb
|
|
605
|
+
|
|
606
|
+
# Install dependencies
|
|
607
|
+
bundle install
|
|
608
|
+
|
|
609
|
+
# Run tests
|
|
610
|
+
ruby test.rb
|
|
611
|
+
|
|
612
|
+
# Generate documentation
|
|
613
|
+
yard doc
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
## Contributing
|
|
617
|
+
|
|
618
|
+
1. Fork the repository
|
|
619
|
+
2. Create a feature branch (`git checkout -b feature/new-feature`)
|
|
620
|
+
3. Add tests for your changes
|
|
621
|
+
4. Ensure all tests pass (`ruby test.rb`)
|
|
622
|
+
5. Commit your changes (`git commit -am 'Add new feature'`)
|
|
623
|
+
6. Push to the branch (`git push origin feature/new-feature`)
|
|
624
|
+
7. Create a Pull Request
|
|
289
625
|
|
|
290
626
|
## License
|
|
291
627
|
|
|
292
|
-
|
|
628
|
+
Available as open source under the [MIT License](https://opensource.org/licenses/MIT).
|
|
293
629
|
|
|
294
|
-
## About
|
|
630
|
+
## About
|
|
295
631
|
|
|
296
|
-
|
|
632
|
+
Maintained by [Sashité](https://sashite.com/) – promoting chess variants and sharing the beauty of board game cultures.
|