portable_move_notation 2.2.0 → 3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9133acb705a120376e5045428a24e3625ff82641c1628b5885feabe69d00259a
4
- data.tar.gz: f898676def7e06bc14df0a998a28eac970413a0793e5b43bd16b67d8fda23312
3
+ metadata.gz: 9f3e75f9a5f80aaa8aeb581c90b4b20d318010cef492504174d970fe1386dc82
4
+ data.tar.gz: 4f5c513685589d247c8f9fa9fb5d98f353489b83c06d33601b46d940956e0fb6
5
5
  SHA512:
6
- metadata.gz: ee79ca0d901bc10a36fffd1d7a6af726fdfa4d73ccb53318722c3c73d71da51f2893f4e7a4e597e239cabdedf87faa98ff92d0f76527e0632c00de44f1083937
7
- data.tar.gz: bd11df77a4ef1d2e6b96d4c648e65609630d711bb128a344cac7d7990b5b45cb7e398da3c978035c34d1fd22969406d40355fe04c00ca52e9a9a26ff6bd80746
6
+ metadata.gz: 2b7a3c4f1a55ce2bd5a98a2770433962df67cf5649581db47abd13e433afe7eb4afdb26d68186fc133bb416c7a689044a0db0de7b53d0f4cda5c48d7853c3cd8
7
+ data.tar.gz: b5ccec6fb58bbb550fdf75df81ba692442799393d802372cc2e158ca44342659e2e0b14b9703810b8859354e1f64ae5566bc739b081d0b855374178dc31b1a1f
data/README.md CHANGED
@@ -13,7 +13,9 @@
13
13
 
14
14
  ## Why PMN?
15
15
 
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.
16
+ PMN expresses **state‑changing actions** using a simple, deterministic array format without embedding game rules. Whether you are writing a Chess engine, a Shogi server, or a hybrid variant, PMN gives you a compact, game‑neutral core that travels well across languages and databases.
17
+
18
+ Each action is represented as a 4-element array: `[source_square, destination_square, piece_name, captured_piece]`.
17
19
 
18
20
  ---
19
21
 
@@ -48,29 +50,24 @@ require "portable_move_notation" # provides the PortableMoveNotation namespace
48
50
 
49
51
  ## Quick Start
50
52
 
51
- Dump a single action (dropping a Shogi pawn on square "27"):
53
+ Create a simple pawn move (e2 to e4):
52
54
 
53
55
  ```ruby
54
56
  require "portable_move_notation"
55
57
 
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
- )
63
- )
58
+ # Create an action using the array format: [source, destination, piece, captured]
59
+ action = PortableMoveNotation::Action.new("e2", "e4", "P", nil)
60
+ move = PortableMoveNotation::Move.new(action)
64
61
 
65
62
  puts move.to_json
66
- # Output: A JSON array with the move data
63
+ # Output: [["e2","e4","P",null]]
67
64
  ```
68
65
 
69
66
  Parse it back:
70
67
 
71
68
  ```ruby
72
69
  restored = PortableMoveNotation::Move.from_json(move.to_json)
73
- puts restored.actions.first.dst_square # => "27"
70
+ restored.actions.first.dst_square # => "e4"
74
71
  ```
75
72
 
76
73
  ---
@@ -82,49 +79,154 @@ puts restored.actions.first.dst_square # => "27"
82
79
  ```ruby
83
80
  require "portable_move_notation"
84
81
 
85
- king = PortableMoveNotation::Action.new(
86
- src_square: "e1", dst_square: "g1", piece_name: "K", piece_hand: nil
87
- )
88
- rook = PortableMoveNotation::Action.new(
89
- src_square: "h1", dst_square: "f1", piece_name: "R", piece_hand: nil
90
- )
82
+ # Two separate actions for castling
83
+ king_move = PortableMoveNotation::Action.new("e1", "g1", "K", nil)
84
+ rook_move = PortableMoveNotation::Action.new("h1", "f1", "R", nil)
85
+
86
+ castling = PortableMoveNotation::Move.new(king_move, rook_move)
87
+ puts castling.to_json
88
+ # Output: [["e1","g1","K",null],["h1","f1","R",null]]
89
+ ```
90
+
91
+ ### Shogi · Drop from Hand
92
+
93
+ ```ruby
94
+ # Drop a pawn onto square 27 (source is nil for drops)
95
+ drop = PortableMoveNotation::Action.new(nil, "27", "p", nil)
96
+ move = PortableMoveNotation::Move.new(drop)
97
+
98
+ puts move.to_json
99
+ # Output: [[null,"27","p",null]]
100
+ ```
91
101
 
