sashite-pan 3.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,19 +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 played on coordinate-based boards. PAN provides a human-readable and space-efficient notation for expressing move actions in a rule-agnostic manner.
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
- 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:
17
-
18
- - Parsing PAN strings into structured move data
19
- - Validating PAN strings according to the specification
20
- - Converting between PAN and other move representations
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.
21
15
 
22
16
  ## Installation
23
17
 
@@ -32,252 +26,607 @@ Or install manually:
32
26
  gem install sashite-pan
33
27
  ```
34
28
 
35
- ## 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
36
88
 
37
- PAN uses three fundamental move types with intuitive operators:
89
+ ### Module Methods
38
90
 
39
- ### Simple Move (Non-capture)
91
+ #### Validation
92
+
93
+ ```ruby
94
+ Sashite::Pan.valid?(pan_string)
40
95
  ```
41
- <source>-<destination>
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
103
+
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
42
110
  ```
43
- **Example**: `e2-e4` - Moves a piece from e2 to e4
44
111
 
45
- ### Capture Move
112
+ #### Parsing
113
+
114
+ ```ruby
115
+ Sashite::Pan.parse(pan_string)
46
116
  ```
47
- <source>x<destination>
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>
48
132
  ```
49
- **Example**: `e4xd5` - Moves a piece from e4 to d5, capturing the piece at d5
50
133
 
51
- ### Drop/Placement
134
+ ### Action Class
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
52
144
  ```
53
- *<destination>
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 # => "..."
54
154
  ```
55
- **Example**: `*e4` - Places a piece at e4 from off-board (hand, reserve, etc.)
56
155
 
57
- ### Coordinate System
156
+ ##### Movement Actions
58
157
 
59
- PAN uses algebraic coordinates consisting of:
60
- - **File**: A single lowercase letter (`a-z`)
61
- - **Rank**: A single digit (`0-9`)
158
+ ```ruby
159
+ Sashite::Pan::Action.move(source, destination, transformation: nil)
160
+ ```
161
+
162
+ Create a move action to an empty square.
62
163
 
63
- Examples: `e4`, `a1`, `h8`, `d5`
164
+ **Parameters:**
165
+ - `source` [String] - Source CELL coordinate
166
+ - `destination` [String] - Destination CELL coordinate
167
+ - `transformation` [String, nil] - Optional EPIN transformation
64
168
 
65
- ## Basic Usage
169
+ **Returns:** [Action] - Move action
66
170
 
67
- ### Parsing PAN Strings
171
+ **Examples:**
172
+ ```ruby
173
+ Sashite::Pan::Action.move("e2", "e4")
174
+ # => "e2-e4"
68
175
 
69
- Convert a PAN string into structured move data:
176
+ Sashite::Pan::Action.move("e7", "e8", transformation: "Q")
177
+ # => "e7-e8=Q"
70
178
 
179
+ Sashite::Pan::Action.move("a7", "a8", transformation: "+R")
180
+ # => "a7-a8=+R"
181
+ ```
182
+
183
+ ---
184
+
185
+ ```ruby
186
+ Sashite::Pan::Action.capture(source, destination, transformation: nil)
187
+ ```
188
+
189
+ Create a capture action at destination.
190
+
191
+ **Parameters:**
192
+ - `source` [String] - Source CELL coordinate
193
+ - `destination` [String] - Destination CELL coordinate (occupied square)
194
+ - `transformation` [String, nil] - Optional EPIN transformation
195
+
196
+ **Returns:** [Action] - Capture action
197
+
198
+ **Examples:**
71
199
  ```ruby
72
- require "sashite-pan"
200
+ Sashite::Pan::Action.capture("d1", "f3")
201
+ # => "d1+f3"
73
202
 
74
- # Simple move
75
- result = Sashite::Pan.parse("e2-e4")
76
- # => {type: :move, source: "e2", destination: "e4"}
203
+ Sashite::Pan::Action.capture("b7", "a8", transformation: "R")
204
+ # => "b7+a8=R"
205
+ ```
77
206
 
78
- # Capture
79
- result = Sashite::Pan.parse("e4xd5")
80
- # => {type: :capture, source: "e4", destination: "d5"}
207
+ ---
81
208
 
82
- # Drop from hand
83
- result = Sashite::Pan.parse("*e4")
84
- # => {type: :drop, destination: "e4"}
209
+ ```ruby
210
+ Sashite::Pan::Action.special(source, destination, transformation: nil)
85
211
  ```
86
212
 
87
- ### Safe Parsing
213
+ Create a special move action with implicit side effects.
88
214
 
89
- Parse a PAN string without raising exceptions:
215
+ **Parameters:**
216
+ - `source` [String] - Source CELL coordinate
217
+ - `destination` [String] - Destination CELL coordinate
218
+ - `transformation` [String, nil] - Optional EPIN transformation
90
219
 
220
+ **Returns:** [Action] - Special action
221
+
222
+ **Examples:**
91
223
  ```ruby
