sashite-gan 3.0.0 → 5.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,507 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sashite/pin"
4
+ require "sashite/snn"
5
+
6
+ module Sashite
7
+ module Gan
8
+ # Represents a game actor in GAN (General Actor Notation) format.
9
+ #
10
+ # An actor combines a style identifier (SNN format) with a piece identifier (PIN format)
11
+ # using a colon separator and consistent case encoding to create an unambiguous
12
+ # representation of a game piece within its style context.
13
+ #
14
+ # GAN represents all four fundamental piece attributes from the Game Protocol:
15
+ # - Type → PIN component (ASCII letter choice)
16
+ # - Side → Consistent case encoding across both SNN and PIN components
17
+ # - State → PIN component (optional prefix modifier)
18
+ # - Style → SNN component (explicit style identifier)
19
+ #
20
+ # All instances are immutable - transformation methods return new instances.
21
+ # This follows the Game Protocol's actor model with complete attribute representation.
22
+ class Actor
23
+ # Colon separator character
24
+ SEPARATOR = ":"
25
+
26
+ # Player side constants
27
+ FIRST_PLAYER = :first
28
+ SECOND_PLAYER = :second
29
+
30
+ # State constants
31
+ NORMAL_STATE = :normal
32
+ ENHANCED_STATE = :enhanced
33
+ DIMINISHED_STATE = :diminished
34
+
35
+ # Valid sides
36
+ VALID_SIDES = [FIRST_PLAYER, SECOND_PLAYER].freeze
37
+
38
+ # Valid states
39
+ VALID_STATES = [NORMAL_STATE, ENHANCED_STATE, DIMINISHED_STATE].freeze
40
+
41
+ # Valid types (A-Z)
42
+ VALID_TYPES = (:A..:Z).to_a.freeze
43
+
44
+ # Error messages
45
+ ERROR_INVALID_GAN = "Invalid GAN format: %s"
46
+ ERROR_CASE_MISMATCH = "Case mismatch between SNN and PIN components in GAN string: %s"
47
+ ERROR_INVALID_NAME = "Name must be a symbol with proper capitalization, got: %s"
48
+ ERROR_INVALID_TYPE = "Type must be a symbol from :A to :Z, got: %s"
49
+ ERROR_INVALID_SIDE = "Side must be :first or :second, got: %s"
50
+ ERROR_INVALID_STATE = "State must be :normal, :enhanced, or :diminished, got: %s"
51
+
52
+ # Create a new actor instance
53
+ #
54
+ # @param name [Symbol] style name (with proper capitalization)
55
+ # @param type [Symbol] piece type (:A to :Z)
56
+ # @param side [Symbol] player side (:first or :second)
57
+ # @param state [Symbol] piece state (:normal, :enhanced, or :diminished)
58
+ # @raise [ArgumentError] if parameters are invalid
59
+ # @example
60
+ # Actor.new(:Chess, :K, :first, :normal)
61
+ # Actor.new(:Shogi, :P, :second, :enhanced)
62
+ def initialize(name, type, side, state = NORMAL_STATE)
63
+ self.class.validate_name(name)
64
+ self.class.validate_type(type)
65
+ self.class.validate_side(side)
66
+ self.class.validate_state(state)
67
+
68
+ @style = Snn::Style.new(name, side)
69
+ @piece = Pin::Piece.new(type, side, state)
70
+
71
+ freeze
72
+ end
73
+
74
+ # Parse a GAN string into an Actor object
75
+ #
76
+ # @param gan_string [String] GAN notation string
77
+ # @return [Actor] new actor instance
78
+ # @raise [ArgumentError] if the GAN string is invalid or has case mismatch
79
+ # @example
80
+ # Actor.parse("CHESS:K") # => #<Actor name=:Chess type=:K side=:first state=:normal>
81
+ # Actor.parse("shogi:+p") # => #<Actor name=:Shogi type=:P side=:second state=:enhanced>
82
+ # Actor.parse("XIANGQI:-G") # => #<Actor name=:Xiangqi type=:G side=:first state=:diminished>
83
+ def self.parse(gan_string)
84
+ string_value = String(gan_string)
85
+
86
+ # Split into SNN and PIN components
87
+ snn_part, pin_part = string_value.split(SEPARATOR, 2)
88
+
89
+ # Validate basic format
90
+ unless snn_part && pin_part && string_value.count(SEPARATOR) == 1
91
+ raise ::ArgumentError, format(ERROR_INVALID_GAN, string_value)
92
+ end
93
+
94
+ # Validate case consistency
95
+ validate_case_consistency(snn_part, pin_part, string_value)
96
+
97
+ # Parse components - let SNN and PIN handle their own validation
98
+ parsed_style = Snn::Style.parse(snn_part)
99
+ parsed_piece = Pin::Piece.parse(pin_part)
100
+
101
+ # Create actor with parsed components
102
+ new(parsed_style.name, parsed_piece.type, parsed_style.side, parsed_piece.state)
103
+ end
104
+
105
+ # Check if a string is a valid GAN notation
106
+ #
107
+ # @param gan_string [String] The string to validate
108
+ # @return [Boolean] true if valid GAN, false otherwise
109
+ #
110
+ # @example
111
+ # Sashite::Gan::Actor.valid?("CHESS:K") # => true
112
+ # Sashite::Gan::Actor.valid?("shogi:+p") # => true
113
+ # Sashite::Gan::Actor.valid?("Chess:K") # => false (mixed case in style)
114
+ # Sashite::Gan::Actor.valid?("CHESS:k") # => false (case mismatch)
115
+ # Sashite::Gan::Actor.valid?("CHESS") # => false (missing piece)
116
+ # Sashite::Gan::Actor.valid?("") # => false (empty string)
117
+ def self.valid?(gan_string)
118
+ return false unless gan_string.is_a?(::String)
119
+ return false if gan_string.empty?
120
+
121
+ # Split into SNN and PIN components
122
+ parts = gan_string.split(SEPARATOR, 2)
123
+ return false unless parts.length == 2
124
+
125
+ snn_part, pin_part = parts
126
+
127
+ # Validate each component with its specific regex
128
+ return false unless snn_part.match?(Snn::Style::SNN_PATTERN)
129
+ return false unless pin_part.match?(Pin::Piece::PIN_PATTERN)
130
+
131
+ # Check case consistency between components
132
+ case_consistent?(snn_part, pin_part)
133
+ end
134
+
135
+ # Convert the actor to its GAN string representation
136
+ #
137
+ # @return [String] GAN notation string
138
+ # @example
139
+ # actor.to_s # => "CHESS:K"
140
+ # actor.to_s # => "shogi:+p"
141
+ # actor.to_s # => "XIANGQI:-G"
142
+ def to_s
143
+ "#{style}#{SEPARATOR}#{piece}"
144
+ end
145
+
146
+ # Convert the actor to its PIN representation (piece component only)
147
+ #
148
+ # @return [String] PIN notation string for the piece component
149
+ # @example
150
+ # actor.to_pin # => "K"
151
+ # promoted_actor.to_pin # => "+p"
152
+ # diminished_actor.to_pin # => "-G"
153
+ def to_pin
154
+ piece.to_s
155
+ end
156
+
157
+ # Convert the actor to its SNN representation (style component only)
158
+ #
159
+ # @return [String] SNN notation string for the style component
160
+ # @example
161
+ # actor.to_snn # => "CHESS"
162
+ # black_actor.to_snn # => "chess"
163
+ # xiangqi_actor.to_snn # => "XIANGQI"
164
+ def to_snn
165
+ style.to_s
166
+ end
167
+
168
+ # Get the style name
169
+ #
170
+ # @return [Symbol] style name (with proper capitalization)
171
+ # @example
172
+ # actor.name # => :Chess
173
+ def name
174
+ style.name
175
+ end
176
+
177
+ # Get the piece type
178
+ #
179
+ # @return [Symbol] piece type (:A to :Z, always uppercase)
180
+ # @example
181
+ # actor.type # => :K
182
+ def type
183
+ piece.type
184
+ end
185
+
186
+ # Get the player side
187
+ #
188
+ # @return [Symbol] player side (:first or :second)
189
+ # @example
190
+ # actor.side # => :first
191
+ def side
192
+ style.side
193
+ end
194
+
195
+ # Get the piece state
196
+ #
197
+ # @return [Symbol] piece state (:normal, :enhanced, or :diminished)
198
+ # @example
199
+ # actor.state # => :normal
200
+ def state
201
+ piece.state
202
+ end
203
+
204
+ # Create a new actor with enhanced piece state
205
+ #
206
+ # @return [Actor] new actor instance with enhanced piece
207
+ # @example
208
+ # actor.enhance # CHESS:K => CHESS:+K
209
+ def enhance
210
+ return self if enhanced?
211
+
212
+ self.class.new(name, type, side, ENHANCED_STATE)
213
+ end
214
+
215
+ # Create a new actor with diminished piece state
216
+ #
217
+ # @return [Actor] new actor instance with diminished piece
218
+ # @example
219
+ # actor.diminish # CHESS:K => CHESS:-K
220
+ def diminish
221
+ return self if diminished?
222
+
223
+ self.class.new(name, type, side, DIMINISHED_STATE)
224
+ end
225
+
226
+ # Create a new actor with normal piece state (no modifiers)
227
+ #
228
+ # @return [Actor] new actor instance with normalized piece
229
+ # @example
230
+ # actor.normalize # CHESS:+K => CHESS:K
231
+ def normalize
232
+ return self if normal?
233
+
234
+ self.class.new(name, type, side, NORMAL_STATE)
235
+ end
236
+
237
+ # Create a new actor with opposite ownership (side)
238
+ #
239
+ # Changes both the style and piece sides consistently.
240
+ # This method is rule-agnostic and preserves all piece modifiers.
241
+ #
242
+ # @return [Actor] new actor instance with flipped side
243
+ # @example
244
+ # actor.flip # CHESS:K => chess:k
245
+ # enhanced.flip # CHESS:+K => chess:+k (modifiers preserved)
246
+ def flip
247
+ self.class.new(name, type, opposite_side, state)
248
+ end
249
+
250
+ # Create a new actor with a different style name (keeping same type, side, and state)
251
+ #
252
+ # @param new_name [Symbol] new style name (with proper capitalization)
253
+ # @return [Actor] new actor instance with different style name
254
+ # @example
255
+ # actor.with_name(:Shogi) # CHESS:K => SHOGI:K
256
+ def with_name(new_name)
257
+ self.class.validate_name(new_name)
258
+ return self if name == new_name
259
+
260
+ self.class.new(new_name, type, side, state)
261
+ end
262
+
263
+ # Create a new actor with a different piece type (keeping same name, side, and state)
264
+ #
265
+ # @param new_type [Symbol] new piece type (:A to :Z)
266
+ # @return [Actor] new actor instance with different piece type
267
+ # @example
268
+ # actor.with_type(:Q) # CHESS:K => CHESS:Q
269
+ def with_type(new_type)
270
+ self.class.validate_type(new_type)
271
+ return self if type == new_type
272
+
273
+ self.class.new(name, new_type, side, state)
274
+ end
275
+
276
+ # Create a new actor with a different side (keeping same name, type, and state)
277
+ #
278
+ # @param new_side [Symbol] :first or :second
279
+ # @return [Actor] new actor instance with different side
280
+ # @example
281
+ # actor.with_side(:second) # CHESS:K => chess:k
282
+ def with_side(new_side)
283
+ self.class.validate_side(new_side)
284
+ return self if side == new_side
285
+
286
+ self.class.new(name, type, new_side, state)
287
+ end
288
+
289
+ # Create a new actor with a different piece state (keeping same name, type, and side)
290
+ #
291
+ # @param new_state [Symbol] :normal, :enhanced, or :diminished
292
+ # @return [Actor] new actor instance with different piece state
293
+ # @example
294
+ # actor.with_state(:enhanced) # CHESS:K => CHESS:+K
295
+ def with_state(new_state)
296
+ self.class.validate_state(new_state)
297
+ return self if state == new_state
298
+
299
+ self.class.new(name, type, side, new_state)
300
+ end
301
+
302
+ # Check if the actor has enhanced state
303
+ #
304
+ # @return [Boolean] true if enhanced
305
+ def enhanced?
306
+ piece.enhanced?
307
+ end
308
+
309
+ # Check if the actor has diminished state
310
+ #
311
+ # @return [Boolean] true if diminished
312
+ def diminished?
313
+ piece.diminished?
314
+ end
315
+
316
+ # Check if the actor has normal state (no modifiers)
317
+ #
318
+ # @return [Boolean] true if no modifiers are present
319
+ def normal?
320
+ piece.normal?
321
+ end
322
+
323
+ # Check if the actor belongs to the first player
324
+ #
325
+ # @return [Boolean] true if first player
326
+ def first_player?
327
+ style.first_player?
328
+ end
329
+
330
+ # Check if the actor belongs to the second player
331
+ #
332
+ # @return [Boolean] true if second player
333
+ def second_player?
334
+ style.second_player?
335
+ end
336
+
337
+ # Check if this actor has the same style name as another
338
+ #
339
+ # @param other [Actor] actor to compare with
340
+ # @return [Boolean] true if same style name
341
+ # @example
342
+ # chess1.same_name?(chess2) # (CHESS:K) and (chess:Q) => true
343
+ def same_name?(other)
344
+ return false unless other.is_a?(self.class)
345
+
346
+ name == other.name
347
+ end
348
+
349
+ # Check if this actor is the same type as another (ignoring name, side, and state)
350
+ #
351
+ # @param other [Actor] actor to compare with
352
+ # @return [Boolean] true if same piece type
353
+ # @example
354
+ # king1.same_type?(king2) # (CHESS:K) and (SHOGI:k) => true
355
+ def same_type?(other)
356
+ return false unless other.is_a?(self.class)
357
+
358
+ type == other.type
359
+ end
360
+
361
+ # Check if this actor belongs to the same side as another
362
+ #
363
+ # @param other [Actor] actor to compare with
364
+ # @return [Boolean] true if same side
365
+ def same_side?(other)
366
+ return false unless other.is_a?(self.class)
367
+
368
+ side == other.side
369
+ end
370
+
371
+ # Check if this actor has the same state as another
372
+ #
373
+ # @param other [Actor] actor to compare with
374
+ # @return [Boolean] true if same piece state
375
+ def same_state?(other)
376
+ return false unless other.is_a?(self.class)
377
+
378
+ state == other.state
379
+ end
380
+
381
+ # Custom equality comparison
382
+ #
383
+ # @param other [Object] object to compare with
384
+ # @return [Boolean] true if actors are equal
385
+ def ==(other)
386
+ return false unless other.is_a?(self.class)
387
+
388
+ name == other.name && type == other.type && side == other.side && state == other.state
389
+ end
390
+
391
+ # Alias for == to ensure Set functionality works correctly
392
+ alias eql? ==
393
+
394
+ # Custom hash implementation for use in collections
395
+ #
396
+ # @return [Integer] hash value
397
+ def hash
398
+ [self.class, name, type, side, state].hash
399
+ end
400
+
401
+ # Validate that the name is a valid symbol with proper capitalization
402
+ #
403
+ # @param name [Symbol] the name to validate
404
+ # @raise [ArgumentError] if invalid
405
+ def self.validate_name(name)
406
+ return if valid_name?(name)
407
+
408
+ raise ::ArgumentError, format(ERROR_INVALID_NAME, name.inspect)
409
+ end
410
+
411
+ # Validate that the type is a valid symbol
412
+ #
413
+ # @param type [Symbol] the type to validate
414
+ # @raise [ArgumentError] if invalid
415
+ def self.validate_type(type)
416
+ return if VALID_TYPES.include?(type)
417
+
418
+ raise ::ArgumentError, format(ERROR_INVALID_TYPE, type.inspect)
419
+ end
420
+
421
+ # Validate that the side is a valid symbol
422
+ #
423
+ # @param side [Symbol] the side to validate
424
+ # @raise [ArgumentError] if invalid
425
+ def self.validate_side(side)
426
+ return if VALID_SIDES.include?(side)
427
+
428
+ raise ::ArgumentError, format(ERROR_INVALID_SIDE, side.inspect)
429
+ end
430
+
431
+ # Validate that the state is a valid symbol
432
+ #
433
+ # @param state [Symbol] the state to validate
434
+ # @raise [ArgumentError] if invalid
435
+ def self.validate_state(state)
436
+ return if VALID_STATES.include?(state)
437
+
438
+ raise ::ArgumentError, format(ERROR_INVALID_STATE, state.inspect)
439
+ end
440
+
441
+ # Check if a name is valid (symbol with proper capitalization)
442
+ #
443
+ # @param name [Object] the name to check
444
+ # @return [Boolean] true if valid
445
+ def self.valid_name?(name)
446
+ return false unless name.is_a?(::Symbol)
447
+
448
+ name_string = name.to_s
449
+ return false if name_string.empty?
450
+
451
+ # Must match proper capitalization pattern: first letter uppercase, rest lowercase/digits
452
+ name_string.match?(/\A[A-Z][a-z0-9]*\z/)
453
+ end
454
+
455
+ # Check case consistency between SNN and PIN components
456
+ #
457
+ # @param snn_part [String] the SNN component
458
+ # @param pin_part [String] the PIN component (with optional prefix)
459
+ # @return [Boolean] true if case is consistent, false otherwise
460
+ def self.case_consistent?(snn_part, pin_part)
461
+ # Extract letter from PIN part (remove optional +/- prefix)
462
+ pin_letter_match = pin_part.match(/[-+]?([A-Za-z])$/)
463
+ return false unless pin_letter_match
464
+
465
+ pin_letter = pin_letter_match[1]
466
+
467
+ snn_uppercase = snn_part == snn_part.upcase
468
+ pin_uppercase = pin_letter == pin_letter.upcase
469
+
470
+ snn_uppercase == pin_uppercase
471
+ end
472
+
473
+ # Validate case consistency between SNN and PIN components
474
+ #
475
+ # @param snn_part [String] the SNN component
476
+ # @param pin_part [String] the PIN component (with optional prefix)
477
+ # @param full_string [String] the full GAN string for error reporting
478
+ # @raise [ArgumentError] if case mismatch detected
479
+ def self.validate_case_consistency(snn_part, pin_part, full_string)
480
+ return if case_consistent?(snn_part, pin_part)
481
+
482
+ raise ::ArgumentError, format(ERROR_CASE_MISMATCH, full_string)
483
+ end
484
+
485
+ private_class_method :valid_name?, :case_consistent?, :validate_case_consistency
486
+
487
+ private
488
+
489
+ # Get the style component
490
+ #
491
+ # @return [Sashite::Snn::Style] the style component
492
+ attr_reader :style
493
+
494
+ # Get the piece component
495
+ #
496
+ # @return [Sashite::Pin::Piece] the piece component
497
+ attr_reader :piece
498
+
499
+ # Get the opposite side
500
+ #
501
+ # @return [Symbol] the opposite side
502
+ def opposite_side
503
+ first_player? ? SECOND_PLAYER : FIRST_PLAYER
504
+ end
505
+ end
506
+ end
507
+ end
data/lib/sashite/gan.rb CHANGED
@@ -1,76 +1,76 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative File.join("gan", "dumper")
4
- require_relative File.join("gan", "parser")
5
- require_relative File.join("gan", "validator")
3
+ require_relative "gan/actor"
6
4
 