92
- puts PortableMoveNotation::Move.new(king, rook).to_json
93
- # Output: A JSON array containing both king and rook move data
102
+ ### Chess · En Passant Capture
103
+
104
+ ```ruby
105
+ # En passant involves removing the captured pawn from its square
106
+ capture_move = PortableMoveNotation::Action.new("d4", "e3", "p", nil)
107
+ remove_pawn = PortableMoveNotation::Action.new("e4", "e4", nil, "P")
108
+
109
+ en_passant = PortableMoveNotation::Move.new(capture_move, remove_pawn)
110
+ puts en_passant.to_json
111
+ # Output: [["d4","e3","p",null],["e4","e4",null,"P"]]
94
112
  ```
95
113
 
114
+ ### Shogi · Promotion with Capture
115
+
116
+ ```ruby
117
+ # Bishop captures a promoted pawn and promotes itself
118
+ promote_capture = PortableMoveNotation::Action.new("36", "27", "+B", "P")
119
+ move = PortableMoveNotation::Move.new(promote_capture)
120
+
121
+ puts move.to_json
122
+ # Output: [["36","27","+B","P"]]
123
+ ```
124
+
125
+ ---
126
+
127
+ ## Core Concepts
128
+
129
+ ### Action Format
130
+
131
+ Each action is a 4-element array representing:
132
+
133
+ 1. **Source square** (`String` or `nil`) - Where the piece comes from (`nil` for drops)
134
+ 2. **Destination square** (`String`) - Where the piece ends up (required)
135
+ 3. **Piece name** (`String`) - What sits on the destination after the action
136
+ 4. **Captured piece** (`String` or `nil`) - What enters the mover's reserve
137
+
138
+ ### Semantic Side Effects
139
+
140
+ Every action produces deterministic changes:
141
+
142
+ - **Board Removal**: Source square becomes empty (if not `nil`)
143
+ - **Board Placement**: Destination square contains the piece
144
+ - **Hand Addition**: Captured piece enters the mover's reserve (if not `nil`)
145
+ - **Hand Removal**: For drops (`source` is `nil`), remove piece from hand
146
+
96
147
  ---
97
148
 
98
149
  ## Validation
99
150
 
100
- `PortableMoveNotation::Move.valid?(data)` checks **shape compliance** against the spec — not game legality:
151
+ The library validates PMN structure but **not game legality**:
101
152
 
102
153
  ```ruby
103
154
  require "portable_move_notation"
104
155
  require "json"
105
156
 
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
157
+ # Valid PMN structure
158
+ pmn_data = [["e2", "e4", "P", nil]]
159
+ puts PortableMoveNotation::Move.valid?(pmn_data) # => true
160
+
161
+ # Parse and validate
162
+ move = PortableMoveNotation::Move.from_pmn(pmn_data)
163
+ puts move.actions.size # => 1
109
164
  ```
110
165
 
111
- You can also validate single actions:
166
+ Individual action validation:
112
167
 
113
168
  ```ruby
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
169
+ # Check array format compliance
170
+ action_array = ["e2", "e4", "P", nil]
171
+ puts PortableMoveNotation::Action.valid?(action_array) # => true
117
172
  ```
118
173
 
119
174
  ---
120
175
 
121
- ## License
176
+ ## Piece Notation
122
177
 