92
- require "sashite-pan"
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
+ ```
93
230
 
94
- # Valid PAN string
95
- result = Sashite::Pan.safe_parse("e2-e4")
96
- # => {type: :move, source: "e2", destination: "e4"}
231
+ ##### Static Capture
97
232
 
98
- # Invalid PAN string
99
- result = Sashite::Pan.safe_parse("invalid")
100
- # => nil
233
+ ```ruby
234
+ Sashite::Pan::Action.static_capture(square)
101
235
  ```
102
236
 
103
- ### Validation
237
+ Create a static capture action (remove piece without movement).
238
+
239
+ **Parameters:**
240
+ - `square` [String] - CELL coordinate of piece to capture
104
241
 
105
- Check if a string is valid PAN notation:
242
+ **Returns:** [Action] - Static capture action
106
243
 
244
+ **Example:**
107
245
  ```ruby
108
- require "sashite-pan"
246
+ Sashite::Pan::Action.static_capture("d4")
247
+ # => "+d4"
248
+ ```
109
249
 
110
- Sashite::Pan.valid?("e2-e4") # => true
111
- Sashite::Pan.valid?("*e4") # => true
112
- Sashite::Pan.valid?("e4xd5") # => true
250
+ ##### Drop Actions
113
251
 
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)
252
+ ```ruby
253
+ Sashite::Pan::Action.drop(destination, piece: nil, transformation: nil)
118
254
  ```
119
255
 
120
- ## Examples
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
262
+
263
+ **Returns:** [Action] - Drop action
121
264
 
122
- ### Chess Examples
265
+ **Examples:**
266
+ ```ruby
267
+ Sashite::Pan::Action.drop("e5", piece: "P")
268
+ # => "P*e5"
269
+
270
+ Sashite::Pan::Action.drop("d4")
271
+ # => "*d4" (piece type inferred from context)
272
+
273
+ Sashite::Pan::Action.drop("c3", piece: "S", transformation: "+S")
274
+ # => "S*c3=+S"
275
+ ```
276
+
277
+ ---
123
278
 
124
279
  ```ruby
125
- require "sashite-pan"
280
+ Sashite::Pan::Action.drop_capture(destination, piece: nil, transformation: nil)
281
+ ```
282
+
283
+ Create a drop action with capture.
126
284
 
127
- # Pawn advance
128
- Sashite::Pan.parse("e2-e4")
129
- # => {type: :move, source: "e2", destination: "e4"}
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
130
289
 
131
- # Capture
132
- Sashite::Pan.parse("exd5")
133
- # => {type: :capture, source: "e4", destination: "d5"}
290
+ **Returns:** [Action] - Drop capture action
134
291
 
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
292
+ **Example:**
293
+ ```ruby
294
+ Sashite::Pan::Action.drop_capture("b4", piece: "L")
295
+ # => "L.b4"
139
296
  ```
140
297
 
141
- ### Shogi Examples
298
+ ##### Modification Action
142
299
 
143
300
  ```ruby
144
- require "sashite-pan"
301
+ Sashite::Pan::Action.modify(square, piece)
302
+ ```
145
303
 
146
- # Piece movement
147
- Sashite::Pan.parse("g7-f7")
148
- # => {type: :move, source: "g7", destination: "f7"}
304
+ Create an in-place transformation action.
149
305
 
150
- # Drop from hand
151
- Sashite::Pan.parse("*e5")
152
- # => {type: :drop, destination: "e5"}
306
+ **Parameters:**
307
+ - `square` [String] - CELL coordinate
308
+ - `piece` [String] - EPIN piece identifier (final state)
153
309
 
154
- # Capture (captured piece goes to hand in Shogi)
155
- Sashite::Pan.parse("h2xg2")
156
- # => {type: :capture, source: "h2", destination: "g2"}
310
+ **Returns:** [Action] - Modification action
157
311
 
158
- # Note: PAN cannot specify which piece type is being dropped
159
- # or whether a piece is promoted
312
+ **Examples:**
313
+ ```ruby
314
+ Sashite::Pan::Action.modify("e4", "+P")
315
+ # => "e4=+P"
316
+
317
+ Sashite::Pan::Action.modify("c3", "k'")
318
+ # => "c3=k'"
160
319
  ```
161
320
 
162
- ## Limitations and Context Dependency
321
+ #### Instance Methods
163
322
 
164
- **Important**: PAN is intentionally minimal and rule-agnostic. It has several important limitations:
323
+ ##### Attribute Access
165
324
 
166
- ### What PAN Cannot Represent
325
+ ```ruby
326
+ action.type
327
+ ```
167
328
 
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
329
+ Get the action type.
173
330
 
174
- ### Examples of Ambiguity
331
+ **Returns:** [Symbol] - One of: `:pass`, `:move`, `:capture`, `:special`, `:static_capture`, `:drop`, `:drop_capture`, `:modify`
332
+
333
+ ---
175
334
 
176
335
  ```ruby
