sashite-pmn 1.0.0 → 1.2.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 +235 -61
- data/lib/sashite/pmn/action.rb +21 -20
- data/lib/sashite/pmn/error.rb +44 -11
- data/lib/sashite/pmn/move.rb +44 -24
- data/lib/sashite/pmn.rb +8 -9
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bbe033acc463b83e9ed43201cb847a8c960f6dd662644b5f4f524691a52f0dae
|
|
4
|
+
data.tar.gz: ca9cd6b39a1738e44459e8609a38647045634bd8aa5d765b925ce8f790ccab75
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f4bb7166a1d52ed805b3910da56f32f44dcbe8326e194f1663f79aef1b8eb2fe5279832f818153ac8fed391d3d82235f8ddcc067764bd7c30fc909d4b77bc5d1
|
|
7
|
+
data.tar.gz: fec38eece56648760fac10727af116007373e085d2d7d069b4441e77a5f6598d2851076ecea86931ecdd1baf11b87e089c12e902ad532779363db108400dc32b
|
data/README.md
CHANGED
|
@@ -52,6 +52,7 @@ move.to_a # => ["e2", "e4", "C:P"]
|
|
|
52
52
|
# Validate PMN arrays
|
|
53
53
|
Sashite::Pmn.valid?(["e2", "e4", "C:P"]) # => true
|
|
54
54
|
Sashite::Pmn.valid?(%w[e2 e4]) # => true (inferred piece)
|
|
55
|
+
Sashite::Pmn.valid?([]) # => true (pass move)
|
|
55
56
|
Sashite::Pmn.valid?(["e2"]) # => false (incomplete)
|
|
56
57
|
|
|
57
58
|
# Create moves programmatically
|
|
@@ -60,6 +61,29 @@ move = Sashite::Pmn.from_actions([
|
|
|
60
61
|
])
|
|
61
62
|
```
|
|
62
63
|
|
|
64
|
+
### Pass Moves
|
|
65
|
+
|
|
66
|
+
A **pass move** is represented by an empty array `[]`, allowing a player to voluntarily conclude their turn without performing any displacement or mutation.
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
# Parse a pass move
|
|
70
|
+
pass = Sashite::Pmn.parse([])
|
|
71
|
+
pass.valid? # => true
|
|
72
|
+
pass.pass? # => true
|
|
73
|
+
pass.empty? # => true
|
|
74
|
+
pass.actions # => []
|
|
75
|
+
pass.to_a # => []
|
|
76
|
+
|
|
77
|
+
# Create a pass move directly
|
|
78
|
+
pass = Sashite::Pmn::Move.new
|
|
79
|
+
pass.pass? # => true
|
|
80
|
+
|
|
81
|
+
# Validate pass moves
|
|
82
|
+
Sashite::Pmn.valid?([]) # => true
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Important**: According to the Sashité Protocol, a pass move that results in a position identical to a previous position violates the uniqueness constraint. Game engines must enforce this constraint.
|
|
86
|
+
|
|
63
87
|
### Action Decomposition
|
|
64
88
|
|
|
65
89
|
```ruby
|
|
@@ -78,9 +102,12 @@ action.piece # => nil
|
|
|
78
102
|
action.piece_specified? # => false
|
|
79
103
|
action.inferred? # => true
|
|
80
104
|
|
|
81
|
-
#
|
|
82
|
-
|
|
83
|
-
|
|
105
|
+
# In-place transformations (source == destination)
|
|
106
|
+
transform = Sashite::Pmn.parse(["e4", "e4", "C:+P"])
|
|
107
|
+
transform.valid? # => true
|
|
108
|
+
action = transform.actions.first
|
|
109
|
+
action.in_place? # => true
|
|
110
|
+
action.transformation? # => true
|
|
84
111
|
|
|
85
112
|
# Reserve operations
|
|
86
113
|
drop = Sashite::Pmn.parse(["*", "e5", "S:P"]) # Drop from reserve
|
|
@@ -96,32 +123,17 @@ castling = Sashite::Pmn.parse([
|
|
|
96
123
|
"h1", "f1", "C:R"
|
|
97
124
|
])
|
|
98
125
|
castling.compound? # => true
|
|
126
|
+
castling.simple? # => false
|
|
127
|
+
castling.pass? # => false
|
|
99
128
|
castling.actions.size # => 2
|
|
100
129
|
|
|
101
|
-
# En passant
|
|
130
|
+
# En passant
|
|
102
131
|
en_passant = Sashite::Pmn.parse([
|
|
103
132
|
"e5", "f6", "C:P",
|
|
104
133
|
"f5", "*", "c:p"
|
|
105
134
|
])
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
### Action Analysis
|
|
110
|
-
|
|
111
|
-
```ruby
|
|
112
|
-
action = move.actions.first
|
|
113
|
-
|
|
114
|
-
# Location predicates
|
|
115
|
-
action.board_to_board? # => true
|
|
116
|
-
action.from_reserve? # => false
|
|
117
|
-
action.to_reserve? # => false
|
|
118
|
-
action.drop? # => false
|
|
119
|
-
action.capture? # => false
|
|
120
|
-
action.board_move? # => true
|
|
121
|
-
|
|
122
|
-
# Validation predicates
|
|
123
|
-
action.valid? # => true
|
|
124
|
-
action.piece_valid? # => true or false depending on piece
|
|
135
|
+
en_passant.has_captures? # => true
|
|
136
|
+
en_passant.board_moves.size # => 1
|
|
125
137
|
```
|
|
126
138
|
|
|
127
139
|
### Move Analysis
|
|
@@ -133,6 +145,7 @@ move = Sashite::Pmn.parse([
|
|
|
133
145
|
])
|
|
134
146
|
|
|
135
147
|
# Structure analysis
|
|
148
|
+
move.pass? # => false
|
|
136
149
|
move.simple? # => false
|
|
137
150
|
move.compound? # => true
|
|
138
151
|
move.size # => 2
|
|
@@ -143,44 +156,67 @@ move.has_drops? # => false
|
|
|
143
156
|
move.has_captures? # => false
|
|
144
157
|
move.board_moves.size # => 2
|
|
145
158
|
|
|
146
|
-
# Extract
|
|
159
|
+
# Extract information
|
|
147
160
|
move.sources # => ["e1", "h1"]
|
|
148
161
|
move.destinations # => ["g1", "f1"]
|
|
149
162
|
move.pieces # => ["C:K", "C:R"]
|
|
150
163
|
move.has_inferred? # => false
|
|
151
164
|
```
|
|
152
165
|
|
|
166
|
+
### Action Predicates
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
action = move.actions.first
|
|
170
|
+
|
|
171
|
+
# Location predicates
|
|
172
|
+
action.board_to_board? # => true
|
|
173
|
+
action.from_reserve? # => false
|
|
174
|
+
action.to_reserve? # => false
|
|
175
|
+
action.drop? # => false
|
|
176
|
+
action.capture? # => false
|
|
177
|
+
action.board_move? # => true
|
|
178
|
+
action.in_place? # => false (source != destination)
|
|
179
|
+
action.transformation? # => false (not in-place with state change)
|
|
180
|
+
|
|
181
|
+
# Validation predicates
|
|
182
|
+
action.valid? # => true
|
|
183
|
+
action.piece_valid? # => true or false depending on piece
|
|
184
|
+
```
|
|
185
|
+
|
|
153
186
|
### Error Handling
|
|
154
187
|
|
|
155
188
|
```ruby
|
|
156
189
|
# Invalid action built directly raises action-level errors
|
|
157
190
|
begin
|
|
158
191
|
Sashite::Pmn::Action.new("invalid", "e4", "C:P")
|
|
159
|
-
rescue Sashite::Pmn::
|
|
192
|
+
rescue Sashite::Pmn::Error::Location => e
|
|
160
193
|
puts e.message
|
|
161
194
|
end
|
|
162
195
|
|
|
163
196
|
begin
|
|
164
197
|
Sashite::Pmn::Action.new("e2", "e4", "InvalidPiece")
|
|
165
|
-
rescue Sashite::Pmn::
|
|
198
|
+
rescue Sashite::Pmn::Error::Piece => e
|
|
166
199
|
puts e.message
|
|
167
200
|
end
|
|
168
201
|
|
|
169
|
-
# Parsing a move wraps action-level errors as
|
|
202
|
+
# Parsing a move wraps action-level errors as Error::Move
|
|
170
203
|
begin
|
|
171
|
-
Sashite::Pmn.parse(["e2"])
|
|
172
|
-
rescue Sashite::Pmn::
|
|
173
|
-
puts e.message
|
|
204
|
+
Sashite::Pmn.parse(["e2"]) # Incomplete action
|
|
205
|
+
rescue Sashite::Pmn::Error::Move => e
|
|
206
|
+
puts e.message
|
|
174
207
|
end
|
|
208
|
+
|
|
209
|
+
# Pass moves are always valid structurally
|
|
210
|
+
Sashite::Pmn.parse([]).valid? # => true (never raises)
|
|
175
211
|
```
|
|
176
212
|
|
|
177
213
|
## API Reference
|
|
178
214
|
|
|
179
215
|
### Main Module Methods
|
|
180
216
|
|
|
181
|
-
* `Sashite::Pmn.parse(array)` — Parse a PMN array into a `Move` object.
|
|
182
|
-
* `Sashite::Pmn.valid?(array)` — Check if an array is valid PMN notation (non-raising).
|
|
183
|
-
* `Sashite::Pmn.from_actions(actions)` — Build a `Move` from `Action` objects.
|
|
217
|
+
* `Sashite::Pmn.parse(array)` — Parse a PMN array into a `Move` object. Accepts empty arrays `[]` for pass moves.
|
|
218
|
+
* `Sashite::Pmn.valid?(array)` — Check if an array is valid PMN notation (non-raising). Returns `true` for `[]`.
|
|
219
|
+
* `Sashite::Pmn.from_actions(actions)` — Build a `Move` from `Action` objects. Pass empty array for pass moves.
|
|
184
220
|
* `Sashite::Pmn.valid_location?(location)` — Check if a location is valid (CELL or `"*"`).
|
|
185
221
|
* `Sashite::Pmn.valid_piece?(piece)` — Check if a piece is valid QPI.
|
|
186
222
|
|
|
@@ -188,28 +224,31 @@ end
|
|
|
188
224
|
|
|
189
225
|
#### Creation
|
|
190
226
|
|
|
191
|
-
* `Sashite::Pmn::Move.new(*elements)` — Create from PMN elements (variadic).
|
|
192
|
-
*
|
|
193
|
-
* `
|
|
227
|
+
* `Sashite::Pmn::Move.new(*elements)` — Create from PMN elements (variadic). Call with no arguments for pass move.
|
|
228
|
+
* `Move.new("e2", "e4", "C:P")` — Standard move
|
|
229
|
+
* `Move.new` — Pass move (empty)
|
|
230
|
+
* **Note**: `Move.new(["e2","e4","C:P"])` is **not** accepted; pass individual arguments.
|
|
231
|
+
* `Sashite::Pmn::Move.from_actions(actions)` — Create from `Action` objects. Pass `[]` for pass move.
|
|
194
232
|
|
|
195
233
|
#### Validation & Data
|
|
196
234
|
|
|
197
235
|
* `#valid?` — Check overall validity.
|
|
198
|
-
* `#actions` — Ordered array of `Action` objects (frozen).
|
|
199
|
-
* `#pmn_array` — Original PMN elements (frozen).
|
|
200
|
-
* `#to_a` — Copy of the PMN elements.
|
|
236
|
+
* `#actions` — Ordered array of `Action` objects (frozen). Empty for pass moves.
|
|
237
|
+
* `#pmn_array` — Original PMN elements (frozen). Empty for pass moves.
|
|
238
|
+
* `#to_a` — Copy of the PMN elements. Returns `[]` for pass moves.
|
|
201
239
|
|
|
202
240
|
#### Structure & Queries
|
|
203
241
|
|
|
204
|
-
* `#size` / `#length` — Number of actions.
|
|
205
|
-
* `#empty?` — No actions?
|
|
206
|
-
* `#
|
|
207
|
-
* `#
|
|
208
|
-
* `#
|
|
209
|
-
* `#
|
|
210
|
-
* `#
|
|
211
|
-
* `#
|
|
212
|
-
* `#
|
|
242
|
+
* `#size` / `#length` — Number of actions. Returns `0` for pass moves.
|
|
243
|
+
* `#empty?` — No actions? Returns `true` for pass moves.
|
|
244
|
+
* `#pass?` — Is this a pass move? Returns `true` only for empty moves.
|
|
245
|
+
* `#simple?` — Exactly one action? Returns `false` for pass moves.
|
|
246
|
+
* `#compound?` — Multiple actions? Returns `false` for pass moves.
|
|
247
|
+
* `#first_action` / `#last_action` — Convenience accessors. Return `nil` for pass moves.
|
|
248
|
+
* `#has_drops?` / `#has_captures?` — Presence of drops/captures. Return `false` for pass moves.
|
|
249
|
+
* `#board_moves` — Actions that are board-to-board. Returns `[]` for pass moves.
|
|
250
|
+
* `#sources` / `#destinations` / `#pieces` — Unique lists. Return `[]` for pass moves.
|
|
251
|
+
* `#has_inferred?` — Any action with inferred piece? Returns `false` for pass moves.
|
|
213
252
|
|
|
214
253
|
### Action Class
|
|
215
254
|
|
|
@@ -229,26 +268,39 @@ end
|
|
|
229
268
|
* `#from_reserve?`, `#to_reserve?`
|
|
230
269
|
* `#reserve_to_board?` (drop), `#board_to_reserve?` (capture), `#board_to_board?`
|
|
231
270
|
* `#drop?` (alias), `#capture?` (alias), `#board_move?`
|
|
271
|
+
* `#in_place?` — Source equals destination?
|
|
272
|
+
* `#transformation?` — In-place with state change?
|
|
232
273
|
* `#valid?`
|
|
233
274
|
|
|
234
275
|
### Exceptions
|
|
235
276
|
|
|
236
277
|
* `Sashite::Pmn::Error` — Base error class
|
|
237
|
-
* `Sashite::Pmn::
|
|
238
|
-
* `Sashite::Pmn::
|
|
239
|
-
* `Sashite::Pmn::
|
|
240
|
-
* `Sashite::Pmn::
|
|
278
|
+
* `Sashite::Pmn::Error::Move` — Invalid PMN sequence / parsing failure
|
|
279
|
+
* `Sashite::Pmn::Error::Action` — Invalid atomic action
|
|
280
|
+
* `Sashite::Pmn::Error::Location` — Invalid location (not CELL or HAND)
|
|
281
|
+
* `Sashite::Pmn::Error::Piece` — Invalid piece (not QPI format)
|
|
241
282
|
|
|
242
283
|
## Format Specification (Summary)
|
|
243
284
|
|
|
244
285
|
### Structure
|
|
245
286
|
|
|
246
|
-
PMN moves are flat **arrays** containing action sequences:
|
|
287
|
+
PMN moves are flat **arrays** containing action sequences or empty for pass moves:
|
|
247
288
|
|
|
248
289
|
```
|
|
249
|
-
[
|
|
290
|
+
[] # Pass move
|
|
291
|
+
[<element-1>, <element-2>, <element-3>, ...] # Action sequence
|
|
250
292
|
```
|
|
251
293
|
|
|
294
|
+
### Pass Move Format
|
|
295
|
+
|
|
296
|
+
A **pass move** is represented by an empty array:
|
|
297
|
+
|
|
298
|
+
```json
|
|
299
|
+
[]
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
This indicates that the active player concludes their turn without performing any action. The resulting position must respect the position uniqueness constraint defined in the Sashité Protocol.
|
|
303
|
+
|
|
252
304
|
### Action Format
|
|
253
305
|
|
|
254
306
|
Each action consists of 2 or 3 consecutive elements:
|
|
@@ -263,18 +315,32 @@ Each action consists of 2 or 3 consecutive elements:
|
|
|
263
315
|
|
|
264
316
|
### Array Length Rules
|
|
265
317
|
|
|
266
|
-
*
|
|
267
|
-
*
|
|
318
|
+
* **Pass move**: 0 elements (empty array)
|
|
319
|
+
* **Action moves**: Minimum 2 elements (one action with inferred piece)
|
|
320
|
+
* Valid non-empty lengths: multiple of 3, **or** multiple of 3 plus 2
|
|
268
321
|
|
|
269
|
-
###
|
|
322
|
+
### In-Place Actions
|
|
270
323
|
|
|
271
324
|
Actions where **source == destination** are allowed, enabling:
|
|
272
325
|
|
|
273
|
-
*
|
|
274
|
-
*
|
|
326
|
+
* In-place transformations with state change (e.g., `["e4", "e4", "C:+P"]`)
|
|
327
|
+
* Context-dependent mutations specified by game rules
|
|
328
|
+
|
|
329
|
+
**Important**: Actions without observable effect (same location, same piece state) should be avoided. Use pass moves `[]` instead for turn-only actions.
|
|
275
330
|
|
|
276
331
|
## Game Examples
|
|
277
332
|
|
|
333
|
+
### Pass Moves
|
|
334
|
+
|
|
335
|
+
```ruby
|
|
336
|
+
# Player passes their turn
|
|
337
|
+
pass = Sashite::Pmn.parse([])
|
|
338
|
+
pass.pass? # => true
|
|
339
|
+
|
|
340
|
+
# In games where passing is strategic (e.g., Go, some variants)
|
|
341
|
+
game_engine.execute_move([]) # Pass turn to opponent
|
|
342
|
+
```
|
|
343
|
+
|
|
278
344
|
### Western Chess
|
|
279
345
|
|
|
280
346
|
```ruby
|
|
@@ -295,6 +361,9 @@ en_passant = Sashite::Pmn.parse([
|
|
|
295
361
|
|
|
296
362
|
# Promotion
|
|
297
363
|
promotion = Sashite::Pmn.parse(["e7", "e8", "C:Q"])
|
|
364
|
+
|
|
365
|
+
# In-place promotion (variant rule)
|
|
366
|
+
in_place_promotion = Sashite::Pmn.parse(["e8", "e8", "C:Q"])
|
|
298
367
|
```
|
|
299
368
|
|
|
300
369
|
### Japanese Shōgi
|
|
@@ -311,6 +380,9 @@ capture = Sashite::Pmn.parse([
|
|
|
311
380
|
|
|
312
381
|
# Promotion
|
|
313
382
|
promotion = Sashite::Pmn.parse(["h8", "i8", "S:+S"])
|
|
383
|
+
|
|
384
|
+
# Pass (uncommon in Shōgi but structurally valid)
|
|
385
|
+
pass = Sashite::Pmn.parse([])
|
|
314
386
|
```
|
|
315
387
|
|
|
316
388
|
### Chinese Xiangqi
|
|
@@ -326,6 +398,20 @@ cannon_capture = Sashite::Pmn.parse([
|
|
|
326
398
|
])
|
|
327
399
|
```
|
|
328
400
|
|
|
401
|
+
### Go
|
|
402
|
+
|
|
403
|
+
```ruby
|
|
404
|
+
# Place stone
|
|
405
|
+
place_stone = Sashite::Pmn.parse(["*", "d4", "G:B"])
|
|
406
|
+
|
|
407
|
+
# Pass (very common in Go)
|
|
408
|
+
pass = Sashite::Pmn.parse([])
|
|
409
|
+
pass.pass? # => true
|
|
410
|
+
|
|
411
|
+
# Consecutive passes typically end the game in Go
|
|
412
|
+
end_game_and_score if last_move.pass? && current_move.pass?
|
|
413
|
+
```
|
|
414
|
+
|
|
329
415
|
## Advanced Usage
|
|
330
416
|
|
|
331
417
|
### Move Composition
|
|
@@ -337,6 +423,10 @@ actions << Sashite::Pmn::Action.new("d7", "d5", "c:p")
|
|
|
337
423
|
|
|
338
424
|
move = Sashite::Pmn.from_actions(actions)
|
|
339
425
|
move.to_a # => ["e2", "e4", "C:P", "d7", "d5", "c:p"]
|
|
426
|
+
|
|
427
|
+
# Create pass move
|
|
428
|
+
pass = Sashite::Pmn.from_actions([])
|
|
429
|
+
pass.pass? # => true
|
|
340
430
|
```
|
|
341
431
|
|
|
342
432
|
### Integration with Game Engines
|
|
@@ -346,26 +436,76 @@ class GameEngine
|
|
|
346
436
|
def execute_move(pmn_array)
|
|
347
437
|
move = Sashite::Pmn.parse(pmn_array)
|
|
348
438
|
|
|
439
|
+
# Handle pass moves
|
|
440
|
+
if move.pass?
|
|
441
|
+
handle_pass_move
|
|
442
|
+
switch_active_player
|
|
443
|
+
return
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
# Execute each action
|
|
349
447
|
move.actions.each do |action|
|
|
350
448
|
if action.from_reserve?
|
|
351
449
|
place_piece(action.destination, action.piece)
|
|
352
450
|
elsif action.to_reserve?
|
|
353
451
|
capture_piece(action.source)
|
|
452
|
+
elsif action.in_place?
|
|
453
|
+
transform_piece(action.source, action.piece)
|
|
354
454
|
else
|
|
355
455
|
move_piece(action.source, action.destination, action.piece)
|
|
356
456
|
end
|
|
357
457
|
end
|
|
458
|
+
|
|
459
|
+
switch_active_player
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
private
|
|
463
|
+
|
|
464
|
+
def handle_pass_move
|
|
465
|
+
# Check position uniqueness constraint
|
|
466
|
+
raise "Pass move creates repeated position (protocol violation)" if position_seen_before?(current_position)
|
|
467
|
+
|
|
468
|
+
# Record pass in game history
|
|
469
|
+
record_move([])
|
|
358
470
|
end
|
|
359
471
|
|
|
360
472
|
# ...
|
|
361
473
|
end
|
|
362
474
|
```
|
|
363
475
|
|
|
476
|
+
### Position Tracking with Pass Moves
|
|
477
|
+
|
|
478
|
+
```ruby
|
|
479
|
+
class PositionTracker
|
|
480
|
+
def initialize
|
|
481
|
+
@seen_positions = Set.new
|
|
482
|
+
@position_history = []
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
def record_move(pmn_array, resulting_position)
|
|
486
|
+
move = Sashite::Pmn.parse(pmn_array)
|
|
487
|
+
|
|
488
|
+
# For pass moves, verify uniqueness constraint
|
|
489
|
+
if move.pass? && @seen_positions.include?(resulting_position)
|
|
490
|
+
raise "Pass move violates position uniqueness constraint"
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
@seen_positions.add(resulting_position)
|
|
494
|
+
@position_history << { move: pmn_array, position: resulting_position }
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
def position_seen?(position)
|
|
498
|
+
@seen_positions.include?(position)
|
|
499
|
+
end
|
|
500
|
+
end
|
|
501
|
+
```
|
|
502
|
+
|
|
364
503
|
## Design Properties
|
|
365
504
|
|
|
366
505
|
* **Rule-agnostic**: Independent of specific game mechanics
|
|
367
506
|
* **Mechanical decomposition**: Breaks complex moves into atomic actions
|
|
368
507
|
* **Array-based**: Simple, interoperable structure
|
|
508
|
+
* **Pass move support**: Empty arrays `[]` for voluntary turn conclusion
|
|
369
509
|
* **Sequential execution**: Actions execute in array order
|
|
370
510
|
* **Piece inference**: Optional piece specification when context is clear
|
|
371
511
|
* **Universal applicability**: Works across board game systems
|
|
@@ -374,13 +514,23 @@ end
|
|
|
374
514
|
|
|
375
515
|
## Mechanical Semantics (Recap)
|
|
376
516
|
|
|
377
|
-
|
|
517
|
+
### Pass Moves
|
|
518
|
+
|
|
519
|
+
Pass moves (`[]`) indicate:
|
|
520
|
+
* No piece displacement occurs
|
|
521
|
+
* No piece mutation occurs
|
|
522
|
+
* The active player voluntarily concludes their turn
|
|
523
|
+
* The resulting position must be unique according to protocol constraints
|
|
378
524
|
|
|
525
|
+
### Action Execution
|
|
526
|
+
|
|
527
|
+
For non-pass moves, each action applies atomically:
|
|
528
|
+
|
|
529
|
+
1. **Source state change**:
|
|
379
530
|
* CELL → becomes empty
|
|
380
531
|
* HAND `"*"` → remove piece from reserve
|
|
381
532
|
|
|
382
533
|
2. **Destination state change**:
|
|
383
|
-
|
|
384
534
|
* CELL → contains final piece
|
|
385
535
|
* HAND `"*"` → add piece to reserve
|
|
386
536
|
|
|
@@ -388,6 +538,28 @@ end
|
|
|
388
538
|
|
|
389
539
|
4. **Atomic commitment**: Each action applies atomically
|
|
390
540
|
|
|
541
|
+
## Protocol Compliance
|
|
542
|
+
|
|
543
|
+
### Position Uniqueness Constraint
|
|
544
|
+
|
|
545
|
+
According to the Sashité Protocol, all positions within a match must be unique. This applies to pass moves:
|
|
546
|
+
|
|
547
|
+
* A pass move that results in a position identical to any previous position **violates** the constraint
|
|
548
|
+
* Game engines must track position history and enforce this rule
|
|
549
|
+
* However, if the position after a pass move is unique (due to time-sensitive state or other factors), the pass is valid
|
|
550
|
+
|
|
551
|
+
### Authorized Operations
|
|
552
|
+
|
|
553
|
+
PMN supports all protocol-authorized operations:
|
|
554
|
+
|
|
555
|
+
* **Board-to-board**: Standard piece movement
|
|
556
|
+
* **Board-to-hand**: Captures to reserve
|
|
557
|
+
* **Hand-to-board**: Drops from reserve
|
|
558
|
+
* **In-place transformations**: State changes without displacement
|
|
559
|
+
* **Pass moves**: Voluntary turn conclusion
|
|
560
|
+
|
|
561
|
+
**Prohibited**: Hand-to-hand transfers are not allowed by the protocol.
|
|
562
|
+
|
|
391
563
|
## License
|
|
392
564
|
|
|
393
565
|
Available as open source under the [MIT License](https://github.com/sashite/pmn.rb/raw/main/LICENSE.md).
|
|
@@ -403,6 +575,8 @@ Bug reports and pull requests are welcome on GitHub at [https://github.com/sashi
|
|
|
403
575
|
* [CELL Specification](https://sashite.dev/specs/cell/)
|
|
404
576
|
* [HAND Specification](https://sashite.dev/specs/hand/)
|
|
405
577
|
* [QPI Specification](https://sashite.dev/specs/qpi/)
|
|
578
|
+
* [Sashité Protocol](https://sashite.dev/protocol/)
|
|
579
|
+
* [Glossary](https://sashite.dev/glossary/)
|
|
406
580
|
|
|
407
581
|
## About
|
|
408
582
|
|
data/lib/sashite/pmn/action.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "sashite/cell"
|
|
4
4
|
require "sashite/hand"
|
|
5
5
|
require "sashite/qpi"
|
|
6
|
+
|
|
6
7
|
require_relative "error"
|
|
7
8
|
|
|
8
9
|
module Sashite
|
|
@@ -30,8 +31,8 @@ module Sashite
|
|
|
30
31
|
# @param source [String] CELL or "*"
|
|
31
32
|
# @param destination [String] CELL or "*"
|
|
32
33
|
# @param piece [String, nil] QPI string (optional)
|
|
33
|
-
# @raise [
|
|
34
|
-
# @raise [
|
|
34
|
+
# @raise [Error::Location] if source/destination are invalid
|
|
35
|
+
# @raise [Error::Piece] if piece is provided but invalid
|
|
35
36
|
def initialize(source, destination, piece = nil)
|
|
36
37
|
validate_source!(source)
|
|
37
38
|
validate_destination!(destination)
|
|
@@ -60,32 +61,32 @@ module Sashite
|
|
|
60
61
|
def piece_valid?
|
|
61
62
|
return false if piece.nil?
|
|
62
63
|
|
|
63
|
-
Qpi.valid?(piece)
|
|
64
|
+
::Sashite::Qpi.valid?(piece)
|
|
64
65
|
end
|
|
65
66
|
|
|
66
67
|
# @return [Boolean] true if source is HAND ("*")
|
|
67
68
|
def from_reserve?
|
|
68
|
-
Hand.reserve?(source)
|
|
69
|
+
::Sashite::Hand.reserve?(source)
|
|
69
70
|
end
|
|
70
71
|
|
|
71
72
|
# @return [Boolean] true if destination is HAND ("*")
|
|
72
73
|
def to_reserve?
|
|
73
|
-
Hand.reserve?(destination)
|
|
74
|
+
::Sashite::Hand.reserve?(destination)
|
|
74
75
|
end
|
|
75
76
|
|
|
76
77
|
# @return [Boolean] true if both endpoints are board locations
|
|
77
78
|
def board_to_board?
|
|
78
|
-
Cell.valid?(source) && Cell.valid?(destination)
|
|
79
|
+
::Sashite::Cell.valid?(source) && ::Sashite::Cell.valid?(destination)
|
|
79
80
|
end
|
|
80
81
|
|
|
81
82
|
# @return [Boolean] true if the action places from reserve to board
|
|
82
83
|
def drop?
|
|
83
|
-
from_reserve? && Cell.valid?(destination)
|
|
84
|
+
from_reserve? && ::Sashite::Cell.valid?(destination)
|
|
84
85
|
end
|
|
85
86
|
|
|
86
87
|
# @return [Boolean] true if the action takes from board to reserve
|
|
87
88
|
def capture?
|
|
88
|
-
Cell.valid?(source) && to_reserve?
|
|
89
|
+
::Sashite::Cell.valid?(source) && to_reserve?
|
|
89
90
|
end
|
|
90
91
|
|
|
91
92
|
# @return [Boolean] true when neither drop nor capture
|
|
@@ -111,11 +112,11 @@ module Sashite
|
|
|
111
112
|
def valid?
|
|
112
113
|
valid_location?(source) &&
|
|
113
114
|
valid_location?(destination) &&
|
|
114
|
-
(piece.nil? || Qpi.valid?(piece))
|
|
115
|
+
(piece.nil? || ::Sashite::Qpi.valid?(piece))
|
|
115
116
|
end
|
|
116
117
|
|
|
117
118
|
# @param other [Object]
|
|
118
|
-
# @return [Boolean] equality by
|
|
119
|
+
# @return [Boolean] equality by source, destination, piece
|
|
119
120
|
def ==(other)
|
|
120
121
|
return false unless other.is_a?(Action)
|
|
121
122
|
|
|
@@ -145,8 +146,8 @@ module Sashite
|
|
|
145
146
|
# @return [Action]
|
|
146
147
|
# @raise [ArgumentError] if required keys are missing
|
|
147
148
|
def self.from_hash(hash)
|
|
148
|
-
raise ArgumentError, "Hash must include :source" unless hash.key?(:source)
|
|
149
|
-
raise ArgumentError, "Hash must include :destination" unless hash.key?(:destination)
|
|
149
|
+
raise ::ArgumentError, "Hash must include :source" unless hash.key?(:source)
|
|
150
|
+
raise ::ArgumentError, "Hash must include :destination" unless hash.key?(:destination)
|
|
150
151
|
|
|
151
152
|
new(hash[:source], hash[:destination], hash[:piece])
|
|
152
153
|
end
|
|
@@ -156,34 +157,34 @@ module Sashite
|
|
|
156
157
|
# ---------- Internal validation helpers -------------------------------
|
|
157
158
|
|
|
158
159
|
# @param src [String]
|
|
159
|
-
# @raise [
|
|
160
|
+
# @raise [Error::Location]
|
|
160
161
|
def validate_source!(src)
|
|
161
162
|
return if valid_location?(src)
|
|
162
163
|
|
|
163
|
-
raise
|
|
164
|
+
raise Error::Location, "Invalid source location: #{src.inspect}"
|
|
164
165
|
end
|
|
165
166
|
|
|
166
167
|
# @param dst [String]
|
|
167
|
-
# @raise [
|
|
168
|
+
# @raise [Error::Location]
|
|
168
169
|
def validate_destination!(dst)
|
|
169
170
|
return if valid_location?(dst)
|
|
170
171
|
|
|
171
|
-
raise
|
|
172
|
+
raise Error::Location, "Invalid destination location: #{dst.inspect}"
|
|
172
173
|
end
|
|
173
174
|
|
|
174
175
|
# @param qpi [String, nil]
|
|
175
|
-
# @raise [
|
|
176
|
+
# @raise [Error::Piece]
|
|
176
177
|
def validate_piece!(qpi)
|
|
177
178
|
return if qpi.nil?
|
|
178
|
-
return if Qpi.valid?(qpi)
|
|
179
|
+
return if ::Sashite::Qpi.valid?(qpi)
|
|
179
180
|
|
|
180
|
-
raise
|
|
181
|
+
raise Error::Piece, "Invalid piece QPI format: #{qpi.inspect}"
|
|
181
182
|
end
|
|
182
183
|
|
|
183
184
|
# @param location [String]
|
|
184
185
|
# @return [Boolean] true if CELL or HAND ("*")
|
|
185
186
|
def valid_location?(location)
|
|
186
|
-
Cell.valid?(location) || Hand.reserve?(location)
|
|
187
|
+
::Sashite::Cell.valid?(location) || ::Sashite::Hand.reserve?(location)
|
|
187
188
|
end
|
|
188
189
|
end
|
|
189
190
|
end
|
data/lib/sashite/pmn/error.rb
CHANGED
|
@@ -2,19 +2,52 @@
|
|
|
2
2
|
|
|
3
3
|
module Sashite
|
|
4
4
|
module Pmn
|
|
5
|
-
# Base
|
|
6
|
-
|
|
5
|
+
# Base error namespace for PMN.
|
|
6
|
+
#
|
|
7
|
+
# Usage patterns:
|
|
8
|
+
# rescue Sashite::Pmn::Error => e
|
|
9
|
+
# rescue Sashite::Pmn::Error::Move
|
|
10
|
+
# rescue Sashite::Pmn::Error::Location, Sashite::Pmn::Error::Piece
|
|
11
|
+
class Error < ::StandardError
|
|
12
|
+
# Raised when a PMN move (sequence) is malformed or invalid.
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# begin
|
|
16
|
+
# Sashite::Pmn::Move.new("e2") # Incomplete action
|
|
17
|
+
# rescue Sashite::Pmn::Error::Move => e
|
|
18
|
+
# warn "Invalid move sequence: #{e.message}"
|
|
19
|
+
# end
|
|
20
|
+
class Move < Error; end
|
|
7
21
|
|
|
8
|
-
|
|
9
|
-
|
|
22
|
+
# Raised when an atomic action is malformed or fails validation.
|
|
23
|
+
#
|
|
24
|
+
# @example
|
|
25
|
+
# begin
|
|
26
|
+
# Sashite::Pmn::Action.new("invalid", "e4", "C:P")
|
|
27
|
+
# rescue Sashite::Pmn::Error::Action => e
|
|
28
|
+
# warn "Invalid atomic action: #{e.message}"
|
|
29
|
+
# end
|
|
30
|
+
class Action < Error; end
|
|
10
31
|
|
|
11
|
-
|
|
12
|
-
|
|
32
|
+
# Raised when a location is neither a valid CELL coordinate nor HAND ("*").
|
|
33
|
+
#
|
|
34
|
+
# @example
|
|
35
|
+
# begin
|
|
36
|
+
# Sashite::Pmn::Action.new("ZZ99", "e4", "C:P")
|
|
37
|
+
# rescue Sashite::Pmn::Error::Location => e
|
|
38
|
+
# warn "Invalid location: #{e.message}"
|
|
39
|
+
# end
|
|
40
|
+
class Location < Action; end
|
|
13
41
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
42
|
+
# Raised when a piece identifier is not valid QPI format.
|
|
43
|
+
#
|
|
44
|
+
# @example
|
|
45
|
+
# begin
|
|
46
|
+
# Sashite::Pmn::Action.new("e2", "e4", "NotQPI")
|
|
47
|
+
# rescue Sashite::Pmn::Error::Piece => e
|
|
48
|
+
# warn "Invalid piece QPI: #{e.message}"
|
|
49
|
+
# end
|
|
50
|
+
class Piece < Action; end
|
|
51
|
+
end
|
|
19
52
|
end
|
|
20
53
|
end
|
data/lib/sashite/pmn/move.rb
CHANGED
|
@@ -7,31 +7,35 @@ module Sashite
|
|
|
7
7
|
module Pmn
|
|
8
8
|
# Represents a complete move in PMN notation.
|
|
9
9
|
#
|
|
10
|
-
# A Move is a sequence of
|
|
11
|
-
# of elements
|
|
10
|
+
# A Move is a sequence of zero or more atomic actions described by a flat list
|
|
11
|
+
# of elements:
|
|
12
|
+
# [] # pass move (empty array)
|
|
12
13
|
# [source, destination] # inferred piece
|
|
13
14
|
# [source, destination, piece] # explicit QPI piece
|
|
14
15
|
#
|
|
15
|
-
# Valid lengths:
|
|
16
|
+
# Valid lengths:
|
|
17
|
+
# - 0 (pass move)
|
|
18
|
+
# - multiple of 3 OR multiple of 3 + 2 (minimum 2 for non-pass moves)
|
|
16
19
|
class Move
|
|
17
|
-
# @return [Array<Action>] ordered sequence of actions
|
|
20
|
+
# @return [Array<Action>] ordered sequence of actions (empty for pass moves)
|
|
18
21
|
attr_reader :actions
|
|
19
22
|
|
|
20
|
-
# @return [Array<String>] original PMN elements (frozen)
|
|
23
|
+
# @return [Array<String>] original PMN elements (frozen, empty for pass moves)
|
|
21
24
|
attr_reader :pmn_array
|
|
22
25
|
|
|
23
26
|
# Create a Move from PMN elements (variadic only).
|
|
24
27
|
#
|
|
25
28
|
# @param pmn_elements [Array<String>] passed as individual args
|
|
26
|
-
# @raise [
|
|
29
|
+
# @raise [Error::Move] if called with a single Array or if invalid
|
|
27
30
|
#
|
|
28
31
|
# @example
|
|
29
|
-
# Move.new("e2","e4","C:P")
|
|
30
|
-
# Move.new("e2","e4")
|
|
32
|
+
# Move.new("e2","e4","C:P") # Standard move
|
|
33
|
+
# Move.new("e2","e4") # Inferred piece
|
|
34
|
+
# Move.new # Pass move (no arguments)
|
|
31
35
|
def initialize(*pmn_elements)
|
|
32
36
|
# single-array form is intentionally not supported (entropy reduction)
|
|
33
|
-
if pmn_elements.size == 1 && pmn_elements.first.is_a?(Array)
|
|
34
|
-
raise
|
|
37
|
+
if pmn_elements.size == 1 && pmn_elements.first.is_a?(::Array)
|
|
38
|
+
raise Error::Move,
|
|
35
39
|
'PMN must be passed as individual arguments, e.g. Move.new("e2","e4","C:P")'
|
|
36
40
|
end
|
|
37
41
|
|
|
@@ -45,12 +49,17 @@ module Sashite
|
|
|
45
49
|
# @return [Boolean] true if PMN length is valid and all actions are valid
|
|
46
50
|
def valid?
|
|
47
51
|
valid_length? && actions.all?(&:valid?)
|
|
48
|
-
rescue StandardError
|
|
52
|
+
rescue ::StandardError
|
|
49
53
|
false
|
|
50
54
|
end
|
|
51
55
|
|
|
52
56
|
# Shape / structure -----------------------------------------------------
|
|
53
57
|
|
|
58
|
+
# @return [Boolean] is this a pass move (no actions)?
|
|
59
|
+
def pass?
|
|
60
|
+
actions.empty?
|
|
61
|
+
end
|
|
62
|
+
|
|
54
63
|
# @return [Boolean] exactly one action?
|
|
55
64
|
def simple?
|
|
56
65
|
actions.size == 1
|
|
@@ -71,13 +80,13 @@ module Sashite
|
|
|
71
80
|
actions.last
|
|
72
81
|
end
|
|
73
82
|
|
|
74
|
-
# @return [Integer] number of actions
|
|
83
|
+
# @return [Integer] number of actions (0 for pass moves)
|
|
75
84
|
def size
|
|
76
85
|
actions.size
|
|
77
86
|
end
|
|
78
87
|
alias length size
|
|
79
88
|
|
|
80
|
-
# @return [Boolean] true if no actions
|
|
89
|
+
# @return [Boolean] true if no actions (pass move)
|
|
81
90
|
def empty?
|
|
82
91
|
actions.empty?
|
|
83
92
|
end
|
|
@@ -121,7 +130,7 @@ module Sashite
|
|
|
121
130
|
|
|
122
131
|
# Conversion ------------------------------------------------------------
|
|
123
132
|
|
|
124
|
-
# @return [Array<String>] copy of original PMN elements
|
|
133
|
+
# @return [Array<String>] copy of original PMN elements (empty array for pass moves)
|
|
125
134
|
def to_a
|
|
126
135
|
pmn_array.dup
|
|
127
136
|
end
|
|
@@ -144,7 +153,11 @@ module Sashite
|
|
|
144
153
|
|
|
145
154
|
# @return [String]
|
|
146
155
|
def inspect
|
|
147
|
-
|
|
156
|
+
if pass?
|
|
157
|
+
"#<#{self.class.name} pass=true>"
|
|
158
|
+
else
|
|
159
|
+
"#<#{self.class.name} actions=#{actions.size} pmn=#{pmn_array.inspect}>"
|
|
160
|
+
end
|
|
148
161
|
end
|
|
149
162
|
|
|
150
163
|
# Functional composition -----------------------------------------------
|
|
@@ -170,18 +183,23 @@ module Sashite
|
|
|
170
183
|
# Validation ------------------------------------------------------------
|
|
171
184
|
|
|
172
185
|
def validate_array!(array)
|
|
173
|
-
raise
|
|
174
|
-
|
|
186
|
+
raise Error::Move, "PMN must be an array, got #{array.class}" unless array.is_a?(::Array)
|
|
187
|
+
|
|
188
|
+
# Empty arrays are valid (pass moves)
|
|
189
|
+
return if array.empty?
|
|
175
190
|
|
|
176
|
-
raise
|
|
191
|
+
raise Error::Move, "All PMN elements must be strings" unless array.all?(::String)
|
|
177
192
|
|
|
178
193
|
return if valid_length?(array)
|
|
179
194
|
|
|
180
|
-
raise
|
|
195
|
+
raise Error::Move, "Invalid PMN array length: #{array.size}"
|
|
181
196
|
end
|
|
182
197
|
|
|
183
|
-
# Valid lengths:
|
|
198
|
+
# Valid lengths:
|
|
199
|
+
# - 0 (pass move)
|
|
200
|
+
# - (size % 3 == 0) OR (size % 3 == 2), minimum 2
|
|
184
201
|
def valid_length?(array = pmn_array)
|
|
202
|
+
return true if array.empty? # Pass move
|
|
185
203
|
return false if array.size < 2
|
|
186
204
|
|
|
187
205
|
r = array.size % 3
|
|
@@ -191,6 +209,8 @@ module Sashite
|
|
|
191
209
|
# Parsing ---------------------------------------------------------------
|
|
192
210
|
|
|
193
211
|
def parse_actions(array)
|
|
212
|
+
return [] if array.empty? # Pass move has no actions
|
|
213
|
+
|
|
194
214
|
actions = []
|
|
195
215
|
index = 0
|
|
196
216
|
|
|
@@ -204,21 +224,21 @@ module Sashite
|
|
|
204
224
|
actions << Action.new(array[index], array[index + 1], array[index + 2])
|
|
205
225
|
index += 3
|
|
206
226
|
else
|
|
207
|
-
raise
|
|
227
|
+
raise Error::Move, "Invalid action group at index #{index}"
|
|
208
228
|
end
|
|
209
229
|
end
|
|
210
230
|
|
|
211
231
|
actions
|
|
212
|
-
rescue
|
|
232
|
+
rescue Error::Action => e
|
|
213
233
|
# Normalize action-level errors as move-level errors during parsing
|
|
214
|
-
raise
|
|
234
|
+
raise Error::Move, "Invalid action while parsing move at index #{index}: #{e.message}"
|
|
215
235
|
end
|
|
216
236
|
|
|
217
237
|
def validate_actions!
|
|
218
238
|
actions.each_with_index do |action, i|
|
|
219
239
|
next if action.valid?
|
|
220
240
|
|
|
221
|
-
raise
|
|
241
|
+
raise Error::Move, "Invalid action at position #{i}: #{action.inspect}"
|
|
222
242
|
end
|
|
223
243
|
end
|
|
224
244
|
end
|
data/lib/sashite/pmn.rb
CHANGED
|
@@ -27,12 +27,12 @@ module Sashite
|
|
|
27
27
|
#
|
|
28
28
|
# @param pmn_array [Array<String>] flat array of PMN elements
|
|
29
29
|
# @return [Sashite::Pmn::Move]
|
|
30
|
-
# @raise [Sashite::Pmn::
|
|
30
|
+
# @raise [Sashite::Pmn::Error::Move] if the array or any action is invalid
|
|
31
31
|
#
|
|
32
32
|
# @example
|
|
33
33
|
# Sashite::Pmn.parse(["e2","e4","C:P"]).actions.size # => 1
|
|
34
34
|
def self.parse(pmn_array)
|
|
35
|
-
raise
|
|
35
|
+
raise Error::Move, "PMN must be an array, got #{pmn_array.class}" unless pmn_array.is_a?(::Array)
|
|
36
36
|
|
|
37
37
|
Move.new(*pmn_array)
|
|
38
38
|
end
|
|
@@ -46,8 +46,7 @@ module Sashite
|
|
|
46
46
|
# Sashite::Pmn.valid?(["e2","e4","C:P"]) # => true
|
|
47
47
|
# Sashite::Pmn.valid?(["e2"]) # => false
|
|
48
48
|
def self.valid?(pmn_array)
|
|
49
|
-
return false unless pmn_array.is_a?(Array)
|
|
50
|
-
return false if pmn_array.empty?
|
|
49
|
+
return false unless pmn_array.is_a?(::Array)
|
|
51
50
|
|
|
52
51
|
move = Move.new(*pmn_array)
|
|
53
52
|
move.valid?
|
|
@@ -66,7 +65,7 @@ module Sashite
|
|
|
66
65
|
# a2 = Sashite::Pmn::Action.new("d7","d5","c:p")
|
|
67
66
|
# move = Sashite::Pmn.from_actions([a1,a2])
|
|
68
67
|
def self.from_actions(actions)
|
|
69
|
-
raise ArgumentError, "Actions must be an array" unless actions.is_a?(Array)
|
|
68
|
+
raise ::ArgumentError, "Actions must be an array" unless actions.is_a?(::Array)
|
|
70
69
|
|
|
71
70
|
pmn_array = actions.flat_map(&:to_a)
|
|
72
71
|
Move.new(*pmn_array)
|
|
@@ -79,9 +78,9 @@ module Sashite
|
|
|
79
78
|
#
|
|
80
79
|
# @api public
|
|
81
80
|
def self.valid_location?(location)
|
|
82
|
-
return false unless location.is_a?(String)
|
|
81
|
+
return false unless location.is_a?(::String)
|
|
83
82
|
|
|
84
|
-
Cell.valid?(location) || Hand.reserve?(location)
|
|
83
|
+
::Sashite::Cell.valid?(location) || ::Sashite::Hand.reserve?(location)
|
|
85
84
|
end
|
|
86
85
|
|
|
87
86
|
# Validate a QPI piece string.
|
|
@@ -91,9 +90,9 @@ module Sashite
|
|
|
91
90
|
#
|
|
92
91
|
# @api public
|
|
93
92
|
def self.valid_piece?(piece)
|
|
94
|
-
return false unless piece.is_a?(String)
|
|
93
|
+
return false unless piece.is_a?(::String)
|
|
95
94
|
|
|
96
|
-
Qpi.valid?(piece)
|
|
95
|
+
::Sashite::Qpi.valid?(piece)
|
|
97
96
|
end
|
|
98
97
|
end
|
|
99
98
|
end
|