sashite-gan 2.2.0 → 4.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.
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sashite
4
+ module Gan
5
+ # Represents a game actor in GAN format
6
+ #
7
+ # An actor combines a style identifier (SNN format) with a piece identifier (PNN format)
8
+ # to create an unambiguous representation of a game piece within its style context.
9
+ # The casing of both components determines player association and piece ownership:
10
+ # - Style casing determines which player uses that style tradition (fixed per game)
11
+ # - Piece casing determines current piece ownership (may change during gameplay)
12
+ #
13
+ # @example
14
+ # # Traditional same-style game
15
+ # white_king = Sashite::Gan::Actor.new("CHESS", "K") # First player's chess king
16
+ # black_king = Sashite::Gan::Actor.new("chess", "k") # Second player's chess king
17
+ #
18
+ # # Cross-style game
19
+ # chess_king = Sashite::Gan::Actor.new("CHESS", "K") # First player uses chess
20
+ # shogi_king = Sashite::Gan::Actor.new("shogi", "k") # Second player uses shogi
21
+ #
22
+ # # Dynamic ownership (piece captured and converted)
23
+ # captured = Sashite::Gan::Actor.new("CHESS", "k") # Chess piece owned by second player
24
+ class Actor
25
+ # @return [Sashite::Snn::Style] The style component
26
+ attr_reader :style
27
+
28
+ # @return [Pnn::Piece] The piece component
29
+ attr_reader :piece
30
+
31
+ # Create a new actor instance
32
+ #
33
+ # @param style [String, Sashite::Snn::Style] The style identifier or style object
34
+ # @param piece [String, Pnn::Piece] The piece identifier or piece object
35
+ # @raise [ArgumentError] if the parameters are invalid
36
+ #
37
+ # @example
38
+ # # With strings
39
+ # actor = Sashite::Gan::Actor.new("CHESS", "K")
40
+ #
41
+ # # With objects
42
+ # style = Sashite::Snn::Style.new("CHESS")
43
+ # piece = Pnn::Piece.new("K")
44
+ # actor = Sashite::Gan::Actor.new(style, piece)
45
+ def initialize(style, piece)
46
+ @style = style.is_a?(Snn::Style) ? style : Snn::Style.new(style.to_s)
47
+ @piece = piece.is_a?(Pnn::Piece) ? piece : Pnn::Piece.parse(piece.to_s)
48
+
49
+ freeze
50
+ end
51
+
52
+ # Parse a GAN string into an actor object
53
+ #
54
+ # @param gan_string [String] GAN notation string
55
+ # @return [Actor] new actor instance
56
+ # @raise [ArgumentError] if the GAN string is invalid
57
+ #
58
+ # @example
59
+ # actor = Sashite::Gan::Actor.parse("CHESS:K")
60
+ # # => #<Sashite::Gan::Actor:0x... style="CHESS" piece="K">
61
+ #
62
+ # enhanced = Sashite::Gan::Actor.parse("SHOGI:+p'")
63
+ # # => #<Sashite::Gan::Actor:0x... style="SHOGI" piece="+p'">
64
+ def self.parse(gan_string)
65
+ style_string, piece_string = Gan.parse_components(gan_string)
66
+ new(style_string, piece_string)
67
+ end
68
+
69
+ # Convert the actor to its GAN string representation
70
+ #
71
+ # @return [String] GAN notation string
72
+ #
73
+ # @example
74
+ # actor.to_s # => "CHESS:K"
75
+ def to_s
76
+ "#{style}:#{piece}"
77
+ end
78
+
79
+ # Get the style name as a string
80
+ #
81
+ # @return [String] The style identifier string
82
+ #
83
+ # @example
84
+ # actor.style_name # => "CHESS"
85
+ def style_name
86
+ style.to_s
87
+ end
88
+
89
+ # Get the piece name as a string
90
+ #
91
+ # @return [String] The piece identifier string
92
+ #
93
+ # @example
94
+ # actor.piece_name # => "K"
95
+ def piece_name
96
+ piece.to_s
97
+ end
98
+
99
+ # Create a new actor with an enhanced piece
100
+ #
101
+ # @return [Actor] new actor instance with enhanced piece
102
+ #
103
+ # @example
104
+ # actor.enhance_piece # SHOGI:P => SHOGI:+P
105
+ def enhance_piece
106
+ self.class.new(style, piece.enhance)
107
+ end
108
+
109
+ # Create a new actor with a diminished piece
110
+ #
111
+ # @return [Actor] new actor instance with diminished piece
112
+ #
113
+ # @example
114
+ # actor.diminish_piece # CHESS:R => CHESS:-R
115
+ def diminish_piece
116
+ self.class.new(style, piece.diminish)
117
+ end
118
+
119
+ # Create a new actor with an intermediate piece state
120
+ #
121
+ # @return [Actor] new actor instance with intermediate piece
122
+ #
123
+ # @example
124
+ # actor.set_piece_intermediate # CHESS:R => CHESS:R'
125
+ def set_piece_intermediate
126
+ self.class.new(style, piece.intermediate)
127
+ end
128
+
129
+ # Create a new actor with a piece without modifiers
130
+ #
131
+ # @return [Actor] new actor instance with bare piece
132
+ #
133
+ # @example
134
+ # actor.bare_piece # SHOGI:+P' => SHOGI:P
135
+ def bare_piece
136
+ self.class.new(style, piece.bare)
137
+ end
138
+
139
+ # Create a new actor with piece ownership flipped
140
+ #
141
+ # Changes the piece ownership (case) while keeping the style unchanged.
142
+ # This method is rule-agnostic and preserves all piece modifiers.
143
+ # If modifier removal is needed, it should be done explicitly.
144
+ #
145
+ # @return [Actor] new actor instance with ownership changed
146
+ #
147
+ # @example
148
+ # actor.change_piece_ownership # SHOGI:P => SHOGI:p
149
+ # enhanced.change_piece_ownership # SHOGI:+P => SHOGI:+p (modifiers preserved)
150
+ #
151
+ # # To remove modifiers explicitly:
152
+ # actor.bare_piece.change_piece_ownership # SHOGI:+P => SHOGI:p
153
+ # # or
154
+ # actor.change_piece_ownership.bare_piece # SHOGI:+P => SHOGI:p
155
+ def change_piece_ownership
156
+ self.class.new(style, piece.flip)
157
+ end
158
+
159
+ # Custom equality comparison
160
+ #
161
+ # @param other [Object] The object to compare with
162
+ # @return [Boolean] true if both objects are Actor instances with the same components
163
+ def ==(other)
164
+ other.is_a?(Actor) && style == other.style && piece == other.piece
165
+ end
166
+
167
+ # Alias for equality comparison
168
+ alias eql? ==
169
+
170
+ # Hash code for use in hashes and sets
171
+ #
172
+ # @return [Integer] The hash code
173
+ def hash
174
+ [self.class, style, piece].hash
175
+ end
176
+
177
+ # String representation for debugging
178
+ #
179
+ # @return [String] A detailed string representation
180
+ def inspect
181
+ "#<#{self.class}:0x#{object_id.to_s(16)} style=#{style_name.inspect} piece=#{piece_name.inspect}>"
182
+ end
183
+ end
184
+ end
185
+ end
data/lib/sashite/gan.rb CHANGED
@@ -1,49 +1,88 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'gan/parser'
3
+ require "sashite/snn"
4
+ require "pnn"
5
+ require_relative "gan/actor"
4
6
 
