sashite-gan 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: 7ebee55a8937fadf15f9b2a4bcf03e672b96f832cd01482be1c0374a1314fb21
4
- data.tar.gz: 3f092d4f12e0b6a2e20d8981c76fad2be914a886a37ca95cb4d3437b9f18e67a
3
+ metadata.gz: 361cb6527615fdfe0c4246c3f5f603598af97a46e6be9ec0cc300d03651dcb4a
4
+ data.tar.gz: 50fea949d4a759c2a68792085e1457c16c49966adf075533eb2e5cd1da5f852a
5
5
  SHA512:
6
- metadata.gz: 472262b63c2401d6392dd65b1e39ca0600515b201cce85fcbbbacf8d245256b978c143d7313c23495cc143cf42c79a6631690a0f0eab51c0a9bf8a78cd64a259
7
- data.tar.gz: a5afb55bc54194750b0406646e0074a08f9dd5952f3c743c411a94b1cfa183aa263e7cb7b393fffce542ce5c2adf385f82ac5322e4ddfbfeb843d4a9ae0c063d
6
+ metadata.gz: '0298e95b598a17d558f85072321ee9006470f83e83aa20fd58235ec5550df98e1ec20f594b715962d8a063f0757ba927c0093577eb9c3c98ec9dd9b5180cdb4a'
7
+ data.tar.gz: 705ad8e30abe365dedbe108d941625d6c06725d0507c2e853f622bfe9da992137109b9fd4c44a09162cfbe1aed5c1f7bb15c1689977b131123095d69897cbebf
data/LICENSE.md CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2014-2020 Sashite
1
+ Copyright (c) 2014-2025 Sashite
2
2
 
3
3
  MIT License
4
4
 
data/README.md CHANGED
@@ -1,181 +1,180 @@
1
- # GAN.rb
1
+ # Gan.rb
2
2
 