7
5
  module Sashite
8
- # This module provides a Ruby interface for serialization and
9
- # deserialization of game actors in GAN format.
6
+ # GAN (General Actor Notation) implementation for Ruby
10
7
  #
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).
8
+ # Provides a rule-agnostic format for identifying game actors in abstract strategy board games
9
+ # by combining Style Name Notation (SNN) and Piece Identifier Notation (PIN) with a colon separator
10
+ # and consistent case encoding.
14
11
  #
15
- # @see https://sashite.dev/documents/gan/1.0.0/
12
+ # GAN represents all four fundamental piece attributes from the Game Protocol:
13
+ # - Type → PIN component (ASCII letter choice)
14
+ # - Side → Consistent case encoding across both SNN and PIN components
15
+ # - State → PIN component (optional prefix modifier)
16
+ # - Style → SNN component (explicit style identifier)
17
+ #
18
+ # Format: <snn>:<pin>
19
+ # - SNN component: Style identifier with case-based side encoding
20
+ # - Colon separator: Literal ":"
21
+ # - PIN component: Piece with optional state and case-based ownership
22
+ # - Case consistency: SNN and PIN components must have matching case
23
+ #
24
+ # Examples:
25
+ # "CHESS:K" - First player chess king
26
+ # "chess:k" - Second player chess king
27
+ # "SHOGI:+P" - First player enhanced shōgi pawn
28
+ # "xiangqi:-g" - Second player diminished xiangqi general
29
+ #
30
+ # See: https://sashite.dev/specs/gan/1.0.0/
16
31
  module Gan