5
7
  module Sashite
6
- # The GAN (General Actor Notation) module.
8
+ # General Actor Notation (GAN) module
7
9
  #
8
- # @see https://developer.sashite.com/specs/general-actor-notation
9
- module GAN
10
- SEPARATOR_CHAR = ':'
10
+ # GAN provides a consistent and rule-agnostic format for identifying game actors
11
+ # in abstract strategy board games. It combines Style Name Notation (SNN) with
12
+ # Piece Name Notation (PNN) to create unambiguous actor identification that
13
+ # eliminates collision problems when multiple piece styles are present.
14
+ #
15
+ # @see https://sashite.dev/documents/gan/1.0.0/ GAN Specification v1.0.0
16
+ module Gan
17
+ # GAN validation regular expression
18
+ # Matches: <snn>:<pnn> where snn and pnn follow their respective specifications
19
+ VALIDATION_REGEX = /\A([A-Z][A-Z0-9]*|[a-z][a-z0-9]*):[-+]?[a-zA-Z]'?\z/
11
20
 
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")
21
+ # Check if a string is valid GAN notation
19
22
  #
20
- # @example Makruk (Thai chess)'s Bishop, White
21
- # GAN.parse("M:B")
23
+ # @param gan_string [String] The string to validate
24
+ # @return [Boolean] true if the string is valid GAN notation, false otherwise
22
25
  #
