portable_move_notation 2.1.0 → 2.1.1
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 +63 -212
- data/lib/portable_move_notation/action.rb +93 -79
- data/lib/portable_move_notation/move.rb +113 -76
- metadata +8 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f936c1b2bb3ed48727315af5aba93953ca053b535b043c95dbcb12a061d82c50
|
4
|
+
data.tar.gz: 56e2bc34c31919f0853f2f97709f576ed49453ef95cc410a8273c60bb76d1250
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3e297be0fab49c4b4a62ee8b57158a1c325b66c32084b54f9aad6640495ae5432d104f852d3ab16f7bce508c242096772e57957f04766127c961c71eedbbaedd
|
7
|
+
data.tar.gz: ec03950b1e5884085491828dc79199958fa76f950c7c79358251a566862de80e8f9777a3c241a6f7b1a782adbb708e818f43d0f1049e51bc1e68617f13d331f1
|
data/README.md
CHANGED
@@ -1,279 +1,130 @@
|
|
1
1
|
# Pmn.rb
|
2
2
|
|
3
|
-
[](https://rubygems.org/gems/portable_move_notation)
|
4
|
+
[](https://github.com/sashite/pmn.rb/tags)
|
5
|
+
[](https://github.com/sashite/pmn.rb/actions/workflows/main.yml)
|
6
|
+
[](https://codecov.io/gh/sashite/pmn.rb)
|
7
|
+
[](https://rubydoc.info/github/sashite/pmn.rb/main)
|
8
|
+
[](https://github.com/sashite/pmn.rb/raw/main/LICENSE.md)
|
7
9
|
|
8
|
-
>
|
10
|
+
> Parse, validate and emit [PMN v1.0.0](https://sashite.dev/documents/pmn/1.0.0/) — the rule‑agnostic *Portable Move Notation* — in pure Ruby.
|
9
11
|
|
10
|
-
|
12
|
+
---
|
11
13
|
|
12
|
-
|
14
|
+
## Why PMN?
|
13
15
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
- Validating PMN data according to the specification
|
16
|
+
PMN expresses **state‑changing actions** without embedding game rules. Whether you are writing a Chess engine, a Shogi server, or a hybrid variant, PMN gives you a deterministic, game‑neutral core that travels well across languages and databases.
|
17
|
+
|
18
|
+
---
|
18
19
|
|
19
20
|
## Installation
|
20
21
|
|
21
|
-
Add
|
22
|
+
Add to your **Gemfile**:
|
22
23
|
|
23
24
|
```ruby
|
25
|
+
# Gemfile
|
24
26
|
gem "portable_move_notation"
|
25
27
|
```
|
26
28
|
|
27
|
-
|
29
|
+
then:
|
28
30
|
|
29
|
-
```
|
31
|
+
```bash
|
30
32
|
bundle install
|
31
33
|
```
|
32
34
|
|
33
|
-
Or
|
35
|
+
Or grab it directly:
|
34
36
|
|
35
|
-
```
|
37
|
+
```bash
|
36
38
|
gem install portable_move_notation
|
37
39
|
```
|
38
40
|
|
39
|
-
|
40
|
-
|
41
|
-
A PMN record consists of an array of one or more action items, where each action item is a JSON object with precisely defined fields:
|
41
|
+
Require it in your code:
|
42
42
|
|
43
|
-
```
|
44
|
-
|
45
|
-
{
|
46
|
-
"src_square": <source-coordinate-or-null>,
|
47
|
-
"dst_square": <destination-coordinate>,
|
48
|
-
"piece_name": <piece-identifier>,
|
49
|
-
"piece_hand": <captured-piece-identifier-or-null>
|
50
|
-
},
|
51
|
-
...
|
52
|
-
]
|
43
|
+
```ruby
|
44
|
+
require "portable_move_notation" # provides the PortableMoveNotation namespace
|
53
45
|
```
|
54
46
|
|
55
|
-
|
47
|
+
---
|
56
48
|
|
57
|
-
|
49
|
+
## Quick Start
|
58
50
|
|
59
|
-
|
51
|
+
Dump a single action (dropping a Shogi pawn on square 27):
|
60
52
|
|
61
53
|
```ruby
|
62
54
|
require "portable_move_notation"
|
63
55
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
56
|
+
move = PortableMoveNotation::Move.new(
|
57
|
+
PortableMoveNotation::Action.new(
|
58
|
+
src_square: nil,
|
59
|
+
dst_square: 27,
|
60
|
+
piece_name: "p",
|
61
|
+
piece_hand: nil
|
62
|
+
)
|
70
63
|
)
|
71
64
|
|
72
|
-
|
73
|
-
|
74
|
-
# => {"src_square"=>52, "dst_square"=>36, "piece_name"=>"P", "piece_hand"=>nil}
|
65
|
+
puts move.to_json
|
66
|
+
# Output: A JSON array with the move data
|
75
67
|
```
|
76
68
|
|
77
|
-
|
78
|
-
|
79
|
-
Create compound moves consisting of multiple actions:
|
69
|
+
Parse it back:
|
80
70
|
|
81
71
|
```ruby
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
# Create actions for a kingside castle in chess:
|
86
|
-
king_action = PortableMoveNotation::Action.new(
|
87
|
-
src_square: 60,
|
88
|
-
dst_square: 62,
|
89
|
-
piece_name: "K",
|
90
|
-
piece_hand: nil
|
91
|
-
)
|
92
|
-
|
93
|
-
rook_action = PortableMoveNotation::Action.new(
|
94
|
-
src_square: 63,
|
95
|
-
dst_square: 61,
|
96
|
-
piece_name: "R",
|
97
|
-
piece_hand: nil
|
98
|
-
)
|
99
|
-
|
100
|
-
# Create a complete move (notice the splat operator for multiple actions)
|
101
|
-
castling_move = PortableMoveNotation::Move.new(king_action, rook_action)
|
102
|
-
|
103
|
-
# Generate JSON representation
|
104
|
-
json_string = castling_move.to_json
|
105
|
-
puts json_string
|
106
|
-
# => [{"src_square":60,"dst_square":62,"piece_name":"K","piece_hand":null},{"src_square":63,"dst_square":61,"piece_name":"R","piece_hand":null}]
|
72
|
+
restored = PortableMoveNotation::Move.from_json(move.to_json)
|
73
|
+
puts restored.actions.first.dst_square # => 27
|
107
74
|
```
|
108
75
|
|
109
|
-
|
76
|
+
---
|
110
77
|
|
111
|
-
|
112
|
-
|
113
|
-
```ruby
|
114
|
-
require "portable_move_notation"
|
78
|
+
## Advanced Examples
|
115
79
|
|
116
|
-
|
117
|
-
pmn_string = '[{"src_square":27,"dst_square":18,"piece_name":"+P","piece_hand":null}]'
|
118
|
-
move = PortableMoveNotation::Move.from_json(pmn_string)
|
119
|
-
|
120
|
-
# Access components of the move
|
121
|
-
puts move.actions.first.piece_name # => +P
|
122
|
-
puts move.actions.first.src_square # => 27
|
123
|
-
puts move.actions.first.dst_square # => 18
|
124
|
-
```
|
125
|
-
|
126
|
-
### Validation
|
127
|
-
|
128
|
-
Validate PMN data for conformance to the specification:
|
80
|
+
### Chess · Kingside Castling
|
129
81
|
|
130
82
|
```ruby
|
131
83
|
require "portable_move_notation"
|
132
|
-
require "json"
|
133
|
-
|
134
|
-
# Valid PMN data
|
135
|
-
valid_data = JSON.parse('[{"dst_square":27,"piece_name":"p"}]')
|
136
|
-
PortableMoveNotation::Move.valid?(valid_data) # => true
|
137
|
-
|
138
|
-
# Invalid PMN data (missing required field)
|
139
|
-
invalid_data = JSON.parse('[{"src_square":27}]')
|
140
|
-
PortableMoveNotation::Move.valid?(invalid_data) # => false
|
141
|
-
```
|
142
|
-
|
143
|
-
## Examples of Common Chess and Shogi Actions
|
144
|
-
|
145
|
-
### Chess: Pawn Move
|
146
|
-
|
147
|
-
A white pawn moves from e2 (52) to e4 (36):
|
148
|
-
|
149
|
-
```ruby
|
150
|
-
action = PortableMoveNotation::Action.new(
|
151
|
-
src_square: 52,
|
152
|
-
dst_square: 36,
|
153
|
-
piece_name: "P",
|
154
|
-
piece_hand: nil
|
155
|
-
)
|
156
84
|
|
157
|
-
|
158
|
-
|
159
|
-
# => [{"src_square":52,"dst_square":36,"piece_name":"P","piece_hand":null}]
|
160
|
-
```
|
161
|
-
|
162
|
-
### Chess: Castling Kingside
|
163
|
-
|
164
|
-
White castles kingside:
|
165
|
-
|
166
|
-
```ruby
|
167
|
-
king_action = PortableMoveNotation::Action.new(
|
168
|
-
src_square: 60,
|
169
|
-
dst_square: 62,
|
170
|
-
piece_name: "K",
|
171
|
-
piece_hand: nil
|
85
|
+
king = PortableMoveNotation::Action.new(
|
86
|
+
src_square: 60, dst_square: 62, piece_name: "K", piece_hand: nil
|
172
87
|
)
|
173
|
-
|
174
|
-
|
175
|
-
src_square: 63,
|
176
|
-
dst_square: 61,
|
177
|
-
piece_name: "R",
|
178
|
-
piece_hand: nil
|
88
|
+
rook = PortableMoveNotation::Action.new(
|
89
|
+
src_square: 63, dst_square: 61, piece_name: "R", piece_hand: nil
|
179
90
|
)
|
180
91
|
|
181
|
-
|
182
|
-
|
183
|
-
# => [{"src_square":60,"dst_square":62,"piece_name":"K","piece_hand":null},{"src_square":63,"dst_square":61,"piece_name":"R","piece_hand":null}]
|
92
|
+
puts PortableMoveNotation::Move.new(king, rook).to_json
|
93
|
+
# Output: A JSON array containing both king and rook move data
|
184
94
|
```
|
185
95
|
|
186
|
-
|
96
|
+
---
|
187
97
|
|
188
|
-
|
98
|
+
## Validation
|
189
99
|
|
190
|
-
|
191
|
-
action = PortableMoveNotation::Action.new(
|
192
|
-
src_square: nil,
|
193
|
-
dst_square: 27,
|
194
|
-
piece_name: "p",
|
195
|
-
piece_hand: nil
|
196
|
-
)
|
197
|
-
|
198
|
-
move = PortableMoveNotation::Move.new(action)
|
199
|
-
move.to_json
|
200
|
-
# => [{"src_square":null,"dst_square":27,"piece_name":"p","piece_hand":null}]
|
201
|
-
```
|
202
|
-
|
203
|
-
### Shogi: Piece Capture and Promotion
|
204
|
-
|
205
|
-
A bishop (B) captures a promoted pawn (+p) at square 27 and becomes available for dropping:
|
100
|
+
`PortableMoveNotation::Move.valid?(data)` checks **shape compliance** against the spec — not game legality:
|
206
101
|
|
207
102
|
```ruby
|
208
|
-
|
209
|
-
|
210
|
-
dst_square: 27,
|
211
|
-
piece_name: "B",
|
212
|
-
piece_hand: "P"
|
213
|
-
)
|
103
|
+
require "portable_move_notation"
|
104
|
+
require "json"
|
214
105
|
|
215
|
-
move
|
216
|
-
|
217
|
-
# =>
|
106
|
+
# Parse a simple JSON move with a pawn at square 27
|
107
|
+
data = JSON.parse('[{ "dst_square" => 27, "piece_name" => "p" }]')
|
108
|
+
puts PortableMoveNotation::Move.valid?(data) # => true
|
218
109
|
```
|
219
110
|
|
220
|
-
|
221
|
-
|
222
|
-
After white plays a double pawn move from e2 (52) to e4 (36), black captures en passant:
|
111
|
+
You can also validate single actions:
|
223
112
|
|
224
113
|
```ruby
|
225
|
-
#
|
226
|
-
|
227
|
-
|
228
|
-
dst_square: 36,
|
229
|
-
piece_name: "P'",
|
230
|
-
piece_hand: nil
|
231
|
-
)
|
232
|
-
|
233
|
-
# Then create the en passant capture (represented as two actions)
|
234
|
-
capture_action1 = PortableMoveNotation::Action.new(
|
235
|
-
src_square: 35,
|
236
|
-
dst_square: 36,
|
237
|
-
piece_name: "p",
|
238
|
-
piece_hand: nil
|
239
|
-
)
|
240
|
-
|
241
|
-
capture_action2 = PortableMoveNotation::Action.new(
|
242
|
-
src_square: 36,
|
243
|
-
dst_square: 44,
|
244
|
-
piece_name: "p",
|
245
|
-
piece_hand: nil
|
246
|
-
)
|
247
|
-
|
248
|
-
en_passant_move = PortableMoveNotation::Move.new(capture_action1, capture_action2)
|
249
|
-
en_passant_move.to_json
|
250
|
-
# => [{"src_square":35,"dst_square":36,"piece_name":"p","piece_hand":null},{"src_square":36,"dst_square":44,"piece_name":"p","piece_hand":null}]
|
114
|
+
# Check if an individual action is valid
|
115
|
+
action_data = { "dst_square" => 12, "piece_name" => "P" }
|
116
|
+
puts PortableMoveNotation::Action.valid?(action_data) # => true
|
251
117
|
```
|
252
118
|
|
253
|
-
|
254
|
-
|
255
|
-
* **Rule-agnostic**: PMN does not encode game-specific legality, validity, or conditions.
|
256
|
-
* **Arbitrary-dimensional**: PMN supports arbitrary board configurations through a unified coordinate system.
|
257
|
-
* **Game-neutral**: PMN provides a common structure applicable to all abstract strategy games with piece movement.
|
258
|
-
* **Hybrid-supporting**: PMN facilitates hybrid or cross-game scenarios where multiple game types coexist.
|
259
|
-
* **Deterministic**: PMN is designed for deterministic representation of all possible state transitions in piece-placement games.
|
260
|
-
|
261
|
-
## Related Specifications
|
262
|
-
|
263
|
-
PMN is part of a family of specifications for representing abstract strategy board games:
|
264
|
-
|
265
|
-
- [Piece Name Notation (PNN)](https://sashite.dev/documents/pnn/1.0.0/) - Defines the format for representing individual pieces.
|
266
|
-
- [Forsyth-Edwards Enhanced Notation (FEEN)](https://sashite.dev/documents/feen/1.0.0/) - Defines the format for representing board positions.
|
267
|
-
|
268
|
-
## Documentation
|
269
|
-
|
270
|
-
- [Official PMN Specification](https://sashite.dev/documents/pmn/1.0.0/)
|
271
|
-
- [API Documentation](https://rubydoc.info/github/sashite/pmn.rb/main)
|
119
|
+
---
|
272
120
|
|
273
121
|
## License
|
274
122
|
|
275
|
-
The [gem](https://rubygems.org/gems/portable_move_notation) is
|
123
|
+
The [gem](https://rubygems.org/gems/portable_move_notation) is released under the [MIT License](https://opensource.org/licenses/MIT).
|
124
|
+
|
125
|
+
---
|
276
126
|
|
277
127
|
## About Sashité
|
278
128
|
|
279
|
-
|
129
|
+
*Celebrating the beauty of Chinese, Japanese, and Western chess cultures.*
|
130
|
+
Find more projects & research at **[sashite.com](https://sashite.com/)**.
|
@@ -1,62 +1,76 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module PortableMoveNotation
|
4
|
-
#
|
4
|
+
# == Action
|
5
5
|
#
|
6
|
-
# An Action is the
|
7
|
-
#
|
6
|
+
# An **Action** is the *atomic* unit of Portable Move Notation. Each instance
|
7
|
+
# describes **one** deterministic transformation applied to either the board
|
8
|
+
# or the mover's reserve.
|
8
9
|
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
10
|
+
# | Field | Meaning |
|
11
|
+
# |--------------|------------------------------------------------------------------------|
|
12
|
+
# | `src_square` | Integer index of the square *vacated* – or +nil+ when dropping |
|
13
|
+
# | `dst_square` | Integer index of the square now **occupied** by {#piece_name} |
|
14
|
+
# | `piece_name` | Post‑action identifier on `dst_square` (may contain prefix/suffix) |
|
15
|
+
# | `piece_hand` | Bare letter that enters the mover's hand – or +nil+ |
|
14
16
|
#
|
15
|
-
#
|
16
|
-
#
|
17
|
+
# The implicit side‑effects are rule‑agnostic:
|
18
|
+
# * `src_square` (when not +nil+) becomes empty.
|
19
|
+
# * `dst_square` now contains {#piece_name}.
|
20
|
+
# * If {#piece_hand} is set, add exactly one such piece to the mover's reserve.
|
21
|
+
# * If `src_square` is +nil+, remove one unmodified copy of {#piece_name} from hand.
|
17
22
|
#
|
18
|
-
#
|
19
|
-
# Action.new(src_square: nil, dst_square: 27, piece_name: "p")
|
23
|
+
# === Examples
|
20
24
|
#
|
21
|
-
# @example
|
22
|
-
# Action.new(src_square:
|
25
|
+
# @example Basic piece movement (Chess pawn e2 → e4)
|
26
|
+
# PortableMoveNotation::Action.new(src_square: 52, dst_square: 36, piece_name: "P")
|
23
27
|
#
|
24
|
-
# @
|
25
|
-
#
|
28
|
+
# @example Drop from hand (Shogi pawn onto 27)
|
29
|
+
# PortableMoveNotation::Action.new(src_square: nil, dst_square: 27, piece_name: "p")
|
30
|
+
#
|
31
|
+
# @example Capture with demotion (Bishop captures +p and acquires P in hand)
|
32
|
+
# PortableMoveNotation::Action.new(src_square: 36, dst_square: 27, piece_name: "B", piece_hand: "P")
|
33
|
+
#
|
34
|
+
# @see https://sashite.dev/documents/pmn/ Portable Move Notation specification
|
35
|
+
# @see https://sashite.dev/documents/pnn/ Piece Name Notation specification
|
26
36
|
class Action
|
27
|
-
# Regular expression
|
28
|
-
#
|
29
|
-
|
30
|
-
|
31
|
-
#
|
32
|
-
|
33
|
-
#
|
37
|
+
# Regular expression for validating piece identifiers as per PNN.
|
38
|
+
# Matches: optional '+'/ '-' prefix, a single ASCII letter, optional "'" suffix.
|
39
|
+
PIECE_NAME_PATTERN = /\A[-+]?[A-Za-z]['"]?\z/
|
40
|
+
|
41
|
+
# ------------------------------------------------------------------
|
42
|
+
# Class helpers
|
43
|
+
# ------------------------------------------------------------------
|
44
|
+
|
45
|
+
# Validates that *action_data* is a structurally correct PMN **action hash**.
|
46
|
+
# (Keys are expected to be *strings*.)
|
34
47
|
#
|
35
|
-
# @param action_data [Hash] PMN action
|
36
|
-
# @return [Boolean] true if valid
|
48
|
+
# @param action_data [Hash] Raw PMN action hash.
|
49
|
+
# @return [Boolean] +true+ if the hash can be converted into a valid {Action}.
|
37
50
|
def self.valid?(action_data)
|
38
51
|
return false unless action_data.is_a?(::Hash)
|
39
52
|
return false unless action_data.key?("dst_square") && action_data.key?("piece_name")
|
40
53
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
rescue ::ArgumentError
|
51
|
-
false
|
52
|
-
end
|
54
|
+
new(
|
55
|
+
src_square: action_data["src_square"],
|
56
|
+
dst_square: action_data["dst_square"],
|
57
|
+
piece_name: action_data["piece_name"],
|
58
|
+
piece_hand: action_data["piece_hand"]
|
59
|
+
)
|
60
|
+
true
|
61
|
+
rescue ::ArgumentError
|
62
|
+
false
|
53
63
|
end
|
54
64
|
|
55
|
-
#
|
65
|
+
# Builds an {Action} from keyword parameters.
|
56
66
|
#
|
57
|
-
# @param params [Hash]
|
58
|
-
# @
|
59
|
-
# @
|
67
|
+
# @param params [Hash] Keyword parameters.
|
68
|
+
# @option params [Integer, nil] :src_square Source coordinate, or +nil+ when dropping from hand.
|
69
|
+
# @option params [Integer] :dst_square Destination coordinate (required).
|
70
|
+
# @option params [String] :piece_name Post‑move piece identifier (required).
|
71
|
+
# @option params [String, nil] :piece_hand Captured piece letter entering hand.
|
72
|
+
# @return [Action]
|
73
|
+
# @raise [KeyError] If +:dst_square+ or +:piece_name+ is missing.
|
60
74
|
def self.from_params(**params)
|
61
75
|
new(
|
62
76
|
src_square: params[:src_square],
|
@@ -66,31 +80,31 @@ module PortableMoveNotation
|
|
66
80
|
)
|
67
81
|
end
|
68
82
|
|
69
|
-
#
|
70
|
-
#
|
71
|
-
|
83
|
+
# ------------------------------------------------------------------
|
84
|
+
# Attributes
|
85
|
+
# ------------------------------------------------------------------
|
72
86
|
|
73
|
-
#
|
74
|
-
|
87
|
+
# @return [Integer, nil] Source square (or +nil+ for drops)
|
88
|
+
attr_reader :src_square
|
89
|
+
# @return [Integer] Destination square
|
75
90
|
attr_reader :dst_square
|
76
|
-
|
77
|
-
# The piece identifier in PNN format
|
78
|
-
# @return [String] Piece name
|
91
|
+
# @return [String] Post‑move piece identifier
|
79
92
|
attr_reader :piece_name
|
80
|
-
|
81
|
-
# The identifier of any captured piece that becomes available for dropping, or nil
|
82
|
-
# @return [String, nil] Captured piece identifier
|
93
|
+
# @return [String, nil] Captured piece that enters hand, or +nil+
|
83
94
|
attr_reader :piece_hand
|
84
95
|
|
85
|
-
#
|
96
|
+
# ------------------------------------------------------------------
|
97
|
+
# Construction
|
98
|
+
# ------------------------------------------------------------------
|
99
|
+
|
100
|
+
# Instantiates a new {Action}.
|
86
101
|
#
|
87
|
-
# @param
|
88
|
-
# @param
|
89
|
-
# @param
|
90
|
-
# @param piece_hand [String, nil] Captured piece
|
91
|
-
# @raise [ArgumentError]
|
102
|
+
# @param dst_square [Integer] Destination coordinate.
|
103
|
+
# @param piece_name [String] Post‑move piece identifier.
|
104
|
+
# @param src_square [Integer, nil] Source coordinate or +nil+.
|
105
|
+
# @param piece_hand [String, nil] Captured piece entering hand.
|
106
|
+
# @raise [ArgumentError] If any value fails validation.
|
92
107
|
def initialize(dst_square:, piece_name:, src_square: nil, piece_hand: nil)
|
93
|
-
# Input validation
|
94
108
|
validate_square(src_square) unless src_square.nil?
|
95
109
|
validate_square(dst_square)
|
96
110
|
validate_piece_name(piece_name)
|
@@ -104,9 +118,13 @@ module PortableMoveNotation
|
|
104
118
|
freeze
|
105
119
|
end
|
106
120
|
|
107
|
-
#
|
121
|
+
# ------------------------------------------------------------------
|
122
|
+
# Serialisation helpers
|
123
|
+
# ------------------------------------------------------------------
|
124
|
+
|
125
|
+
# Returns a **symbol‑keyed** parameter hash (useful for duplication).
|
108
126
|
#
|
109
|
-
# @return [Hash]
|
127
|
+
# @return [Hash]
|
110
128
|
def to_params
|
111
129
|
{
|
112
130
|
src_square:,
|
@@ -116,11 +134,9 @@ module PortableMoveNotation
|
|
116
134
|
}.compact
|
117
135
|
end
|
118
136
|
|
119
|
-
#
|
120
|
-
#
|
121
|
-
# This creates a hash with string keys as required by the PMN JSON format
|
137
|
+
# Returns a **string‑keyed** hash that conforms to the PMN JSON schema.
|
122
138
|
#
|
123
|
-
# @return [Hash]
|
139
|
+
# @return [Hash]
|
124
140
|
def to_h
|
125
141
|
{
|
126
142
|
"src_square" => src_square,
|
@@ -132,36 +148,34 @@ module PortableMoveNotation
|
|
132
148
|
|
133
149
|
private
|
134
150
|
|
135
|
-
# Validates that
|
151
|
+
# Validates that *square* is a non‑negative integer.
|
136
152
|
#
|
137
|
-
# @param square [Object]
|
138
|
-
# @raise [ArgumentError]
|
153
|
+
# @param square [Object]
|
154
|
+
# @raise [ArgumentError] If invalid.
|
139
155
|
def validate_square(square)
|
140
156
|
return if square.is_a?(::Integer) && square >= 0
|
141
157
|
|
142
158
|
raise ::ArgumentError, "Square must be a non-negative integer"
|
143
159
|
end
|
144
160
|
|
145
|
-
# Validates
|
161
|
+
# Validates {#piece_name} format.
|
146
162
|
#
|
147
|
-
# @param piece_name [Object]
|
148
|
-
# @raise [ArgumentError]
|
163
|
+
# @param piece_name [Object]
|
164
|
+
# @raise [ArgumentError] If invalid.
|
149
165
|
def validate_piece_name(piece_name)
|
150
166
|
return if piece_name.is_a?(::String) && piece_name.match?(PIECE_NAME_PATTERN)
|
151
167
|
|
152
|
-
raise ::ArgumentError, "Invalid piece_name format: #{piece_name}"
|
168
|
+
raise ::ArgumentError, "Invalid piece_name format: #{piece_name.inspect}"
|
153
169
|
end
|
154
170
|
|
155
|
-
# Validates
|
156
|
-
#
|
157
|
-
# Piece hand must be a single letter with no modifiers
|
171
|
+
# Validates {#piece_hand} format.
|
158
172
|
#
|
159
|
-
# @param piece_hand [Object]
|
160
|
-
# @raise [ArgumentError]
|
173
|
+
# @param piece_hand [Object]
|
174
|
+
# @raise [ArgumentError] If invalid.
|
161
175
|
def validate_piece_hand(piece_hand)
|
162
|
-
return if piece_hand.is_a?(::String) && piece_hand.match?(/\A[
|
176
|
+
return if piece_hand.is_a?(::String) && piece_hand.match?(/\A[A-Za-z]\z/)
|
163
177
|
|
164
|
-
raise ::ArgumentError, "Invalid piece_hand format: #{piece_hand}"
|
178
|
+
raise ::ArgumentError, "Invalid piece_hand format: #{piece_hand.inspect}"
|
165
179
|
end
|
166
180
|
end
|
167
181
|
end
|
@@ -1,129 +1,166 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "json"
|
4
|
+
|
3
5
|
module PortableMoveNotation
|
4
|
-
#
|
5
|
-
#
|
6
|
+
# == Move
|
7
|
+
#
|
8
|
+
# A **Move** is an *ordered list* of {Action} instances that, applied **in
|
9
|
+
# order**, realise a deterministic change of game state under Portable Move
|
10
|
+
# Notation (PMN). A move can be as small as a single pawn push or as large as
|
11
|
+
# a compound fairy‐move that relocates several pieces at once.
|
12
|
+
#
|
13
|
+
# The class is deliberately **rule‑agnostic**: it guarantees only that the
|
14
|
+
# underlying data *matches the PMN schema*. Whether the move is *legal* in a
|
15
|
+
# given game is beyond its responsibility and must be enforced by an engine
|
16
|
+
# or referee layer.
|
17
|
+
#
|
18
|
+
# === Quick start
|
19
|
+
#
|
20
|
+
# ```ruby
|
21
|
+
# require "portable_move_notation"
|
22
|
+
#
|
23
|
+
# # Plain chess pawn move: e2 → e4
|
24
|
+
# pawn = PortableMoveNotation::Action.new(
|
25
|
+
# src_square: 52,
|
26
|
+
# dst_square: 36,
|
27
|
+
# piece_name: "P"
|
28
|
+
# )
|
29
|
+
#
|
30
|
+
# move = PortableMoveNotation::Move.new(pawn)
|
31
|
+
# puts move.to_json
|
32
|
+
# # => JSON representation of the move
|
6
33
|
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
34
|
+
# parsed = PortableMoveNotation::Move.from_json(move.to_json)
|
35
|
+
# parsed.actions.first.dst_square # => 36
|
36
|
+
# ```
|
10
37
|
#
|
11
|
-
#
|
12
|
-
# action = Action.new(src_square: 52, dst_square: 36, piece_name: "P")
|
13
|
-
# move = Move.new(action)
|
38
|
+
# === Composite example (Chess kingside castling)
|
14
39
|
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
40
|
+
# ```ruby
|
41
|
+
# king = PortableMoveNotation::Action.new(
|
42
|
+
# src_square: 60, dst_square: 62, piece_name: "K"
|
43
|
+
# )
|
44
|
+
# rook = PortableMoveNotation::Action.new(
|
45
|
+
# src_square: 63, dst_square: 61, piece_name: "R"
|
46
|
+
# )
|
19
47
|
#
|
20
|
-
#
|
48
|
+
# castle = PortableMoveNotation::Move.new(king, rook)
|
49
|
+
# ```
|
50
|
+
#
|
51
|
+
# @see https://sashite.dev/documents/pmn/ Portable Move Notation specification
|
21
52
|
class Move
|
22
|
-
#
|
53
|
+
# --------------------------------------------------------------------
|
54
|
+
# Class helpers
|
55
|
+
# --------------------------------------------------------------------
|
56
|
+
|
57
|
+
# Validates that *pmn_data* is an **array of PMN action hashes**.
|
58
|
+
# The method does **not** instantiate {Action} objects on success; it merely
|
59
|
+
# checks that each element *could* be turned into one.
|
60
|
+
#
|
61
|
+
# @param pmn_data [Array<Hash>] Raw PMN structure (commonly the result of
|
62
|
+
# `JSON.parse`).
|
63
|
+
# @return [Boolean] +true+ when every element passes {Action.valid?}.
|
23
64
|
#
|
24
|
-
# @
|
25
|
-
#
|
65
|
+
# @example Validate PMN parsed from JSON
|
66
|
+
# data = JSON.parse('[{"dst_square":27,"piece_name":"p"}]')
|
67
|
+
# PortableMoveNotation::Move.valid?(data) # => true
|
26
68
|
def self.valid?(pmn_data)
|
27
|
-
return false unless pmn_data.is_a?(Array) && !pmn_data.empty?
|
69
|
+
return false unless pmn_data.is_a?(::Array) && !pmn_data.empty?
|
28
70
|
|
29
|
-
pmn_data.all? { |
|
71
|
+
pmn_data.all? { |hash| Action.valid?(hash) }
|
30
72
|
end
|
31
73
|
|
32
|
-
#
|
74
|
+
# Constructs a {Move} from its canonical **JSON** representation.
|
33
75
|
#
|
34
|
-
# @param json_string [String] JSON
|
35
|
-
# @return [Move]
|
36
|
-
# @raise [JSON::ParserError]
|
37
|
-
# @raise [KeyError]
|
76
|
+
# @param json_string [String] PMN‑formatted JSON.
|
77
|
+
# @return [Move]
|
78
|
+
# @raise [JSON::ParserError] If +json_string+ is not valid JSON.
|
79
|
+
# @raise [KeyError] If an action hash lacks required keys.
|
80
|
+
#
|
81
|
+
# @example
|
82
|
+
# json = '[{"src_square":nil,"dst_square":27,"piece_name":"p"}]'
|
83
|
+
# PortableMoveNotation::Move.from_json(json)
|
38
84
|
def self.from_json(json_string)
|
39
|
-
|
40
|
-
|
41
|
-
actions = json_data.map do |action_data|
|
42
|
-
Action.new(
|
43
|
-
src_square: action_data["src_square"],
|
44
|
-
dst_square: action_data.fetch("dst_square"),
|
45
|
-
piece_name: action_data.fetch("piece_name"),
|
46
|
-
piece_hand: action_data["piece_hand"]
|
47
|
-
)
|
48
|
-
end
|
49
|
-
|
50
|
-
new(*actions)
|
85
|
+
from_pmn(::JSON.parse(json_string))
|
51
86
|
end
|
52
87
|
|
53
|
-
#
|
88
|
+
# Constructs a {Move} from an *already parsed* PMN array.
|
54
89
|
#
|
55
|
-
# @param pmn_array [Array<Hash>]
|
56
|
-
# @return [Move]
|
57
|
-
# @raise [KeyError]
|
90
|
+
# @param pmn_array [Array<Hash>] PMN action hashes (string keys).
|
91
|
+
# @return [Move]
|
92
|
+
# @raise [KeyError] If an action hash lacks required keys.
|
58
93
|
def self.from_pmn(pmn_array)
|
59
|
-
actions = pmn_array.map do |
|
94
|
+
actions = pmn_array.map do |hash|
|
60
95
|
Action.new(
|
61
|
-
src_square:
|
62
|
-
dst_square:
|
63
|
-
piece_name:
|
64
|
-
piece_hand:
|
96
|
+
src_square: hash["src_square"],
|
97
|
+
dst_square: hash.fetch("dst_square"),
|
98
|
+
piece_name: hash.fetch("piece_name"),
|
99
|
+
piece_hand: hash["piece_hand"]
|
65
100
|
)
|
66
101
|
end
|
67
|
-
|
68
102
|
new(*actions)
|
69
103
|
end
|
70
104
|
|
71
|
-
#
|
105
|
+
# Constructs a {Move} from keyword parameters.
|
106
|
+
#
|
107
|
+
# @param actions [Array<Action, Hash>] One or more {Action} objects *or*
|
108
|
+
# parameter hashes accepted by {Action.from_params}.
|
109
|
+
# @return [Move]
|
110
|
+
# @raise [KeyError] If +actions+ is missing.
|
72
111
|
#
|
73
|
-
# @
|
74
|
-
#
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
actions = Array(params.fetch(:actions)).map do |action_params|
|
79
|
-
if action_params.is_a?(Action)
|
80
|
-
action_params
|
81
|
-
else
|
82
|
-
Action.from_params(**action_params)
|
83
|
-
end
|
112
|
+
# @example
|
113
|
+
# Move.from_params(actions: [src_square: nil, dst_square: 27, piece_name: "p"])
|
114
|
+
def self.from_params(actions:)
|
115
|
+
array = Array(actions).map do |obj|
|
116
|
+
obj.is_a?(Action) ? obj : Action.from_params(**obj)
|
84
117
|
end
|
85
|
-
|
86
|
-
new(*actions)
|
118
|
+
new(*array)
|
87
119
|
end
|
88
120
|
|
89
|
-
#
|
90
|
-
#
|
91
|
-
#
|
121
|
+
# --------------------------------------------------------------------
|
122
|
+
# Attributes & construction
|
123
|
+
# --------------------------------------------------------------------
|
124
|
+
|
125
|
+
# @return [Array<Action>] Ordered, frozen list of actions.
|
92
126
|
attr_reader :actions
|
93
127
|
|
94
|
-
#
|
128
|
+
# Creates a new {Move}.
|
95
129
|
#
|
96
|
-
# @param actions [Array<Action>]
|
97
|
-
# @raise [ArgumentError]
|
130
|
+
# @param actions [Array<Action>] One or more {Action} objects.
|
131
|
+
# @raise [ArgumentError] If +actions+ is empty or contains non‑Action items.
|
98
132
|
def initialize(*actions)
|
99
|
-
validate_actions(
|
133
|
+
validate_actions(actions)
|
100
134
|
@actions = actions.freeze
|
101
|
-
|
102
135
|
freeze
|
103
136
|
end
|
104
137
|
|
105
|
-
#
|
138
|
+
# --------------------------------------------------------------------
|
139
|
+
# Serialisation helpers
|
140
|
+
# --------------------------------------------------------------------
|
141
|
+
|
142
|
+
# Converts the move to an **array of PMN hashes** (string keys).
|
106
143
|
#
|
107
|
-
# @return [Array<Hash>]
|
144
|
+
# @return [Array<Hash>]
|
108
145
|
def to_pmn
|
109
146
|
actions.map(&:to_h)
|
110
147
|
end
|
111
148
|
|
112
|
-
# Converts the move to a JSON string
|
149
|
+
# Converts the move to a **JSON string**.
|
113
150
|
#
|
114
|
-
# @return [String]
|
151
|
+
# @return [String]
|
115
152
|
def to_json(*_args)
|
116
153
|
::JSON.generate(to_pmn)
|
117
154
|
end
|
118
155
|
|
119
156
|
private
|
120
157
|
|
121
|
-
#
|
158
|
+
# Ensures +actions+ is a non‑empty array of {Action} instances.
|
122
159
|
#
|
123
|
-
# @param actions [Object]
|
124
|
-
# @raise [ArgumentError]
|
125
|
-
def validate_actions(
|
126
|
-
return if
|
160
|
+
# @param actions [Array<Object>] Items to validate.
|
161
|
+
# @raise [ArgumentError] If validation fails.
|
162
|
+
def validate_actions(actions)
|
163
|
+
return if actions.any? && actions.all?(Action)
|
127
164
|
|
128
165
|
raise ::ArgumentError, "Actions must be a non-empty array of Action objects"
|
129
166
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: portable_move_notation
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.1.
|
4
|
+
version: 2.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Cyril Kato
|
@@ -9,10 +9,11 @@ bindir: bin
|
|
9
9
|
cert_chain: []
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
11
11
|
dependencies: []
|
12
|
-
description:
|
13
|
-
|
14
|
-
|
15
|
-
|
12
|
+
description: Portable Move Notation (PMN) is a rule-agnostic, JSON-based format for
|
13
|
+
representing moves in abstract strategy board games. This gem provides a consistent
|
14
|
+
Ruby interface for serializing, deserializing, and validating actions across Chess,
|
15
|
+
Shogi, Xiangqi, and other traditional or non-traditional variants, focusing on deterministic
|
16
|
+
state transformations independent of game-specific rules.
|
16
17
|
email: contact@cyril.email
|
17
18
|
executables: []
|
18
19
|
extensions: []
|
@@ -49,5 +50,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
49
50
|
requirements: []
|
50
51
|
rubygems_version: 3.6.7
|
51
52
|
specification_version: 4
|
52
|
-
summary:
|
53
|
+
summary: A pure Ruby implementation of Portable Move Notation (PMN) for abstract strategy
|
54
|
+
board games.
|
53
55
|
test_files: []
|