17
- # Serializes an actor into a GAN string.
32
+ # Check if a string is valid GAN notation
18
33
  #
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
+ # @param gan_string [String] The string to validate
35
+ # @return [Boolean] true if valid GAN, false otherwise
34
36
  #
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)
37
+ # @example Validate various GAN formats
38
+ # Sashite::Gan.valid?("CHESS:K") # => true
39
+ # Sashite::Gan.valid?("shogi:+p") # => true
40
+ # Sashite::Gan.valid?("Chess:K") # => false (mixed case in style)
41
+ # Sashite::Gan.valid?("CHESS:k") # => false (case mismatch)
42
+ # Sashite::Gan.valid?("CHESS") # => false (missing piece)
43
+ # Sashite::Gan.valid?("") # => false (empty string)
44
+ def self.valid?(gan_string)
45
+ Actor.valid?(gan_string)
47
46
  end
48
47
 
49
- # Safely parses a GAN string into its component parts without raising exceptions.
48
+ # Parse a GAN string into an Actor object
50
49
  #
51
50
  # @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: "'" }
57
- #
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)
51
+ # @return [Gan::Actor] new actor instance
52
+ # @raise [ArgumentError] if the GAN string is invalid
53
+ # @example Parse different GAN formats
54
+ # Sashite::Gan.parse("CHESS:K") # => #<Gan::Actor name=:Chess type=:K side=:first state=:normal>
55
+ # Sashite::Gan.parse("shogi:+p") # => #<Gan::Actor name=:Shogi type=:P side=:second state=:enhanced>
56
+ # Sashite::Gan.parse("XIANGQI:-G") # => #<Gan::Actor name=:Xiangqi type=:G side=:first state=:diminished>
57
+ def self.parse(gan_string)
58
+ Actor.parse(gan_string)
63
59
  end