23
- # @example Shogi (Japanese chess)'s King, Gote
24
- # GAN.parse("s:-k")
26
+ # @example
27
+ # Sashite::Gan.valid?("CHESS:K") # => true
28
+ # Sashite::Gan.valid?("shogi:+p'") # => true
29
+ # Sashite::Gan.valid?("Chess:K") # => false (mixed case in style)
30
+ # Sashite::Gan.valid?("CHESS") # => false (missing piece)
31
+ # Sashite::Gan.valid?("") # => false (empty string)
32
+ def self.valid?(gan_string)
33
+ return false unless gan_string.is_a?(String)
34
+ return false if gan_string.empty?
35
+
36
+ # Quick regex check first
37
+ return false unless VALIDATION_REGEX.match?(gan_string)
38
+
39
+ # Split and validate components individually for more precise validation
40
+ parts = gan_string.split(":", 2)
41
+ return false unless parts.length == 2
42
+
43
+ style_part, piece_part = parts
44
+
45
+ # Validate SNN and PNN components using their respective libraries
46
+ Snn.valid?(style_part) && Pnn.valid?(piece_part)
47
+ end
48
+
49
+ # Convenience method to create an actor object
25
50
  #
26
- # @example Shogi (Japanese chess)'s King, Sente
27
- # GAN.parse("S:-K")
51
+ # @param style [String, Sashite::Snn::Style] The style identifier or style object
52
+ # @param piece [String, Pnn::Piece] The piece identifier or piece object
53
+ # @return [Sashite::Gan::Actor] A new actor object
54
+ # @raise [ArgumentError] if the parameters are invalid
28
55
  #
29
- # @example Shogi (Japanese chess)'s promoted Pawn, Sente
30
- # GAN.parse("S:+P")
56
+ # @example
57
+ # actor = Sashite::Gan.actor("CHESS", "K")
58
+ # # => #<Sashite::Gan::Actor:0x... style="CHESS" piece="K">
31
59
  #
32
- # @example Xiangqi (Chinese chess)'s General, Red
33
- # GAN.parse("X:-G")
60
+ # # With objects
61
+ # style = Sashite::Snn::Style.new("CHESS")
62
+ # piece = Pnn::Piece.new("K")
63
+ # actor = Sashite::Gan.actor(style, piece)
64
+ def self.actor(style, piece)
65
+ Actor.new(style, piece)
66
+ end
67
+
68
+ # Parse a GAN string into component parts
34
69
  #
35
- # @example Xiangqi (Chinese chess)'s Flying General, Red
36
- # GAN.parse("X:+-G")
70
+ # @param gan_string [String] The GAN string to parse
71
+ # @return [Array<String>] An array containing [style_string, piece_string]
72
+ # @raise [ArgumentError] if the string is invalid GAN notation
37
73
  #
38
- # @example Dai Dai Shogi (huge Japanese chess)'s Phoenix, Sente
39
- # GAN.parse("DAI_DAI_SHOGI:PH")
74
+ # @example
75
+ # Sashite::Gan.parse_components("CHESS:K")
76
+ # # => ["CHESS", "K"]
40
77
  #
41
- # @example Another FOO chess variant's promoted Z piece, Bottom-side
42
- # GAN.parse("FOO:+Z")
78
+ # Sashite::Gan.parse_components("shogi:+p'")
79
+ # # => ["shogi", "+p'"]
43
80
  #
44
- # @return [Piece] An instance of the piece.
45
- def self.parse(string)
46
- Parser.call(string)
81
+ # @api private
82
+ def self.parse_components(gan_string)
83
+ raise ArgumentError, "Invalid GAN format: #{gan_string.inspect}" unless valid?(gan_string)
84
+
85
+ gan_string.split(":", 2)
47
86
  end
48
87
  end
49
88
  end
