class Sashite::Gan::Actor
Represents a game actor in GAN (General Actor
Notation) format.
An actor combines a style identifier (SNN format) with a piece identifier (PIN format) using a colon separator and consistent case encoding to create an unambiguous representation of a game piece within its style context.
GAN represents all four fundamental piece attributes from the Game Protocol:
-
Type → PIN component (ASCII letter choice)
-
Side → Consistent case encoding across both SNN and PIN components
-
State → PIN component (optional prefix modifier)
-
Style → SNN component (explicit style identifier)
All instances are immutable - transformation methods return new instances. This follows the Game Protocol’s actor model with complete attribute representation.
Constants
- DIMINISHED_STATE
- ENHANCED_STATE
- ERROR_CASE_MISMATCH
- ERROR_INVALID_GAN
-
Error messages
- ERROR_INVALID_NAME
- ERROR_INVALID_SIDE
- ERROR_INVALID_STATE
- ERROR_INVALID_TYPE
- FIRST_PLAYER
-
Player side constants
- NORMAL_STATE
-
State constants
- SECOND_PLAYER
- SEPARATOR
-
Colon separator character
- VALID_SIDES
-
Valid sides
- VALID_STATES
-
Valid states
- VALID_TYPES
-
Valid types (A-Z)
Attributes
Get the piece component
@return [Sashite::Pin::Piece] the piece component
Get the style component
@return [Sashite::Snn::Style] the style component
Public Class Methods
Source
# File lib/sashite/gan/actor.rb, line 62 def initialize(name, type, side, state = NORMAL_STATE) self.class.validate_name(name) self.class.validate_type(type) self.class.validate_side(side) self.class.validate_state(state) @style = Snn::Style.new(name, side) @piece = Pin::Piece.new(type, side, state) freeze end
Create a new actor instance
@param name [Symbol] style name (with proper capitalization) @param type [Symbol] piece type (:A to :Z) @param side [Symbol] player side (:first or :second) @param state [Symbol] piece state (:normal, :enhanced, or :diminished) @raise [ArgumentError] if parameters are invalid @example
Actor.new(:Chess, :K, :first, :normal) Actor.new(:Shogi, :P, :second, :enhanced)
Source
# File lib/sashite/gan/actor.rb, line 83 def self.parse(gan_string) string_value = String(gan_string) # Split into SNN and PIN components snn_part, pin_part = string_value.split(SEPARATOR, 2) # Validate basic format unless snn_part && pin_part && string_value.count(SEPARATOR) == 1 raise ::ArgumentError, format(ERROR_INVALID_GAN, string_value) end # Validate case consistency validate_case_consistency(snn_part, pin_part, string_value) # Parse components - let SNN and PIN handle their own validation parsed_style = Snn::Style.parse(snn_part) parsed_piece = Pin::Piece.parse(pin_part) # Create actor with parsed components new(parsed_style.name, parsed_piece.type, parsed_style.side, parsed_piece.state) end
Parse a GAN string into an Actor
object
@param gan_string [String] GAN notation string @return [Actor] new actor instance @raise [ArgumentError] if the GAN string is invalid or has case mismatch @example
Actor.parse("CHESS:K") # => #<Actor name=:Chess type=:K side=:first state=:normal> Actor.parse("shogi:+p") # => #<Actor name=:Shogi type=:P side=:second state=:enhanced> Actor.parse("XIANGQI:-G") # => #<Actor name=:Xiangqi type=:G side=:first state=:diminished>
Source
# File lib/sashite/gan/actor.rb, line 117 def self.valid?(gan_string) return false unless gan_string.is_a?(::String) return false if gan_string.empty? # Split into SNN and PIN components parts = gan_string.split(SEPARATOR, 2) return false unless parts.length == 2 snn_part, pin_part = parts # Validate each component with its specific regex return false unless snn_part.match?(Snn::Style::SNN_PATTERN) return false unless pin_part.match?(Pin::Piece::PIN_PATTERN) # Check case consistency between components case_consistent?(snn_part, pin_part) end
Check if a string is a valid GAN notation
@param gan_string [String] The string to validate @return [Boolean] true if valid GAN, false otherwise
@example
Sashite::Gan::Actor.valid?("CHESS:K") # => true Sashite::Gan::Actor.valid?("shogi:+p") # => true Sashite::Gan::Actor.valid?("Chess:K") # => false (mixed case in style) Sashite::Gan::Actor.valid?("CHESS:k") # => false (case mismatch) Sashite::Gan::Actor.valid?("CHESS") # => false (missing piece) Sashite::Gan::Actor.valid?("") # => false (empty string)
Source
# File lib/sashite/gan/actor.rb, line 405 def self.validate_name(name) return if valid_name?(name) raise ::ArgumentError, format(ERROR_INVALID_NAME, name.inspect) end
Validate that the name is a valid symbol with proper capitalization
@param name [Symbol] the name to validate @raise [ArgumentError] if invalid
Source
# File lib/sashite/gan/actor.rb, line 425 def self.validate_side(side) return if VALID_SIDES.include?(side) raise ::ArgumentError, format(ERROR_INVALID_SIDE, side.inspect) end
Validate that the side is a valid symbol
@param side [Symbol] the side to validate @raise [ArgumentError] if invalid
Source
# File lib/sashite/gan/actor.rb, line 435 def self.validate_state(state) return if VALID_STATES.include?(state) raise ::ArgumentError, format(ERROR_INVALID_STATE, state.inspect) end
Validate that the state is a valid symbol
@param state [Symbol] the state to validate @raise [ArgumentError] if invalid
Source
# File lib/sashite/gan/actor.rb, line 415 def self.validate_type(type) return if VALID_TYPES.include?(type) raise ::ArgumentError, format(ERROR_INVALID_TYPE, type.inspect) end
Validate that the type is a valid symbol
@param type [Symbol] the type to validate @raise [ArgumentError] if invalid
Private Class Methods
Source
# File lib/sashite/gan/actor.rb, line 460 def self.case_consistent?(snn_part, pin_part) # Extract letter from PIN part (remove optional +/- prefix) pin_letter_match = pin_part.match(/[-+]?([A-Za-z])$/) return false unless pin_letter_match pin_letter = pin_letter_match[1] snn_uppercase = snn_part == snn_part.upcase pin_uppercase = pin_letter == pin_letter.upcase snn_uppercase == pin_uppercase end
Check case consistency between SNN and PIN components
@param snn_part [String] the SNN component @param pin_part [String] the PIN component (with optional prefix) @return [Boolean] true if case is consistent, false otherwise
Source
# File lib/sashite/gan/actor.rb, line 445 def self.valid_name?(name) return false unless name.is_a?(::Symbol) name_string = name.to_s return false if name_string.empty? # Must match proper capitalization pattern: first letter uppercase, rest lowercase/digits name_string.match?(/\A[A-Z][a-z0-9]*\z/) end
Check if a name is valid (symbol with proper capitalization)
@param name [Object] the name to check @return [Boolean] true if valid
Source
# File lib/sashite/gan/actor.rb, line 479 def self.validate_case_consistency(snn_part, pin_part, full_string) return if case_consistent?(snn_part, pin_part) raise ::ArgumentError, format(ERROR_CASE_MISMATCH, full_string) end
Validate case consistency between SNN and PIN components
@param snn_part [String] the SNN component @param pin_part [String] the PIN component (with optional prefix) @param full_string [String] the full GAN string for error reporting @raise [ArgumentError] if case mismatch detected
Public Instance Methods
Source
# File lib/sashite/gan/actor.rb, line 385 def ==(other) return false unless other.is_a?(self.class) name == other.name && type == other.type && side == other.side && state == other.state end
Custom equality comparison
@param other [Object] object to compare with @return [Boolean] true if actors are equal
Source
# File lib/sashite/gan/actor.rb, line 220 def diminish return self if diminished? self.class.new(name, type, side, DIMINISHED_STATE) end
Create a new actor with diminished piece state
@return [Actor] new actor instance with diminished piece @example
actor.diminish # CHESS:K => CHESS:-K
Source
# File lib/sashite/gan/actor.rb, line 312 def diminished? piece.diminished? end
Check if the actor has diminished state
@return [Boolean] true if diminished
Source
# File lib/sashite/gan/actor.rb, line 209 def enhance return self if enhanced? self.class.new(name, type, side, ENHANCED_STATE) end
Create a new actor with enhanced piece state
@return [Actor] new actor instance with enhanced piece @example
actor.enhance # CHESS:K => CHESS:+K
Source
# File lib/sashite/gan/actor.rb, line 305 def enhanced? piece.enhanced? end
Check if the actor has enhanced state
@return [Boolean] true if enhanced
Source
# File lib/sashite/gan/actor.rb, line 326 def first_player? style.first_player? end
Check if the actor belongs to the first player
@return [Boolean] true if first player
Source
# File lib/sashite/gan/actor.rb, line 246 def flip self.class.new(name, type, opposite_side, state) end
Create a new actor with opposite ownership (side)
Changes both the style and piece sides consistently. This method is rule-agnostic and preserves all piece modifiers.
@return [Actor] new actor instance with flipped side @example
actor.flip # CHESS:K => chess:k enhanced.flip # CHESS:+K => chess:+k (modifiers preserved)
Source
# File lib/sashite/gan/actor.rb, line 397 def hash [self.class, name, type, side, state].hash end
Custom hash implementation for use in collections
@return [Integer] hash value
Source
# File lib/sashite/gan/actor.rb, line 173 def name style.name end
Get the style name
@return [Symbol] style name (with proper capitalization) @example
actor.name # => :Chess
Source
# File lib/sashite/gan/actor.rb, line 319 def normal? piece.normal? end
Check if the actor has normal state (no modifiers)
@return [Boolean] true if no modifiers are present
Source
# File lib/sashite/gan/actor.rb, line 231 def normalize return self if normal? self.class.new(name, type, side, NORMAL_STATE) end
Create a new actor with normal piece state (no modifiers)
@return [Actor] new actor instance with normalized piece @example
actor.normalize # CHESS:+K => CHESS:K
Source
# File lib/sashite/gan/actor.rb, line 343 def same_name?(other) return false unless other.is_a?(self.class) name == other.name end
Check if this actor has the same style name as another
@param other [Actor] actor to compare with @return [Boolean] true if same style name @example
chess1.same_name?(chess2) # (CHESS:K) and (chess:Q) => true
Source
# File lib/sashite/gan/actor.rb, line 365 def same_side?(other) return false unless other.is_a?(self.class) side == other.side end
Check if this actor belongs to the same side as another
@param other [Actor] actor to compare with @return [Boolean] true if same side
Source
# File lib/sashite/gan/actor.rb, line 375 def same_state?(other) return false unless other.is_a?(self.class) state == other.state end
Check if this actor has the same state as another
@param other [Actor] actor to compare with @return [Boolean] true if same piece state
Source
# File lib/sashite/gan/actor.rb, line 355 def same_type?(other) return false unless other.is_a?(self.class) type == other.type end
Check if this actor is the same type as another (ignoring name, side, and state)
@param other [Actor] actor to compare with @return [Boolean] true if same piece type @example
king1.same_type?(king2) # (CHESS:K) and (SHOGI:k) => true
Source
# File lib/sashite/gan/actor.rb, line 333 def second_player? style.second_player? end
Check if the actor belongs to the second player
@return [Boolean] true if second player
Source
# File lib/sashite/gan/actor.rb, line 191 def side style.side end
Get the player side
@return [Symbol] player side (:first or :second) @example
actor.side # => :first
Source
# File lib/sashite/gan/actor.rb, line 200 def state piece.state end
Get the piece state
@return [Symbol] piece state (:normal, :enhanced, or :diminished) @example
actor.state # => :normal
Source
# File lib/sashite/gan/actor.rb, line 153 def to_pin piece.to_s end
Convert the actor to its PIN representation (piece component only)
@return [String] PIN notation string for the piece component @example
actor.to_pin # => "K" promoted_actor.to_pin # => "+p" diminished_actor.to_pin # => "-G"
Source
# File lib/sashite/gan/actor.rb, line 142 def to_s "#{style}#{SEPARATOR}#{piece}" end
Convert the actor to its GAN string representation
@return [String] GAN notation string @example
actor.to_s # => "CHESS:K" actor.to_s # => "shogi:+p" actor.to_s # => "XIANGQI:-G"
Source
# File lib/sashite/gan/actor.rb, line 164 def to_snn style.to_s end
Convert the actor to its SNN representation (style component only)
@return [String] SNN notation string for the style component @example
actor.to_snn # => "CHESS" black_actor.to_snn # => "chess" xiangqi_actor.to_snn # => "XIANGQI"
Source
# File lib/sashite/gan/actor.rb, line 182 def type piece.type end
Get the piece type
@return [Symbol] piece type (:A to :Z, always uppercase) @example
actor.type # => :K
Source
# File lib/sashite/gan/actor.rb, line 256 def with_name(new_name) self.class.validate_name(new_name) return self if name == new_name self.class.new(new_name, type, side, state) end
Create a new actor with a different style name (keeping same type, side, and state)
@param new_name [Symbol] new style name (with proper capitalization) @return [Actor] new actor instance with different style name @example
actor.with_name(:Shogi) # CHESS:K => SHOGI:K
Source
# File lib/sashite/gan/actor.rb, line 282 def with_side(new_side) self.class.validate_side(new_side) return self if side == new_side self.class.new(name, type, new_side, state) end
Create a new actor with a different side (keeping same name, type, and state)
@param new_side [Symbol] :first or :second @return [Actor] new actor instance with different side @example
actor.with_side(:second) # CHESS:K => chess:k
Source
# File lib/sashite/gan/actor.rb, line 295 def with_state(new_state) self.class.validate_state(new_state) return self if state == new_state self.class.new(name, type, side, new_state) end
Create a new actor with a different piece state (keeping same name, type, and side)
@param new_state [Symbol] :normal, :enhanced, or :diminished @return [Actor] new actor instance with different piece state @example
actor.with_state(:enhanced) # CHESS:K => CHESS:+K
Source
# File lib/sashite/gan/actor.rb, line 269 def with_type(new_type) self.class.validate_type(new_type) return self if type == new_type self.class.new(name, new_type, side, state) end
Create a new actor with a different piece type (keeping same name, side, and state)
@param new_type [Symbol] new piece type (:A to :Z) @return [Actor] new actor instance with different piece type @example
actor.with_type(:Q) # CHESS:K => CHESS:Q
Private Instance Methods
Source
# File lib/sashite/gan/actor.rb, line 502 def opposite_side first_player? ? SECOND_PLAYER : FIRST_PLAYER end
Get the opposite side
@return [Symbol] the opposite side