portable_move_notation 2.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 235dc58e056f7f8191c003a52248c1dadec9baabb5cc0393fd741bc7109abefa
4
- data.tar.gz: f694111d3222b01a42e93eb39f72bd6dbaf369639ed8f7a5874dde60d09d66ef
3
+ metadata.gz: f936c1b2bb3ed48727315af5aba93953ca053b535b043c95dbcb12a061d82c50
4
+ data.tar.gz: 56e2bc34c31919f0853f2f97709f576ed49453ef95cc410a8273c60bb76d1250
5
5
  SHA512:
6
- metadata.gz: d294c9796295c57415879911ab0aabfc370aff963f772fd088f1be7f8f9532f93773fe0c1f4748f89534fd20609c1b303e68f0962d4e43e39ee8e48410cda5ad
7
- data.tar.gz: 7f2e37453b62b9407240cead2ac809d230fe464f2a9ce2a4877262e5b6531384d564fe43ec20089d22b4b9337b3a3c05507eb9d4acc562de1ca7eb11cc533d22
6
+ metadata.gz: 3e297be0fab49c4b4a62ee8b57158a1c325b66c32084b54f9aad6640495ae5432d104f852d3ab16f7bce508c242096772e57957f04766127c961c71eedbbaedd
7
+ data.tar.gz: ec03950b1e5884085491828dc79199958fa76f950c7c79358251a566862de80e8f9777a3c241a6f7b1a782adbb708e818f43d0f1049e51bc1e68617f13d331f1
data/README.md CHANGED
@@ -1,279 +1,130 @@
1
1
  # Pmn.rb
2
2
 