177
- # These PAN strings are syntactically valid but may be ambiguous:
336
+ action.source
337
+ ```
178
338
 
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
339
+ Get the source coordinate (for movement actions).
340
+
341
+ **Returns:** [String, nil] - CELL coordinate or nil
342
+
343
+ ---
344
+
345
+ ```ruby
346
+ action.destination
183
347
  ```
184
348
 
185
- ### When PAN is Insufficient
349
+ Get the destination coordinate.
186
350
 
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
351
+ **Returns:** [String, nil] - CELL coordinate or nil
191
352
 
192
- ## Error Handling
353
+ ---
193
354
 
194
- The library provides detailed error messages for invalid input:
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
+ ---
195
364
 
196
365
  ```ruby
197
- require "sashite-pan"
366
+ action.transformation
367
+ ```
198
368
 
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
369
+ Get the transformation piece (for actions with `=<piece>`).
204
370
 
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
371
+ **Returns:** [String, nil] - EPIN identifier or nil
372
+
373
+ ---
374
+
375
+ ```ruby
376
+ action.to_s
377
+ ```
378
+
379
+ Convert action to PAN string representation.
380
+
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
+ ```
391
+
392
+ ##### Type Queries
393
+
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
405
+ ```
406
+
407
+ Check action type.
408
+
409
+ **Returns:** [Boolean]
410
+
411
+ **Examples:**
412
+ ```ruby
413
+ action = Sashite::Pan.parse("e2-e4")
414
+ action.move? # => true
415
+ action.movement? # => true
416
+ action.pass? # => false
417
+
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
+ ```
431
+
432
+ Check equality between actions.
433
+
434
+ **Parameters:**
435
+ - `other` [Action] - Action to compare with
436
+
437
+ **Returns:** [Boolean] - true if actions are identical
438
+
439
+ **Example:**
440
+ ```ruby
441
+ action1 = Sashite::Pan.parse("e2-e4")
442
+ action2 = Sashite::Pan::Action.move("e2", "e4")
443
+ action1 == action2 # => true
444
+ ```
445
+
446
+ ## Advanced Usage
447
+
448
+ ### Parsing Game Sequences
449
+
450
+ ```ruby
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) }
210
454
 
211
- begin
212
- Sashite::Pan.parse("") # Empty string
213
- rescue Sashite::Pan::Parser::Error => e
214
- puts e.message # => "PAN string cannot be empty"
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
+ ```
463
+
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
215
485
  end
486
+
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"
216
491
  ```
217
492
 
218
- ## Regular Expression Pattern
493
+ ### Transformation Detection
219
494
 
220
- PAN strings can be validated using this pattern:
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
221
508
 
222
509
  ```ruby
223
- PAN_PATTERN = /\A(\*|[a-z][0-9][-x])([a-z][0-9])\z/
510
+ class MoveBuilder
511
+ def initialize(source)
512
+ @source = source
513
+ end
224
514
 
225
- def valid_pan?(string)
226
- return false unless string.match?(PAN_PATTERN)
515
+ def to(destination)
516
+ Sashite::Pan::Action.move(@source, destination)
517
+ end
227
518
 
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
519
+ def captures(destination)
520
+ Sashite::Pan::Action.capture(@source, destination)
233
521
  end
234
522
 
235
- true
523
+ def to_promoting(destination, piece)
524
+ Sashite::Pan::Action.move(@source, destination, transformation: piece)
525
+ end
236
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"
237
533
  ```
238
534
 
239
- ## Use Cases
535
+ ### Validation Before Parsing
240
536
 
241
- ### Optimal for PAN
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
544
+ end
242
545
 
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
546
+ safe_parse("e2-e4") # => #<Pan::Action ...>
547
+ safe_parse("invalid") # => nil
548
+ ```
247
549
 
248
- ### Consider Alternatives When
550
+ ### Pattern Matching (Ruby 3.0+)
249
551
 
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
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
254
569
 
255
- ## Integration Considerations
570
+ action = Sashite::Pan.parse("e7-e8=Q")
571
+ analyze(action) # => "Promotion to Q"
572
+ ```
256
573
 
257
- When using PAN in your applications:
574
+ ## Properties
258
575
 
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
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
263
584
 
264
- ## Properties of PAN
585
+ ## Related Specifications
265
586
 
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
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
271
592
 
272
593
  ## Documentation
273
594
 
274
- - [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/)
275
596
  - [API Documentation](https://rubydoc.info/github/sashite/pan.rb/main)
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
276
625
 
277
626
  ## License
278
627
 
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).
628
+ Available as open source under the [MIT License](https://opensource.org/licenses/MIT).
280
629
 
281
- ## About Sashité
630
+ ## About
282
631
 
283
- 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.