portable_move_notation 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bae8f26119505e33c79cbf389b10c8f737a60aca132e4c39b7f1b0dcd820e2c9
4
- data.tar.gz: 735910e4fa03d366cb8d7a63889637a3e3bc4d360e168a2fbe85d28416ae33cb
3
+ metadata.gz: 9133acb705a120376e5045428a24e3625ff82641c1628b5885feabe69d00259a
4
+ data.tar.gz: f898676def7e06bc14df0a998a28eac970413a0793e5b43bd16b67d8fda23312
5
5
  SHA512:
6
- metadata.gz: 91d8644574aad3f7b1224198cc8505aad36c35380bafe257640add785baa273237616187f6c93fa9db0433edc6436483106be7396fa68f33921ca4264f3bfdb1
7
- data.tar.gz: 1271f9c59dcff69bdf8a305b4f03daa4cc71982d5e92db7fd28fac6e2ee8d3663b9a63d989db076a7c14d7c544b9b92c212d0ca3b4624ba86c203fe64db5d08f
6
+ metadata.gz: ee79ca0d901bc10a36fffd1d7a6af726fdfa4d73ccb53318722c3c73d71da51f2893f4e7a4e597e239cabdedf87faa98ff92d0f76527e0632c00de44f1083937
7
+ data.tar.gz: bd11df77a4ef1d2e6b96d4c648e65609630d711bb128a344cac7d7990b5b45cb7e398da3c978035c34d1fd22969406d40355fe04c00ca52e9a9a26ff6bd80746
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: "e1", dst_square: "g1", 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: "h1", dst_square: "f1", 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: 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" => "e4", "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,62 +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` | String label of the square *vacated* – or +nil+ when dropping |
13
+ # | `dst_square` | String label 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: "e2", dst_square: "e4", 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
- # Regular expression pattern for validating PNN piece names
28
- # Format: [prefix]letter[suffix]
29
- # - prefix: optional '+' or '-'
30
- # - letter: required 'a-z' or 'A-Z'
31
- # - suffix: optional "'"
32
- PIECE_NAME_PATTERN = /\A[-+]?[a-zA-Z][']?\z/
33
- # 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*.)
34
47
  #
35
- # @param action_data [Hash] PMN action data to validate
36
- # @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}.
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
- begin
42
- # Use existing validation logic by attempting to create an instance
43
- new(
44
- src_square: action_data["src_square"],
45
- dst_square: action_data["dst_square"],
46
- piece_name: action_data["piece_name"],
47
- piece_hand: action_data["piece_hand"]
48
- )
49
- true
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
- # Creates an Action instance from parameters
65
+ # Builds an {Action} from keyword parameters.
56
66
  #
57
- # @param params [Hash] Action parameters
58
- # @return [Action] A new action instance
59
- # @raise [KeyError] if required parameters are missing
67
+ # @param params [Hash] Keyword parameters.
68
+ # @option params [String, nil] :src_square Source coordinate, or +nil+ when dropping from hand.
69
+ # @option params [String] :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
- # The source coordinate of the action, or nil for drops
70
- # @return [Integer, nil] Source square coordinate
71
- attr_reader :src_square
83
+ # ------------------------------------------------------------------
84
+ # Attributes
85
+ # ------------------------------------------------------------------
72
86
 
73
- # The destination coordinate of the action
74
- # @return [Integer] Destination square coordinate
87
+ # @return [String, nil] Source square (or +nil+ for drops)
88
+ attr_reader :src_square
89
+ # @return [String] 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
- # Initializes a new action
96
+ # ------------------------------------------------------------------
97
+ # Construction
98
+ # ------------------------------------------------------------------
99
+
100
+ # Instantiates a new {Action}.
86
101
  #
87
- # @param src_square [Integer, nil] Source square (nil for placements from outside the board)
88
- # @param dst_square [Integer] Destination square (required)
89
- # @param piece_name [String] Piece identifier in PNN format (required)
90
- # @param piece_hand [String, nil] Captured piece identifier that becomes droppable, or nil
91
- # @raise [ArgumentError] if any validation fails
102
+ # @param dst_square [String] Destination coordinate.
103
+ # @param piece_name [String] Post‑move piece identifier.
104
+ # @param src_square [String, 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
- # Converts the action to a parameter hash
121
+ # ------------------------------------------------------------------
122
+ # Serialisation helpers
123
+ # ------------------------------------------------------------------
124
+
125
+ # Returns a **symbol‑keyed** parameter hash (useful for duplication).
108
126
  #
109
- # @return [Hash] Parameter hash representation of the action
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
- # Converts the action to a PMN-compatible hash
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] PMN-compatible hash representation
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 a square coordinate is a non-negative integer
151
+ # Validates that *square* is a non‑empty string.
136
152
  #
137
- # @param square [Object] Value to validate
138
- # @raise [ArgumentError] if the value is not a non-negative integer
153
+ # @param square [Object]
154
+ # @raise [ArgumentError] If invalid.
139
155
  def validate_square(square)
140
- return if square.is_a?(::Integer) && square >= 0
156
+ return if square.is_a?(::String) && !square.empty?
141
157
 
142
- raise ::ArgumentError, "Square must be a non-negative integer"
158
+ raise ::ArgumentError, "Square must be a non-empty string"
143
159
  end
144
160
 
145
- # Validates the piece name format according to PNN specification
161
+ # Validates {#piece_name} format.
146
162
  #
147
- # @param piece_name [Object] Piece name to validate
148
- # @raise [ArgumentError] if the format is invalid
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 the piece hand format according to PNN specification
156
- #
157
- # Piece hand must be a single letter with no modifiers
171
+ # Validates {#piece_hand} format.
158
172
  #
159
- # @param piece_hand [Object] Piece hand value to validate
160
- # @raise [ArgumentError] if the format is invalid
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[a-zA-Z]\z/)
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
- # 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: "e2",
26
+ # dst_square: "e4",
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 # => "e4"
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: "e1", dst_square: "g1", piece_name: "K"
43
+ # )
44
+ # rook = PortableMoveNotation::Action.new(
45
+ # src_square: "h1", dst_square: "f1", 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":"e7","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":null,"dst_square":"e7","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: "e7", 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.1.0
4
+ version: 2.2.0
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: []