64
60
 
65
- # Validates if the given string is a valid GAN string
61
+ # Create a new actor instance
66
62
  #
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)
63
+ # @param name [Symbol] style name (with proper capitalization)
64
+ # @param type [Symbol] piece type (:A to :Z)
65
+ # @param side [Symbol] player side (:first or :second)
66
+ # @param state [Symbol] piece state (:normal, :enhanced, or :diminished)
67
+ # @return [Gan::Actor] new actor instance
68
+ # @raise [ArgumentError] if parameters are invalid
69
+ # @example Create actors directly
70
+ # Sashite::Gan.actor(:Chess, :K, :first, :normal) # => #<Gan::Actor name=:Chess type=:K side=:first state=:normal>
71
+ # Sashite::Gan.actor(:Shogi, :P, :second, :enhanced) # => #<Gan::Actor name=:Shogi type=:P side=:second state=:enhanced>
72
+ def self.actor(name, type, side, state = :normal)
73
+ Actor.new(name, type, side, state)
74
74
  end
75
75
  end
76
76
  end
data/lib/sashite-gan.rb CHANGED
@@ -1,7 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Sashité namespace
3
+ require_relative "sashite/gan"
4
+
5
+ # Sashité namespace for board game notation libraries
6
+ #
7
+ # Sashité provides a collection of libraries for representing and manipulating
8
+ # board game concepts according to the Game Protocol specifications.
9
+ #
10
+ # @see https://sashite.dev/game-protocol/ Game Protocol Foundation
11
+ # @see https://sashite.dev/specs/ Sashité Specifications
12
+ # @author Sashité
4
13
  module Sashite
5
14
  end
6
-
7
- require_relative "sashite/gan"