123
- The [gem](https://rubygems.org/gems/portable_move_notation) is released under the [MIT License](https://opensource.org/licenses/MIT).
178
+ PMN is agnostic to piece notation systems. This implementation supports any UTF-8 string for piece identifiers:
179
+
180
+ - **Traditional**: `"K"`, `"Q"`, `"R"`, `"B"`, `"N"`, `"P"`
181
+ - **Descriptive**: `"WhiteKing"`, `"BlackQueen"`
182
+ - **Shogi**: `"p"`, `"+P"`, `"B'"`
183
+ - **Custom**: `"MagicDragon_powered"`, `"42"`
184
+
185
+ ---
186
+
187
+ ## JSON Schema Compliance
188
+
189
+ All output conforms to the official PMN JSON Schema:
190
+
191
+ - **Schema URL**: [`https://sashite.dev/schemas/pmn/1.0.0/schema.json`](https://sashite.dev/schemas/pmn/1.0.0/schema.json)
192
+ - **Format**: Array of 4-element arrays
193
+ - **Types**: `[string|null, string, string, string|null]`
194
+
195
+ ---
196
+
197
+ ## Implementation Notes
198
+
199
+ ### Performance
200
+
201
+ - Actions are immutable (frozen) after creation
202
+ - Moves are lightweight containers for action arrays
203
+ - JSON serialization follows the official schema exactly
204
+
205
+ ### Thread Safety
206
+
207
+ All objects are immutable after construction, making them thread-safe by design.
208
+
209
+ ### Error Handling
210
+
211
+ - `ArgumentError` for malformed data during construction
212
+ - `JSON::ParserError` for invalid JSON input
213
+ - `KeyError` for missing required elements
124
214
 
125
215
  ---
126
216
 
217
+ ## Related Specifications
218
+
219
+ - [Portable Move Notation (PMN)](https://sashite.dev/documents/pmn/1.0.0/) — This specification
220
+ - [Piece Name Notation (PNN)](https://sashite.dev/documents/pnn/1.0.0/) — Piece identifier format
221
+ - [General Actor Notation (GAN)](https://sashite.dev/documents/gan/1.0.0/) — Game-qualified pieces
222
+ - [Forsyth‑Edwards Enhanced Notation (FEEN)](https://sashite.dev/documents/feen/1.0.0/) — Static positions
223
+
224
+ ---
225
+
226
+ ## License
227
+
228
+ The [gem](https://rubygems.org/gems/portable_move_notation) is released under the [MIT License](https://opensource.org/licenses/MIT).
229
+
127
230
  ## About Sashité
128
231
 
129
- *Celebrating the beauty of Chinese, Japanese, and Western chess cultures.*
130
- Find more projects & research at **[sashite.com](https://sashite.com/)**.
232
+ This project is maintained by [Sashité](https://sashite.com/) — promoting chess variants and sharing the beauty of Chinese, Japanese, and Western chess cultures.
@@ -3,117 +3,115 @@
3
3
  module PortableMoveNotation
4
4
  # == Action
5
5
  #
6
- # An **Action** is the *atomic* unit of Portable Move Notation. Each instance
6
+ # An **Action** is the *atomic* unit of Portable Move Notation. Each instance
7
7
  # describes **one** deterministic transformation applied to either the board
8
- # or the mover's reserve.
8
+ # or the mover's reserve using the PMN v1.0.0 array format.
9
9
  #
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+ |
10
+ # PMN v1.0.0 uses a 4-element array format:
11
+ # `[source_square, destination_square, piece_name, captured_piece]`
12
+ #
13
+ # | Index | Field | Type | Meaning |
14
+ # |-------|------------------|----------------|------------------------------------------------------------|
15
+ # | 0 | `src_square` | String or nil | Square vacated (nil when dropping from hand) |
16
+ # | 1 | `dst_square` | String | Square now occupied by piece_name |
17
+ # | 2 | `piece_name` | String | Post-action piece identifier (may contain modifiers) |
18
+ # | 3 | `captured_piece` | String or nil | Piece entering mover's reserve (nil if nothing captured) |
16
19
  #
17
20
  # 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.
21
+ # * `src_square` (when not nil) becomes empty.
22
+ # * `dst_square` now contains `piece_name`.
23
+ # * If `captured_piece` is set, add exactly one such piece to the mover's reserve.
24
+ # * If `src_square` is nil, remove one copy of `piece_name` from hand.
22
25
  #
23
26
  # === Examples
24
27
  #
25
28
  # @example Basic piece movement (Chess pawn e2 → e4)
26
- # PortableMoveNotation::Action.new(src_square: "e2", dst_square: "e4", piece_name: "P")
29
+ # PortableMoveNotation::Action.new("e2", "e4", "P", nil)
27
30
  #
28
31
  # @example Drop from hand (Shogi pawn onto 27)
29
- # PortableMoveNotation::Action.new(src_square: nil, dst_square: "27", piece_name: "p")
32
+ # PortableMoveNotation::Action.new(nil, "27", "p", nil)
30
33
  #
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")
34
+ # @example Capture with promotion (Bishop captures +p, promotes, P enters hand)
35
+ # PortableMoveNotation::Action.new("36", "27", "+B", "P")
33
36
  #
34
- # @see https://sashite.dev/documents/pmn/ Portable Move Notation specification
35
- # @see https://sashite.dev/documents/pnn/ Piece Name Notation specification
37
+ # @see https://sashite.dev/documents/pmn/1.0.0/ Portable Move Notation specification
36
38
  class Action
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
39
  # ------------------------------------------------------------------
42
40
  # Class helpers
43
41
  # ------------------------------------------------------------------
44
42
 
45
- # Validates that *action_data* is a structurally correct PMN **action hash**.
46
- # (Keys are expected to be *strings*.)
43
+ # Validates that *action_data* is a structurally correct PMN **action array**.
44
+ # Expects a 4-element array following the PMN v1.0.0 format.
47
45
  #
48
- # @param action_data [Hash] Raw PMN action hash.
49
- # @return [Boolean] +true+ if the hash can be converted into a valid {Action}.
46
+ # @param action_data [Array] Raw PMN action array.
47
+ # @return [Boolean] +true+ if the array can be converted into a valid {Action}.
50
48
  def self.valid?(action_data)
51
- return false unless action_data.is_a?(::Hash)
52
- return false unless action_data.key?("dst_square") && action_data.key?("piece_name")
53
-
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
- )
49
+ return false unless action_data.is_a?(::Array)
50
+ return false unless action_data.size == 4
51
+
52
+ src_square, dst_square, piece_name, captured_piece = action_data
53
+
54
+ # Validate dst_square and piece_name are non-empty strings
55
+ return false unless dst_square.is_a?(::String) && !dst_square.empty?
56
+ return false unless piece_name.is_a?(::String) && !piece_name.empty?
57
+
58
+ # Validate src_square is either nil or non-empty string
59
+ return false unless src_square.nil? || (src_square.is_a?(::String) && !src_square.empty?)
60
+
61
+ # Validate captured_piece is either nil or non-empty string
62
+ return false unless captured_piece.nil? || (captured_piece.is_a?(::String) && !captured_piece.empty?)
63
+
60
64
  true
61
- rescue ::ArgumentError
65
+ rescue StandardError
62
66
  false
63
67
  end
64
68
 
65
- # Builds an {Action} from keyword parameters.
69
+ # Builds an {Action} from an array following PMN v1.0.0 format.
66
70
  #
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.
71
+ # @param action_array [Array] 4-element array [src_square, dst_square, piece_name, captured_piece]
72
72
  # @return [Action]
73
- # @raise [KeyError] If +:dst_square+ or +:piece_name+ is missing.
74
- def self.from_params(**params)
75
- new(
76
- src_square: params[:src_square],
77
- dst_square: params.fetch(:dst_square),
78
- piece_name: params.fetch(:piece_name),
79
- piece_hand: params[:piece_hand]
80
- )
73
+ # @raise [ArgumentError] If array format is invalid.
74
+ def self.from_array(action_array)
75
+ raise ArgumentError, "Expected 4-element array" unless action_array.is_a?(::Array) && action_array.size == 4
76
+
77
+ src_square, dst_square, piece_name, captured_piece = action_array
78
+ new(src_square, dst_square, piece_name, captured_piece)
81
79
  end
82
80
 
83
81
  # ------------------------------------------------------------------
84
82
  # Attributes
85
83
  # ------------------------------------------------------------------
86
84
 
87
- # @return [String, nil] Source square (or +nil+ for drops)
85
+ # @return [String, nil] Source square (or nil for drops)
88
86
  attr_reader :src_square
89
87
  # @return [String] Destination square
90
88
  attr_reader :dst_square
91
- # @return [String] Post‑move piece identifier
89
+ # @return [String] Post‑action piece identifier
92
90
  attr_reader :piece_name
93
- # @return [String, nil] Captured piece that enters hand, or +nil+
94
- attr_reader :piece_hand
91
+ # @return [String, nil] Captured piece that enters hand, or nil
92
+ attr_reader :captured_piece
95
93
 
96
94
  # ------------------------------------------------------------------
97
95
  # Construction
98
96
  # ------------------------------------------------------------------
99
97
 
100
- # Instantiates a new {Action}.
98
+ # Instantiates a new {Action} using PMN v1.0.0 array semantics.
101
99
  #
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.
100
+ # @param src_square [String, nil] Source coordinate or nil for drops.
101
+ # @param dst_square [String] Destination coordinate (required).
102
+ # @param piece_name [String] Post‑action piece identifier (required).
103
+ # @param captured_piece [String, nil] Captured piece entering hand.
106
104
  # @raise [ArgumentError] If any value fails validation.
107
- def initialize(dst_square:, piece_name:, src_square: nil, piece_hand: nil)
105
+ def initialize(src_square, dst_square, piece_name, captured_piece)
108
106
  validate_square(src_square) unless src_square.nil?
109
107
  validate_square(dst_square)
110
108
  validate_piece_name(piece_name)
111
- validate_piece_hand(piece_hand) unless piece_hand.nil?
109
+ validate_captured_piece(captured_piece) unless captured_piece.nil?
112
110
 
113
111
  @src_square = src_square
114
112
  @dst_square = dst_square
115
113
  @piece_name = piece_name
116
- @piece_hand = piece_hand
114
+ @captured_piece = captured_piece
117
115
 
118
116
  freeze
119
117
  end
@@ -122,28 +120,42 @@ module PortableMoveNotation
122
120
  # Serialisation helpers
123
121
  # ------------------------------------------------------------------
124
122
 
125
- # Returns a **symbol‑keyed** parameter hash (useful for duplication).
123
+ # Returns the PMN v1.0.0 array representation.
124
+ # This is the canonical format for JSON serialization.
126
125
  #
127
- # @return [Hash]
128
- def to_params
129
- {
130
- src_square:,
131
- dst_square:,
132
- piece_name:,
133
- piece_hand:
134
- }.compact
126
+ # @return [Array] 4-element array [src_square, dst_square, piece_name, captured_piece]
127
+ def to_a
128
+ [src_square, dst_square, piece_name, captured_piece]
135
129
  end
136
130
 
137
- # Returns a **string‑keyed** hash that conforms to the PMN JSON schema.
138
- #
139
- # @return [Hash]
140
- def to_h
141
- {
142
- "src_square" => src_square,
143
- "dst_square" => dst_square,
144
- "piece_name" => piece_name,
145
- "piece_hand" => piece_hand
146
- }
131
+ # Alias for to_a for backward compatibility
132
+ alias to_pmn to_a
133
+
134
+ # ------------------------------------------------------------------
135
+ # Comparison and inspection
136
+ # ------------------------------------------------------------------
137
+
138
+ # Compare actions based on their array representation
139
+ def ==(other)
140
+ other.is_a?(Action) && to_a == other.to_a
141
+ end
142
+
143
+ # Hash based on array representation
144
+ def hash
145
+ to_a.hash
146
+ end
147
+
148
+ def eql?(other)
149
+ self == other
150
+ end
151
+
152
+ # Human-readable string representation
153
+ def inspect
154
+ "#<#{self.class.name} #{to_a.inspect}>"
155
+ end
156
+
157
+ def to_s
158
+ to_a.to_s
147
159
  end
148
160
 
149
161
  private
@@ -155,27 +167,29 @@ module PortableMoveNotation
155
167
  def validate_square(square)
156
168
  return if square.is_a?(::String) && !square.empty?
157
169
 
158
- raise ::ArgumentError, "Square must be a non-empty string"
170
+ raise ::ArgumentError, "Square must be a non-empty string, got #{square.inspect}"
159
171
  end
160
172
 
161
- # Validates {#piece_name} format.
173
+ # Validates piece_name format.
174
+ # PMN v1.0.0 allows any non-empty UTF-8 string for piece identifiers.
162
175
  #
163
176
  # @param piece_name [Object]
164
177
  # @raise [ArgumentError] If invalid.
165
178
  def validate_piece_name(piece_name)
166
- return if piece_name.is_a?(::String) && piece_name.match?(PIECE_NAME_PATTERN)
179
+ return if piece_name.is_a?(::String) && !piece_name.empty?
167
180
 
168
- raise ::ArgumentError, "Invalid piece_name format: #{piece_name.inspect}"
181
+ raise ::ArgumentError, "Piece name must be a non-empty string, got #{piece_name.inspect}"
169
182
  end
170
183
 
171
- # Validates {#piece_hand} format.
184
+ # Validates captured_piece format.
185
+ # PMN v1.0.0 allows any non-empty UTF-8 string for piece identifiers.
172
186
  #
173
- # @param piece_hand [Object]
187
+ # @param captured_piece [Object]
174
188
  # @raise [ArgumentError] If invalid.
175
- def validate_piece_hand(piece_hand)
176
- return if piece_hand.is_a?(::String) && piece_hand.match?(/\A[A-Za-z]\z/)
189
+ def validate_captured_piece(captured_piece)
190
+ return if captured_piece.is_a?(::String) && !captured_piece.empty?
177
191
 
178
- raise ::ArgumentError, "Invalid piece_hand format: #{piece_hand.inspect}"
192
+ raise ::ArgumentError, "Captured piece must be a non-empty string, got #{captured_piece.inspect}"
179
193
  end
180
194
  end
181
195
  end
@@ -7,11 +7,14 @@ module PortableMoveNotation
7
7
  #
8
8
  # A **Move** is an *ordered list* of {Action} instances that, applied **in
9
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.
10
+ # Notation (PMN) v1.0.0. A move can be as small as a single pawn push or as
11
+ # large as a compound fairy‐move that relocates several pieces at once.
12
+ #
13
+ # PMN v1.0.0 uses an array-of-arrays format where each inner array represents
14
+ # a single action: `[source_square, destination_square, piece_name, captured_piece]`
12
15
  #
13
16
  # 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
17
+ # underlying data *matches the PMN schema*. Whether the move is *legal* in a
15
18
  # given game is beyond its responsibility and must be enforced by an engine
16
19
  # or referee layer.
17
20
  #
@@ -21,15 +24,10 @@ module PortableMoveNotation
21
24
  # require "portable_move_notation"
22
25
  #
23
26
  # # 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
- #
27
+ # pawn = PortableMoveNotation::Action.new("e2", "e4", "P", nil)
30
28
  # move = PortableMoveNotation::Move.new(pawn)
31
29
  # puts move.to_json
32
- # # => JSON representation of the move
30
+ # # => [["e2","e4","P",null]]
33
31
  #
34
32
  # parsed = PortableMoveNotation::Move.from_json(move.to_json)
35
33
  # parsed.actions.first.dst_square # => "e4"
@@ -38,48 +36,42 @@ module PortableMoveNotation
38
36
  # === Composite example (Chess kingside castling)
39
37
  #
40
38
  # ```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
- # )
47
- #
39
+ # king = PortableMoveNotation::Action.new("e1", "g1", "K", nil)
40
+ # rook = PortableMoveNotation::Action.new("h1", "f1", "R", nil)
48
41
  # castle = PortableMoveNotation::Move.new(king, rook)
49
42
  # ```
50
43
  #
51
- # @see https://sashite.dev/documents/pmn/ Portable Move Notation specification
44
+ # @see https://sashite.dev/documents/pmn/1.0.0/ Portable Move Notation specification
52
45
  class Move
53
46
  # --------------------------------------------------------------------
54
47
  # Class helpers
55
48
  # --------------------------------------------------------------------
56
49
 
57
- # Validates that *pmn_data* is an **array of PMN action hashes**.
50
+ # Validates that *pmn_data* is an **array of PMN action arrays**.
58
51
  # The method does **not** instantiate {Action} objects on success; it merely
59
52
  # checks that each element *could* be turned into one.
60
53
  #
61
- # @param pmn_data [Array<Hash>] Raw PMN structure (commonly the result of
62
- # `JSON.parse`).
54
+ # @param pmn_data [Array<Array>] Raw PMN structure (array of 4-element arrays).
63
55
  # @return [Boolean] +true+ when every element passes {Action.valid?}.
64
56
  #
65
57
  # @example Validate PMN parsed from JSON
66
- # data = JSON.parse('[{"dst_square":"e7","piece_name":"p"}]')
58
+ # data = JSON.parse('[["e2","e4","P",null]]')
67
59
  # PortableMoveNotation::Move.valid?(data) # => true
68
60
  def self.valid?(pmn_data)
69
61
  return false unless pmn_data.is_a?(::Array) && !pmn_data.empty?
70
62
 
71
- pmn_data.all? { |hash| Action.valid?(hash) }
63
+ pmn_data.all? { |action_array| Action.valid?(action_array) }
72
64
  end
73
65
 
74
66
  # Constructs a {Move} from its canonical **JSON** representation.
75
67
  #
76
- # @param json_string [String] PMN‑formatted JSON.
68
+ # @param json_string [String] PMN‑formatted JSON (array of action arrays).
77
69
  # @return [Move]
78
70
  # @raise [JSON::ParserError] If +json_string+ is not valid JSON.
79
- # @raise [KeyError] If an action hash lacks required keys.
71
+ # @raise [ArgumentError] If an action array is malformed.
80
72
  #
81
73
  # @example
82
- # json = '[{"src_square":null,"dst_square":"e7","piece_name":"p"}]'
74
+ # json = '[["e2","e4","P",null]]'
83
75
  # PortableMoveNotation::Move.from_json(json)
84
76
  def self.from_json(json_string)
85
77
  from_pmn(::JSON.parse(json_string))
@@ -87,37 +79,16 @@ module PortableMoveNotation
87
79
 
88
80
  # Constructs a {Move} from an *already parsed* PMN array.
89
81
  #
90
- # @param pmn_array [Array<Hash>] PMN action hashes (string keys).
82
+ # @param pmn_array [Array<Array>] PMN action arrays (4-element arrays).
91
83
  # @return [Move]
92
- # @raise [KeyError] If an action hash lacks required keys.
84
+ # @raise [ArgumentError] If an action array is malformed.
93
85
  def self.from_pmn(pmn_array)
94
- actions = pmn_array.map do |hash|
95
- Action.new(
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"]
100
- )
86
+ actions = pmn_array.map do |action_array|
87
+ Action.from_array(action_array)
101
88
  end
102
89
  new(*actions)
103
90
  end
104
91
 
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.
111
- #
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)
117
- end
118
- new(*array)
119
- end
120
-
121
92
  # --------------------------------------------------------------------
122
93
  # Attributes & construction
123
94
  # --------------------------------------------------------------------
@@ -139,20 +110,75 @@ module PortableMoveNotation
139
110
  # Serialisation helpers
140
111
  # --------------------------------------------------------------------
141
112
 
142
- # Converts the move to an **array of PMN hashes** (string keys).
113
+ # Converts the move to an **array of PMN action arrays**.
114
+ # This is the canonical PMN v1.0.0 format.
143
115
  #
144
- # @return [Array<Hash>]
116
+ # @return [Array<Array>] Array of 4-element action arrays
145
117
  def to_pmn
146
- actions.map(&:to_h)
118
+ actions.map(&:to_a)
147
119
  end
148
120
 
149
- # Converts the move to a **JSON string**.
121
+ # Alias for to_pmn for clarity
122
+ alias to_a to_pmn
123
+
124
+ # Converts the move to a **JSON string** following PMN v1.0.0 format.
150
125
  #
151
- # @return [String]
126
+ # @return [String] JSON representation as array of action arrays
152
127
  def to_json(*_args)
153
128
  ::JSON.generate(to_pmn)
154
129
  end
155
130
 
131
+ # --------------------------------------------------------------------
132
+ # Comparison and inspection
133
+ # --------------------------------------------------------------------
134
+
135
+ # Compare moves based on their PMN array representation
136
+ def ==(other)
137
+ other.is_a?(Move) && to_pmn == other.to_pmn
138
+ end
139
+
140
+ # Hash based on PMN array representation
141
+ def hash
142
+ to_pmn.hash
143
+ end
144
+
145
+ def eql?(other)
146
+ self == other
147
+ end
148
+
149
+ # Human-readable string representation
150
+ def inspect
151
+ "#<#{self.class.name} #{to_pmn.inspect}>"
152
+ end
153
+
154
+ def to_s
155
+ to_pmn.to_s
156
+ end
157
+
158
+ # --------------------------------------------------------------------
159
+ # Utility methods
160
+ # --------------------------------------------------------------------
161
+
162
+ # Number of actions in this move
163
+ def size
164
+ actions.size
165
+ end
166
+
167
+ # Check if move is empty (shouldn't happen with current validation)
168
+ def empty?
169
+ actions.empty?
170
+ end
171
+
172
+ # Iterate over actions
173
+ def each(&)
174
+ actions.each(&)
175
+ end
176
+
177
+ # Access individual actions by index
178
+ def [](index)
179
+ actions[index]
180
+ end
181
+
156
182
  private
157
183
 
158
184
  # Ensures +actions+ is a non‑empty array of {Action} instances.
@@ -2,8 +2,44 @@
2
2
 
3
3
  # Portable Move Notation module
4
4
  #
5
- # @see https://sashite.dev/documents/pmn/1.0.0/
5
+ # PMN v1.0.0 implementation providing rule-agnostic representation of
6
+ # state-changing actions in abstract strategy board games.
7
+ #
8
+ # This implementation follows the PMN v1.0.0 specification which uses
9
+ # an array-of-arrays format: each action is a 4-element array containing
10
+ # [source_square, destination_square, piece_name, captured_piece].
11
+ #
12
+ # @see https://sashite.dev/documents/pmn/1.0.0/ PMN v1.0.0 Specification
13
+ # @see https://sashite.dev/schemas/pmn/1.0.0/schema.json JSON Schema
6
14
  module PortableMoveNotation
15
+ # Schema URL for validation
16
+ SCHEMA_URL = "https://sashite.dev/schemas/pmn/1.0.0/schema.json"
17
+
18
+ # Quick validation method for PMN data
19
+ #
20
+ # @param pmn_data [Array] Array of action arrays to validate
21
+ # @return [Boolean] true if data conforms to PMN v1.0.0 format
22
+ def self.valid?(pmn_data)
23
+ Move.valid?(pmn_data)
24
+ end
25
+
26
+ # Parse PMN JSON string into a Move object
27
+ #
28
+ # @param json_string [String] JSON string containing PMN data
29
+ # @return [Move] Parsed move object
30
+ # @raise [JSON::ParserError] If JSON is invalid
31
+ # @raise [ArgumentError] If PMN structure is invalid
32
+ def self.parse(json_string)
33
+ Move.from_json(json_string)
34
+ end
35
+
36
+ # Generate PMN JSON from a Move object
37
+ #
38
+ # @param move [Move] Move object to serialize
39
+ # @return [String] JSON string in PMN v1.0.0 format
40
+ def self.generate(move)
41
+ move.to_json
42
+ end
7
43
  end
8
44
 
9
45
  require_relative File.join("portable_move_notation", "move")
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.2.0
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cyril Kato
@@ -9,11 +9,13 @@ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies: []
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.
12
+ description: 'Portable Move Notation (PMN) v1.0.0 is a rule-agnostic, JSON-based format
13
+ using arrays to represent deterministic state-changing actions in abstract strategy
14
+ board games. This gem provides a consistent Ruby interface for serializing, deserializing,
15
+ and validating moves across Chess, Shogi, Xiangqi, and other traditional or non-traditional
16
+ variants. The v1.0.0 format uses simple 4-element arrays: [source_square, destination_square,
17
+ piece_name, captured_piece], making it compact and language-agnostic while focusing
18
+ on deterministic state transformations independent of game-specific rules.'
17
19
  email: contact@cyril.email
18
20
  executables: []
19
21
  extensions: []
@@ -50,6 +52,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
50
52
  requirements: []
51
53
  rubygems_version: 3.6.7
52
54
  specification_version: 4
53
- summary: A pure Ruby implementation of Portable Move Notation (PMN) for abstract strategy
54
- board games.
55
+ summary: A pure Ruby implementation of Portable Move Notation (PMN) v1.0.0 for abstract
56
+ strategy board games.
55
57
  test_files: []