3
- [![Version](https://img.shields.io/github/v/tag/sashite/pmn.rb?label=Version&logo=github)](https://github.com/sashite/pmn.rb/tags)
4
- [![Yard documentation](https://img.shields.io/badge/Yard-documentation-blue.svg?logo=github)](https://rubydoc.info/github/sashite/pmn.rb/main)
5
- ![Ruby](https://github.com/sashite/pmn.rb/actions/workflows/main.yml/badge.svg?branch=main)
6
- [![License](https://img.shields.io/github/license/sashite/pmn.rb?label=License&logo=github)](https://github.com/sashite/pmn.rb/raw/main/LICENSE.md)
3
+ [![Gem Version](https://img.shields.io/gem/v/portable_move_notation.svg?logo=rubygems)](https://rubygems.org/gems/portable_move_notation)
4
+ [![GitHub Version](https://img.shields.io/github/v/tag/sashite/pmn.rb?label=GitHub\&logo=github)](https://github.com/sashite/pmn.rb/tags)
5
+ [![Build](https://github.com/sashite/pmn.rb/actions/workflows/main.yml/badge.svg?branch=main)](https://github.com/sashite/pmn.rb/actions/workflows/main.yml)
6
+ [![Codecov](https://img.shields.io/codecov/c/github/sashite/pmn.rb?logo=codecov)](https://codecov.io/gh/sashite/pmn.rb)
7
+ [![YARD Docs](https://img.shields.io/badge/YARD-Documentation-blue.svg?logo=ruby)](https://rubydoc.info/github/sashite/pmn.rb/main)
8
+ [![License](https://img.shields.io/github/license/sashite/pmn.rb?label=License)](https://github.com/sashite/pmn.rb/raw/main/LICENSE.md)
7
9
 
8
- > **PMN** (Portable Move Notation) support for the Ruby language.
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
- ## What is PMN?
12
+ ---
11
13
 
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.
14
+ ## Why PMN?
13
15
 
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
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 this line to your application's Gemfile:
22
+ Add to your **Gemfile**:
22
23
 
23
24
  ```ruby
25
+ # Gemfile
24
26
  gem "portable_move_notation"
25
27
  ```
26
28
 
27
- And then execute:
29
+ then:
28
30
 
29
- ```sh
31
+ ```bash
30
32
  bundle install
31
33
  ```
32
34
 
33
- Or install it yourself as:
35
+ Or grab it directly:
34
36
 
35
- ```sh
37
+ ```bash
36
38
  gem install portable_move_notation
37
39
  ```
38
40
 
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:
41
+ Require it in your code:
42
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
- ]
43
+ ```ruby
44
+ require "portable_move_notation" # provides the PortableMoveNotation namespace
53
45
  ```
54
46
 
55
- ## Basic Usage
47
+ ---
56
48
 
57
- ### Working with PMN Actions
49
+ ## Quick Start
58
50
 
59
- Create individual actions representing piece movement:
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
- # 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
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
- # Generate the PMN representation
73
- pawn_move.to_h
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
- ### Working with PMN Moves
78
-
79
- Create compound moves consisting of multiple actions:
69
+ Parse it back:
80
70
 
81
71
  ```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}]
72
+ restored = PortableMoveNotation::Move.from_json(move.to_json)
73
+ puts restored.actions.first.dst_square # => 27
107
74
  ```
108
75
 
109
- ### Parsing PMN Data
76
+ ---
110
77
 
111
- Parse PMN data from JSON:
112
-
113
- ```ruby
114
- require "portable_move_notation"
78
+ ## Advanced Examples
115
79
 
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
- ```
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
- move = PortableMoveNotation::Move.new(action)
158
- move.to_json
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
- rook_action = PortableMoveNotation::Action.new(
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
- 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}]
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
- ### Shogi: Dropping a Pawn
96
+ ---
187
97
 
188
- A pawn is dropped onto square 27 from the player's hand:
98
+ ## Validation
189
99
 
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
- ```
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
- action = PortableMoveNotation::Action.new(
209
- src_square: 36,
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 = PortableMoveNotation::Move.new(action)
216
- move.to_json
217
- # => [{"src_square":36,"dst_square":27,"piece_name":"B","piece_hand":"P"}]
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
- ### Chess: En Passant Capture
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
- # 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}]
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
- ## 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)
119
+ ---
272
120
 
273
121
  ## License
274
122
 
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).
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
- This project is maintained by [Sashité](https://sashite.com/) — promoting chess variants and sharing the beauty of Chinese, Japanese, and Western chess cultures.
129
+ *Celebrating the beauty of Chinese, Japanese, and Western chess cultures.*
130
+ Find more projects & research at **[sashite.com](https://sashite.com/)**.
@@ -1,56 +1,76 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PortableMoveNotation
4
- # Represents an atomic action in PMN format
4
+ # == Action
5
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.
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
- # 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)
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
- # @example Basic piece movement
16
- # Action.new(src_square: 52, dst_square: 36, piece_name: "P")
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
- # @example Piece drop (from outside the board)
19
- # Action.new(src_square: nil, dst_square: 27, piece_name: "p")
23
+ # === Examples
20
24
  #
21
- # @example Capture with piece becoming available for dropping
22
- # Action.new(src_square: 33, dst_square: 24, piece_name: "+P", piece_hand: "R")
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
- # @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
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
- # Validates a PMN action hash
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*.)
28
47
  #
29
- # @param action_data [Hash] PMN action data to validate
30
- # @return [Boolean] true if valid, false otherwise
48
+ # @param action_data [Hash] Raw PMN action hash.
49
+ # @return [Boolean] +true+ if the hash can be converted into a valid {Action}.
31
50
  def self.valid?(action_data)
32
51
  return false unless action_data.is_a?(::Hash)
33
52
  return false unless action_data.key?("dst_square") && action_data.key?("piece_name")
34
53
 
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
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
47
63
  end
48
64
 
49
- # Creates an Action instance from parameters
65
+ # Builds an {Action} from keyword parameters.
50
66
  #
51
- # @param params [Hash] Action parameters
52
- # @return [Action] A new action instance
53
- # @raise [KeyError] if required parameters are missing
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.
54
74
  def self.from_params(**params)
55
75
  new(
56
76
  src_square: params[:src_square],
@@ -60,31 +80,31 @@ module PortableMoveNotation
60
80
  )
61
81
  end
62
82
 
63
- # The source coordinate of the action, or nil for drops
64
- # @return [Integer, nil] Source square coordinate
65
- attr_reader :src_square
83
+ # ------------------------------------------------------------------
84
+ # Attributes
85
+ # ------------------------------------------------------------------
66
86
 
67
- # The destination coordinate of the action
68
- # @return [Integer] Destination square coordinate
87
+ # @return [Integer, nil] Source square (or +nil+ for drops)
88
+ attr_reader :src_square
89
+ # @return [Integer] Destination square
69
90
  attr_reader :dst_square
70
-
71
- # The piece identifier in PNN format
72
- # @return [String] Piece name
91
+ # @return [String] Post‑move piece identifier
73
92
  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
93
+ # @return [String, nil] Captured piece that enters hand, or +nil+
77
94
  attr_reader :piece_hand
78
95
 
79
- # Initializes a new action
96
+ # ------------------------------------------------------------------
97
+ # Construction
98
+ # ------------------------------------------------------------------
99
+
100
+ # Instantiates a new {Action}.
80
101
  #
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
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.
86
107
  def initialize(dst_square:, piece_name:, src_square: nil, piece_hand: nil)
87
- # Input validation
88
108
  validate_square(src_square) unless src_square.nil?
89
109
  validate_square(dst_square)
90
110
  validate_piece_name(piece_name)
@@ -98,9 +118,13 @@ module PortableMoveNotation
98
118
  freeze
99
119
  end
100
120
 
101
- # Converts the action to a parameter hash
121
+ # ------------------------------------------------------------------
122
+ # Serialisation helpers
123
+ # ------------------------------------------------------------------
124
+
125
+ # Returns a **symbol‑keyed** parameter hash (useful for duplication).
102
126
  #
103
- # @return [Hash] Parameter hash representation of the action
127
+ # @return [Hash]
104
128
  def to_params
105
129
  {
106
130
  src_square:,
@@ -110,11 +134,9 @@ module PortableMoveNotation
110
134
  }.compact
111
135
  end
112
136
 
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
137
+ # Returns a **string‑keyed** hash that conforms to the PMN JSON schema.
116
138
  #
117
- # @return [Hash] PMN-compatible hash representation
139
+ # @return [Hash]
118
140
  def to_h
119
141
  {
120
142
  "src_square" => src_square,
@@ -126,36 +148,34 @@ module PortableMoveNotation
126
148
 
127
149
  private
128
150
 
129
- # Validates that a square coordinate is a non-negative integer
151
+ # Validates that *square* is a nonnegative integer.
130
152
  #
131
- # @param square [Object] Value to validate
132
- # @raise [ArgumentError] if the value is not a non-negative integer
153
+ # @param square [Object]
154
+ # @raise [ArgumentError] If invalid.
133
155
  def validate_square(square)
134
156
  return if square.is_a?(::Integer) && square >= 0
135
157
 
136
158
  raise ::ArgumentError, "Square must be a non-negative integer"
137
159
  end
138
160
 
139
- # Validates the piece name format according to PNN specification
161
+ # Validates {#piece_name} format.
140
162
  #
141
- # @param piece_name [Object] Piece name to validate
142
- # @raise [ArgumentError] if the format is invalid
163
+ # @param piece_name [Object]
164
+ # @raise [ArgumentError] If invalid.
143
165
  def validate_piece_name(piece_name)
144
- return if piece_name.is_a?(::String) && piece_name.match?(/\A[-+]?[a-zA-Z][=<>]?\z/)
166
+ return if piece_name.is_a?(::String) && piece_name.match?(PIECE_NAME_PATTERN)
145
167
 
146
- raise ::ArgumentError, "Invalid piece_name format: #{piece_name}"
168
+ raise ::ArgumentError, "Invalid piece_name format: #{piece_name.inspect}"
147
169
  end
148
170
 
149
- # Validates the piece hand format according to PNN specification
150
- #
151
- # Piece hand must be a single letter with no modifiers
171
+ # Validates {#piece_hand} format.
152
172
  #
153
- # @param piece_hand [Object] Piece hand value to validate
154
- # @raise [ArgumentError] if the format is invalid
173
+ # @param piece_hand [Object]
174
+ # @raise [ArgumentError] If invalid.
155
175
  def validate_piece_hand(piece_hand)
156
- return if piece_hand.is_a?(::String) && piece_hand.match?(/\A[a-zA-Z]\z/)
176
+ return if piece_hand.is_a?(::String) && piece_hand.match?(/\A[A-Za-z]\z/)
157
177
 
158
- raise ::ArgumentError, "Invalid piece_hand format: #{piece_hand}"
178
+ raise ::ArgumentError, "Invalid piece_hand format: #{piece_hand.inspect}"
159
179
  end
160
180
  end
161
181
  end
@@ -1,129 +1,166 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "json"
4
+
3
5
  module PortableMoveNotation
4
- # Represents a PMN move - a collection of one or more atomic actions
5
- # that together describe a game state transition
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
- # 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.
34
+ # parsed = PortableMoveNotation::Move.from_json(move.to_json)
35
+ # parsed.actions.first.dst_square # => 36
36
+ # ```
10
37
  #
11
- # @example Creating a simple move
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
- # @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)
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
- # @see https://sashite.dev/documents/pmn/1.0.0/ PMN Specification
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
- # Validates a PMN array data structure
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
- # @param pmn_data [Array] PMN data to validate
25
- # @return [Boolean] true if valid, false otherwise
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? { |action_data| Action.valid?(action_data) }
71
+ pmn_data.all? { |hash| Action.valid?(hash) }
30
72
  end
31
73
 
32
- # Creates a Move instance from a JSON string in PMN format
74
+ # Constructs a {Move} from its canonical **JSON** representation.
33
75
  #
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
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
- 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)
85
+ from_pmn(::JSON.parse(json_string))
51
86
  end
52
87
 
53
- # Creates a Move instance from an array of PMN action hashes
88
+ # Constructs a {Move} from an *already parsed* PMN array.
54
89
  #
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
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 |action_data|
94
+ actions = pmn_array.map do |hash|
60
95
  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"]
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
- # Creates a Move instance from a parameters hash
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
- # @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
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
- # The list of actions that compose this move
90
- #
91
- # @return [Array<Action>] List of action objects (frozen)
121
+ # --------------------------------------------------------------------
122
+ # Attributes & construction
123
+ # --------------------------------------------------------------------
124
+
125
+ # @return [Array<Action>] Ordered, frozen list of actions.
92
126
  attr_reader :actions
93
127
 
94
- # Initializes a new move with the given actions
128
+ # Creates a new {Move}.
95
129
  #
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
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(*actions)
133
+ validate_actions(actions)
100
134
  @actions = actions.freeze
101
-
102
135
  freeze
103
136
  end
104
137
 
105
- # Converts the move to PMN format (array of hashes)
138
+ # --------------------------------------------------------------------
139
+ # Serialisation helpers
140
+ # --------------------------------------------------------------------
141
+
142
+ # Converts the move to an **array of PMN hashes** (string keys).
106
143
  #
107
- # @return [Array<Hash>] PMN representation of the move
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] JSON string of the move in PMN format
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
- # Validates the actions array
158
+ # Ensures +actions+ is a non‑empty array of {Action} instances.
122
159
  #
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)
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.0.0
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: 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.
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: PMN (Portable Move Notation) support for the Ruby language.
53
+ summary: A pure Ruby implementation of Portable Move Notation (PMN) for abstract strategy
54
+ board games.
53
55
  test_files: []