sashite-pan 1.3.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: 12680869115791fc6fe8332162cbeaa71174b8ba5c5d74ebb531a805355dae1b
4
- data.tar.gz: 1147d462de504b726ec26337bf8bfa69b28a8d3e195dbf6b2922b9b3ccaa9cf3
3
+ metadata.gz: 2e35bec227c5721965c2355d4534738acc1cc54807a7e3358a944c0c60cf3fd3
4
+ data.tar.gz: 30ba0f48da191edf2b971e8eb2fb3d94ff03a7f39580bfb25d334b15cb692a5f
5
5
  SHA512:
6
- metadata.gz: 8bcb5c10788c3ba49269612062c6eea16251f372a176efd8c390fbe2e7967562bb4c83a27f7f70dd4f5af4af8f3a9ee33695f5d2a1394d105dff00e65043b45d
7
- data.tar.gz: 0afbe377cfe6b68a5748f74221ef4e6084c55e0439ae6d7234e6864d5403e65a3b2d19f1c5f6579869a795c994c1ccc3f8b1ef0c8bc5ed2df8734318817c0c9a
6
+ metadata.gz: 44bb851835eeb4327ddd27aa82068332460571083f02af9100d4bcb37aa8944dea0e700a606496eeb1ae295f377a0e8dda2f737b00d50d191c9489083a21e629
7
+ data.tar.gz: ad48f345973e0ccf6582446ab35cf02828eab72e0399297fef89dd713e43173f47780ea6214880d6924da73965037ac95175bf79d5cfc40da0b65d518314cced
data/LICENSE.md CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2014-2021 Cyril Kato
1
+ Copyright (c) 2014-2025 Cyril Kato
2
2
 
3
3
  MIT License
4
4
 
data/README.md CHANGED
@@ -1,102 +1,283 @@
1
- # Portable Action Notation
1
+ # Pan.rb
2
2
 
