portable_move_notation 1.2.0 → 2.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 +4 -4
- data/LICENSE.md +2 -2
- data/README.md +237 -38
- data/lib/portable_move_notation/action.rb +161 -0
- data/lib/portable_move_notation/move.rb +126 -3
- data/lib/portable_move_notation.rb +3 -9
- metadata +16 -141
- data/lib/portable_move_notation/dumper.rb +0 -15
- data/lib/portable_move_notation/parser.rb +0 -39
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 235dc58e056f7f8191c003a52248c1dadec9baabb5cc0393fd741bc7109abefa
|
4
|
+
data.tar.gz: f694111d3222b01a42e93eb39f72bd6dbaf369639ed8f7a5874dde60d09d66ef
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d294c9796295c57415879911ab0aabfc370aff963f772fd088f1be7f8f9532f93773fe0c1f4748f89534fd20609c1b303e68f0962d4e43e39ee8e48410cda5ad
|
7
|
+
data.tar.gz: 7f2e37453b62b9407240cead2ac809d230fe464f2a9ce2a4877262e5b6531384d564fe43ec20089d22b4b9337b3a3c05507eb9d4acc562de1ca7eb11cc533d22
|
data/LICENSE.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
|
-
The MIT License
|
1
|
+
# The MIT License
|
2
2
|
|
3
|
-
Copyright (c) 2019-
|
3
|
+
Copyright (c) 2019-2025 Sashité
|
4
4
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
data/README.md
CHANGED
@@ -1,80 +1,279 @@
|
|
1
|
-
#
|
1
|
+
# Pmn.rb
|
2
2
|
|
3
|
-
|
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) support for the Ruby language.
|
9
|
+
|
10
|
+
## What is PMN?
|
11
|
+
|
12
|
+
PMN (Portable Move Notation) is a rule-agnostic JSON-based format for representing moves in abstract strategy board games. It provides a consistent representation system for game actions across both traditional and non-traditional board games, supporting arbitrary dimensions and hybrid configurations while maintaining neutrality toward game-specific rules.
|
13
|
+
|
14
|
+
This gem implements the [PMN Specification v1.0.0](https://sashite.dev/documents/pmn/1.0.0/), providing a Ruby interface for:
|
15
|
+
- Serializing game actions to PMN format
|
16
|
+
- Parsing PMN data into structured Ruby objects
|
17
|
+
- Validating PMN data according to the specification
|
4
18
|
|
5
19
|
## Installation
|
6
20
|
|
7
21
|
Add this line to your application's Gemfile:
|
8
22
|
|
9
23
|
```ruby
|
10
|
-
gem
|
24
|
+
gem "portable_move_notation"
|
11
25
|
```
|
12
26
|
|
13
27
|
And then execute:
|
14
28
|
|
15
|
-
|
29
|
+
```sh
|
30
|
+
bundle install
|
31
|
+
```
|
16
32
|
|
17
33
|
Or install it yourself as:
|
18
34
|
|
19
|
-
|
35
|
+
```sh
|
36
|
+
gem install portable_move_notation
|
37
|
+
```
|
38
|
+
|
39
|
+
## PMN Format
|
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:
|
42
|
+
|
43
|
+
```json
|
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
|
+
]
|
53
|
+
```
|
54
|
+
|
55
|
+
## Basic Usage
|
20
56
|
|
21
|
-
|
57
|
+
### Working with PMN Actions
|
22
58
|
|
23
|
-
|
59
|
+
Create individual actions representing piece movement:
|
24
60
|
|
25
61
|
```ruby
|
26
|
-
require
|
62
|
+
require "portable_move_notation"
|
63
|
+
|
64
|
+
# Create an action representing a chess pawn moving from e2 (52) to e4 (36)
|
65
|
+
pawn_move = PortableMoveNotation::Action.new(
|
66
|
+
src_square: 52,
|
67
|
+
dst_square: 36,
|
68
|
+
piece_name: "P",
|
69
|
+
piece_hand: nil
|
70
|
+
)
|
71
|
+
|
72
|
+
# Generate the PMN representation
|
73
|
+
pawn_move.to_h
|
74
|
+
# => {"src_square"=>52, "dst_square"=>36, "piece_name"=>"P", "piece_hand"=>nil}
|
75
|
+
```
|
27
76
|
|
28
|
-
|
77
|
+
### Working with PMN Moves
|
29
78
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
79
|
+
Create compound moves consisting of multiple actions:
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
require "portable_move_notation"
|
83
|
+
require "json"
|
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}]
|
107
|
+
```
|
108
|
+
|
109
|
+
### Parsing PMN Data
|
110
|
+
|
111
|
+
Parse PMN data from JSON:
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
require "portable_move_notation"
|
115
|
+
|
116
|
+
# Parse a PMN string (representing a shogi pawn promotion)
|
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
|
+
```
|
35
125
|
|
36
|
-
|
126
|
+
### Validation
|
37
127
|
|
38
|
-
|
128
|
+
Validate PMN data for conformance to the specification:
|
39
129
|
|
40
|
-
|
130
|
+
```ruby
|
131
|
+
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
|
41
141
|
```
|
42
142
|
|
43
|
-
## Examples
|
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):
|
44
148
|
|
45
149
|
```ruby
|
46
|
-
|
150
|
+
action = PortableMoveNotation::Action.new(
|
151
|
+
src_square: 52,
|
152
|
+
dst_square: 36,
|
153
|
+
piece_name: "P",
|
154
|
+
piece_hand: nil
|
155
|
+
)
|
156
|
+
|
157
|
+
move = PortableMoveNotation::Move.new(action)
|
158
|
+
move.to_json
|
159
|
+
# => [{"src_square":52,"dst_square":36,"piece_name":"P","piece_hand":null}]
|
160
|
+
```
|
47
161
|
|
48
|
-
|
49
|
-
PortableMoveNotation.parse('60,62,♔;63,61,♖') # => [[60, 62, "♔", nil, 63, 61, "♖", nil]]
|
162
|
+
### Chess: Castling Kingside
|
50
163
|
|
51
|
-
|
164
|
+
White castles kingside:
|
52
165
|
|
53
|
-
|
54
|
-
PortableMoveNotation.
|
166
|
+
```ruby
|
167
|
+
king_action = PortableMoveNotation::Action.new(
|
168
|
+
src_square: 60,
|
169
|
+
dst_square: 62,
|
170
|
+
piece_name: "K",
|
171
|
+
piece_hand: nil
|
172
|
+
)
|
173
|
+
|
174
|
+
rook_action = PortableMoveNotation::Action.new(
|
175
|
+
src_square: 63,
|
176
|
+
dst_square: 61,
|
177
|
+
piece_name: "R",
|
178
|
+
piece_hand: nil
|
179
|
+
)
|
180
|
+
|
181
|
+
castling_move = PortableMoveNotation::Move.new(king_action, rook_action)
|
182
|
+
castling_move.to_json
|
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}]
|
184
|
+
```
|
55
185
|
|
56
|
-
|
186
|
+
### Shogi: Dropping a Pawn
|
57
187
|
|
58
|
-
|
59
|
-
PortableMoveNotation.parse('33,24,+P,R') # => [[33, 24, "+P", "R"]]
|
188
|
+
A pawn is dropped onto square 27 from the player's hand:
|
60
189
|
|
61
|
-
|
190
|
+
```ruby
|
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
|
+
```
|
62
202
|
|
63
|
-
|
64
|
-
PortableMoveNotation.parse('*,42,P') # => [[nil, 42, "P", nil]]
|
203
|
+
### Shogi: Piece Capture and Promotion
|
65
204
|
|
66
|
-
|
205
|
+
A bishop (B) captures a promoted pawn (+p) at square 27 and becomes available for dropping:
|
67
206
|
|
68
|
-
|
69
|
-
PortableMoveNotation.
|
207
|
+
```ruby
|
208
|
+
action = PortableMoveNotation::Action.new(
|
209
|
+
src_square: 36,
|
210
|
+
dst_square: 27,
|
211
|
+
piece_name: "B",
|
212
|
+
piece_hand: "P"
|
213
|
+
)
|
214
|
+
|
215
|
+
move = PortableMoveNotation::Move.new(action)
|
216
|
+
move.to_json
|
217
|
+
# => [{"src_square":36,"dst_square":27,"piece_name":"B","piece_hand":"P"}]
|
70
218
|
```
|
71
219
|
|
72
|
-
|
220
|
+
### Chess: En Passant Capture
|
73
221
|
|
74
|
-
|
222
|
+
After white plays a double pawn move from e2 (52) to e4 (36), black captures en passant:
|
223
|
+
|
224
|
+
```ruby
|
225
|
+
# First create the initial pawn move
|
226
|
+
initial_move = PortableMoveNotation::Action.new(
|
227
|
+
src_square: 52,
|
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: null
|
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}]
|
251
|
+
```
|
252
|
+
|
253
|
+
## Properties of PMN
|
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)
|
272
|
+
|
273
|
+
## License
|
75
274
|
|
76
|
-
|
275
|
+
The [gem](https://rubygems.org/gems/portable_move_notation) is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
77
276
|
|
78
|
-
|
277
|
+
## About Sashité
|
79
278
|
|
80
|
-
|
279
|
+
This project is maintained by [Sashité](https://sashite.com/) — promoting chess variants and sharing the beauty of Chinese, Japanese, and Western chess cultures.
|
@@ -0,0 +1,161 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PortableMoveNotation
|
4
|
+
# Represents an atomic action in PMN format
|
5
|
+
#
|
6
|
+
# An Action is the fundamental unit of PMN, representing a single piece movement
|
7
|
+
# from a source square to a destination square, with optional capture information.
|
8
|
+
#
|
9
|
+
# PMN actions consist of four primary components:
|
10
|
+
# - src_square: Source coordinate (or nil for drops)
|
11
|
+
# - dst_square: Destination coordinate (required)
|
12
|
+
# - piece_name: Identifier of the moving piece (required)
|
13
|
+
# - piece_hand: Identifier of any captured piece (or nil)
|
14
|
+
#
|
15
|
+
# @example Basic piece movement
|
16
|
+
# Action.new(src_square: 52, dst_square: 36, piece_name: "P")
|
17
|
+
#
|
18
|
+
# @example Piece drop (from outside the board)
|
19
|
+
# Action.new(src_square: nil, dst_square: 27, piece_name: "p")
|
20
|
+
#
|
21
|
+
# @example Capture with piece becoming available for dropping
|
22
|
+
# Action.new(src_square: 33, dst_square: 24, piece_name: "+P", piece_hand: "R")
|
23
|
+
#
|
24
|
+
# @see https://sashite.dev/documents/pmn/1.0.0/ PMN Specification
|
25
|
+
# @see https://sashite.dev/documents/pnn/1.0.0/ PNN Specification for piece format
|
26
|
+
class Action
|
27
|
+
# Validates a PMN action hash
|
28
|
+
#
|
29
|
+
# @param action_data [Hash] PMN action data to validate
|
30
|
+
# @return [Boolean] true if valid, false otherwise
|
31
|
+
def self.valid?(action_data)
|
32
|
+
return false unless action_data.is_a?(::Hash)
|
33
|
+
return false unless action_data.key?("dst_square") && action_data.key?("piece_name")
|
34
|
+
|
35
|
+
begin
|
36
|
+
# Use existing validation logic by attempting to create an instance
|
37
|
+
new(
|
38
|
+
src_square: action_data["src_square"],
|
39
|
+
dst_square: action_data["dst_square"],
|
40
|
+
piece_name: action_data["piece_name"],
|
41
|
+
piece_hand: action_data["piece_hand"]
|
42
|
+
)
|
43
|
+
true
|
44
|
+
rescue ::ArgumentError
|
45
|
+
false
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Creates an Action instance from parameters
|
50
|
+
#
|
51
|
+
# @param params [Hash] Action parameters
|
52
|
+
# @return [Action] A new action instance
|
53
|
+
# @raise [KeyError] if required parameters are missing
|
54
|
+
def self.from_params(**params)
|
55
|
+
new(
|
56
|
+
src_square: params[:src_square],
|
57
|
+
dst_square: params.fetch(:dst_square),
|
58
|
+
piece_name: params.fetch(:piece_name),
|
59
|
+
piece_hand: params[:piece_hand]
|
60
|
+
)
|
61
|
+
end
|
62
|
+
|
63
|
+
# The source coordinate of the action, or nil for drops
|
64
|
+
# @return [Integer, nil] Source square coordinate
|
65
|
+
attr_reader :src_square
|
66
|
+
|
67
|
+
# The destination coordinate of the action
|
68
|
+
# @return [Integer] Destination square coordinate
|
69
|
+
attr_reader :dst_square
|
70
|
+
|
71
|
+
# The piece identifier in PNN format
|
72
|
+
# @return [String] Piece name
|
73
|
+
attr_reader :piece_name
|
74
|
+
|
75
|
+
# The identifier of any captured piece that becomes available for dropping, or nil
|
76
|
+
# @return [String, nil] Captured piece identifier
|
77
|
+
attr_reader :piece_hand
|
78
|
+
|
79
|
+
# Initializes a new action
|
80
|
+
#
|
81
|
+
# @param src_square [Integer, nil] Source square (nil for placements from outside the board)
|
82
|
+
# @param dst_square [Integer] Destination square (required)
|
83
|
+
# @param piece_name [String] Piece identifier in PNN format (required)
|
84
|
+
# @param piece_hand [String, nil] Captured piece identifier that becomes droppable, or nil
|
85
|
+
# @raise [ArgumentError] if any validation fails
|
86
|
+
def initialize(dst_square:, piece_name:, src_square: nil, piece_hand: nil)
|
87
|
+
# Input validation
|
88
|
+
validate_square(src_square) unless src_square.nil?
|
89
|
+
validate_square(dst_square)
|
90
|
+
validate_piece_name(piece_name)
|
91
|
+
validate_piece_hand(piece_hand) unless piece_hand.nil?
|
92
|
+
|
93
|
+
@src_square = src_square
|
94
|
+
@dst_square = dst_square
|
95
|
+
@piece_name = piece_name
|
96
|
+
@piece_hand = piece_hand
|
97
|
+
|
98
|
+
freeze
|
99
|
+
end
|
100
|
+
|
101
|
+
# Converts the action to a parameter hash
|
102
|
+
#
|
103
|
+
# @return [Hash] Parameter hash representation of the action
|
104
|
+
def to_params
|
105
|
+
{
|
106
|
+
src_square:,
|
107
|
+
dst_square:,
|
108
|
+
piece_name:,
|
109
|
+
piece_hand:
|
110
|
+
}.compact
|
111
|
+
end
|
112
|
+
|
113
|
+
# Converts the action to a PMN-compatible hash
|
114
|
+
#
|
115
|
+
# This creates a hash with string keys as required by the PMN JSON format
|
116
|
+
#
|
117
|
+
# @return [Hash] PMN-compatible hash representation
|
118
|
+
def to_h
|
119
|
+
{
|
120
|
+
"src_square" => src_square,
|
121
|
+
"dst_square" => dst_square,
|
122
|
+
"piece_name" => piece_name,
|
123
|
+
"piece_hand" => piece_hand
|
124
|
+
}
|
125
|
+
end
|
126
|
+
|
127
|
+
private
|
128
|
+
|
129
|
+
# Validates that a square coordinate is a non-negative integer
|
130
|
+
#
|
131
|
+
# @param square [Object] Value to validate
|
132
|
+
# @raise [ArgumentError] if the value is not a non-negative integer
|
133
|
+
def validate_square(square)
|
134
|
+
return if square.is_a?(::Integer) && square >= 0
|
135
|
+
|
136
|
+
raise ::ArgumentError, "Square must be a non-negative integer"
|
137
|
+
end
|
138
|
+
|
139
|
+
# Validates the piece name format according to PNN specification
|
140
|
+
#
|
141
|
+
# @param piece_name [Object] Piece name to validate
|
142
|
+
# @raise [ArgumentError] if the format is invalid
|
143
|
+
def validate_piece_name(piece_name)
|
144
|
+
return if piece_name.is_a?(::String) && piece_name.match?(/\A[-+]?[a-zA-Z][=<>]?\z/)
|
145
|
+
|
146
|
+
raise ::ArgumentError, "Invalid piece_name format: #{piece_name}"
|
147
|
+
end
|
148
|
+
|
149
|
+
# Validates the piece hand format according to PNN specification
|
150
|
+
#
|
151
|
+
# Piece hand must be a single letter with no modifiers
|
152
|
+
#
|
153
|
+
# @param piece_hand [Object] Piece hand value to validate
|
154
|
+
# @raise [ArgumentError] if the format is invalid
|
155
|
+
def validate_piece_hand(piece_hand)
|
156
|
+
return if piece_hand.is_a?(::String) && piece_hand.match?(/\A[a-zA-Z]\z/)
|
157
|
+
|
158
|
+
raise ::ArgumentError, "Invalid piece_hand format: #{piece_hand}"
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
@@ -1,10 +1,133 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module PortableMoveNotation
|
4
|
-
#
|
4
|
+
# Represents a PMN move - a collection of one or more atomic actions
|
5
|
+
# that together describe a game state transition
|
6
|
+
#
|
7
|
+
# A move may consist of one or more actions, allowing for representation
|
8
|
+
# of complex moves such as castling, en passant captures, or multi-step
|
9
|
+
# actions in various abstract strategy games.
|
10
|
+
#
|
11
|
+
# @example Creating a simple move
|
12
|
+
# action = Action.new(src_square: 52, dst_square: 36, piece_name: "P")
|
13
|
+
# move = Move.new(action)
|
14
|
+
#
|
15
|
+
# @example Creating a castling move
|
16
|
+
# king_action = Action.new(src_square: 60, dst_square: 62, piece_name: "K")
|
17
|
+
# rook_action = Action.new(src_square: 63, dst_square: 61, piece_name: "R")
|
18
|
+
# castling = Move.new(king_action, rook_action)
|
19
|
+
#
|
20
|
+
# @see https://sashite.dev/documents/pmn/1.0.0/ PMN Specification
|
5
21
|
class Move
|
6
|
-
|
7
|
-
|
22
|
+
# Validates a PMN array data structure
|
23
|
+
#
|
24
|
+
# @param pmn_data [Array] PMN data to validate
|
25
|
+
# @return [Boolean] true if valid, false otherwise
|
26
|
+
def self.valid?(pmn_data)
|
27
|
+
return false unless pmn_data.is_a?(Array) && !pmn_data.empty?
|
28
|
+
|
29
|
+
pmn_data.all? { |action_data| Action.valid?(action_data) }
|
30
|
+
end
|
31
|
+
|
32
|
+
# Creates a Move instance from a JSON string in PMN format
|
33
|
+
#
|
34
|
+
# @param json_string [String] JSON string to parse
|
35
|
+
# @return [Move] A new move instance
|
36
|
+
# @raise [JSON::ParserError] if the JSON string is malformed
|
37
|
+
# @raise [KeyError] if required fields are missing
|
38
|
+
def self.from_json(json_string)
|
39
|
+
json_data = ::JSON.parse(json_string)
|
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)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Creates a Move instance from an array of PMN action hashes
|
54
|
+
#
|
55
|
+
# @param pmn_array [Array<Hash>] Array of PMN action hashes
|
56
|
+
# @return [Move] A new move instance
|
57
|
+
# @raise [KeyError] if required fields are missing
|
58
|
+
def self.from_pmn(pmn_array)
|
59
|
+
actions = pmn_array.map do |action_data|
|
60
|
+
Action.new(
|
61
|
+
src_square: action_data["src_square"],
|
62
|
+
dst_square: action_data.fetch("dst_square"),
|
63
|
+
piece_name: action_data.fetch("piece_name"),
|
64
|
+
piece_hand: action_data["piece_hand"]
|
65
|
+
)
|
66
|
+
end
|
67
|
+
|
68
|
+
new(*actions)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Creates a Move instance from a parameters hash
|
72
|
+
#
|
73
|
+
# @param params [Hash] Move parameters
|
74
|
+
# @option params [Array<Action, Hash>] :actions List of actions or action params
|
75
|
+
# @return [Move] A new move instance
|
76
|
+
# @raise [KeyError] if the :actions key is missing
|
77
|
+
def self.from_params(**params)
|
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
|
84
|
+
end
|
85
|
+
|
86
|
+
new(*actions)
|
87
|
+
end
|
88
|
+
|
89
|
+
# The list of actions that compose this move
|
90
|
+
#
|
91
|
+
# @return [Array<Action>] List of action objects (frozen)
|
92
|
+
attr_reader :actions
|
93
|
+
|
94
|
+
# Initializes a new move with the given actions
|
95
|
+
#
|
96
|
+
# @param actions [Array<Action>] List of actions as splat arguments
|
97
|
+
# @raise [ArgumentError] if actions is not a non-empty array of Action objects
|
98
|
+
def initialize(*actions)
|
99
|
+
validate_actions(*actions)
|
100
|
+
@actions = actions.freeze
|
101
|
+
|
102
|
+
freeze
|
103
|
+
end
|
104
|
+
|
105
|
+
# Converts the move to PMN format (array of hashes)
|
106
|
+
#
|
107
|
+
# @return [Array<Hash>] PMN representation of the move
|
108
|
+
def to_pmn
|
109
|
+
actions.map(&:to_h)
|
110
|
+
end
|
111
|
+
|
112
|
+
# Converts the move to a JSON string
|
113
|
+
#
|
114
|
+
# @return [String] JSON string of the move in PMN format
|
115
|
+
def to_json(*_args)
|
116
|
+
::JSON.generate(to_pmn)
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
# Validates the actions array
|
122
|
+
#
|
123
|
+
# @param actions [Object] Actions to validate
|
124
|
+
# @raise [ArgumentError] if actions is not a non-empty array of Action objects
|
125
|
+
def validate_actions(*actions)
|
126
|
+
return if !actions.empty? && actions.all?(Action)
|
127
|
+
|
128
|
+
raise ::ArgumentError, "Actions must be a non-empty array of Action objects"
|
8
129
|
end
|
9
130
|
end
|
10
131
|
end
|
132
|
+
|
133
|
+
require_relative "action"
|
@@ -1,15 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Portable Move Notation module
|
4
|
+
#
|
5
|
+
# @see https://sashite.dev/documents/pmn/1.0.0/
|
4
6
|
module PortableMoveNotation
|
5
|
-
def self.dump(*moves)
|
6
|
-
Dumper.call(*moves)
|
7
|
-
end
|
8
|
-
|
9
|
-
def self.parse(string)
|
10
|
-
Parser.call(string)
|
11
|
-
end
|
12
7
|
end
|
13
8
|
|
14
|
-
require_relative
|
15
|
-
require_relative 'portable_move_notation/parser'
|
9
|
+
require_relative File.join("portable_move_notation", "move")
|
metadata
CHANGED
@@ -1,143 +1,18 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: portable_move_notation
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Cyril Kato
|
8
|
-
autorequire:
|
9
8
|
bindir: bin
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
12
|
-
dependencies:
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
- - "~>"
|
18
|
-
- !ruby/object:Gem::Version
|
19
|
-
version: 1.2.0
|
20
|
-
type: :runtime
|
21
|
-
prerelease: false
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
23
|
-
requirements:
|
24
|
-
- - "~>"
|
25
|
-
- !ruby/object:Gem::Version
|
26
|
-
version: 1.2.0
|
27
|
-
- !ruby/object:Gem::Dependency
|
28
|
-
name: awesome_print
|
29
|
-
requirement: !ruby/object:Gem::Requirement
|
30
|
-
requirements:
|
31
|
-
- - ">="
|
32
|
-
- !ruby/object:Gem::Version
|
33
|
-
version: '0'
|
34
|
-
type: :development
|
35
|
-
prerelease: false
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
37
|
-
requirements:
|
38
|
-
- - ">="
|
39
|
-
- !ruby/object:Gem::Version
|
40
|
-
version: '0'
|
41
|
-
- !ruby/object:Gem::Dependency
|
42
|
-
name: bundler
|
43
|
-
requirement: !ruby/object:Gem::Requirement
|
44
|
-
requirements:
|
45
|
-
- - ">="
|
46
|
-
- !ruby/object:Gem::Version
|
47
|
-
version: '0'
|
48
|
-
type: :development
|
49
|
-
prerelease: false
|
50
|
-
version_requirements: !ruby/object:Gem::Requirement
|
51
|
-
requirements:
|
52
|
-
- - ">="
|
53
|
-
- !ruby/object:Gem::Version
|
54
|
-
version: '0'
|
55
|
-
- !ruby/object:Gem::Dependency
|
56
|
-
name: byebug
|
57
|
-
requirement: !ruby/object:Gem::Requirement
|
58
|
-
requirements:
|
59
|
-
- - ">="
|
60
|
-
- !ruby/object:Gem::Version
|
61
|
-
version: '0'
|
62
|
-
type: :development
|
63
|
-
prerelease: false
|
64
|
-
version_requirements: !ruby/object:Gem::Requirement
|
65
|
-
requirements:
|
66
|
-
- - ">="
|
67
|
-
- !ruby/object:Gem::Version
|
68
|
-
version: '0'
|
69
|
-
- !ruby/object:Gem::Dependency
|
70
|
-
name: rake
|
71
|
-
requirement: !ruby/object:Gem::Requirement
|
72
|
-
requirements:
|
73
|
-
- - ">="
|
74
|
-
- !ruby/object:Gem::Version
|
75
|
-
version: '0'
|
76
|
-
type: :development
|
77
|
-
prerelease: false
|
78
|
-
version_requirements: !ruby/object:Gem::Requirement
|
79
|
-
requirements:
|
80
|
-
- - ">="
|
81
|
-
- !ruby/object:Gem::Version
|
82
|
-
version: '0'
|
83
|
-
- !ruby/object:Gem::Dependency
|
84
|
-
name: rubocop-performance
|
85
|
-
requirement: !ruby/object:Gem::Requirement
|
86
|
-
requirements:
|
87
|
-
- - ">="
|
88
|
-
- !ruby/object:Gem::Version
|
89
|
-
version: '0'
|
90
|
-
type: :development
|
91
|
-
prerelease: false
|
92
|
-
version_requirements: !ruby/object:Gem::Requirement
|
93
|
-
requirements:
|
94
|
-
- - ">="
|
95
|
-
- !ruby/object:Gem::Version
|
96
|
-
version: '0'
|
97
|
-
- !ruby/object:Gem::Dependency
|
98
|
-
name: rubocop-thread_safety
|
99
|
-
requirement: !ruby/object:Gem::Requirement
|
100
|
-
requirements:
|
101
|
-
- - ">="
|
102
|
-
- !ruby/object:Gem::Version
|
103
|
-
version: '0'
|
104
|
-
type: :development
|
105
|
-
prerelease: false
|
106
|
-
version_requirements: !ruby/object:Gem::Requirement
|
107
|
-
requirements:
|
108
|
-
- - ">="
|
109
|
-
- !ruby/object:Gem::Version
|
110
|
-
version: '0'
|
111
|
-
- !ruby/object:Gem::Dependency
|
112
|
-
name: simplecov
|
113
|
-
requirement: !ruby/object:Gem::Requirement
|
114
|
-
requirements:
|
115
|
-
- - ">="
|
116
|
-
- !ruby/object:Gem::Version
|
117
|
-
version: '0'
|
118
|
-
type: :development
|
119
|
-
prerelease: false
|
120
|
-
version_requirements: !ruby/object:Gem::Requirement
|
121
|
-
requirements:
|
122
|
-
- - ">="
|
123
|
-
- !ruby/object:Gem::Version
|
124
|
-
version: '0'
|
125
|
-
- !ruby/object:Gem::Dependency
|
126
|
-
name: yard
|
127
|
-
requirement: !ruby/object:Gem::Requirement
|
128
|
-
requirements:
|
129
|
-
- - ">="
|
130
|
-
- !ruby/object:Gem::Version
|
131
|
-
version: '0'
|
132
|
-
type: :development
|
133
|
-
prerelease: false
|
134
|
-
version_requirements: !ruby/object:Gem::Requirement
|
135
|
-
requirements:
|
136
|
-
- - ">="
|
137
|
-
- !ruby/object:Gem::Version
|
138
|
-
version: '0'
|
139
|
-
description: A Ruby interface for data serialization in PMN (Portable Move Notation)
|
140
|
-
format.
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
|
+
dependencies: []
|
12
|
+
description: A Ruby interface for serialization and deserialization of moves in PMN
|
13
|
+
format. PMN is a rule-agnostic JSON-based format for representing moves in abstract
|
14
|
+
strategy board games, providing a consistent representation system for game actions
|
15
|
+
across both traditional and non-traditional board games.
|
141
16
|
email: contact@cyril.email
|
142
17
|
executables: []
|
143
18
|
extensions: []
|
@@ -146,17 +21,18 @@ files:
|
|
146
21
|
- LICENSE.md
|
147
22
|
- README.md
|
148
23
|
- lib/portable_move_notation.rb
|
149
|
-
- lib/portable_move_notation/
|
24
|
+
- lib/portable_move_notation/action.rb
|
150
25
|
- lib/portable_move_notation/move.rb
|
151
|
-
|
152
|
-
homepage: https://developer.sashite.com/specs/portable-move-notation
|
26
|
+
homepage: https://github.com/sashite/pmn.rb
|
153
27
|
licenses:
|
154
28
|
- MIT
|
155
29
|
metadata:
|
156
30
|
bug_tracker_uri: https://github.com/sashite/pmn.rb/issues
|
157
|
-
documentation_uri: https://rubydoc.info/
|
31
|
+
documentation_uri: https://rubydoc.info/github/sashite/pmn.rb/main
|
32
|
+
homepage_uri: https://github.com/sashite/pmn.rb
|
158
33
|
source_code_uri: https://github.com/sashite/pmn.rb
|
159
|
-
|
34
|
+
specification_uri: https://sashite.dev/documents/pmn/1.0.0/
|
35
|
+
rubygems_mfa_required: 'true'
|
160
36
|
rdoc_options: []
|
161
37
|
require_paths:
|
162
38
|
- lib
|
@@ -164,15 +40,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
164
40
|
requirements:
|
165
41
|
- - ">="
|
166
42
|
- !ruby/object:Gem::Version
|
167
|
-
version:
|
43
|
+
version: 3.2.0
|
168
44
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
169
45
|
requirements:
|
170
46
|
- - ">="
|
171
47
|
- !ruby/object:Gem::Version
|
172
48
|
version: '0'
|
173
49
|
requirements: []
|
174
|
-
rubygems_version: 3.
|
175
|
-
signing_key:
|
50
|
+
rubygems_version: 3.6.7
|
176
51
|
specification_version: 4
|
177
|
-
summary:
|
52
|
+
summary: PMN (Portable Move Notation) support for the Ruby language.
|
178
53
|
test_files: []
|
@@ -1,15 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative 'move'
|
4
|
-
|
5
|
-
module PortableMoveNotation
|
6
|
-
# Dumper class
|
7
|
-
class Dumper < Move
|
8
|
-
def self.call(*moves)
|
9
|
-
moves.map { |move| ::Sashite::PAN::Dumper.call(*move.each_slice(4)) }
|
10
|
-
.join(separator)
|
11
|
-
end
|
12
|
-
end
|
13
|
-
end
|
14
|
-
|
15
|
-
require 'sashite/pan/dumper'
|
@@ -1,39 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative 'move'
|
4
|
-
|
5
|
-
module PortableMoveNotation
|
6
|
-
# Parser class
|
7
|
-
class Parser < Move
|
8
|
-
def self.call(string)
|
9
|
-
string.split(separator)
|
10
|
-
.map { |serialized_move| new(serialized_move).call }
|
11
|
-
end
|
12
|
-
|
13
|
-
attr_reader :serialized_actions
|
14
|
-
|
15
|
-
def initialize(serialized_move)
|
16
|
-
@serialized_actions = serialized_move.split(';')
|
17
|
-
end
|
18
|
-
|
19
|
-
def call
|
20
|
-
serialized_actions.flat_map { |string| action_items(*string.split(',')) }
|
21
|
-
end
|
22
|
-
|
23
|
-
private
|
24
|
-
|
25
|
-
def action_items(*args)
|
26
|
-
src_square = args.fetch(0)
|
27
|
-
src_square = src_square.eql?(drop_char) ? nil : Integer(src_square)
|
28
|
-
dst_square = Integer(args.fetch(1))
|
29
|
-
piece_name = args.fetch(2)
|
30
|
-
piece_hand = args.fetch(3, nil)
|
31
|
-
|
32
|
-
[src_square, dst_square, piece_name, piece_hand]
|
33
|
-
end
|
34
|
-
|
35
|
-
def drop_char
|
36
|
-
'*'
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|