3
- [![Build Status](https://travis-ci.org/sashite/gan.rb.svg?branch=master)](https://travis-ci.org/sashite/gan.rb)
3
+ [![Version](https://img.shields.io/github/v/tag/sashite/gan.rb?label=Version&logo=github)](https://github.com/sashite/gan.rb/tags)
4
+ [![Yard documentation](https://img.shields.io/badge/Yard-documentation-blue.svg?logo=github)](https://rubydoc.info/github/sashite/gan.rb/main)
5
+ ![Ruby](https://github.com/sashite/gan.rb/actions/workflows/main.yml/badge.svg?branch=main)
6
+ [![License](https://img.shields.io/github/license/sashite/gan.rb?label=License&logo=github)](https://github.com/sashite/gan.rb/raw/main/LICENSE.md)
4
7
 
5
- > A Ruby interface for data serialization in [General Actor Notation](https://developer.sashite.com/specs/general-actor-notation) format ♟️
8
+ > **GAN** (General Actor Notation) support for the Ruby language.
9
+
10
+ ## What is GAN?
11
+
12
+ GAN (General Actor Notation) defines a consistent and rule-agnostic format for representing game actors in abstract strategy board games. Building upon Piece Name Notation (PNN), GAN eliminates ambiguity by associating each piece with its originating game, allowing for unambiguous gameplay application and cross-game distinctions.
13
+
14
+ This gem implements the [GAN Specification v1.0.0](https://sashite.dev/documents/gan/1.0.0/), providing a Ruby interface for:
15
+
16
+ - Serializing game actors to GAN strings
17
+ - Parsing GAN strings into their component parts
18
+ - Validating GAN strings according to the specification
6
19
 
7
20
  ## Installation
8
21
 
9
- 1. Add the dependency to your `Gemfile`:
22
+ ```ruby
23
+ # In your Gemfile
24
+ gem "sashite-gan"
25
+ ```
26
+
27
+ Or install manually:
28
+
29
+ ```sh
30
+ gem install sashite-gan
31
+ ```
32
+
33
+ ## GAN Format
34
+
35
+ A GAN record consists of a game identifier, followed by a colon, followed by a piece identifier that follows the PNN specification:
36
+
37
+ ```
38
+ <game-id>:<piece-id>
39
+ ```
40
+
41
+ Where:
10
42
 
11
- ```ruby
12
- gem 'sashite-gan'
13
- ```
43
+ - `<game-id>` is a sequence of alphabetic characters identifying the game variant.
44
+ - `:` is a literal colon character, serving as a separator.
45
+ - `<piece-id>` is a piece representation following the PNN specification: `[<prefix>]<letter>[<suffix>]`.
14
46
 
15
- 2. Run `bundle install`
47
+ The casing of the game identifier reflects the player:
16
48
 
17
- ## Usage
49
+ - **Uppercase** game identifiers (e.g., `CHESS:`) denote pieces belonging to the first player.
50
+ - **Lowercase** game identifiers (e.g., `chess:`) denote pieces belonging to the second player.
51
+
52
+ ## Basic Usage
53
+
54
+ ### Parsing GAN Strings
55
+
56
+ Convert a GAN string into a structured Ruby hash:
18
57
 
19
58
  ```ruby
20
- require 'sashite-gan'
21
-
22
-
23
- # Chess (Western chess)'s Rook, White
24
- piece = Sashite::GAN.parse("C:R")
25
-
26
- piece.abbr.to_s # => "R"
27
- piece.style # => "C"
28
- piece.topside? # => false
29
- piece.bottomside? # => true
30
- piece.to_s # => "C:R"
31
- piece.topside.to_s # => "c:r"
32
- piece.bottomside.to_s # => "C:R"
33
- piece.oppositeside.to_s # => "c:r"
34
- piece.promote.to_s # => "C:+R"
35
- piece.unpromote.to_s # => "C:R"
36
-
37
-
38
- # Chess (Western chess)'s King, Black
39
- piece = Sashite::GAN.parse("c:-k")
40
-
41
- piece.abbr.to_s # => "-k"
42
- piece.style # => "c"
43
- piece.topside? # => true
44
- piece.bottomside? # => false
45
- piece.to_s # => "c:-k"
46
- piece.topside.to_s # => "c:-k"
47
- piece.bottomside.to_s # => "C:-K"
48
- piece.oppositeside.to_s # => "C:-K"
49
- piece.promote.to_s # => "c:+-k"
50
- piece.unpromote.to_s # => "c:-k"
51
-
52
-
53
- # Makruk (Thai chess)'s Bishop, White
54
- piece = Sashite::GAN.parse("M:B")
55
-
56
- piece.abbr.to_s # => "B"
57
- piece.style # => "M"
58
- piece.topside? # => false
59
- piece.bottomside? # => true
60
- piece.to_s # => "M:B"
61
- piece.topside.to_s # => "m:b"
62
- piece.bottomside.to_s # => "M:B"
63
- piece.oppositeside.to_s # => "m:b"
64
- piece.promote.to_s # => "M:+B"
65
- piece.unpromote.to_s # => "M:B"
66
-
67
-
68
- # Shogi (Japanese chess)'s King, Gote
69
- piece = Sashite::GAN.parse("s:-k")
70
-
71
- piece.abbr.to_s # => "-k"
72
- piece.style # => "s"
73
- piece.topside? # => true
74
- piece.bottomside? # => false
75
- piece.to_s # => "s:-k"
76
- piece.topside.to_s # => "s:-k"
77
- piece.bottomside.to_s # => "S:-K"
78
- piece.oppositeside.to_s # => "S:-K"
79
- piece.promote.to_s # => "s:+-k"
80
- piece.unpromote.to_s # => "s:-k"
81
-
82
-
83
- # Shogi (Japanese chess)'s King, Sente
84
- piece = Sashite::GAN.parse("S:-K")
85
-
86
- piece.abbr.to_s # => "-K"
87
- piece.style # => "S"
88
- piece.topside? # => false
89
- piece.bottomside? # => true
90
- piece.to_s # => "S:-K"
91
- piece.topside.to_s # => "s:-k"
92
- piece.bottomside.to_s # => "S:-K"
93
- piece.oppositeside.to_s # => "s:-k"
94
- piece.promote.to_s # => "S:+-K"
95
- piece.unpromote.to_s # => "S:-K"
96
-
97
-
98
- # Shogi (Japanese chess)'s promoted Pawn, Sente
99
- piece = Sashite::GAN.parse("S:+P")
100
-
101
- piece.abbr.to_s # => "+P"
102
- piece.style # => "S"
103
- piece.topside? # => false
104
- piece.bottomside? # => true
105
- piece.to_s # => "S:+P"
106
- piece.topside.to_s # => "s:+p"
107
- piece.bottomside.to_s # => "S:+P"
108
- piece.oppositeside.to_s # => "s:+p"
109
- piece.promote.to_s # => "S:+P"
110
- piece.unpromote.to_s # => "S:P"
111
-
112
-
113
- # Xiangqi (Chinese chess)'s General, Red
114
- piece = Sashite::GAN.parse("X:-G")
115
-
116
- piece.abbr.to_s # => "-G"
117
- piece.style # => "X"
118
- piece.topside? # => false
119
- piece.bottomside? # => true
120
- piece.to_s # => "X:-G"
121
- piece.topside.to_s # => "x:-g"
122
- piece.bottomside.to_s # => "X:-G"
123
- piece.oppositeside.to_s # => "x:-g"
124
- piece.promote.to_s # => "X:+-G"
125
- piece.unpromote.to_s # => "X:-G"
126
-
127
-
128
- # Xiangqi (Chinese chess)'s Flying General, Red
129
- piece = Sashite::GAN.parse("X:+-G")
130
-
131
- piece.abbr.to_s # => "+-G"
132
- piece.style # => "X"
133
- piece.topside? # => false
134
- piece.bottomside? # => true
135
- piece.to_s # => "X:+-G"
136
- piece.topside.to_s # => "x:+-g"
137
- piece.bottomside.to_s # => "X:+-G"
138
- piece.oppositeside.to_s # => "x:+-g"
139
- piece.promote.to_s # => "X:+-G"
140
- piece.unpromote.to_s # => "X:-G"
141
-
142
-
143
- # Dai Dai Shogi (huge Japanese chess)'s Phoenix, Sente
144
- piece = Sashite::GAN.parse("DAI_DAI_SHOGI:PH")
145
-
146
- piece.abbr.to_s # => "PH"
147
- piece.style # => "DAI_DAI_SHOGI"
148
- piece.topside? # => false
149
- piece.bottomside? # => true
150
- piece.to_s # => "DAI_DAI_SHOGI:PH"
151
- piece.topside.to_s # => "dai_dai_shogi:ph"
152
- piece.bottomside.to_s # => "DAI_DAI_SHOGI:PH"
153
- piece.oppositeside.to_s # => "dai_dai_shogi:ph"
154
- piece.promote.to_s # => "DAI_DAI_SHOGI:+PH"
155
- piece.unpromote.to_s # => "DAI_DAI_SHOGI:PH"
156
-
157
-
158
- # A random FOO chess variant's promoted Z piece, Bottom-side
159
- piece = Sashite::GAN.parse("FOO:+Z")
160
-
161
- piece.abbr.to_s # => "+Z"
162
- piece.style # => "FOO"
163
- piece.topside? # => false
164
- piece.bottomside? # => true
165
- piece.to_s # => "FOO:+Z"
166
- piece.topside.to_s # => "foo:+z"
167
- piece.bottomside.to_s # => "FOO:+Z"
168
- piece.oppositeside.to_s # => "foo:+z"
169
- piece.promote.to_s # => "FOO:+Z"
170
- piece.unpromote.to_s # => "FOO:Z"
59
+ require "sashite-gan"
60
+
61
+ # Basic actor
62
+ result = Sashite::Gan.parse("CHESS:K")
63
+ # => { game_id: "CHESS", letter: "K" }
64
+
65
+ # With piece prefix
66
+ result = Sashite::Gan.parse("SHOGI:+P")
67
+ # => { game_id: "SHOGI", letter: "P", prefix: "+" }
68
+
69
+ # With piece suffix
70
+ result = Sashite::Gan.parse("CHESS:K'")
71
+ # => { game_id: "CHESS", letter: "K", suffix: "'" }
72
+
73
+ # With both piece prefix and suffix
74
+ result = Sashite::Gan.parse("SHOGI:+R'")
75
+ # => { game_id: "SHOGI", letter: "R", prefix: "+", suffix: "'" }
171
76
  ```
172
77
 
173
- ## License
78
+ ### Safe Parsing
79
+
80
+ Parse a GAN string without raising exceptions:
81
+
82
+ ```ruby
83
+ require "sashite-gan"
84
+
85
+ # Valid GAN string
86
+ result = Sashite::Gan.safe_parse("CHESS:K'")
87
+ # => { game_id: "CHESS", letter: "K", suffix: "'" }
88
+
89
+ # Invalid GAN string
90
+ result = Sashite::Gan.safe_parse("invalid gan string")
91
+ # => nil
92
+ ```
174
93
 
175
- The code is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
94
+ ### Creating GAN Strings
95
+
96
+ Convert actor components into a GAN string:
97
+
98
+ ```ruby
99
+ require "sashite-gan"
100
+
101
+ # Basic actor
102
+ Sashite::Gan.dump(game_id: "CHESS", letter: "K")
103
+ # => "CHESS:K"
104
+
105
+ # With piece prefix
106
+ Sashite::Gan.dump(game_id: "SHOGI", letter: "P", prefix: "+")
107
+ # => "SHOGI:+P"
108
+
109
+ # With piece suffix
110
+ Sashite::Gan.dump(game_id: "CHESS", letter: "K", suffix: "'")
111
+ # => "CHESS:K'"
112
+
113
+ # With both piece prefix and suffix
114
+ Sashite::Gan.dump(game_id: "SHOGI", letter: "R", prefix: "+", suffix: "'")
115
+ # => "SHOGI:+R'"
116
+ ```
117
+
118
+ ### Validation
119
+
120
+ Check if a string is valid GAN notation:
121
+
122
+ ```ruby
123
+ require "sashite-gan"
124
+
125
+ Sashite::Gan.valid?("CHESS:K") # => true
126
+ Sashite::Gan.valid?("SHOGI:+P") # => true
127
+ Sashite::Gan.valid?("CHESS:K'") # => true
128
+ Sashite::Gan.valid?("chess:k") # => true
129
+
130
+ Sashite::Gan.valid?("") # => false
131
+ Sashite::Gan.valid?("CHESS:k") # => false (mismatched casing)
132
+ Sashite::Gan.valid?("CHESS::K") # => false
133
+ Sashite::Gan.valid?("CHESS-K") # => false
134
+ ```
135
+
136
+ ## Casing Rules
137
+
138
+ The casing of the game identifier must match the piece letter casing:
139
+
140
+ - **Uppercase** game IDs must have **uppercase** piece letters for the first player
141
+ - **Lowercase** game IDs must have **lowercase** piece letters for the second player
142
+
143
+ This ensures consistency with the FEEN specification's third field.
144
+
145
+ ## Examples
146
+
147
+ ### Chess Pieces
148
+
149
+ | PNN | GAN (First Player) | GAN (Second Player) |
150
+ |-------|--------------------|--------------------|
151
+ | `K'` | `CHESS:K'` | `chess:k'` |
152
+ | `Q` | `CHESS:Q` | `chess:q` |
153
+ | `R` | `CHESS:R` | `chess:r` |
154
+ | `B` | `CHESS:B` | `chess:b` |
155
+ | `N` | `CHESS:N` | `chess:n` |
156
+ | `P` | `CHESS:P` | `chess:p` |
157
+
158
+ ### Disambiguated Collisions
159
+
160
+ These examples show how GAN resolves ambiguities between pieces that would have identical PNN representation:
161
+
162
+ | Description | PNN | GAN |
163
+ |-------------|-------|---------------------|
164
+ | Chess Rook (white) | `R` | `CHESS:R` |
165
+ | Makruk Rook (white) | `R` | `MAKRUK:R` |
166
+ | Shogi Rook (sente) | `R` | `SHOGI:R` |
167
+ | Promoted Shogi Rook (sente) | `+R` | `SHOGI:+R` |
168
+
169
+ ## Documentation
170
+
171
+ - [Official GAN Specification](https://sashite.dev/documents/gan/1.0.0/)
172
+ - [API Documentation](https://rubydoc.info/github/sashite/gan.rb/main)
173
+
174
+ ## License
176
175
 
177
- ## About Sashite
176
+ The [gem](https://rubygems.org/gems/sashite-gan) is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
178
177
 
179
- This [gem](https://rubygems.org/gems/sashite-gan) is maintained by [Sashite](https://sashite.com/).
178
+ ## About Sashité
180
179
 
181
- 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!
180
+ 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,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pnn"
4
+
5
+ module Sashite
6
+ module Gan
7
+ # Serializes actor components into GAN (General Actor Notation) strings.
8
+ #
9
+ # The dumper transforms piece data and game identifiers into properly
10
+ # formatted GAN strings, ensuring consistency between game ID casing
11
+ # and piece letter casing according to the GAN specification.
12
+ #
13
+ # According to the specification, game IDs must be either all uppercase
14
+ # or all lowercase, and their casing must match the casing of the piece letter.
15
+ class Dumper
16
+ # Pattern for validating game identifiers - must be all uppercase OR all lowercase
17
+ GAME_ID_PATTERN = /\A([A-Z]+|[a-z]+)\z/
18
+
19
+ # Error message templates
20
+ INVALID_GAME_ID_ERROR = "Game ID must be a non-empty string containing only ASCII letters and must be either all uppercase or all lowercase: %s"
21
+ CASING_MISMATCH_ERROR = "Game ID casing (%s) must match piece letter casing (%s)"
22
+
23
+ # Serializes actor components into a GAN string
24
+ #
25
+ # @param game_id [String] The game identifier (e.g., "CHESS", "shogi")
26
+ # @param piece_params [Hash] Piece parameters as accepted by Pnn.dump:
27
+ # @option piece_params [String] :letter The single ASCII letter identifier (required)
28
+ # @option piece_params [String, nil] :prefix Optional prefix modifier for the piece ("+", "-")
29
+ # @option piece_params [String, nil] :suffix Optional suffix modifier for the piece ("'")
30
+ # @return [String] A properly formatted GAN notation string (e.g., "CHESS:K'")
31
+ # @raise [ArgumentError] If game_id is invalid or casing is inconsistent with piece letter
32
+ # @example Create a GAN string for a white chess king with castling rights
33
+ # Dumper.dump(game_id: "CHESS", letter: "K", suffix: "'")
34
+ # # => "CHESS:K'"
35
+ # @example Create a GAN string for a promoted shogi pawn
36
+ # Dumper.dump(game_id: "SHOGI", letter: "P", prefix: "+")
37
+ # # => "SHOGI:+P"
38
+ def self.dump(game_id:, **piece_params)
39
+ game_id = String(game_id)
40
+ validate_game_id!(game_id)
41
+
42
+ # Build the piece string using the PNN gem
43
+ pnn_string = ::Pnn.dump(**piece_params)
44
+
45
+ # Verify casing consistency
46
+ validate_casing_consistency!(game_id, pnn_string)
47
+
48
+ "#{game_id}:#{pnn_string}"
49
+ end
50
+
51
+ # @api private
52
+ # Validates that the game_id contains only ASCII letters
53
+ #
54
+ # @param game_id [String] The game identifier to validate
55
+ # @return [void]
56
+ # @raise [ArgumentError] If game_id contains non-letter characters
57
+ def self.validate_game_id!(game_id)
58
+ return if game_id.match?(GAME_ID_PATTERN)
59
+
60
+ raise ::ArgumentError, format(INVALID_GAME_ID_ERROR, game_id)
61
+ end
62
+ private_class_method :validate_game_id!
63
+
64
+ # @api private
65
+ # Validates that the casing of the game_id is consistent with the piece letter
66
+ #
67
+ # According to GAN specification, if game_id is uppercase, piece letter must be uppercase,
68
+ # and if game_id is lowercase, piece letter must be lowercase.
69
+ #
70
+ # @param game_id [String] The game identifier
71
+ # @param pnn_string [String] The PNN string
72
+ # @return [void]
73
+ # @raise [ArgumentError] If casing is inconsistent
74
+ def self.validate_casing_consistency!(game_id, pnn_string)
75
+ return if casing_consistent?(game_id, pnn_string)
76
+
77
+ raise ::ArgumentError, format(CASING_MISMATCH_ERROR, game_id, pnn_string)
78
+ end
79
+ private_class_method :validate_casing_consistency!
80
+
81
+ # @api private
82
+ # Verifies that the casing of the game_id matches the casing of the piece letter
83
+ #
84
+ # @param game_id [String] The game identifier
85
+ # @param pnn_string [String] The PNN string
86
+ # @return [Boolean] True if casing is consistent
87
+ def self.casing_consistent?(game_id, pnn_string)
88
+ # Both must be uppercase or both must be lowercase
89
+ (game_id == game_id.upcase) == (pnn_string == pnn_string.upcase)
90
+ end
91
+ private_class_method :casing_consistent?
92
+ end
93
+ end
94
+ end
@@ -1,30 +1,57 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'error'
4
- require_relative 'piece'
3
+ require "pnn"
5
4
 
6
5
  module Sashite
7
- module GAN
8
- # The notation parser.
9
- module Parser
10
- def self.call(arg)
11
- raise Error::String, "Invalid: #{arg.inspect}" unless valid?(arg)
12
-
13
- style, abbr = arg.split(SEPARATOR_CHAR)
14
-
15
- Piece.new(
16
- abbr.delete('-+'),
17
- is_king: abbr.include?('-'),
18
- is_promoted: abbr.include?('+'),
19
- is_topside: style.downcase.eql?(style),
20
- style: style
21
- )
6
+ module Gan
7
+ # Parses GAN strings into their component parts
8
+ class Parser
9
+ # GAN regex pattern for parsing
10
+ PATTERN = /\A(?<game_id>[a-zA-Z]+):(?<pnn_part>[-+]?[a-zA-Z][']?)\z/
11
+
12
+ # Parse a GAN string into its components
13
+ #
14
+ # @param gan_string [String] The GAN string to parse
15
+ # @return [Hash] Hash containing the parsed components
16
+ # @raise [ArgumentError] If the GAN string is invalid
17
+ def self.parse(gan_string)
18
+ gan_string = String(gan_string)
19
+
20
+ matches = PATTERN.match(gan_string)
21
+ raise ArgumentError, "Invalid GAN string: #{gan_string}" if matches.nil?
22
+
23
+ game_id = matches[:game_id]
24
+ pnn_part = matches[:pnn_part]
25
+
26
+ # Parse the PNN part using the PNN gem
27
+ pnn_result = Pnn.parse(pnn_part)
28
+
29
+ # Verify casing consistency
30
+ unless casing_consistent?(game_id, pnn_result[:letter])
31
+ raise ArgumentError, "Game ID casing (#{game_id}) must match piece letter casing (#{pnn_result[:letter]})"
32
+ end
33
+
34
+ # Merge the game_id with the piece parameters for a flatter structure
35
+ { game_id: game_id }.merge(pnn_result)
22
36
  end
23
37
 
24
- def self.valid?(arg)
25
- raise ::TypeError, arg.class.inspect unless arg.is_a?(::String)
38
+ # Safely parse a GAN string without raising exceptions
39
+ #
40
+ # @param gan_string [String] The GAN string to parse
41
+ # @return [Hash, nil] Hash containing the parsed components or nil if invalid
42
+ def self.safe_parse(gan_string)
43
+ parse(gan_string)
44
+ rescue ArgumentError
45
+ nil
46
+ end
26
47
 
27
- arg.match?(/\A([a-z_]+:\+?-?[a-z]{1,2}|[A-Z_]+:\+?-?[A-Z]{1,2})\z/)
48
+ # Verifies that the casing of the game_id matches the casing of the piece letter
49
+ #
50
+ # @param game_id [String] The game identifier
51
+ # @param letter [String] The piece letter
52
+ # @return [Boolean] True if casing is consistent
53
+ def self.casing_consistent?(game_id, letter)
54
+ (game_id == game_id.upcase) == (letter == letter.upcase)
28
55
  end
29
56
  end
30
57
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pnn"
4
+
5
+ module Sashite
6
+ module Gan
7
+ # Validates GAN strings
8
+ class Validator
9
+ # GAN regex pattern for validation
10
+ PATTERN = /\A([A-Z]+:[-+]?[A-Z][']?|[a-z]+:[-+]?[a-z][']?)\z/
11
+
12
+ # Validates if the given string is a valid GAN string
13
+ #
14
+ # @param gan_string [String] The GAN string to validate
15
+ # @return [Boolean] True if the string is a valid GAN string
16
+ def self.valid?(gan_string)
17
+ return false unless gan_string.is_a?(String)
18
+
19
+ PATTERN.match?(gan_string)
20
+ end
21
+ end
22
+ end
23
+ end
data/lib/sashite/gan.rb CHANGED
@@ -1,49 +1,76 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'gan/parser'
3
+ require_relative File.join("gan", "dumper")
4
+ require_relative File.join("gan", "parser")
5
+ require_relative File.join("gan", "validator")
4
6
 
5
7
  module Sashite
6
- # The GAN (General Actor Notation) module.
8
+ # This module provides a Ruby interface for serialization and
9
+ # deserialization of game actors in GAN format.
7
10
  #
8
- # @see https://developer.sashite.com/specs/general-actor-notation
9
- module GAN
10
- SEPARATOR_CHAR = ':'
11
-
12
- # Parse the GAN string into a Ruby object structure and return it.
13
- #
14
- # @example Chess (Western chess)'s Rook, White
15
- # GAN.parse("C:R")
16
- #
17
- # @example Chess (Western chess)'s King, Black
18
- # GAN.parse("c:-k")
19
- #
20
- # @example Makruk (Thai chess)'s Bishop, White
21
- # GAN.parse("M:B")
22
- #
23
- # @example Shogi (Japanese chess)'s King, Gote
24
- # GAN.parse("s:-k")
25
- #
26
- # @example Shogi (Japanese chess)'s King, Sente
27
- # GAN.parse("S:-K")
28
- #
29
- # @example Shogi (Japanese chess)'s promoted Pawn, Sente
30
- # GAN.parse("S:+P")
11
+ # GAN (General Actor Notation) defines a consistent and rule-agnostic
12
+ # format for representing game actors in abstract strategy board games,
13
+ # building upon Piece Name Notation (PNN).
14
+ #
15
+ # @see https://sashite.dev/documents/gan/1.0.0/
16
+ module Gan
17
+ # Serializes an actor into a GAN string.
31
18
  #
32
- # @example Xiangqi (Chinese chess)'s General, Red
33
- # GAN.parse("X:-G")
19
+ # @param game_id [String] The game identifier
20
+ # @param piece_params [Hash] Piece parameters as accepted by Pnn.dump
21
+ # @option piece_params [String] :letter The single ASCII letter identifier (required)
22
+ # @option piece_params [String, nil] :prefix Optional prefix modifier for the piece ("+", "-")
23
+ # @option piece_params [String, nil] :suffix Optional suffix modifier for the piece ("'")
24
+ # @return [String] GAN notation string
25
+ # @raise [ArgumentError] If any parameter is invalid
26
+ # @example
27
+ # Sashite::Gan.dump(game_id: "CHESS", letter: "K", suffix: "'")
28
+ # # => "CHESS:K'"
29
+ def self.dump(game_id:, **piece_params)
30
+ Dumper.dump(game_id:, **piece_params)
31
+ end
32
+
33
+ # Parses a GAN string into its component parts.
34
34
  #
35
- # @example Xiangqi (Chinese chess)'s Flying General, Red
36
- # GAN.parse("X:+-G")
35
+ # @param gan_string [String] GAN notation string
36
+ # @return [Hash] Hash containing the parsed actor data with the following keys:
37
+ # - :game_id [String] - The game identifier
38
+ # - :letter [String] - The base letter identifier
39
+ # - :prefix [String, nil] - The prefix modifier if present
40
+ # - :suffix [String, nil] - The suffix modifier if present
41
+ # @raise [ArgumentError] If the GAN string is invalid
42
+ # @example
43
+ # Sashite::Gan.parse("CHESS:K'")
44
+ # # => { game_id: "CHESS", letter: "K", suffix: "'" }
45
+ def self.parse(gan_string)
46
+ Parser.parse(gan_string)
47
+ end
48
+
49
+ # Safely parses a GAN string into its component parts without raising exceptions.
37
50
  #
38
- # @example Dai Dai Shogi (huge Japanese chess)'s Phoenix, Sente
39
- # GAN.parse("DAI_DAI_SHOGI:PH")
51
+ # @param gan_string [String] GAN notation string
52
+ # @return [Hash, nil] Hash containing the parsed actor data or nil if parsing fails
53
+ # @example
54
+ # # Valid GAN string
55
+ # Sashite::Gan.safe_parse("CHESS:K'")
56
+ # # => { game_id: "CHESS", letter: "K", suffix: "'" }
40
57
  #
41
- # @example Another FOO chess variant's promoted Z piece, Bottom-side
42
- # GAN.parse("FOO:+Z")
58
+ # # Invalid GAN string
59
+ # Sashite::Gan.safe_parse("invalid")
60
+ # # => nil
61
+ def self.safe_parse(gan_string)
62
+ Parser.safe_parse(gan_string)
63
+ end
64
+
65
+ # Validates if the given string is a valid GAN string
43
66
  #
44
- # @return [Piece] An instance of the piece.
45
- def self.parse(string)
46
- Parser.call(string)
67
+ # @param gan_string [String] GAN string to validate
68
+ # @return [Boolean] True if the string is a valid GAN string
69
+ # @example
70
+ # Sashite::Gan.valid?("CHESS:K'") # => true
71
+ # Sashite::Gan.valid?("invalid") # => false
72
+ def self.valid?(gan_string)
73
+ Validator.valid?(gan_string)
47
74
  end
48
75
  end
49
76
  end
data/lib/sashite-gan.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Sashite namespace
4
- module Sashite; end
3
+ # Sashité namespace
4
+ module Sashite
5
+ end
5
6
 
6
- require_relative 'sashite/gan'
7
+ require_relative "sashite/gan"
metadata CHANGED
@@ -1,114 +1,32 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sashite-gan
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
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2020-08-07 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
- name: brutal
13
+ name: pnn
15
14
  requirement: !ruby/object:Gem::Requirement
16
15
  requirements:
17
- - - ">="
16
+ - - "~>"
18
17
  - !ruby/object:Gem::Version
19
- version: '0'
20
- type: :development
18
+ version: 1.1.0
19
+ type: :runtime
21
20
  prerelease: false
22
21
  version_requirements: !ruby/object:Gem::Requirement
23
22
  requirements:
24
- - - ">="
23
+ - - "~>"
25
24
  - !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: rake
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: rubocop-performance
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-thread_safety
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: simplecov
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: yard
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
- description: A Ruby interface for data serialization in GAN format ♟️
25
+ version: 1.1.0
26
+ description: A Ruby interface for serialization and deserialization of game actors
27
+ in GAN format. GAN is a consistent and rule-agnostic format for representing game
28
+ actors in abstract strategy board games, providing a standardized way to identify
29
+ pieces with their originating game.
112
30
  email: contact@cyril.email
113
31
  executables: []
114
32
  extensions: []
@@ -118,22 +36,19 @@ files:
118
36
  - README.md
119
37
  - lib/sashite-gan.rb
120
38
  - lib/sashite/gan.rb
121
- - lib/sashite/gan/abbr.rb
122
- - lib/sashite/gan/error.rb
123
- - lib/sashite/gan/error/string.rb
124
- - lib/sashite/gan/error/style.rb
125
- - lib/sashite/gan/error/type.rb
39
+ - lib/sashite/gan/dumper.rb
126
40
  - lib/sashite/gan/parser.rb
127
- - lib/sashite/gan/piece.rb
128
- homepage: https://developer.sashite.com/specs/general-actor-notation
41
+ - lib/sashite/gan/validator.rb
42
+ homepage: https://github.com/sashite/gan.rb
129
43
  licenses:
130
44
  - MIT
131
45
  metadata:
132
46
  bug_tracker_uri: https://github.com/sashite/gan.rb/issues
133
- documentation_uri: https://rubydoc.info/gems/sashite-gan/index
47
+ documentation_uri: https://rubydoc.info/github/sashite/gan.rb/main
48
+ homepage_uri: https://github.com/sashite/gan.rb
134
49
  source_code_uri: https://github.com/sashite/gan.rb
135
- wiki_uri: https://github.com/sashite/gan.rb/wiki
136
- post_install_message:
50
+ specification_uri: https://sashite.dev/documents/gan/1.0.0/
51
+ rubygems_mfa_required: 'true'
137
52
  rdoc_options: []
138
53
  require_paths:
139
54
  - lib
@@ -141,15 +56,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
141
56
  requirements:
142
57
  - - ">="
143
58
  - !ruby/object:Gem::Version
144
- version: 2.3.0
59
+ version: 3.2.0
145
60
  required_rubygems_version: !ruby/object:Gem::Requirement
146
61
  requirements:
147
62
  - - ">="
148
63
  - !ruby/object:Gem::Version
149
64
  version: '0'
150
65
  requirements: []
151
- rubygems_version: 3.1.2
152
- signing_key:
66
+ rubygems_version: 3.6.7
153
67
  specification_version: 4
154
- summary: A GAN implementation in Ruby.
68
+ summary: GAN (General Actor Notation) support for the Ruby language.
155
69
  test_files: []
@@ -1,76 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Sashite
4
- module GAN
5
- # The piece's abbreviation.
6
- class Abbr
7
- # The piece's type.
8
- #
9
- # @!attribute [r] type
10
- # @return [String] The type of the piece.
11
- attr_reader :type
12
-
13
- def initialize(type, is_promoted:, is_king:)
14
- @type = TypeString(type)
15
- @is_promoted = Boolean(is_promoted)
16
- @is_king = Boolean(is_king)
17
-
18
- freeze
19
- end
20
-
21
- # @return [Boolean] Is the piece a king?
22
- def king?
23
- @is_king
24
- end
25
-
26
- # @return [Boolean] Is the piece promoted?
27
- def promoted?
28
- @is_promoted
29
- end
30
-
31
- # @return [String] The abbreviation of the piece.
32
- def to_s
33
- str = type
34
- str = "-#{str}" if king?
35
- str = "+#{str}" if promoted?
36
- str
37
- end
38
-
39
- def inspect
40
- to_s
41
- end
42
-
43
- def ==(other)
44
- other.to_s == to_s
45
- end
46
-
47
- def eql?(other)
48
- self == other
49
- end
50
-
51
- private
52
-
53
- # rubocop:disable Naming/MethodName
54
-
55
- # Ensures `arg` is a boolean, and returns it. Otherwise, raises a
56
- # `TypeError`.
57
- def Boolean(arg)
58
- raise ::TypeError, arg.class.inspect unless [false, true].include?(arg)
59
-
60
- arg
61
- end
62
-
63
- # Ensures `arg` is a type, and returns it. Otherwise, raises an error.
64
- def TypeString(arg)
65
- raise ::TypeError, arg.class.inspect unless arg.is_a?(::String)
66
- raise Error::Type, arg.inspect unless arg.match?(/\A[a-z]{1,2}\z/i)
67
-
68
- arg
69
- end
70
-
71
- # rubocop:enable Naming/MethodName
72
- end
73
- end
74
- end
75
-
76
- require_relative 'error'
@@ -1,10 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Sashite
4
- module GAN
5
- module Error
6
- # `String` is the base class for GAN string errors.
7
- class String < ::StandardError; end
8
- end
9
- end
10
- end
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'string'
4
-
5
- module Sashite
6
- module GAN
7
- module Error
8
- # Raised when encountering an invalid sequence of characters.
9
- class Style < String; end
10
- end
11
- end
12
- end
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'string'
4
-
5
- module Sashite
6
- module GAN
7
- module Error
8
- # Raised when encountering an invalid sequence of characters.
9
- class Type < String; end
10
- end
11
- end
12
- end
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Sashite
4
- module GAN
5
- # The error namespace.
6
- module Error; end
7
- end
8
- end
9
-
10
- Dir[File.join(File.dirname(__FILE__), 'error', '*.rb')].each do |fname|
11
- require_relative fname
12
- end
@@ -1,150 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Sashite
4
- module GAN
5
- # A piece abstraction.
6
- class Piece
7
- # The abbreviation of the piece.
8
- #
9
- # @!attribute [r] abbr
10
- # @return [String] The abbreviation of the piece.
11
- attr_reader :abbr
12
-
13
- # The piece's style.
14
- #
15
- # @!attribute [r] style
16
- # @return [String] The piece's style.
17
- attr_reader :style
18
-
19
- # Initialize a piece.
20
- #
21
- # @param type [String] The type of the piece.
22
- # @param is_king [Boolean] Is it a King (or a Xiangqi General),
23
- # so it could be checkmated?
24
- # @param is_promoted [Boolean] Is it promoted?
25
- # @param is_topside [Boolean] Is it owned by top-side player?
26
- # @param style [String] The piece's style.
27
- def initialize(type, is_king:, is_promoted:, is_topside:, style:)
28
- @abbr = Abbr.new(type, is_king: is_king, is_promoted: is_promoted)
29
- @is_topside = Boolean(is_topside)
30
- @style = StyleString(style)
31
-
32
- freeze
33
- end
34
-
35
- def king?
36
- abbr.king?
37
- end
38
-
39
- # Is it owned by top-side player?
40
- #
41
- # @return [Boolean] Returns `true` if the top-side player own the piece,
42
- # `false` otherwise.
43
- def topside?
44
- @is_topside
45
- end
46
-
47
- # Is it owned by bottom-side player?
48
- #
49
- # @return [Boolean] Returns `true` if the bottom-side player own the
50
- # piece, `false` otherwise.
51
- def bottomside?
52
- !topside?
53
- end
54
-
55
- # @see https://developer.sashite.com/specs/general-actor-notation
56
- # @return [String] The notation of the piece.
57
- def to_s
58
- topside? ? raw.downcase : raw.upcase
59
- end
60
-
61
- def inspect
62
- to_s
63
- end
64
-
65
- # @return [Piece] The top-side side version of the piece.
66
- def topside
67
- topside? ? self : oppositeside
68
- end
69
-
70
- # @return [Piece] The bottom-side side version of the piece.
71
- def bottomside
72
- topside? ? oppositeside : self
73
- end
74
-
75
- # @return [Piece] The opposite side version of the piece.
76
- def oppositeside
77
- self.class.new(abbr.type,
78
- is_king: abbr.king?,
79
- is_promoted: abbr.promoted?,
80
- is_topside: !topside?,
81
- style: style
82
- )
83
- end
84
-
85
- # @return [Piece] The promoted version of the piece.
86
- def promote
87
- self.class.new(abbr.type,
88
- is_king: abbr.king?,
89
- is_promoted: true,
90
- is_topside: topside?,
91
- style: style
92
- )
93
- end
94
-
95
- # @return [Piece] The unpromoted version of the piece.
96
- def unpromote
97
- self.class.new(abbr.type,
98
- is_king: abbr.king?,
99
- is_promoted: false,
100
- is_topside: topside?,
101
- style: style
102
- )
103
- end
104
-
105
- def ==(other)
106
- other.to_s == to_s
107
- end
108
-
109
- def eql?(other)
110
- self == other
111
- end
112
-
113
- private
114
-
115
- # @return [String] The style and the abbreviation of the piece (without
116
- # case).
117
- def raw
118
- params.join(SEPARATOR_CHAR)
119
- end
120
-
121
- # @return [Array] The style and the abbreviation of the piece.
122
- def params
123
- [style, abbr]
124
- end
125
-
126
- # rubocop:disable Naming/MethodName
127
-
128
- # Ensures `arg` is a boolean, and returns it. Otherwise, raises a
129
- # `TypeError`.
130
- def Boolean(arg)
131
- raise ::TypeError, arg.class.inspect unless [false, true].include?(arg)
132
-
133
- arg
134
- end
135
-
136
- # Ensures `arg` is a style, and returns it. Otherwise, raises an error.
137
- def StyleString(arg)
138
- raise ::TypeError, arg.class.inspect unless arg.is_a?(::String)
139
- raise Error::Style, arg.inspect unless arg.match?(/\A[a-z_]+\z/i)
140
-
141
- arg
142
- end
143
-
144
- # rubocop:enable Naming/MethodName
145
- end
146
- end
147
- end
148
-
149
- require_relative 'abbr'
150
- require_relative 'error'