portable_move_notation 1.2.0 → 2.0.0

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