3
- [![Version](https://img.shields.io/github/v/tag/sashite/pan.rb?label=Version&logo=github)](https://github.com/sashite/pan.rb/releases)
3
+ [![Version](https://img.shields.io/github/v/tag/sashite/pan.rb?label=Version&logo=github)](https://github.com/sashite/pan.rb/tags)
4
4
  [![Yard documentation](https://img.shields.io/badge/Yard-documentation-blue.svg?logo=github)](https://rubydoc.info/github/sashite/pan.rb/main)
5
- [![CI](https://github.com/sashite/pan.rb/workflows/CI/badge.svg?branch=main)](https://github.com/sashite/pan.rb/actions?query=workflow%3Aci+branch%3Amain)
6
- [![RuboCop](https://github.com/sashite/pan.rb/workflows/RuboCop/badge.svg?branch=main)](https://github.com/sashite/pan.rb/actions?query=workflow%3Arubocop+branch%3Amain)
5
+ ![Ruby](https://github.com/sashite/pan.rb/actions/workflows/main.yml/badge.svg?branch=main)
7
6
  [![License](https://img.shields.io/github/license/sashite/pan.rb?label=License&logo=github)](https://github.com/sashite/pan.rb/raw/main/LICENSE.md)
8
7
 
9
- A Ruby interface for data serialization in [PAN](https://developer.sashite.com/specs/portable-action-notation) format.
8
+ > **PAN** (Portable Action Notation) support for the Ruby language.
10
9
 
11
- ## Installation
10
+ ## What is PAN?
11
+
12
+ PAN (Portable Action Notation) is a compact, string-based format for representing **executed moves** in abstract strategy board games played on coordinate-based boards. PAN provides a human-readable and space-efficient notation for expressing move actions in a rule-agnostic manner.
13
+
14
+ PAN focuses on representing the spatial aspects of moves: where pieces move from and to, and whether the move involves capture or placement. The notation is designed to be intuitive and compatible with standard algebraic coordinate systems.
15
+
16
+ This gem implements the [PAN Specification v1.0.0](https://sashite.dev/documents/pan/1.0.0/), providing a Ruby interface for:
12
17
 
13
- Add this line to your application's Gemfile:
18
+ - Parsing PAN strings into structured move data
19
+ - Validating PAN strings according to the specification
20
+ - Converting between PAN and other move representations
21
+
22
+ ## Installation
14
23
 
15
24
  ```ruby
25
+ # In your Gemfile
16
26
  gem "sashite-pan"
17
27
  ```
18
28
 
19
- And then execute:
29
+ Or install manually:
20
30
 
21
31
  ```sh
22
- bundle
32
+ gem install sashite-pan
23
33
  ```
24
34
 
25
- Or install it yourself as:
35
+ ## PAN Format
26
36
 
27
- ```sh
28
- gem install sashite-pan
37
+ PAN uses three fundamental move types with intuitive operators:
38
+
39
+ ### Simple Move (Non-capture)
40
+ ```
41
+ <source>-<destination>
42
+ ```
43
+ **Example**: `e2-e4` - Moves a piece from e2 to e4
44
+
45
+ ### Capture Move
46
+ ```
47
+ <source>x<destination>
48
+ ```
49
+ **Example**: `e4xd5` - Moves a piece from e4 to d5, capturing the piece at d5
50
+
51
+ ### Drop/Placement
29
52
  ```
53
+ *<destination>
54
+ ```
55
+ **Example**: `*e4` - Places a piece at e4 from off-board (hand, reserve, etc.)
56
+
57
+ ### Coordinate System
58
+
59
+ PAN uses algebraic coordinates consisting of:
60
+ - **File**: A single lowercase letter (`a-z`)
61
+ - **Rank**: A single digit (`0-9`)
62
+
63
+ Examples: `e4`, `a1`, `h8`, `d5`
30
64
 
31
- ## Usage
65
+ ## Basic Usage
32
66
 
33
- Working with PAN can be very simple, for example:
67
+ ### Parsing PAN Strings
68
+
69
+ Convert a PAN string into structured move data:
34
70
 
35
71
  ```ruby
36
- require "sashite/pan"
72
+ require "sashite-pan"
73
+
74
+ # Simple move
75
+ result = Sashite::Pan.parse("e2-e4")
76
+ # => {type: :move, source: "e2", destination: "e4"}
77
+
78
+ # Capture
79
+ result = Sashite::Pan.parse("e4xd5")
80
+ # => {type: :capture, source: "e4", destination: "d5"}
81
+
82
+ # Drop from hand
83
+ result = Sashite::Pan.parse("*e4")
84
+ # => {type: :drop, destination: "e4"}
85
+ ```
37
86
 
38
- # Emit a PAN string
87
+ ### Safe Parsing
39
88
 
40
- actions = [
41
- [52, 36, "♙"]
42
- ]
89
+ Parse a PAN string without raising exceptions:
43
90
 
44
- Sashite::PAN.dump(*actions) # => "52,36,♙"
91
+ ```ruby
92
+ require "sashite-pan"
45
93
 
46
- # Parse a PAN string
94
+ # Valid PAN string
95
+ result = Sashite::Pan.safe_parse("e2-e4")
96
+ # => {type: :move, source: "e2", destination: "e4"}
47
97
 
48
- Sashite::PAN.parse("52,36,♙") # => [[52, 36, "♙", nil]]
98
+ # Invalid PAN string
99
+ result = Sashite::Pan.safe_parse("invalid")
100
+ # => nil
49
101
  ```
50
102
 
51
- ## Example
103
+ ### Validation
52
104
 
53
- ### Promoting a chess pawn into a knight
105
+ Check if a string is valid PAN notation:
54
106
 
55
107
  ```ruby
56
- Sashite::PAN.dump([12, 4, ""]) # => "12,4,♘"
57
- Sashite::PAN.parse("12,4,♘") # => [[12, 4, "♘", nil]]
108
+ require "sashite-pan"
109
+
110
+ Sashite::Pan.valid?("e2-e4") # => true
111
+ Sashite::Pan.valid?("*e4") # => true
112
+ Sashite::Pan.valid?("e4xd5") # => true
113
+
114
+ Sashite::Pan.valid?("") # => false
115
+ Sashite::Pan.valid?("e2-e2") # => false (source equals destination)
116
+ Sashite::Pan.valid?("E2-e4") # => false (uppercase file)
117
+ Sashite::Pan.valid?("e2 - e4") # => false (spaces not allowed)
58
118
  ```
59
119
 
60
- ### Capturing a rook and promoting a shogi pawn
120
+ ## Examples
121
+
122
+ ### Chess Examples
61
123
 
62
124
  ```ruby
63
- Sashite::PAN.dump([33, 24, "+P", "R"]) # => "33,24,+P,R"
64
- Sashite::PAN.parse("33,24,+P,R") # => [[33, 24, "+P", "R"]]
125
+ require "sashite-pan"
126
+
127
+ # Pawn advance
128
+ Sashite::Pan.parse("e2-e4")
129
+ # => {type: :move, source: "e2", destination: "e4"}
130
+
131
+ # Capture
132
+ Sashite::Pan.parse("exd5")
133
+ # => {type: :capture, source: "e4", destination: "d5"}
134
+
135
+ # Note: PAN cannot distinguish piece types or promotion choices
136
+ # These moves require game context for complete interpretation:
137
+ Sashite::Pan.parse("e7-e8") # Could be pawn promotion to any piece
138
+ Sashite::Pan.parse("a1-a8") # Could be rook, queen, or promoted piece
65
139
  ```
66
140
 
67
- ### Dropping a shogi pawn
141
+ ### Shogi Examples
68
142
 
69
143
  ```ruby
70
- Sashite::PAN.dump([nil, 42, "P"]) # => "*,42,P"
71
- Sashite::PAN.parse("*,42,P") # => [[nil, 42, "P", nil]]
144
+ require "sashite-pan"
145
+
146
+ # Piece movement
147
+ Sashite::Pan.parse("g7-f7")
148
+ # => {type: :move, source: "g7", destination: "f7"}
149
+
150
+ # Drop from hand
151
+ Sashite::Pan.parse("*e5")
152
+ # => {type: :drop, destination: "e5"}
153
+
154
+ # Capture (captured piece goes to hand in Shogi)
155
+ Sashite::Pan.parse("h2xg2")
156
+ # => {type: :capture, source: "h2", destination: "g2"}
157
+
158
+ # Note: PAN cannot specify which piece type is being dropped
159
+ # or whether a piece is promoted
72
160
  ```
73
161
 
74
- ***
162
+ ## Limitations and Context Dependency
75
163
 
76
- In the context of a game with several possible actions per turn, like in
77
- Western chess, more than one action could be consider like a move, and joined
78
- thanks to the [`portable_move_notation`](https://rubygems.org/gems/portable_move_notation) gem.
164
+ **Important**: PAN is intentionally minimal and rule-agnostic. It has several important limitations:
79
165
 
80
- ### Black castles on king-side
166
+ ### What PAN Cannot Represent
167
+
168
+ - **Piece types**: Cannot distinguish between different pieces making the same move
169
+ - **Promotion choices**: Cannot specify what piece a pawn promotes to
170
+ - **Game state**: No encoding of check, checkmate, or game conditions
171
+ - **Complex moves**: Castling requires external representation
172
+ - **Piece identity**: Multiple pieces of the same type making similar moves
173
+
174
+ ### Examples of Ambiguity
81
175
 
82
176
  ```ruby
83
- Sashite::PAN.dump([60, 62, "♔"], [63, 61, "♖"]) # => "60,62,♔;63,61,♖"
84
- Sashite::PAN.parse("60,62,♔;63,61,♖") # => [[60, 62, "♔", nil], [63, 61, "♖", nil]]
177
+ # These PAN strings are syntactically valid but may be ambiguous:
178
+
179
+ "e7-e8" # Pawn promotion - but to what piece?
180
+ "*g4" # Drop - but which piece from hand?
181
+ "a1-a8" # Movement - but which piece type?
182
+ "e1-g1" # Could be castling, but rook movement not shown
85
183
  ```
86
184
 
87
- ### Capturing a white chess pawn en passant
185
+ ### When PAN is Insufficient
186
+
187
+ - Games where multiple pieces can make the same spatial move
188
+ - Games requiring promotion choice specification
189
+ - Analysis requiring piece type identification
190
+ - Self-contained game records without context
191
+
192
+ ## Error Handling
193
+
194
+ The library provides detailed error messages for invalid input:
88
195
 
89
196
  ```ruby
90
- Sashite::PAN.dump([33, 32, ""], [32, 40, "♟"]) # => "33,32,♟;32,40,♟"
91
- Sashite::PAN.parse("33,32,♟;32,40,♟") # => [[33, 32, "♟", nil], [32, 40, "♟", nil]]
197
+ require "sashite-pan"
198
+
199
+ begin
200
+ Sashite::Pan.parse("e2-e2") # Source equals destination
201
+ rescue Sashite::Pan::Parser::Error => e
202
+ puts e.message # => "Source and destination cannot be identical"
203
+ end
204
+
205
+ begin
206
+ Sashite::Pan.parse("E2-e4") # Invalid uppercase file
207
+ rescue Sashite::Pan::Parser::Error => e
208
+ puts e.message # => "Invalid PAN format: E2-e4"
209
+ end
210
+
211
+ begin
212
+ Sashite::Pan.parse("") # Empty string
213
+ rescue Sashite::Pan::Parser::Error => e
214
+ puts e.message # => "PAN string cannot be empty"
215
+ end
92
216
  ```
93
217
 
94
- ## License
218
+ ## Regular Expression Pattern
219
+
220
+ PAN strings can be validated using this pattern:
221
+
222
+ ```ruby
223
+ PAN_PATTERN = /\A(\*|[a-z][0-9][-x])([a-z][0-9])\z/
224
+
225
+ def valid_pan?(string)
226
+ return false unless string.match?(PAN_PATTERN)
227
+
228
+ # Additional validation for source != destination
229
+ if string.include?('-') || string.include?('x')
230
+ source = string[0..1]
231
+ destination = string[-2..-1]
232
+ return source != destination
233
+ end
95
234
 
96
- The code is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
235
+ true
236
+ end
237
+ ```
238
+
239
+ ## Use Cases
240
+
241
+ ### Optimal for PAN
242
+
243
+ - **Move logs**: Simple game records where context is available
244
+ - **User interfaces**: Command input for move entry
245
+ - **Network protocols**: Compact move transmission
246
+ - **Quick notation**: Manual notation for simple games
247
+
248
+ ### Consider Alternatives When
249
+
250
+ - **Ambiguous games**: Multiple pieces can make the same spatial move
251
+ - **Complex promotions**: Games with multiple promotion choices
252
+ - **Analysis tools**: When piece identity is crucial
253
+ - **Self-contained records**: When context is not available
254
+
255
+ ## Integration Considerations
256
+
257
+ When using PAN in your applications:
258
+
259
+ 1. **Always pair with context**: Store board state alongside PAN moves
260
+ 2. **Document assumptions**: Clearly specify how ambiguities are resolved
261
+ 3. **Validate rigorously**: Check both syntax and semantic validity
262
+ 4. **Handle edge cases**: Plan for promotion and drop ambiguities
263
+
264
+ ## Properties of PAN
265
+
266
+ - **Rule-agnostic**: Does not encode piece types, legality, or game-specific conditions
267
+ - **Compact**: Minimal character overhead (3-5 characters per move)
268
+ - **Human-readable**: Intuitive algebraic notation
269
+ - **Space-efficient**: Excellent for large game databases
270
+ - **Context-dependent**: Requires external game state for complete interpretation
271
+
272
+ ## Documentation
273
+
274
+ - [Official PAN Specification](https://sashite.dev/documents/pan/1.0.0/)
275
+ - [API Documentation](https://rubydoc.info/github/sashite/pan.rb/main)
276
+
277
+ ## License
97
278
 
98
- ## About Sashite
279
+ The [gem](https://rubygems.org/gems/sashite-pan) is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
99
280
 
100
- This [gem](https://rubygems.org/gems/sashite-pan) is maintained by [Sashite](https://sashite.com/).
281
+ ## About Sashité
101
282
 
102
- 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!
283
+ This project is maintained by [Sashité](https://sashite.com/) promoting chess variants and sharing the beauty of Chinese, Japanese, and Western chess cultures.
@@ -1,35 +1,120 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "action"
4
-
5
3
  module Sashite
6
- module PAN
7
- # Dumper class
8
- class Dumper < Action
9
- def self.call(*actions)
10
- actions.map { |action_items| new(*action_items).call }
11
- .join(separator)
4
+ module Pan
5
+ # Dumper for converting structured move data to PAN strings
6
+ module Dumper
7
+ class Error < ::StandardError
12
8
  end
13
9
 
14
- def initialize(src_square, dst_square, piece_name, piece_hand = nil)
15
- super()
10
+ # Convert structured move data to PAN string
11
+ #
12
+ # @param move_data [Hash] Move data with type, source, destination
13
+ # @return [String] PAN string representation
14
+ # @raise [Dumper::Error] If the move data is invalid
15
+ def self.call(move_data)
16
+ raise Dumper::Error, "Move data cannot be nil" if move_data.nil?
17
+ raise Dumper::Error, "Move data must be a Hash" unless move_data.is_a?(::Hash)
16
18
 
17
- @src_square = src_square.nil? ? drop_char : Integer(src_square)
18
- @dst_square = Integer(dst_square)
19
- @piece_name = piece_name.to_s
20
- @piece_hand = piece_hand&.to_s
21
- end
19
+ validate_move_data(move_data)
22
20
 
23
- def call
24
- action_items.join(separator)
21
+ case move_data[:type]
22
+ when :move
23
+ dump_simple_move(move_data)
24
+ when :capture
25
+ dump_capture_move(move_data)
26
+ when :drop
27
+ dump_drop_move(move_data)
28
+ else
29
+ raise Dumper::Error, "Invalid move type: #{move_data[:type]}"
30
+ end
25
31
  end
26
32
 
27
33
  private
28
34
 
29
- def action_items
30
- return [src_square, dst_square, piece_name] if piece_hand.nil?
35
+ # Validate the structure of move data
36
+ #
37
+ # @param move_data [Hash] Move data to validate
38
+ # @raise [Dumper::Error] If move data is invalid
39
+ def self.validate_move_data(move_data)
40
+ unless move_data.key?(:type)
41
+ raise Dumper::Error, "Move data must have :type key"
42
+ end
43
+
44
+ unless move_data.key?(:destination)
45
+ raise Dumper::Error, "Move data must have :destination key"
46
+ end
47
+
48
+ validate_coordinate(move_data[:destination], "destination")
49
+
50
+ case move_data[:type]
51
+ when :move, :capture
52
+ unless move_data.key?(:source)
53
+ raise Dumper::Error, "Move and capture types must have :source key"
54
+ end
55
+ validate_coordinate(move_data[:source], "source")
56
+ validate_different_coordinates(move_data[:source], move_data[:destination])
57
+ when :drop
58
+ if move_data.key?(:source)
59
+ raise Dumper::Error, "Drop type cannot have :source key"
60
+ end
61
+ else
62
+ raise Dumper::Error, "Invalid move type: #{move_data[:type]}"
63
+ end
64
+ end
65
+
66
+ # Validate a coordinate follows PAN format
67
+ #
68
+ # @param coordinate [String] Coordinate to validate
69
+ # @param field_name [String] Name of the field for error messages
70
+ # @raise [Dumper::Error] If coordinate is invalid
71
+ def self.validate_coordinate(coordinate, field_name)
72
+ if coordinate.nil? || coordinate.empty?
73
+ raise Dumper::Error, "#{field_name.capitalize} coordinate cannot be nil or empty"
74
+ end
75
+
76
+ unless coordinate.is_a?(::String)
77
+ raise Dumper::Error, "#{field_name.capitalize} coordinate must be a String"
78
+ end
79
+
80
+ unless coordinate.match?(/\A[a-z][0-9]\z/)
81
+ raise Dumper::Error, "Invalid #{field_name} coordinate format: #{coordinate}. Must be lowercase letter followed by digit (e.g., 'e4')"
82
+ end
83
+ end
84
+
85
+ # Validate that source and destination are different
86
+ #
87
+ # @param source [String] Source coordinate
88
+ # @param destination [String] Destination coordinate
89
+ # @raise [Dumper::Error] If coordinates are the same
90
+ def self.validate_different_coordinates(source, destination)
91
+ if source == destination
92
+ raise Dumper::Error, "Source and destination coordinates cannot be identical: #{source}"
93
+ end
94
+ end
95
+
96
+ # Generate PAN string for simple move
97
+ #
98
+ # @param move_data [Hash] Move data with :source and :destination
99
+ # @return [String] PAN string in format "source-destination"
100
+ def self.dump_simple_move(move_data)
101
+ "#{move_data[:source]}-#{move_data[:destination]}"
102
+ end
103
+
104
+ # Generate PAN string for capture move
105
+ #
106
+ # @param move_data [Hash] Move data with :source and :destination
107
+ # @return [String] PAN string in format "sourcexdestination"
108
+ def self.dump_capture_move(move_data)
109
+ "#{move_data[:source]}x#{move_data[:destination]}"
110
+ end
31
111
 
32
- [src_square, dst_square, piece_name, piece_hand]
112
+ # Generate PAN string for drop move
113
+ #
114
+ # @param move_data [Hash] Move data with :destination
115
+ # @return [String] PAN string in format "*destination"
116
+ def self.dump_drop_move(move_data)
117
+ "*#{move_data[:destination]}"
33
118
  end
34
119
  end
35
120
  end
@@ -1,29 +1,103 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "action"
4
-
5
3
  module Sashite
6
- module PAN
7
- # Parser class
8
- class Parser < Action
9
- def self.call(serialized_move)
10
- serialized_move.split(separator)
11
- .map { |serialized_action| new(serialized_action).call }
4
+ module Pan
5
+ # Parser for Portable Action Notation (PAN) strings
6
+ module Parser
7
+ class Error < ::StandardError
8
+ end
9
+
10
+ # Regular expression pattern for validating PAN strings
11
+ PAN_PATTERN = /\A(\*|[a-z][0-9][-x])([a-z][0-9])\z/
12
+
13
+ # Parse a PAN string into structured move data
14
+ #
15
+ # @param pan_string [String] The PAN string to parse
16
+ # @return [Hash] Structured move data with type, source, and destination
17
+ # @raise [Parser::Error] If the PAN string is invalid
18
+ def self.call(pan_string)
19
+ raise Parser::Error, "PAN string cannot be nil" if pan_string.nil?
20
+ raise Parser::Error, "PAN string must be a String" unless pan_string.is_a?(::String)
21
+ raise Parser::Error, "PAN string cannot be empty" if pan_string.empty?
22
+
23
+ validate_format(pan_string)
24
+ parse_move(pan_string)
25
+ end
26
+
27
+ private
28
+
29
+ # Validate the basic format of a PAN string
30
+ #
31
+ # @param pan_string [String] The PAN string to validate
32
+ # @raise [Parser::Error] If format is invalid
33
+ def self.validate_format(pan_string)
34
+ unless pan_string.match?(PAN_PATTERN)
35
+ raise Parser::Error, "Invalid PAN format: #{pan_string}"
36
+ end
37
+
38
+ # Additional validation for source != destination in moves and captures
39
+ if pan_string.include?('-') || pan_string.include?('x')
40
+ source = pan_string[0..1]
41
+ destination = pan_string[-2..-1]
42
+ if source == destination
43
+ raise Parser::Error, "Source and destination cannot be identical: #{source}"
44
+ end
45
+ end
12
46
  end
13
47
 
14
- def initialize(serialized_action)
15
- super()
48
+ # Parse the move based on its type
49
+ #
50
+ # @param pan_string [String] The validated PAN string
51
+ # @return [Hash] Structured move data
52
+ def self.parse_move(pan_string)
53
+ case pan_string
54
+ when /\A([a-z][0-9])-([a-z][0-9])\z/
55
+ parse_simple_move($1, $2)
56
+ when /\A([a-z][0-9])x([a-z][0-9])\z/
57
+ parse_capture_move($1, $2)
58
+ when /\A\*([a-z][0-9])\z/
59
+ parse_drop_move($1)
60
+ else
61
+ # This should never happen due to earlier validation
62
+ raise Parser::Error, "Unexpected PAN format: #{pan_string}"
63
+ end
64
+ end
65
+
66
+ # Parse a simple move (non-capture)
67
+ #
68
+ # @param source [String] Source coordinate
69
+ # @param destination [String] Destination coordinate
70
+ # @return [Hash] Move data for simple move
71
+ def self.parse_simple_move(source, destination)
72
+ {
73
+ type: :move,
74
+ source: source,
75
+ destination: destination
76
+ }
77
+ end
16
78
 
17
- action_args = serialized_action.split(separator)
18
- src_square = action_args.fetch(0)
19
- @src_square = src_square.eql?(drop_char) ? nil : Integer(src_square)
20
- @dst_square = Integer(action_args.fetch(1))
21
- @piece_name = action_args.fetch(2)
22
- @piece_hand = action_args.fetch(3, nil)
79
+ # Parse a capture move
80
+ #
81
+ # @param source [String] Source coordinate
82
+ # @param destination [String] Destination coordinate
83
+ # @return [Hash] Move data for capture move
84
+ def self.parse_capture_move(source, destination)
85
+ {
86
+ type: :capture,
87
+ source: source,
88
+ destination: destination
89
+ }
23
90
  end
24
91
 
25
- def call
26
- [src_square, dst_square, piece_name, piece_hand]
92
+ # Parse a drop/placement move
93
+ #
94
+ # @param destination [String] Destination coordinate
95
+ # @return [Hash] Move data for drop move
96
+ def self.parse_drop_move(destination)
97
+ {
98
+ type: :drop,
99
+ destination: destination
100
+ }
27
101
  end
28
102
  end
29
103
  end
data/lib/sashite/pan.rb CHANGED
@@ -1,17 +1,165 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "pan/dumper"
4
+ require_relative "pan/parser"
5
+
3
6
  module Sashite
4
7
  # The PAN (Portable Action Notation) module
5
- module PAN
6
- def self.dump(*actions)
7
- Dumper.call(*actions)
8
+ module Pan
9
+ # Main interface for PAN operations
10
+ module_function
11
+
12
+ # Parse a PAN string into structured move data
13
+ #
14
+ # @param pan_string [String] The PAN string to parse
15
+ # @return [Hash] Structured move data with type, source, and destination
16
+ # @raise [Parser::Error] If the PAN string is invalid
17
+ # @example
18
+ # Sashite::Pan.parse("e2-e4")
19
+ # # => {type: :move, source: "e2", destination: "e4"}
20
+ #
21
+ # Sashite::Pan.parse("e4xd5")
22
+ # # => {type: :capture, source: "e4", destination: "d5"}
23
+ #
24
+ # Sashite::Pan.parse("*e4")
25
+ # # => {type: :drop, destination: "e4"}
26
+ def parse(pan_string)
27
+ Parser.call(pan_string)
28
+ end
29
+
30
+ # Convert structured move data to PAN string
31
+ #
32
+ # @param move_data [Hash] Structured move data with type, source, and destination
33
+ # @return [String] PAN string representation
34
+ # @raise [Dumper::Error] If the move data is invalid
35
+ # @example
36
+ # Sashite::Pan.dump({type: :move, source: "e2", destination: "e4"})
37
+ # # => "e2-e4"
38
+ #
39
+ # Sashite::Pan.dump({type: :capture, source: "e4", destination: "d5"})
40
+ # # => "e4xd5"
41
+ #
42
+ # Sashite::Pan.dump({type: :drop, destination: "e4"})
43
+ # # => "*e4"
44
+ def dump(move_data)
45
+ Dumper.call(move_data)
46
+ end
47
+
48
+ # Validate a PAN string without raising exceptions
49
+ #
50
+ # @param pan_string [String] The PAN string to validate
51
+ # @return [Boolean] True if valid, false otherwise
52
+ # @example
53
+ # Sashite::Pan.valid?("e2-e4") # => true
54
+ # Sashite::Pan.valid?("*e4") # => true
55
+ # Sashite::Pan.valid?("e4xd5") # => true
56
+ # Sashite::Pan.valid?("") # => false
57
+ # Sashite::Pan.valid?("e2-e2") # => false
58
+ # Sashite::Pan.valid?("E2-e4") # => false
59
+ def valid?(pan_string)
60
+ parse(pan_string)
61
+ true
62
+ rescue Parser::Error
63
+ false
64
+ end
65
+
66
+ # Parse a PAN string without raising exceptions
67
+ #
68
+ # @param pan_string [String] The PAN string to parse
69
+ # @return [Hash, nil] Structured move data or nil if invalid
70
+ # @example
71
+ # Sashite::Pan.safe_parse("e2-e4")
72
+ # # => {type: :move, source: "e2", destination: "e4"}
73
+ #
74
+ # Sashite::Pan.safe_parse("invalid")
75
+ # # => nil
76
+ def safe_parse(pan_string)
77
+ parse(pan_string)
78
+ rescue Parser::Error
79
+ nil
80
+ end
81
+
82
+ # Convert structured move data to PAN string without raising exceptions
83
+ #
84
+ # @param move_data [Hash] Structured move data with type, source, and destination
85
+ # @return [String, nil] PAN string or nil if invalid
86
+ # @example
87
+ # Sashite::Pan.safe_dump({type: :move, source: "e2", destination: "e4"})
88
+ # # => "e2-e4"
89
+ #
90
+ # Sashite::Pan.safe_dump({invalid: :data})
91
+ # # => nil
92
+ def safe_dump(move_data)
93
+ dump(move_data)
94
+ rescue Dumper::Error
95
+ nil
96
+ end
97
+
98
+ # Check if a coordinate is valid according to PAN specification
99
+ #
100
+ # @param coordinate [String] The coordinate to validate
101
+ # @return [Boolean] True if valid, false otherwise
102
+ # @example
103
+ # Sashite::Pan.valid_coordinate?("e4") # => true
104
+ # Sashite::Pan.valid_coordinate?("a1") # => true
105
+ # Sashite::Pan.valid_coordinate?("E4") # => false (uppercase)
106
+ # Sashite::Pan.valid_coordinate?("e10") # => false (multi-digit rank)
107
+ def valid_coordinate?(coordinate)
108
+ return false unless coordinate.is_a?(::String)
109
+ coordinate.match?(/\A[a-z][0-9]\z/)
8
110
  end
9
111
 
10
- def self.parse(string)
11
- Parser.call(string)
112
+ # Get the regular expression pattern used for PAN validation
113
+ #
114
+ # @return [Regexp] The regex pattern for PAN strings
115
+ # @example
116
+ # pattern = Sashite::Pan.pattern
117
+ # pattern.match?("e2-e4") # => true
118
+ def pattern
119
+ Parser::PAN_PATTERN
120
+ end
121
+
122
+ # Convert a PAN string to a human-readable description
123
+ #
124
+ # @param pan_string [String] The PAN string to describe
125
+ # @return [String] Human-readable description
126
+ # @raise [Parser::Error] If the PAN string is invalid
127
+ # @example
128
+ # Sashite::Pan.describe("e2-e4")
129
+ # # => "Move from e2 to e4"
130
+ #
131
+ # Sashite::Pan.describe("e4xd5")
132
+ # # => "Capture from e4 to d5"
133
+ #
134
+ # Sashite::Pan.describe("*e4")
135
+ # # => "Drop to e4"
136
+ def describe(pan_string)
137
+ move_data = parse(pan_string)
138
+
139
+ case move_data[:type]
140
+ when :move
141
+ "Move from #{move_data[:source]} to #{move_data[:destination]}"
142
+ when :capture
143
+ "Capture from #{move_data[:source]} to #{move_data[:destination]}"
144
+ when :drop
145
+ "Drop to #{move_data[:destination]}"
146
+ end
147
+ end
148
+
149
+ # Convert a PAN string to a human-readable description without raising exceptions
150
+ #
151
+ # @param pan_string [String] The PAN string to describe
152
+ # @return [String, nil] Human-readable description or nil if invalid
153
+ # @example
154
+ # Sashite::Pan.safe_describe("e2-e4")
155
+ # # => "Move from e2 to e4"
156
+ #
157
+ # Sashite::Pan.safe_describe("invalid")
158
+ # # => nil
159
+ def safe_describe(pan_string)
160
+ describe(pan_string)
161
+ rescue Parser::Error
162
+ nil
12
163
  end
13
164
  end
14
165
  end
15
-
16
- require_relative "pan/dumper"
17
- require_relative "pan/parser"
data/lib/sashite-pan.rb CHANGED
@@ -1,6 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Sashite namespace
4
- module Sashite; end
3
+ # Sashité namespace for board game notation libraries
4
+ module Sashite
5
+ # Portable Action Notation (PAN) implementation for Ruby
6
+ #
7
+ # PAN is a compact, string-based format for representing executed moves
8
+ # in abstract strategy board games played on coordinate-based boards.
9
+ #
10
+ # @see https://sashite.dev/documents/pan/1.0.0/ PAN Specification v1.0.0
11
+ # @author Sashité
12
+ # @since 1.0.0
13
+ end
5
14
 
6
15
  require_relative "sashite/pan"
metadata CHANGED
@@ -1,157 +1,20 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sashite-pan
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 3.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: 2021-08-17 00:00:00.000000000 Z
12
- dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: awesome_print
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: '0'
20
- type: :development
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - ">="
25
- - !ruby/object:Gem::Version
26
- version: '0'
27
- - !ruby/object:Gem::Dependency
28
- name: bundler
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: byebug
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: rake
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: rubocop-md
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-rake
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: rubocop-thread_safety
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: simplecov
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
- - !ruby/object:Gem::Dependency
140
- name: yard
141
- requirement: !ruby/object:Gem::Requirement
142
- requirements:
143
- - - ">="
144
- - !ruby/object:Gem::Version
145
- version: '0'
146
- type: :development
147
- prerelease: false
148
- version_requirements: !ruby/object:Gem::Requirement
149
- requirements:
150
- - - ">="
151
- - !ruby/object:Gem::Version
152
- version: '0'
153
- description: A Ruby interface for data serialization in PAN (Portable Action Notation)
154
- format.
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: |
13
+ Parse and generate Portable Action Notation (PAN) strings for representing moves
14
+ in chess, shogi, and other strategy board games. PAN provides a compact,
15
+ human-readable format for move logging, game transmission, and database storage.
16
+ Supports simple moves (e2-e4), captures (exd5), and piece drops (*e4) with
17
+ comprehensive validation and error handling.
155
18
  email: contact@cyril.email
156
19
  executables: []
157
20
  extensions: []
@@ -161,17 +24,18 @@ files:
161
24
  - README.md
162
25
  - lib/sashite-pan.rb
163
26
  - lib/sashite/pan.rb
164
- - lib/sashite/pan/action.rb
165
27
  - lib/sashite/pan/dumper.rb
166
28
  - lib/sashite/pan/parser.rb
167
- homepage: https://developer.sashite.com/specs/portable-action-notation
29
+ homepage: https://github.com/sashite/pan.rb
168
30
  licenses:
169
31
  - MIT
170
32
  metadata:
171
33
  bug_tracker_uri: https://github.com/sashite/pan.rb/issues
172
- documentation_uri: https://rubydoc.info/gems/sashite-pan/index
34
+ documentation_uri: https://rubydoc.info/github/sashite/pan.rb/main
35
+ homepage_uri: https://github.com/sashite/pan.rb
173
36
  source_code_uri: https://github.com/sashite/pan.rb
174
- post_install_message:
37
+ specification_uri: https://sashite.dev/documents/pan/1.0.0/
38
+ rubygems_mfa_required: 'true'
175
39
  rdoc_options: []
176
40
  require_paths:
177
41
  - lib
@@ -179,15 +43,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
179
43
  requirements:
180
44
  - - ">="
181
45
  - !ruby/object:Gem::Version
182
- version: 2.7.0
46
+ version: 3.2.0
183
47
  required_rubygems_version: !ruby/object:Gem::Requirement
184
48
  requirements:
185
49
  - - ">="
186
50
  - !ruby/object:Gem::Version
187
51
  version: '0'
188
52
  requirements: []
189
- rubygems_version: 3.2.15
190
- signing_key:
53
+ rubygems_version: 3.6.9
191
54
  specification_version: 4
192
- summary: Data serialization in PAN format.
55
+ summary: Compact notation for board game moves - parse chess, shogi, and strategy
56
+ game actions
193
57
  test_files: []
@@ -1,24 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Sashite
4
- module PAN
5
- # Action class
6
- class Action
7
- attr_reader :src_square, :dst_square, :piece_name, :piece_hand
8
-
9
- private_class_method def self.separator
10
- ";"
11
- end
12
-
13
- private
14
-
15
- def separator
16
- ","
17
- end
18
-
19
- def drop_char
20
- "*"
21
- end
22
- end
23
- end
24
- end