sashite-pmn 1.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 +7 -0
- data/LICENSE.md +21 -0
- data/README.md +409 -0
- data/lib/sashite/pmn/action.rb +190 -0
- data/lib/sashite/pmn/error.rb +20 -0
- data/lib/sashite/pmn/move.rb +226 -0
- data/lib/sashite/pmn.rb +99 -0
- data/lib/sashite-pmn.rb +14 -0
- metadata +105 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: c3ebaf0b731c5fa324e1515369a8a98bc3fc4a6a8094d67023728a04c650a963
|
4
|
+
data.tar.gz: 6a4e1ba06a8afd9015ae8dcc89795fa9f28a7b573d4502b13e52d5ed380c3630
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c6624a7cb869903504622c7a7a34f9a5a73a3c63967ed6c7d0ff89e9e4fe8782e806b94742ea3156fd41b54159cc81c5921e59e5cd27c97bf076ca90482148d2
|
7
|
+
data.tar.gz: 7d871f7cdc7509b9c8ec3ea77c0375652423940be53f157e60e4debebc604ed2a10558ecc7d0a9e6e7a892e9a771bbabab0151d2929127de87d81ee2ac5c332f
|
data/LICENSE.md
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# The MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2019-2025 Sashité
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,409 @@
|
|
1
|
+
# Pmn.rb
|
2
|
+
|
3
|
+
[](https://github.com/sashite/pmn.rb/tags)
|
4
|
+
[](https://rubydoc.info/github/sashite/pmn.rb/main)
|
5
|
+

|
6
|
+
[](https://github.com/sashite/pmn.rb/raw/main/LICENSE.md)
|
7
|
+
|
8
|
+
> **PMN** (Portable Move Notation) implementation for the Ruby language.
|
9
|
+
|
10
|
+
## What is PMN?
|
11
|
+
|
12
|
+
PMN (Portable Move Notation) is a rule-agnostic, **array-based** format for describing the mechanical decomposition of moves in abstract strategy board games. PMN breaks down complex movements into sequences of atomic actions, revealing the underlying mechanics while remaining completely independent of specific game rules, validation logic, or gameplay concepts.
|
13
|
+
|
14
|
+
This gem implements the [PMN Specification v1.0.0](https://sashite.dev/specs/pmn/1.0.0/), providing a small, functional Ruby interface for working with mechanical move decomposition across any board game system.
|
15
|
+
|
16
|
+
## Installation
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
# In your Gemfile
|
20
|
+
gem "sashite-pmn"
|
21
|
+
```
|
22
|
+
|
23
|
+
Or install manually:
|
24
|
+
|
25
|
+
```sh
|
26
|
+
gem install sashite-pmn
|
27
|
+
```
|
28
|
+
|
29
|
+
## Dependencies
|
30
|
+
|
31
|
+
PMN builds upon three foundational Sashité specifications:
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
gem "sashite-cell" # Multi-dimensional coordinate encoding
|
35
|
+
gem "sashite-hand" # Reserve location notation
|
36
|
+
gem "sashite-qpi" # Qualified Piece Identifier
|
37
|
+
```
|
38
|
+
|
39
|
+
## Usage
|
40
|
+
|
41
|
+
### Basic Operations
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
require "sashite/pmn"
|
45
|
+
|
46
|
+
# Parse PMN arrays into move objects
|
47
|
+
move = Sashite::Pmn.parse(["e2", "e4", "C:P"])
|
48
|
+
move.valid? # => true
|
49
|
+
move.actions # => [#<Sashite::Pmn::Action ...>]
|
50
|
+
move.to_a # => ["e2", "e4", "C:P"]
|
51
|
+
|
52
|
+
# Validate PMN arrays
|
53
|
+
Sashite::Pmn.valid?(["e2", "e4", "C:P"]) # => true
|
54
|
+
Sashite::Pmn.valid?(%w[e2 e4]) # => true (inferred piece)
|
55
|
+
Sashite::Pmn.valid?(["e2"]) # => false (incomplete)
|
56
|
+
|
57
|
+
# Create moves programmatically
|
58
|
+
move = Sashite::Pmn.from_actions([
|
59
|
+
Sashite::Pmn::Action.new("e2", "e4", "C:P")
|
60
|
+
])
|
61
|
+
```
|
62
|
+
|
63
|
+
### Action Decomposition
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
# Simple move with explicit piece
|
67
|
+
move = Sashite::Pmn.parse(["e2", "e4", "C:P"])
|
68
|
+
action = move.actions.first
|
69
|
+
action.source # => "e2"
|
70
|
+
action.destination # => "e4"
|
71
|
+
action.piece # => "C:P"
|
72
|
+
action.piece_specified? # => true
|
73
|
+
|
74
|
+
# Move with inferred piece
|
75
|
+
move = Sashite::Pmn.parse(%w[e2 e4])
|
76
|
+
action = move.actions.first
|
77
|
+
action.piece # => nil
|
78
|
+
action.piece_specified? # => false
|
79
|
+
action.inferred? # => true
|
80
|
+
|
81
|
+
# Pass moves (source == destination) are allowed
|
82
|
+
pass = Sashite::Pmn.parse(["e4", "e4", "C:P"])
|
83
|
+
pass.valid? # => true
|
84
|
+
|
85
|
+
# Reserve operations
|
86
|
+
drop = Sashite::Pmn.parse(["*", "e5", "S:P"]) # Drop from reserve
|
87
|
+
capture = Sashite::Pmn.parse(["e4", "*"]) # Capture to reserve (inferred piece)
|
88
|
+
```
|
89
|
+
|
90
|
+
### Complex Moves
|
91
|
+
|
92
|
+
```ruby
|
93
|
+
# Multi-action move (castling)
|
94
|
+
castling = Sashite::Pmn.parse([
|
95
|
+
"e1", "g1", "C:K",
|
96
|
+
"h1", "f1", "C:R"
|
97
|
+
])
|
98
|
+
castling.compound? # => true
|
99
|
+
castling.actions.size # => 2
|
100
|
+
|
101
|
+
# En passant (explicit + inferred variant)
|
102
|
+
en_passant = Sashite::Pmn.parse([
|
103
|
+
"e5", "f6", "C:P",
|
104
|
+
"f5", "*", "c:p"
|
105
|
+
])
|
106
|
+
Sashite::Pmn.parse(%w[e5 f6]).valid? # => true (context-dependent)
|
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
|
125
|
+
```
|
126
|
+
|
127
|
+
### Move Analysis
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
move = Sashite::Pmn.parse([
|
131
|
+
"e1", "g1", "C:K",
|
132
|
+
"h1", "f1", "C:R"
|
133
|
+
])
|
134
|
+
|
135
|
+
# Structure analysis
|
136
|
+
move.simple? # => false
|
137
|
+
move.compound? # => true
|
138
|
+
move.size # => 2
|
139
|
+
move.empty? # => false
|
140
|
+
|
141
|
+
# Drop/capture checks
|
142
|
+
move.has_drops? # => false
|
143
|
+
move.has_captures? # => false
|
144
|
+
move.board_moves.size # => 2
|
145
|
+
|
146
|
+
# Extract info
|
147
|
+
move.sources # => ["e1", "h1"]
|
148
|
+
move.destinations # => ["g1", "f1"]
|
149
|
+
move.pieces # => ["C:K", "C:R"]
|
150
|
+
move.has_inferred? # => false
|
151
|
+
```
|
152
|
+
|
153
|
+
### Error Handling
|
154
|
+
|
155
|
+
```ruby
|
156
|
+
# Invalid action built directly raises action-level errors
|
157
|
+
begin
|
158
|
+
Sashite::Pmn::Action.new("invalid", "e4", "C:P")
|
159
|
+
rescue Sashite::Pmn::InvalidLocationError => e
|
160
|
+
puts e.message
|
161
|
+
end
|
162
|
+
|
163
|
+
begin
|
164
|
+
Sashite::Pmn::Action.new("e2", "e4", "InvalidPiece")
|
165
|
+
rescue Sashite::Pmn::InvalidPieceError => e
|
166
|
+
puts e.message
|
167
|
+
end
|
168
|
+
|
169
|
+
# Parsing a move wraps action-level errors as InvalidMoveError
|
170
|
+
begin
|
171
|
+
Sashite::Pmn.parse(["e2"]) # Incomplete action
|
172
|
+
rescue Sashite::Pmn::InvalidMoveError => e
|
173
|
+
puts e.message # => "Invalid PMN array length: 1", etc.
|
174
|
+
end
|
175
|
+
```
|
176
|
+
|
177
|
+
## API Reference
|
178
|
+
|
179
|
+
### Main Module Methods
|
180
|
+
|
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.
|
184
|
+
* `Sashite::Pmn.valid_location?(location)` — Check if a location is valid (CELL or `"*"`).
|
185
|
+
* `Sashite::Pmn.valid_piece?(piece)` — Check if a piece is valid QPI.
|
186
|
+
|
187
|
+
### Move Class
|
188
|
+
|
189
|
+
#### Creation
|
190
|
+
|
191
|
+
* `Sashite::Pmn::Move.new(*elements)` — Create from PMN elements (variadic).
|
192
|
+
*Note*: `Move.new(["e2","e4","C:P"])` is **not** accepted; pass individual arguments.
|
193
|
+
* `Sashite::Pmn::Move.from_actions(actions)` — Create from `Action` objects.
|
194
|
+
|
195
|
+
#### Validation & Data
|
196
|
+
|
197
|
+
* `#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.
|
201
|
+
|
202
|
+
#### Structure & Queries
|
203
|
+
|
204
|
+
* `#size` / `#length` — Number of actions.
|
205
|
+
* `#empty?` — No actions?
|
206
|
+
* `#simple?` — Exactly one action?
|
207
|
+
* `#compound?` — Multiple actions?
|
208
|
+
* `#first_action` / `#last_action` — Convenience accessors.
|
209
|
+
* `#has_drops?` / `#has_captures?` — Presence of drops/captures.
|
210
|
+
* `#board_moves` — Actions that are board-to-board.
|
211
|
+
* `#sources` / `#destinations` / `#pieces` — Unique lists.
|
212
|
+
* `#has_inferred?` — Any action with inferred piece?
|
213
|
+
|
214
|
+
### Action Class
|
215
|
+
|
216
|
+
#### Creation
|
217
|
+
|
218
|
+
* `Sashite::Pmn::Action.new(source, destination, piece = nil)`
|
219
|
+
|
220
|
+
#### Data & Conversion
|
221
|
+
|
222
|
+
* `#source`, `#destination`, `#piece`
|
223
|
+
* `#to_a` — `["src", "dst"]` or `["src", "dst", "piece"]`
|
224
|
+
* `#to_h` — `{ source:, destination:, piece: }` (piece omitted if `nil`)
|
225
|
+
|
226
|
+
#### Predicates
|
227
|
+
|
228
|
+
* `#inferred?`, `#piece_specified?`, `#piece_valid?`
|
229
|
+
* `#from_reserve?`, `#to_reserve?`
|
230
|
+
* `#reserve_to_board?` (drop), `#board_to_reserve?` (capture), `#board_to_board?`
|
231
|
+
* `#drop?` (alias), `#capture?` (alias), `#board_move?`
|
232
|
+
* `#valid?`
|
233
|
+
|
234
|
+
### Exceptions
|
235
|
+
|
236
|
+
* `Sashite::Pmn::Error` — Base error class
|
237
|
+
* `Sashite::Pmn::InvalidMoveError` — Invalid PMN sequence / parsing failure
|
238
|
+
* `Sashite::Pmn::InvalidActionError` — Invalid atomic action
|
239
|
+
* `Sashite::Pmn::InvalidLocationError` — Invalid location (not CELL or HAND)
|
240
|
+
* `Sashite::Pmn::InvalidPieceError` — Invalid piece (not QPI format)
|
241
|
+
|
242
|
+
## Format Specification (Summary)
|
243
|
+
|
244
|
+
### Structure
|
245
|
+
|
246
|
+
PMN moves are flat **arrays** containing action sequences:
|
247
|
+
|
248
|
+
```
|
249
|
+
[<element-1>, <element-2>, <element-3>, <element-4>, <element-5>, <element-6>, ...]
|
250
|
+
```
|
251
|
+
|
252
|
+
### Action Format
|
253
|
+
|
254
|
+
Each action consists of 2 or 3 consecutive elements:
|
255
|
+
|
256
|
+
```
|
257
|
+
[<source>, <destination>, <piece>?]
|
258
|
+
```
|
259
|
+
|
260
|
+
* **Source**: CELL coordinate or `"*"` (reserve)
|
261
|
+
* **Destination**: CELL coordinate or `"*"` (reserve)
|
262
|
+
* **Piece**: QPI string (optional; may be inferred)
|
263
|
+
|
264
|
+
### Array Length Rules
|
265
|
+
|
266
|
+
* Minimum: 2 elements (one action with inferred piece)
|
267
|
+
* Valid lengths: multiple of 3, **or** multiple of 3 plus 2
|
268
|
+
|
269
|
+
### Pass & Same-Location Actions
|
270
|
+
|
271
|
+
Actions where **source == destination** are allowed, enabling:
|
272
|
+
|
273
|
+
* Pass moves (turn-only or rule-driven)
|
274
|
+
* In-place transformations (e.g., promotions specified with QPI)
|
275
|
+
|
276
|
+
## Game Examples
|
277
|
+
|
278
|
+
### Western Chess
|
279
|
+
|
280
|
+
```ruby
|
281
|
+
# Pawn move
|
282
|
+
pawn_move = Sashite::Pmn.parse(["e2", "e4", "C:P"])
|
283
|
+
|
284
|
+
# Castling kingside
|
285
|
+
castling = Sashite::Pmn.parse([
|
286
|
+
"e1", "g1", "C:K",
|
287
|
+
"h1", "f1", "C:R"
|
288
|
+
])
|
289
|
+
|
290
|
+
# En passant
|
291
|
+
en_passant = Sashite::Pmn.parse([
|
292
|
+
"e5", "f6", "C:P",
|
293
|
+
"f5", "*", "c:p"
|
294
|
+
])
|
295
|
+
|
296
|
+
# Promotion
|
297
|
+
promotion = Sashite::Pmn.parse(["e7", "e8", "C:Q"])
|
298
|
+
```
|
299
|
+
|
300
|
+
### Japanese Shōgi
|
301
|
+
|
302
|
+
```ruby
|
303
|
+
# Drop piece from hand
|
304
|
+
drop = Sashite::Pmn.parse(["*", "e5", "S:P"])
|
305
|
+
|
306
|
+
# Capture and convert
|
307
|
+
capture = Sashite::Pmn.parse([
|
308
|
+
"a1", "*", "S:L",
|
309
|
+
"b2", "a1", "S:S"
|
310
|
+
])
|
311
|
+
|
312
|
+
# Promotion
|
313
|
+
promotion = Sashite::Pmn.parse(["h8", "i8", "S:+S"])
|
314
|
+
```
|
315
|
+
|
316
|
+
### Chinese Xiangqi
|
317
|
+
|
318
|
+
```ruby
|
319
|
+
# General move
|
320
|
+
general_move = Sashite::Pmn.parse(["e1", "e2", "X:G"])
|
321
|
+
|
322
|
+
# Cannon capture (jumping)
|
323
|
+
cannon_capture = Sashite::Pmn.parse([
|
324
|
+
"b3", "*", "x:s",
|
325
|
+
"b1", "b9", "X:C"
|
326
|
+
])
|
327
|
+
```
|
328
|
+
|
329
|
+
## Advanced Usage
|
330
|
+
|
331
|
+
### Move Composition
|
332
|
+
|
333
|
+
```ruby
|
334
|
+
actions = []
|
335
|
+
actions << Sashite::Pmn::Action.new("e2", "e4", "C:P")
|
336
|
+
actions << Sashite::Pmn::Action.new("d7", "d5", "c:p")
|
337
|
+
|
338
|
+
move = Sashite::Pmn.from_actions(actions)
|
339
|
+
move.to_a # => ["e2", "e4", "C:P", "d7", "d5", "c:p"]
|
340
|
+
```
|
341
|
+
|
342
|
+
### Integration with Game Engines
|
343
|
+
|
344
|
+
```ruby
|
345
|
+
class GameEngine
|
346
|
+
def execute_move(pmn_array)
|
347
|
+
move = Sashite::Pmn.parse(pmn_array)
|
348
|
+
|
349
|
+
move.actions.each do |action|
|
350
|
+
if action.from_reserve?
|
351
|
+
place_piece(action.destination, action.piece)
|
352
|
+
elsif action.to_reserve?
|
353
|
+
capture_piece(action.source)
|
354
|
+
else
|
355
|
+
move_piece(action.source, action.destination, action.piece)
|
356
|
+
end
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
# ...
|
361
|
+
end
|
362
|
+
```
|
363
|
+
|
364
|
+
## Design Properties
|
365
|
+
|
366
|
+
* **Rule-agnostic**: Independent of specific game mechanics
|
367
|
+
* **Mechanical decomposition**: Breaks complex moves into atomic actions
|
368
|
+
* **Array-based**: Simple, interoperable structure
|
369
|
+
* **Sequential execution**: Actions execute in array order
|
370
|
+
* **Piece inference**: Optional piece specification when context is clear
|
371
|
+
* **Universal applicability**: Works across board game systems
|
372
|
+
* **Functional design**: Immutable data structures
|
373
|
+
* **Dependency integration**: CELL, HAND, and QPI specs
|
374
|
+
|
375
|
+
## Mechanical Semantics (Recap)
|
376
|
+
|
377
|
+
1. **Source state change**:
|
378
|
+
|
379
|
+
* CELL → becomes empty
|
380
|
+
* HAND `"*"` → remove piece from reserve
|
381
|
+
|
382
|
+
2. **Destination state change**:
|
383
|
+
|
384
|
+
* CELL → contains final piece
|
385
|
+
* HAND `"*"` → add piece to reserve
|
386
|
+
|
387
|
+
3. **Piece transformation**: Final state (specified or inferred)
|
388
|
+
|
389
|
+
4. **Atomic commitment**: Each action applies atomically
|
390
|
+
|
391
|
+
## License
|
392
|
+
|
393
|
+
Available as open source under the [MIT License](https://github.com/sashite/pmn.rb/raw/main/LICENSE.md).
|
394
|
+
|
395
|
+
## Contributing
|
396
|
+
|
397
|
+
Bug reports and pull requests are welcome on GitHub at [https://github.com/sashite/pmn.rb](https://github.com/sashite/pmn.rb).
|
398
|
+
|
399
|
+
## See Also
|
400
|
+
|
401
|
+
* [PMN Specification v1.0.0](https://sashite.dev/specs/pmn/1.0.0/)
|
402
|
+
* [PMN Examples](https://sashite.dev/specs/pmn/1.0.0/examples/)
|
403
|
+
* [CELL Specification](https://sashite.dev/specs/cell/)
|
404
|
+
* [HAND Specification](https://sashite.dev/specs/hand/)
|
405
|
+
* [QPI Specification](https://sashite.dev/specs/qpi/)
|
406
|
+
|
407
|
+
## About
|
408
|
+
|
409
|
+
Maintained by [Sashité](https://sashite.com/) — promoting chess variants and sharing the beauty of board game cultures.
|
@@ -0,0 +1,190 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "sashite/cell"
|
4
|
+
require "sashite/hand"
|
5
|
+
require "sashite/qpi"
|
6
|
+
require_relative "error"
|
7
|
+
|
8
|
+
module Sashite
|
9
|
+
module Pmn
|
10
|
+
# Represents an atomic action within a PMN move.
|
11
|
+
#
|
12
|
+
# Each action encodes a single transformation:
|
13
|
+
# [source, destination] # inferred piece
|
14
|
+
# [source, destination, piece] # explicit QPI piece
|
15
|
+
#
|
16
|
+
# Locations are either CELL coordinates or HAND ("*").
|
17
|
+
# Actions where source == destination are allowed (pass / in-place).
|
18
|
+
class Action
|
19
|
+
# @return [String] the source location (CELL or "*")
|
20
|
+
attr_reader :source
|
21
|
+
|
22
|
+
# @return [String] the destination location (CELL or "*")
|
23
|
+
attr_reader :destination
|
24
|
+
|
25
|
+
# @return [String, nil] the piece in QPI format, or nil if inferred
|
26
|
+
attr_reader :piece
|
27
|
+
|
28
|
+
# Create an immutable action.
|
29
|
+
#
|
30
|
+
# @param source [String] CELL or "*"
|
31
|
+
# @param destination [String] CELL or "*"
|
32
|
+
# @param piece [String, nil] QPI string (optional)
|
33
|
+
# @raise [InvalidLocationError] if source/destination are invalid
|
34
|
+
# @raise [InvalidPieceError] if piece is provided but invalid
|
35
|
+
def initialize(source, destination, piece = nil)
|
36
|
+
validate_source!(source)
|
37
|
+
validate_destination!(destination)
|
38
|
+
validate_piece!(piece) if piece
|
39
|
+
|
40
|
+
@source = source.freeze
|
41
|
+
@destination = destination.freeze
|
42
|
+
@piece = piece&.freeze
|
43
|
+
|
44
|
+
freeze
|
45
|
+
end
|
46
|
+
|
47
|
+
# ---------- Predicates -------------------------------------------------
|
48
|
+
|
49
|
+
# @return [Boolean] true if piece is not explicitly specified
|
50
|
+
def inferred?
|
51
|
+
piece.nil?
|
52
|
+
end
|
53
|
+
|
54
|
+
# @return [Boolean] true if a piece string is explicitly specified
|
55
|
+
def piece_specified?
|
56
|
+
!piece.nil?
|
57
|
+
end
|
58
|
+
|
59
|
+
# @return [Boolean] true if a specified piece is valid QPI (false if none)
|
60
|
+
def piece_valid?
|
61
|
+
return false if piece.nil?
|
62
|
+
|
63
|
+
Qpi.valid?(piece)
|
64
|
+
end
|
65
|
+
|
66
|
+
# @return [Boolean] true if source is HAND ("*")
|
67
|
+
def from_reserve?
|
68
|
+
Hand.reserve?(source)
|
69
|
+
end
|
70
|
+
|
71
|
+
# @return [Boolean] true if destination is HAND ("*")
|
72
|
+
def to_reserve?
|
73
|
+
Hand.reserve?(destination)
|
74
|
+
end
|
75
|
+
|
76
|
+
# @return [Boolean] true if both endpoints are board locations
|
77
|
+
def board_to_board?
|
78
|
+
Cell.valid?(source) && Cell.valid?(destination)
|
79
|
+
end
|
80
|
+
|
81
|
+
# @return [Boolean] true if the action places from reserve to board
|
82
|
+
def drop?
|
83
|
+
from_reserve? && Cell.valid?(destination)
|
84
|
+
end
|
85
|
+
|
86
|
+
# @return [Boolean] true if the action takes from board to reserve
|
87
|
+
def capture?
|
88
|
+
Cell.valid?(source) && to_reserve?
|
89
|
+
end
|
90
|
+
|
91
|
+
# @return [Boolean] true when neither drop nor capture
|
92
|
+
def board_move?
|
93
|
+
!drop? && !capture?
|
94
|
+
end
|
95
|
+
|
96
|
+
# ---------- Conversions -----------------------------------------------
|
97
|
+
|
98
|
+
# @return [Array<String>] 2 elems if inferred, 3 if explicit
|
99
|
+
def to_a
|
100
|
+
inferred? ? [source, destination] : [source, destination, piece]
|
101
|
+
end
|
102
|
+
|
103
|
+
# @return [Hash] { source:, destination:, piece: } (piece omitted if nil)
|
104
|
+
def to_h
|
105
|
+
{ source: source, destination: destination, piece: piece }.compact
|
106
|
+
end
|
107
|
+
|
108
|
+
# ---------- Validation & Equality -------------------------------------
|
109
|
+
|
110
|
+
# @return [Boolean] true if all components are valid
|
111
|
+
def valid?
|
112
|
+
valid_location?(source) &&
|
113
|
+
valid_location?(destination) &&
|
114
|
+
(piece.nil? || Qpi.valid?(piece))
|
115
|
+
end
|
116
|
+
|
117
|
+
# @param other [Object]
|
118
|
+
# @return [Boolean] equality by {source, destination, piece}
|
119
|
+
def ==(other)
|
120
|
+
return false unless other.is_a?(Action)
|
121
|
+
|
122
|
+
source == other.source &&
|
123
|
+
destination == other.destination &&
|
124
|
+
piece == other.piece
|
125
|
+
end
|
126
|
+
alias eql? ==
|
127
|
+
|
128
|
+
# @return [Integer]
|
129
|
+
def hash
|
130
|
+
[source, destination, piece].hash
|
131
|
+
end
|
132
|
+
|
133
|
+
# @return [String]
|
134
|
+
def inspect
|
135
|
+
attrs = ["source=#{source.inspect}", "destination=#{destination.inspect}"]
|
136
|
+
attrs << "piece=#{piece.inspect}" if piece
|
137
|
+
"#<#{self.class.name} #{attrs.join(' ')}>"
|
138
|
+
end
|
139
|
+
|
140
|
+
# ---------- Factories --------------------------------------------------
|
141
|
+
|
142
|
+
# Build from a hash with keys :source, :destination, optional :piece.
|
143
|
+
#
|
144
|
+
# @param hash [Hash]
|
145
|
+
# @return [Action]
|
146
|
+
# @raise [ArgumentError] if required keys are missing
|
147
|
+
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)
|
150
|
+
|
151
|
+
new(hash[:source], hash[:destination], hash[:piece])
|
152
|
+
end
|
153
|
+
|
154
|
+
private
|
155
|
+
|
156
|
+
# ---------- Internal validation helpers -------------------------------
|
157
|
+
|
158
|
+
# @param src [String]
|
159
|
+
# @raise [InvalidLocationError]
|
160
|
+
def validate_source!(src)
|
161
|
+
return if valid_location?(src)
|
162
|
+
|
163
|
+
raise InvalidLocationError, "Invalid source location: #{src.inspect}"
|
164
|
+
end
|
165
|
+
|
166
|
+
# @param dst [String]
|
167
|
+
# @raise [InvalidLocationError]
|
168
|
+
def validate_destination!(dst)
|
169
|
+
return if valid_location?(dst)
|
170
|
+
|
171
|
+
raise InvalidLocationError, "Invalid destination location: #{dst.inspect}"
|
172
|
+
end
|
173
|
+
|
174
|
+
# @param qpi [String, nil]
|
175
|
+
# @raise [InvalidPieceError]
|
176
|
+
def validate_piece!(qpi)
|
177
|
+
return if qpi.nil?
|
178
|
+
return if Qpi.valid?(qpi)
|
179
|
+
|
180
|
+
raise InvalidPieceError, "Invalid piece QPI format: #{qpi.inspect}"
|
181
|
+
end
|
182
|
+
|
183
|
+
# @param location [String]
|
184
|
+
# @return [Boolean] true if CELL or HAND ("*")
|
185
|
+
def valid_location?(location)
|
186
|
+
Cell.valid?(location) || Hand.reserve?(location)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sashite
|
4
|
+
module Pmn
|
5
|
+
# Base class for all PMN-related errors
|
6
|
+
class Error < StandardError; end
|
7
|
+
|
8
|
+
# Raised when a PMN move (sequence) is malformed or invalid
|
9
|
+
class InvalidMoveError < Error; end
|
10
|
+
|
11
|
+
# Raised when an atomic action is malformed or fails validation
|
12
|
+
class InvalidActionError < Error; end
|
13
|
+
|
14
|
+
# Raised when a location is neither a valid CELL coordinate nor HAND ("*")
|
15
|
+
class InvalidLocationError < InvalidActionError; end
|
16
|
+
|
17
|
+
# Raised when a piece identifier is not valid QPI
|
18
|
+
class InvalidPieceError < InvalidActionError; end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,226 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "action"
|
4
|
+
require_relative "error"
|
5
|
+
|
6
|
+
module Sashite
|
7
|
+
module Pmn
|
8
|
+
# Represents a complete move in PMN notation.
|
9
|
+
#
|
10
|
+
# A Move is a sequence of one or more atomic actions described by a flat list
|
11
|
+
# of elements. Every 2 or 3 consecutive elements form an action:
|
12
|
+
# [source, destination] # inferred piece
|
13
|
+
# [source, destination, piece] # explicit QPI piece
|
14
|
+
#
|
15
|
+
# Valid lengths: multiple of 3 OR multiple of 3 + 2 (minimum 2).
|
16
|
+
class Move
|
17
|
+
# @return [Array<Action>] ordered sequence of actions
|
18
|
+
attr_reader :actions
|
19
|
+
|
20
|
+
# @return [Array<String>] original PMN elements (frozen)
|
21
|
+
attr_reader :pmn_array
|
22
|
+
|
23
|
+
# Create a Move from PMN elements (variadic only).
|
24
|
+
#
|
25
|
+
# @param pmn_elements [Array<String>] passed as individual args
|
26
|
+
# @raise [InvalidMoveError] if called with a single Array or if invalid
|
27
|
+
#
|
28
|
+
# @example
|
29
|
+
# Move.new("e2","e4","C:P")
|
30
|
+
# Move.new("e2","e4")
|
31
|
+
def initialize(*pmn_elements)
|
32
|
+
# single-array form is intentionally not supported (entropy reduction)
|
33
|
+
if pmn_elements.size == 1 && pmn_elements.first.is_a?(Array)
|
34
|
+
raise InvalidMoveError,
|
35
|
+
'PMN must be passed as individual arguments, e.g. Move.new("e2","e4","C:P")'
|
36
|
+
end
|
37
|
+
|
38
|
+
validate_array!(pmn_elements)
|
39
|
+
@pmn_array = pmn_elements.dup.freeze
|
40
|
+
@actions = parse_actions(@pmn_array).freeze
|
41
|
+
validate_actions!
|
42
|
+
freeze
|
43
|
+
end
|
44
|
+
|
45
|
+
# @return [Boolean] true if PMN length is valid and all actions are valid
|
46
|
+
def valid?
|
47
|
+
valid_length? && actions.all?(&:valid?)
|
48
|
+
rescue StandardError
|
49
|
+
false
|
50
|
+
end
|
51
|
+
|
52
|
+
# Shape / structure -----------------------------------------------------
|
53
|
+
|
54
|
+
# @return [Boolean] exactly one action?
|
55
|
+
def simple?
|
56
|
+
actions.size == 1
|
57
|
+
end
|
58
|
+
|
59
|
+
# @return [Boolean] multiple actions?
|
60
|
+
def compound?
|
61
|
+
actions.size > 1
|
62
|
+
end
|
63
|
+
|
64
|
+
# @return [Action, nil]
|
65
|
+
def first_action
|
66
|
+
actions.first
|
67
|
+
end
|
68
|
+
|
69
|
+
# @return [Action, nil]
|
70
|
+
def last_action
|
71
|
+
actions.last
|
72
|
+
end
|
73
|
+
|
74
|
+
# @return [Integer] number of actions
|
75
|
+
def size
|
76
|
+
actions.size
|
77
|
+
end
|
78
|
+
alias length size
|
79
|
+
|
80
|
+
# @return [Boolean] true if no actions
|
81
|
+
def empty?
|
82
|
+
actions.empty?
|
83
|
+
end
|
84
|
+
|
85
|
+
# Content helpers -------------------------------------------------------
|
86
|
+
|
87
|
+
# @return [Boolean] any drop?
|
88
|
+
def has_drops?
|
89
|
+
actions.any?(&:drop?)
|
90
|
+
end
|
91
|
+
|
92
|
+
# @return [Boolean] any capture?
|
93
|
+
def has_captures?
|
94
|
+
actions.any?(&:capture?)
|
95
|
+
end
|
96
|
+
|
97
|
+
# @return [Array<Action>] only board-to-board actions
|
98
|
+
def board_moves
|
99
|
+
actions.select(&:board_to_board?)
|
100
|
+
end
|
101
|
+
|
102
|
+
# @return [Array<String>] unique sources
|
103
|
+
def sources
|
104
|
+
actions.map(&:source).uniq
|
105
|
+
end
|
106
|
+
|
107
|
+
# @return [Array<String>] unique destinations
|
108
|
+
def destinations
|
109
|
+
actions.map(&:destination).uniq
|
110
|
+
end
|
111
|
+
|
112
|
+
# @return [Array<String>] unique specified pieces (excludes inferred)
|
113
|
+
def pieces
|
114
|
+
actions.filter_map(&:piece).uniq
|
115
|
+
end
|
116
|
+
|
117
|
+
# @return [Boolean] true if any action has inferred piece
|
118
|
+
def has_inferred?
|
119
|
+
actions.any?(&:inferred?)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Conversion ------------------------------------------------------------
|
123
|
+
|
124
|
+
# @return [Array<String>] copy of original PMN elements
|
125
|
+
def to_a
|
126
|
+
pmn_array.dup
|
127
|
+
end
|
128
|
+
|
129
|
+
# Equality / hashing / debug -------------------------------------------
|
130
|
+
|
131
|
+
# @param other [Object]
|
132
|
+
# @return [Boolean] equality by original PMN elements
|
133
|
+
def ==(other)
|
134
|
+
return false unless other.is_a?(Move)
|
135
|
+
|
136
|
+
pmn_array == other.pmn_array
|
137
|
+
end
|
138
|
+
alias eql? ==
|
139
|
+
|
140
|
+
# @return [Integer]
|
141
|
+
def hash
|
142
|
+
pmn_array.hash
|
143
|
+
end
|
144
|
+
|
145
|
+
# @return [String]
|
146
|
+
def inspect
|
147
|
+
"#<#{self.class.name} actions=#{actions.size} pmn=#{pmn_array.inspect}>"
|
148
|
+
end
|
149
|
+
|
150
|
+
# Functional composition -----------------------------------------------
|
151
|
+
|
152
|
+
# @param actions_to_add [Array<Action>] actions to append
|
153
|
+
# @return [Move] new Move with appended actions
|
154
|
+
def with_actions(actions_to_add)
|
155
|
+
combined = pmn_array + actions_to_add.flat_map(&:to_a)
|
156
|
+
self.class.new(*combined)
|
157
|
+
end
|
158
|
+
|
159
|
+
# Build a move from Action objects.
|
160
|
+
#
|
161
|
+
# @param actions [Array<Action>]
|
162
|
+
# @return [Move]
|
163
|
+
def self.from_actions(actions)
|
164
|
+
pmn = actions.flat_map(&:to_a)
|
165
|
+
new(*pmn)
|
166
|
+
end
|
167
|
+
|
168
|
+
private
|
169
|
+
|
170
|
+
# Validation ------------------------------------------------------------
|
171
|
+
|
172
|
+
def validate_array!(array)
|
173
|
+
raise InvalidMoveError, "PMN must be an array, got #{array.class}" unless array.is_a?(Array)
|
174
|
+
raise InvalidMoveError, "PMN array cannot be empty" if array.empty?
|
175
|
+
|
176
|
+
raise InvalidMoveError, "All PMN elements must be strings" unless array.all?(String)
|
177
|
+
|
178
|
+
return if valid_length?(array)
|
179
|
+
|
180
|
+
raise InvalidMoveError, "Invalid PMN array length: #{array.size}"
|
181
|
+
end
|
182
|
+
|
183
|
+
# Valid lengths: (size % 3 == 0) OR (size % 3 == 2), minimum 2.
|
184
|
+
def valid_length?(array = pmn_array)
|
185
|
+
return false if array.size < 2
|
186
|
+
|
187
|
+
r = array.size % 3
|
188
|
+
[0, 2].include?(r)
|
189
|
+
end
|
190
|
+
|
191
|
+
# Parsing ---------------------------------------------------------------
|
192
|
+
|
193
|
+
def parse_actions(array)
|
194
|
+
actions = []
|
195
|
+
index = 0
|
196
|
+
|
197
|
+
while index < array.size
|
198
|
+
remaining = array.size - index
|
199
|
+
|
200
|
+
if remaining == 2
|
201
|
+
actions << Action.new(array[index], array[index + 1])
|
202
|
+
index += 2
|
203
|
+
elsif remaining >= 3
|
204
|
+
actions << Action.new(array[index], array[index + 1], array[index + 2])
|
205
|
+
index += 3
|
206
|
+
else
|
207
|
+
raise InvalidMoveError, "Invalid action group at index #{index}"
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
actions
|
212
|
+
rescue InvalidActionError => e
|
213
|
+
# Normalize action-level errors as move-level errors during parsing
|
214
|
+
raise InvalidMoveError, "Invalid action while parsing move at index #{index}: #{e.message}"
|
215
|
+
end
|
216
|
+
|
217
|
+
def validate_actions!
|
218
|
+
actions.each_with_index do |action, i|
|
219
|
+
next if action.valid?
|
220
|
+
|
221
|
+
raise InvalidMoveError, "Invalid action at position #{i}: #{action.inspect}"
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
data/lib/sashite/pmn.rb
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "sashite/cell"
|
4
|
+
require "sashite/hand"
|
5
|
+
require "sashite/qpi"
|
6
|
+
|
7
|
+
require_relative "pmn/action"
|
8
|
+
require_relative "pmn/move"
|
9
|
+
require_relative "pmn/error"
|
10
|
+
|
11
|
+
module Sashite
|
12
|
+
# PMN (Portable Move Notation) implementation for Ruby
|
13
|
+
#
|
14
|
+
# PMN is an array-based, rule-agnostic format that decomposes a move into a
|
15
|
+
# sequence of atomic actions. Each action is 2 or 3 elements:
|
16
|
+
# [source, destination] # inferred piece
|
17
|
+
# [source, destination, piece] # explicit piece (QPI)
|
18
|
+
#
|
19
|
+
# Valid PMN arrays have length:
|
20
|
+
# - multiple of 3, or
|
21
|
+
# - multiple of 3 + 2
|
22
|
+
# with a minimum of 2.
|
23
|
+
#
|
24
|
+
# See specs: https://sashite.dev/specs/pmn/1.0.0/
|
25
|
+
module Pmn
|
26
|
+
# Parse a PMN array into a Move object.
|
27
|
+
#
|
28
|
+
# @param pmn_array [Array<String>] flat array of PMN elements
|
29
|
+
# @return [Sashite::Pmn::Move]
|
30
|
+
# @raise [Sashite::Pmn::InvalidMoveError] if the array or any action is invalid
|
31
|
+
#
|
32
|
+
# @example
|
33
|
+
# Sashite::Pmn.parse(["e2","e4","C:P"]).actions.size # => 1
|
34
|
+
def self.parse(pmn_array)
|
35
|
+
raise InvalidMoveError, "PMN must be an array, got #{pmn_array.class}" unless pmn_array.is_a?(Array)
|
36
|
+
|
37
|
+
Move.new(*pmn_array)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Check if an array is valid PMN notation (non-raising).
|
41
|
+
#
|
42
|
+
# @param pmn_array [Array]
|
43
|
+
# @return [Boolean] true if valid, false otherwise
|
44
|
+
#
|
45
|
+
# @example
|
46
|
+
# Sashite::Pmn.valid?(["e2","e4","C:P"]) # => true
|
47
|
+
# Sashite::Pmn.valid?(["e2"]) # => false
|
48
|
+
def self.valid?(pmn_array)
|
49
|
+
return false unless pmn_array.is_a?(Array)
|
50
|
+
return false if pmn_array.empty?
|
51
|
+
|
52
|
+
move = Move.new(*pmn_array)
|
53
|
+
move.valid?
|
54
|
+
rescue Error
|
55
|
+
false
|
56
|
+
end
|
57
|
+
|
58
|
+
# Create a Move from Action objects.
|
59
|
+
#
|
60
|
+
# @param actions [Array<Sashite::Pmn::Action>]
|
61
|
+
# @return [Sashite::Pmn::Move]
|
62
|
+
# @raise [ArgumentError] if actions is not an Array
|
63
|
+
#
|
64
|
+
# @example
|
65
|
+
# a1 = Sashite::Pmn::Action.new("e2","e4","C:P")
|
66
|
+
# a2 = Sashite::Pmn::Action.new("d7","d5","c:p")
|
67
|
+
# move = Sashite::Pmn.from_actions([a1,a2])
|
68
|
+
def self.from_actions(actions)
|
69
|
+
raise ArgumentError, "Actions must be an array" unless actions.is_a?(Array)
|
70
|
+
|
71
|
+
pmn_array = actions.flat_map(&:to_a)
|
72
|
+
Move.new(*pmn_array)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Validate a location string (CELL or HAND "*").
|
76
|
+
#
|
77
|
+
# @param location [String]
|
78
|
+
# @return [Boolean]
|
79
|
+
#
|
80
|
+
# @api public
|
81
|
+
def self.valid_location?(location)
|
82
|
+
return false unless location.is_a?(String)
|
83
|
+
|
84
|
+
Cell.valid?(location) || Hand.reserve?(location)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Validate a QPI piece string.
|
88
|
+
#
|
89
|
+
# @param piece [String]
|
90
|
+
# @return [Boolean]
|
91
|
+
#
|
92
|
+
# @api public
|
93
|
+
def self.valid_piece?(piece)
|
94
|
+
return false unless piece.is_a?(String)
|
95
|
+
|
96
|
+
Qpi.valid?(piece)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
data/lib/sashite-pmn.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "sashite/pmn"
|
4
|
+
|
5
|
+
# Sashité namespace for board game notation libraries
|
6
|
+
#
|
7
|
+
# Sashité provides a collection of libraries for representing and manipulating
|
8
|
+
# board game concepts according to the Sashité Protocol specifications.
|
9
|
+
#
|
10
|
+
# @see https://sashite.dev/protocol/ Sashité Protocol
|
11
|
+
# @see https://sashite.dev/specs/ Sashité Specifications
|
12
|
+
# @author Sashité
|
13
|
+
module Sashite
|
14
|
+
end
|
metadata
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sashite-pmn
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Cyril Kato
|
8
|
+
bindir: bin
|
9
|
+
cert_chain: []
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: sashite-cell
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - "~>"
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '2.0'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - "~>"
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '2.0'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: sashite-hand
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - "~>"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '1.0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '1.0'
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: sashite-qpi
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '1.0'
|
47
|
+
type: :runtime
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '1.0'
|
54
|
+
description: |
|
55
|
+
PMN (Portable Move Notation) provides a rule-agnostic, JSON-based format for describing
|
56
|
+
the mechanical decomposition of moves in abstract strategy board games. This gem implements
|
57
|
+
the PMN Specification v1.0.0 with a functional Ruby interface, breaking down complex movements
|
58
|
+
into sequences of atomic actions while remaining completely independent of specific game rules.
|
59
|
+
PMN reveals the underlying mechanics of any board game move through sequential action
|
60
|
+
decomposition, supporting both explicit and inferred piece specifications. Built on CELL
|
61
|
+
(coordinate encoding), HAND (reserve notation), and QPI (piece identification) specifications,
|
62
|
+
it enables universal move representation across chess variants, shōgi, xiangqi, and any
|
63
|
+
abstract strategy game. Perfect for game engines, move validators, and board game analysis tools.
|
64
|
+
email: contact@cyril.email
|
65
|
+
executables: []
|
66
|
+
extensions: []
|
67
|
+
extra_rdoc_files: []
|
68
|
+
files:
|
69
|
+
- LICENSE.md
|
70
|
+
- README.md
|
71
|
+
- lib/sashite-pmn.rb
|
72
|
+
- lib/sashite/pmn.rb
|
73
|
+
- lib/sashite/pmn/action.rb
|
74
|
+
- lib/sashite/pmn/error.rb
|
75
|
+
- lib/sashite/pmn/move.rb
|
76
|
+
homepage: https://github.com/sashite/pmn.rb
|
77
|
+
licenses:
|
78
|
+
- MIT
|
79
|
+
metadata:
|
80
|
+
bug_tracker_uri: https://github.com/sashite/pmn.rb/issues
|
81
|
+
documentation_uri: https://rubydoc.info/github/sashite/pmn.rb/main
|
82
|
+
homepage_uri: https://github.com/sashite/pmn.rb
|
83
|
+
source_code_uri: https://github.com/sashite/pmn.rb
|
84
|
+
specification_uri: https://sashite.dev/specs/pmn/1.0.0/
|
85
|
+
wiki_uri: https://sashite.dev/specs/pmn/1.0.0/examples/
|
86
|
+
funding_uri: https://github.com/sponsors/sashite
|
87
|
+
rubygems_mfa_required: 'true'
|
88
|
+
rdoc_options: []
|
89
|
+
require_paths:
|
90
|
+
- lib
|
91
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: 3.2.0
|
96
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
97
|
+
requirements:
|
98
|
+
- - ">="
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
version: '0'
|
101
|
+
requirements: []
|
102
|
+
rubygems_version: 3.7.1
|
103
|
+
specification_version: 4
|
104
|
+
summary: PMN (Portable Move Notation) implementation for Ruby with functional decomposition
|
105
|
+
test_files: []
|