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.
data/README.md CHANGED
@@ -5,20 +5,13 @@
5
5
  ![Ruby](https://github.com/sashite/pan.rb/actions/workflows/main.yml/badge.svg?branch=main)
6
6
  [![License](https://img.shields.io/github/license/sashite/pan.rb?label=License&logo=github)](https://github.com/sashite/pan.rb/raw/main/LICENSE.md)
7
7
 
8
- > **PAN** (Portable Action Notation) support for the Ruby language.
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 compact, string-based format for representing **executed moves** in abstract strategy board games. PAN serves as a human-readable and space-efficient alternative to PMN (Portable Move Notation), expressing the same semantic information in a condensed textual format.
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
- While PMN uses JSON arrays to describe move sequences, PAN encodes the same information using a delimited string format that is easier to read, write, and transmit in contexts where JSON overhead is undesirable.
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
- ## PAN Format
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
- A PAN string represents one or more **actions** that constitute a complete move in a game. The format structure is:
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
- ### Single Action
112
+ #### Parsing
41
113
 
114
+ ```ruby
115
+ Sashite::Pan.parse(pan_string)
42
116
  ```
43
- <source>,<destination>,<piece>[,<hand_piece>]
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
- ### Multiple Actions
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
- <action1>;<action2>[;<action3>...]
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
- Where:
156
+ ##### Movement Actions
53
157
 
54
- - **source**: Origin square label, or `*` for drops from hand
55
- - **destination**: Target square label
56
- - **piece**: Piece being moved (PNN format with optional modifiers)
57
- - **hand_piece**: Optional piece added to mover's hand (captures, promotions)
158
+ ```ruby
159
+ Sashite::Pan::Action.move(source, destination, transformation: nil)
160
+ ```
58
161
 
59
- ## Basic Usage
162
+ Create a move action to an empty square.
60
163
 
61
- ### Parsing PAN Strings
164
+ **Parameters:**
165
+ - `source` [String] - Source CELL coordinate
166
+ - `destination` [String] - Destination CELL coordinate
167
+ - `transformation` [String, nil] - Optional EPIN transformation
62
168
 
63
- Convert a PAN string into PMN format (array of action hashes):
169
+ **Returns:** [Action] - Move action
64
170
 
171
+ **Examples:**
65
172
  ```ruby
66
- require "sashite-pan"
173
+ Sashite::Pan::Action.move("e2", "e4")
174
+ # => "e2-e4"
67
175
 
68
- # Simple move
69
- result = Sashite::Pan.parse("27,18,+P")
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
- # Capture with hand piece
73
- result = Sashite::Pan.parse("36,27,B,P")
74
- # => [{"src_square"=>"36", "dst_square"=>"27", "piece_name"=>"B", "piece_hand"=>"P"}]
179
+ Sashite::Pan::Action.move("a7", "a8", transformation: "+R")
180
+ # => "a7-a8=+R"
181
+ ```
75
182
 
76
- # Drop from hand
77
- result = Sashite::Pan.parse("*,27,p")
78
- # => [{"src_square"=>nil, "dst_square"=>"27", "piece_name"=>"p"}]
183
+ ---
79
184
 
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
- # ]
185
+ ```ruby
186
+ Sashite::Pan::Action.capture(source, destination, transformation: nil)
86
187
  ```
87
188
 
88
- ### Safe Parsing
189
+ Create a capture action at destination.
89
190
 
90
- Parse a PAN string without raising exceptions:
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
- require "sashite-pan"
200
+ Sashite::Pan::Action.capture("d1", "f3")
201
+ # => "d1+f3"
94
202
 
95
- # Valid PAN string
96
- result = Sashite::Pan.safe_parse("e2,e4,P'")
97
- # => [{"src_square"=>"e2", "dst_square"=>"e4", "piece_name"=>"P'"}]
203
+ Sashite::Pan::Action.capture("b7", "a8", transformation: "R")
204
+ # => "b7+a8=R"
205
+ ```
98
206
 
99
- # Invalid PAN string
100
- result = Sashite::Pan.safe_parse("invalid pan string")
101
- # => nil
207
+ ---
208
+
209
+ ```ruby
210
+ Sashite::Pan::Action.special(source, destination, transformation: nil)
102
211
  ```
103
212
 
104
- ### Creating PAN Strings
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
- Convert PMN actions (array of hashes) into a PAN string:
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
- require "sashite-pan"
234
+ Sashite::Pan::Action.static_capture(square)
235
+ ```
110
236
 
111
- # Simple move
112
- pmn_actions = [{"src_square" => "27", "dst_square" => "18", "piece_name" => "+P"}]
113
- pan_string = Sashite::Pan.dump(pmn_actions)
114
- # => "27,18,+P"
237
+ Create a static capture action (remove piece without movement).
115
238
 
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"
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
- # Drop from hand
122
- pmn_actions = [{"src_square" => nil, "dst_square" => "27", "piece_name" => "p"}]
123
- pan_string = Sashite::Pan.dump(pmn_actions)
124
- # => "*,27,p"
250
+ ##### Drop Actions
125
251
 
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"
252
+ ```ruby
253
+ Sashite::Pan::Action.drop(destination, piece: nil, transformation: nil)
133
254
  ```
134
255
 
135
- ### Safe Dumping
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
- Create PAN strings without raising exceptions:
263
+ **Returns:** [Action] - Drop action
138
264
 
265
+ **Examples:**
139
266
  ```ruby
140
- require "sashite-pan"
267
+ Sashite::Pan::Action.drop("e5", piece: "P")
268
+ # => "P*e5"
141
269
 
142
- # Valid PMN data
143
- pmn_actions = [{"src_square" => "e2", "dst_square" => "e4", "piece_name" => "P"}]
144
- result = Sashite::Pan.safe_dump(pmn_actions)
145
- # => "e2,e4,P"
270
+ Sashite::Pan::Action.drop("d4")
271
+ # => "*d4" (piece type inferred from context)
146
272
 
147
- # Invalid PMN data
148
- invalid_data = [{"invalid" => "data"}]
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
- ### Validation
277
+ ---
278
+
279
+ ```ruby
280
+ Sashite::Pan::Action.drop_capture(destination, piece: nil, transformation: nil)
281
+ ```
154
282
 
155
- Check if a string is valid PAN notation:
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
- require "sashite-pan"
294
+ Sashite::Pan::Action.drop_capture("b4", piece: "L")
295
+ # => "L.b4"
296
+ ```
159
297
 
160
- Sashite::Pan.valid?("27,18,+P") # => true
161
- Sashite::Pan.valid?("*,27,p") # => true
162
- Sashite::Pan.valid?("e1,g1,K;h1,f1,R") # => true
298
+ ##### Modification Action
163
299
 
164
- Sashite::Pan.valid?("") # => false
165
- Sashite::Pan.valid?("invalid") # => false
166
- Sashite::Pan.valid?("27,18") # => false (missing piece)
300
+ ```ruby
301
+ Sashite::Pan::Action.modify(square, piece)
167
302
  ```
168
303
 
169
- ## Examples
304
+ Create an in-place transformation action.
170
305
 
171
- ### Shogi Examples
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
- require "sashite-pan"
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
- # Pawn promotion
177
- Sashite::Pan.parse("27,18,+P")
178
- # => [{"src_square"=>"27", "dst_square"=>"18", "piece_name"=>"+P"}]
321
+ #### Instance Methods
179
322
 
180
- # Bishop captures promoted pawn
181
- Sashite::Pan.parse("36,27,B,P")
182
- # => [{"src_square"=>"36", "dst_square"=>"27", "piece_name"=>"B", "piece_hand"=>"P"}]
323
+ ##### Attribute Access
183
324
 
184
- # Drop pawn from hand
185
- Sashite::Pan.parse("*,27,p")
186
- # => [{"src_square"=>nil, "dst_square"=>"27", "piece_name"=>"p"}]
325
+ ```ruby
326
+ action.type
187
327
  ```
188
328
 
189
- ### Chess Examples
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
- require "sashite-pan"
336
+ action.source
337
+ ```
193
338
 
194
- # Kingside castling
195
- Sashite::Pan.parse("e1,g1,K;h1,f1,R")
196
- # => [
197
- # {"src_square"=>"e1", "dst_square"=>"g1", "piece_name"=>"K"},
198
- # {"src_square"=>"h1", "dst_square"=>"f1", "piece_name"=>"R"}
199
- # ]
339
+ Get the source coordinate (for movement actions).
200
340
 
201
- # Pawn with state modifier (can be captured en passant)
202
- Sashite::Pan.parse("e2,e4,P'")
203
- # => [{"src_square"=>"e2", "dst_square"=>"e4", "piece_name"=>"P'"}]
341
+ **Returns:** [String, nil] - CELL coordinate or nil
204
342
 
205
- # En passant capture (multi-step)
206
- Sashite::Pan.parse("d4,e3,p;e3,e4,p")
207
- # => [
208
- # {"src_square"=>"d4", "dst_square"=>"e3", "piece_name"=>"p"},
209
- # {"src_square"=>"e3", "dst_square"=>"e4", "piece_name"=>"p"}
210
- # ]
343
+ ---
344
+
345
+ ```ruby
346
+ action.destination
211
347
  ```
212
348
 
213
- ## Integration with PMN
349
+ Get the destination coordinate.
214
350
 
215
- PAN is designed to work seamlessly with PMN (Portable Move Notation). You can easily convert between the two formats:
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
- require "sashite-pan"
219
- require "portable_move_notation"
376
+ action.to_s
377
+ ```
220
378
 
221
- # Start with a PAN string
222
- pan_string = "e2,e4,P';d7,d5,p"
379
+ Convert action to PAN string representation.
223
380
 
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
- # ]
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
- # Use with PMN library
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
- # Convert back to PAN
237
- new_pan_string = Sashite::Pan.dump(pmn_actions)
238
- # => "e2,e4,P';d7,d5,p"
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
- ## Use Cases
407
+ Check action type.
408
+
409
+ **Returns:** [Boolean]
242
410
 
243
- PAN is optimal for:
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
- - **Move logging and game records**: Compact storage of game moves
246
- - **Network transmission**: Efficient move data transmission
247
- - **Command-line interfaces**: Human-readable move input/output
248
- - **Quick manual entry**: Easy to type and edit move sequences
249
- - **Storage optimization**: Space-efficient alternative to JSON
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
- PMN is optimal for:
432
+ Check equality between actions.
252
433
 
253
- - **Programmatic analysis**: Complex move processing and validation
254
- - **JSON-based systems**: Direct integration with JSON APIs
255
- - **Structured data processing**: Schema validation and type checking
434
+ **Parameters:**
435
+ - `other` [Action] - Action to compare with
256
436
 
257
- ## Properties of PAN
437
+ **Returns:** [Boolean] - true if actions are identical
258
438
 
259
- - **Rule-agnostic**: PAN does not encode legality, validity, or game-specific conditions
260
- - **Space-efficient**: Significantly more compact than equivalent JSON representation
261
- - **Human-readable**: Easy to read, write, and understand
262
- - **Lossless conversion**: Perfect bidirectional conversion with PMN format
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
- ## Error Handling
446
+ ## Advanced Usage
265
447
 
266
- The library provides detailed error messages for invalid input:
448
+ ### Parsing Game Sequences
267
449
 
268
450
  ```ruby
269
- require "sashite-pan"
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
- begin
272
- Sashite::Pan.parse("invalid,pan") # Missing piece component
273
- rescue Sashite::Pan::Parser::Error => e
274
- puts e.message # => "Action must have at least 3 components (source, destination, piece)"
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
- begin
278
- Sashite::Pan.dump([{"invalid" => "data"}]) # Missing required fields
279
- rescue Sashite::Pan::Dumper::Error => e
280
- puts e.message # => "Action must have dst_square"
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/documents/pan/1.0.0/)
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
- - [PMN Specification](https://sashite.dev/documents/pmn/1.0.0/)
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
- 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).
628
+ Available as open source under the [MIT License](https://opensource.org/licenses/MIT).
293
629
 
294
- ## About Sashité
630
+ ## About
295
631
 
296
- This project is maintained by [Sashité](https://sashite.com/) promoting chess variants and sharing the beauty of Chinese, Japanese, and Western chess cultures.
632
+ Maintained by [Sashité](https://sashite.com/) promoting chess variants and sharing the beauty of board game cultures.