data/lib/sashite-gan.rb CHANGED
@@ -1,6 +1,18 @@
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
+ # General Actor Notation (GAN) implementation for Ruby
6
+ #
7
+ # GAN defines a consistent and rule-agnostic format for identifying game actors
8
+ # in abstract strategy board games. GAN provides unambiguous identification of
9
+ # pieces by combining Style Name Notation (SNN) with Piece Name Notation (PNN),
10
+ # eliminating collision problems when multiple piece styles are present in the
11
+ # same context.
12
+ #
13
+ # @see https://sashite.dev/documents/gan/1.0.0/ GAN Specification v1.0.0
14
+ # @author Sashité
15
+ # @since 1.0.0
16
+ end
5
17
 
6
- require_relative 'sashite/gan'
18
+ require_relative "sashite/gan"
metadata CHANGED
@@ -1,114 +1,46 @@
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: 4.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: 2.0.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'
25
+ version: 2.0.0
27
26
  - !ruby/object:Gem::Dependency
28
- name: bundler
27
+ name: sashite-snn
29
28
  requirement: !ruby/object:Gem::Requirement
30
29
  requirements:
31
- - - ">="
30
+ - - "~>"
32
31
  - !ruby/object:Gem::Version
33
- version: '0'
34
- type: :development
32
+ version: 1.0.0
33
+ type: :runtime
35
34
  prerelease: false
36
35
  version_requirements: !ruby/object:Gem::Requirement
37
36
  requirements:
38
- - - ">="
37
+ - - "~>"
39
38
  - !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 ♟️
39
+ version: 1.0.0
40
+ description: A Ruby interface for serialization and deserialization of game actors
41
+ in GAN format. GAN is a consistent and rule-agnostic format for representing game
42
+ actors in abstract strategy board games, providing a standardized way to identify
43
+ pieces with their originating game.
112
44
  email: contact@cyril.email
113
45
  executables: []
114
46
  extensions: []
@@ -118,22 +50,17 @@ files:
118
50
  - README.md
119
51
  - lib/sashite-gan.rb
120
52
  - 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
126
- - lib/sashite/gan/parser.rb
127
- - lib/sashite/gan/piece.rb
128
- homepage: https://developer.sashite.com/specs/general-actor-notation
53
+ - lib/sashite/gan/actor.rb
54
+ homepage: https://github.com/sashite/gan.rb
129
55
  licenses:
130
56
  - MIT
131
57
  metadata:
132
58
  bug_tracker_uri: https://github.com/sashite/gan.rb/issues
133
- documentation_uri: https://rubydoc.info/gems/sashite-gan/index
59
+ documentation_uri: https://rubydoc.info/github/sashite/gan.rb/main
60
+ homepage_uri: https://github.com/sashite/gan.rb
134
61
  source_code_uri: https://github.com/sashite/gan.rb
135
- wiki_uri: https://github.com/sashite/gan.rb/wiki
136
- post_install_message:
62
+ specification_uri: https://sashite.dev/documents/gan/1.0.0/
63
+ rubygems_mfa_required: 'true'
137
64
  rdoc_options: []
138
65
  require_paths:
139
66
  - lib
@@ -141,15 +68,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
141
68
  requirements:
142
69
  - - ">="
143
70
  - !ruby/object:Gem::Version
144
- version: 2.3.0
71
+ version: 3.2.0
145
72
  required_rubygems_version: !ruby/object:Gem::Requirement
146
73
  requirements:
147
74
  - - ">="
148
75
  - !ruby/object:Gem::Version
149
76
  version: '0'
150
77
  requirements: []
151
- rubygems_version: 3.1.2
152
- signing_key:
78
+ rubygems_version: 3.6.7
153
79
  specification_version: 4
154
- summary: A GAN implementation in Ruby.
80
+ summary: GAN (General Actor Notation) support for the Ruby language.
155
81
  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,31 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'error'
4
- require_relative 'piece'
5
-
6
- 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
- )
22
- end
23
-
24
- def self.valid?(arg)
25
- raise ::TypeError, arg.class.inspect unless arg.is_a?(::String)
26
-
27
- arg.match?(/\A([a-z_]+:\+?-?[a-z]{1,2}|[A-Z_]+:\+?-?[A-Z]{1,2})\z/)
28
- end
29
- end
30
- end
31
- end