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.
data/README.md CHANGED
@@ -5,17 +5,19 @@
5
5
  ![Ruby](https://github.com/sashite/gan.rb/actions/workflows/main.yml/badge.svg?branch=main)
6
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)
7
7
 
8
- > **GAN** (General Actor Notation) support for the Ruby language.
8
+ > **GAN** (General Actor Notation) implementation for the Ruby language.
9
9
 
10
10
  ## What is GAN?
11
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.
12
+ GAN (General Actor Notation) provides a rule-agnostic format for identifying game actors in abstract strategy board games by combining [Style Name Notation (SNN)](https://sashite.dev/specs/snn/1.0.0/) and [Piece Identifier Notation (PIN)](https://sashite.dev/specs/pin/1.0.0/) with a colon separator and consistent case encoding.
13
13
 
14
- This gem implements the [GAN Specification v1.0.0](https://sashite.dev/documents/gan/1.0.0/), providing a Ruby interface for:
14
+ GAN represents **all four fundamental piece attributes** from the [Game Protocol](https://sashite.dev/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)
15
19
 
16
- - Serializing game actors to GAN strings
17
- - Parsing GAN strings into their component parts
18
- - Validating GAN strings according to the specification
20
+ This gem implements the [GAN Specification v1.0.0](https://sashite.dev/specs/gan/1.0.0/), providing a modern Ruby interface with immutable actor objects and functional programming principles built upon the [sashite-snn](https://rubygems.org/gems/sashite-snn) and [sashite-pin](https://rubygems.org/gems/sashite-pin) gems.
19
21
 
20
22
  ## Installation
21
23
 
@@ -30,151 +32,677 @@ Or install manually:
30
32
  gem install sashite-gan
31
33
  ```
32
34
 
33
- ## GAN Format
35
+ ## Usage
34
36
 
35
- A GAN record consists of a game identifier, followed by a colon, followed by a piece identifier that follows the PNN specification:
37
+ ```ruby
38
+ require "sashite/gan"
39
+
40
+ # Parse GAN strings into actor objects
41
+ actor = Sashite::Gan.parse("CHESS:K") # => #<Gan::Actor name=:Chess type=:K side=:first state=:normal>
42
+ actor.to_s # => "CHESS:K"
43
+ actor.name # => :Chess
44
+ actor.type # => :K
45
+ actor.side # => :first
46
+ actor.state # => :normal
47
+
48
+ # Extract individual components
49
+ actor.to_snn # => "CHESS"
50
+ actor.to_pin # => "K"
51
+
52
+ # Create actors directly
53
+ actor = Sashite::Gan.actor(:Chess, :K, :first, :normal) # => #<Gan::Actor name=:Chess type=:K side=:first state=:normal>
54
+ actor = Sashite::Gan::Actor.new(:Shogi, :P, :second, :enhanced) # => #<Gan::Actor name=:Shogi type=:P side=:second state=:enhanced>
55
+
56
+ # Validate GAN strings
57
+ Sashite::Gan.valid?("CHESS:K") # => true
58
+ Sashite::Gan.valid?("shogi:+p") # => true
59
+ Sashite::Gan.valid?("Chess:K") # => false (mixed case)
60
+ Sashite::Gan.valid?("CHESS") # => false (missing piece)
61
+
62
+ # Class-level validation (same as module method)
63
+ Sashite::Gan::Actor.valid?("CHESS:K") # => true
64
+ Sashite::Gan::Actor.valid?("chess:k") # => true
65
+ Sashite::Gan::Actor.valid?("Chess:K") # => false (mixed case)
66
+ Sashite::Gan::Actor.valid?("CHESS:k") # => false (case mismatch)
67
+
68
+ # State manipulation (returns new immutable instances)
69
+ enhanced = actor.enhance # => #<Gan::Actor name=:Chess type=:K side=:first state=:enhanced>
70
+ enhanced.to_s # => "CHESS:+K"
71
+ enhanced.to_pin # => "+K"
72
+ diminished = actor.diminish # => #<Gan::Actor name=:Chess type=:K side=:first state=:diminished>
73
+ diminished.to_s # => "CHESS:-K"
74
+ diminished.to_pin # => "-K"
75
+
76
+ # Side manipulation
77
+ flipped = actor.flip # => #<Gan::Actor name=:Chess type=:K side=:second state=:normal>
78
+ flipped.to_s # => "chess:k"
79
+ flipped.to_snn # => "chess"
80
+ flipped.to_pin # => "k"
81
+
82
+ # Style manipulation
83
+ shogi_actor = actor.with_name(:Shogi) # => #<Gan::Actor name=:Shogi type=:K side=:first state=:normal>
84
+ shogi_actor.to_s # => "SHOGI:K"
85
+ shogi_actor.to_snn # => "SHOGI"
86
+
87
+ # Type manipulation
88
+ queen = actor.with_type(:Q) # => #<Gan::Actor name=:Chess type=:Q side=:first state=:normal>
89
+ queen.to_s # => "CHESS:Q"
90
+ queen.to_pin # => "Q"
91
+
92
+ # State queries
93
+ actor.normal? # => true
94
+ enhanced.enhanced? # => true
95
+ diminished.diminished? # => true
96
+
97
+ # Side queries
98
+ actor.first_player? # => true
99
+ flipped.second_player? # => true
100
+
101
+ # Component comparison
102
+ chess1 = Sashite::Gan.parse("CHESS:K")
103
+ chess2 = Sashite::Gan.parse("chess:k")
104
+ shogi = Sashite::Gan.parse("SHOGI:K")
105
+
106
+ chess1.same_name?(chess2) # => true (both chess)
107
+ chess1.same_side?(shogi) # => true (both first player)
108
+ chess1.same_type?(chess2) # => true (both kings)
109
+ chess1.same_name?(shogi) # => false (different styles)
110
+
111
+ # Functional transformations can be chained
112
+ black_promoted = Sashite::Gan.parse("CHESS:P").flip.enhance
113
+ black_promoted.to_s # => "chess:+p"
114
+ black_promoted.to_snn # => "chess"
115
+ black_promoted.to_pin # => "+p"
116
+ ```
36
117
 
118
+ ## Format Specification
119
+
120
+ ### Structure
37
121
  ```
38
- <game-id>:<piece-id>
122
+ <snn>:<pin>
39
123
  ```
40
124
 
41
- Where:
125
+ ### Components
126
+
127
+ - **SNN Component** (Style Name Notation): Style identifier with case-based side encoding
128
+ - Uppercase: First player styles (`CHESS`, `SHOGI`, `XIANGQI`)
129
+ - Lowercase: Second player styles (`chess`, `shogi`, `xiangqi`)
130
+ - **Colon Separator**: Literal `:` character
131
+ - **PIN Component** (Piece Identifier Notation): Piece with optional state and case-based ownership
132
+ - Letter case matches SNN case (case consistency requirement)
133
+ - Optional state prefix: `+` (enhanced), `-` (diminished)
42
134
 
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>]`.
135
+ ### Case Consistency Requirement
46
136
 
47
- The casing of the game identifier reflects the player:
137
+ **Critical Rule**: The case of the SNN component must match the case of the PIN component:
48
138
 
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.
139
+ ```ruby
140
+ # Valid combinations
141
+ Sashite::Gan.valid?("CHESS:K") # => true (both uppercase = first player)
142
+ Sashite::Gan.valid?("chess:k") # => true (both lowercase = second player)
143
+ Sashite::Gan.valid?("SHOGI:+R") # => true (both uppercase = first player)
144
+ Sashite::Gan.valid?("xiangqi:-g") # => true (both lowercase = second player)
145
+
146
+ # ❌ Invalid combinations
147
+ Sashite::Gan.valid?("CHESS:k") # => false (case mismatch)
148
+ Sashite::Gan.valid?("chess:K") # => false (case mismatch)
149
+ Sashite::Gan.valid?("SHOGI:+r") # => false (case mismatch)
150
+ ```
51
151
 
52
- ## Basic Usage
152
+ ### Validation Architecture
53
153
 
54
- ### Parsing GAN Strings
154
+ GAN validation delegates to the underlying components for maximum consistency:
155
+ - **SNN validation**: Uses `Sashite::Snn::Style::SNN_PATTERN` for style validation
156
+ - **PIN validation**: Uses `Sashite::Pin::Piece::PIN_PATTERN` for piece validation
157
+ - **Case consistency**: Ensures matching case between SNN and PIN components
55
158
 
56
- Convert a GAN string into a structured Ruby hash:
159
+ This modular approach avoids code duplication and ensures that GAN validation automatically inherits improvements from the underlying SNN and PIN libraries.
57
160
 
58
- ```ruby
59
- require "sashite-gan"
161
+ ### Examples
162
+ - `CHESS:K` - First player chess king
163
+ - `chess:k` - Second player chess king
164
+ - `SHOGI:+P` - First player enhanced shōgi pawn
165
+ - `xiangqi:-g` - Second player diminished xiangqi general
60
166
 
61
- # Basic actor
62
- result = Sashite::Gan.parse("CHESS:K")
63
- # => { game_id: "CHESS", letter: "K" }
167
+ ## Game Examples
64
168
 
65
- # With piece prefix
66
- result = Sashite::Gan.parse("SHOGI:+P")
67
- # => { game_id: "SHOGI", letter: "P", prefix: "+" }
169
+ ### Traditional Same-Style Games
68
170
 
69
- # With piece suffix
70
- result = Sashite::Gan.parse("CHESS:K'")
71
- # => { game_id: "CHESS", letter: "K", suffix: "'" }
171
+ In traditional games where both players use the same piece style:
72
172
 
73
- # With both piece prefix and suffix
74
- result = Sashite::Gan.parse("SHOGI:+R'")
75
- # => { game_id: "SHOGI", letter: "R", prefix: "+", suffix: "'" }
173
+ ```ruby
174
+ # Chess pieces
175
+ white_king = Sashite::Gan.parse("CHESS:K")
176
+ black_king = Sashite::Gan.parse("chess:k")
177
+ white_queen = Sashite::Gan.parse("CHESS:Q")
178
+ black_queen = Sashite::Gan.parse("chess:q")
179
+
180
+ # Shōgi pieces
181
+ sente_king = Sashite::Gan.parse("SHOGI:K")
182
+ gote_king = Sashite::Gan.parse("shogi:k")
183
+ sente_gold = Sashite::Gan.parse("SHOGI:G")
184
+ gote_gold = Sashite::Gan.parse("shogi:g")
185
+
186
+ # Enhanced states for special conditions
187
+ castling_rook = Sashite::Gan.parse("CHESS:+R") # Castling-eligible rook
188
+ vulnerable_pawn = Sashite::Gan.parse("CHESS:-P") # En passant vulnerable pawn
189
+ promoted_pawn = Sashite::Gan.parse("SHOGI:+P") # Tokin (promoted pawn)
76
190
  ```
77
191
 
78
- ### Safe Parsing
192
+ ### Cross-Style Games
79
193
 
80
- Parse a GAN string without raising exceptions:
194
+ GAN's explicit style naming enables games where players use different piece traditions:
81
195
 
82
196
  ```ruby
83
- require "sashite-gan"
197
+ # Chess vs Shōgi
198
+ chess_king = Sashite::Gan.parse("CHESS:K")
199
+ shogi_king = Sashite::Gan.parse("shogi:k")
200
+
201
+ # Makruk vs Xiangqi
202
+ makruk_queen = Sashite::Gan.parse("MAKRUK:M") # Met (Makruk queen)
203
+ xiangqi_general = Sashite::Gan.parse("xiangqi:g") # Xiangqi general
204
+
205
+ # Multi-tradition setup
206
+ def create_cross_style_game
207
+ [
208
+ Sashite::Gan.parse("CHESS:K"), # First player uses chess
209
+ Sashite::Gan.parse("CHESS:Q"),
210
+ Sashite::Gan.parse("shogi:k"), # Second player uses shōgi
211
+ Sashite::Gan.parse("shogi:g")
212
+ ]
213
+ end
214
+ ```
215
+
216
+ ### Capture Mechanics Examples
84
217
 
85
- # Valid GAN string
86
- result = Sashite::Gan.safe_parse("CHESS:K'")
87
- # => { game_id: "CHESS", letter: "K", suffix: "'" }
218
+ GAN can represent the different capture mechanics described in the specification:
88
219
 
89
- # Invalid GAN string
90
- result = Sashite::Gan.safe_parse("invalid gan string")
91
- # => nil
220
+ ```ruby
221
+ # Chess vs Chess (traditional capture)
222
+ def chess_capture(captured_piece)
223
+ # In chess, captured pieces retain their identity but become inactive
224
+ captured_piece # GAN remains unchanged: chess:p stays chess:p
225
+ end
226
+
227
+ # Shōgi vs Shōgi (side-changing capture)
228
+ def shogi_capture(captured_piece)
229
+ # In shōgi, captured pieces change sides and lose promotions
230
+ captured_piece.flip.normalize # shogi:+p becomes SHOGI:P
231
+ end
232
+
233
+ # Cross-style capture (style transformation)
234
+ def cross_style_capture(captured_piece, capturing_style)
235
+ # Captured piece transforms to capturing player's style
236
+ captured_piece.flip.with_name(capturing_style).normalize
237
+ # chess:q captured by Ōgi player becomes OGI:P
238
+ end
92
239
  ```
93
240
 
94
- ### Creating GAN Strings
241
+ ## API Reference
242
+
243
+ ### Main Module Methods
244
+
245
+ - `Sashite::Gan.valid?(gan_string)` - Check if string is valid GAN notation
246
+ - `Sashite::Gan.parse(gan_string)` - Parse GAN string into Actor object
247
+ - `Sashite::Gan.actor(name, type, side, state = :normal)` - Create actor instance directly
248
+
249
+ ### Actor Class
250
+
251
+ #### Creation and Parsing
252
+ - `Sashite::Gan::Actor.new(name, type, side, state = :normal)` - Create actor instance
253
+ - `Sashite::Gan::Actor.parse(gan_string)` - Parse GAN string (same as module method)
254
+ - `Sashite::Gan::Actor.valid?(gan_string)` - Validate GAN string (class method)
255
+
256
+ #### Attribute Access
257
+ - `#name` - Get style name (symbol with proper capitalization)
258
+ - `#type` - Get piece type (symbol :A to :Z, always uppercase)
259
+ - `#side` - Get player side (:first or :second)
260
+ - `#state` - Get piece state (:normal, :enhanced, or :diminished)
261
+ - `#to_s` - Convert to GAN string representation
262
+ - `#to_pin` - Convert to PIN string representation (piece component only)
263
+ - `#to_snn` - Convert to SNN string representation (style component only)
95
264
 
96
- Convert actor components into a GAN string:
265
+ #### Component Extraction
266
+
267
+ The `to_pin` and `to_snn` methods allow extraction of individual notation components:
268
+
269
+ ```ruby
270
+ actor = Sashite::Gan.parse("CHESS:+K")
271
+
272
+ # Full GAN representation
273
+ actor.to_s # => "CHESS:+K"
274
+
275
+ # Individual components
276
+ actor.to_snn # => "CHESS" (style component)
277
+ actor.to_pin # => "+K" (piece component)
278
+
279
+ # Component transformation example
280
+ flipped = actor.flip
281
+ flipped.to_s # => "chess:+k"
282
+ flipped.to_snn # => "chess" (lowercase for second player)
283
+ flipped.to_pin # => "+k" (lowercase with state preserved)
284
+
285
+ # State manipulation example
286
+ normalized = actor.normalize
287
+ normalized.to_s # => "CHESS:K"
288
+ normalized.to_pin # => "K" (state modifier removed)
289
+ normalized.to_snn # => "CHESS" (style unchanged)
290
+ ```
291
+
292
+ #### Component Handling
293
+
294
+ **Important**: Following PIN and SNN conventions:
295
+ - **Style names** are stored with proper capitalization (`:Chess`, `:Shogi`)
296
+ - **Piece types** are stored as uppercase symbols (`:K`, `:P`)
297
+ - **Display case** is determined by `side` during rendering
97
298
 
98
299
  ```ruby
99
- require "sashite-gan"
300
+ # Both create the same internal representation
301
+ actor1 = Sashite::Gan.parse("CHESS:K") # name: :Chess, type: :K, side: :first
302
+ actor2 = Sashite::Gan.parse("chess:k") # name: :Chess, type: :K, side: :second
303
+
304
+ actor1.name # => :Chess (proper capitalization)
305
+ actor2.name # => :Chess (same style name)
306
+ actor1.type # => :K (uppercase type)
307
+ actor2.type # => :K (same type)
308
+
309
+ actor1.to_s # => "CHESS:K" (uppercase display)
310
+ actor2.to_s # => "chess:k" (lowercase display)
311
+ actor1.to_snn # => "CHESS" (uppercase style)
312
+ actor2.to_snn # => "chess" (lowercase style)
313
+ actor1.to_pin # => "K" (uppercase piece)
314
+ actor2.to_pin # => "k" (lowercase piece)
315
+ ```
316
+
317
+ #### State Queries
318
+ - `#normal?` - Check if normal state (no modifiers)
319
+ - `#enhanced?` - Check if enhanced state
320
+ - `#diminished?` - Check if diminished state
100
321
 
101
- # Basic actor
102
- Sashite::Gan.dump(game_id: "CHESS", letter: "K")
103
- # => "CHESS:K"
322
+ #### Side Queries
323
+ - `#first_player?` - Check if first player actor
324
+ - `#second_player?` - Check if second player actor
104
325
 
105
- # With piece prefix
106
- Sashite::Gan.dump(game_id: "SHOGI", letter: "P", prefix: "+")
107
- # => "SHOGI:+P"
326
+ #### State Transformations (immutable - return new instances)
327
+ - `#enhance` - Create enhanced version
328
+ - `#diminish` - Create diminished version
329
+ - `#normalize` - Remove all state modifiers
330
+ - `#flip` - Switch player (change side)
108
331
 
109
- # With piece suffix
110
- Sashite::Gan.dump(game_id: "CHESS", letter: "K", suffix: "'")
111
- # => "CHESS:K'"
332
+ #### Attribute Transformations (immutable - return new instances)
333
+ - `#with_name(new_name)` - Create actor with different style name
334
+ - `#with_type(new_type)` - Create actor with different piece type
335
+ - `#with_side(new_side)` - Create actor with different side
336
+ - `#with_state(new_state)` - Create actor with different state
112
337
 
113
- # With both piece prefix and suffix
114
- Sashite::Gan.dump(game_id: "SHOGI", letter: "R", prefix: "+", suffix: "'")
115
- # => "SHOGI:+R'"
338
+ #### Comparison Methods
339
+ - `#same_name?(other)` - Check if same style name
340
+ - `#same_type?(other)` - Check if same piece type
341
+ - `#same_side?(other)` - Check if same side
342
+ - `#same_state?(other)` - Check if same state
343
+ - `#==(other)` - Full equality comparison
344
+
345
+ ### Constants
346
+ - `Sashite::Gan::Actor::SEPARATOR` - Colon separator character
347
+
348
+ ## Advanced Usage
349
+
350
+ ### Component Extraction and Manipulation
351
+
352
+ The `to_pin` and `to_snn` methods enable powerful component-based operations:
353
+
354
+ ```ruby
355
+ # Extract and manipulate components
356
+ actor = Sashite::Gan.parse("SHOGI:+P")
357
+
358
+ # Component extraction
359
+ style_str = actor.to_snn # => "SHOGI"
360
+ piece_str = actor.to_pin # => "+P"
361
+
362
+ # Reconstruct from components
363
+ reconstructed = "#{style_str}:#{piece_str}" # => "SHOGI:+P"
364
+
365
+ # Cross-component analysis
366
+ actors = [
367
+ Sashite::Gan.parse("CHESS:K"),
368
+ Sashite::Gan.parse("SHOGI:K"),
369
+ Sashite::Gan.parse("chess:k")
370
+ ]
371
+
372
+ # Group by style component
373
+ by_style = actors.group_by(&:to_snn)
374
+ # => {"CHESS" => [...], "SHOGI" => [...], "chess" => [...]}
375
+
376
+ # Group by piece component
377
+ by_piece = actors.group_by(&:to_pin)
378
+ # => {"K" => [...], "k" => [...]}
379
+
380
+ # Component-based filtering
381
+ uppercase_styles = actors.select { |a| a.to_snn == a.to_snn.upcase }
382
+ enhanced_pieces = actors.select { |a| a.to_pin.start_with?("+") }
116
383
  ```
117
384
 
118
- ### Validation
385
+ ### Component Reconstruction Patterns
386
+
387
+ ```ruby
388
+ # Template-based reconstruction
389
+ def apply_style_template(actors, new_style)
390
+ actors.map do |actor|
391
+ pin_part = actor.to_pin
392
+ side = actor.side
393
+
394
+ # Apply new style while preserving piece and side
395
+ new_style_str = side == :first ? new_style.to_s.upcase : new_style.to_s.downcase
396
+ Sashite::Gan.parse("#{new_style_str}:#{pin_part}")
397
+ end
398
+ end
399
+
400
+ # Convert chess pieces to shōgi style
401
+ chess_pieces = [
402
+ Sashite::Gan.parse("CHESS:K"),
403
+ Sashite::Gan.parse("chess:+q")
404
+ ]
405
+
406
+ shogi_pieces = apply_style_template(chess_pieces, :Shogi)
407
+ # => [SHOGI:K, shogi:+q]
408
+
409
+ # Component swapping
410
+ def swap_components(actor1, actor2)
411
+ [
412
+ Sashite::Gan.parse("#{actor1.to_snn}:#{actor2.to_pin}"),
413
+ Sashite::Gan.parse("#{actor2.to_snn}:#{actor1.to_pin}")
414
+ ]
415
+ end
416
+
417
+ chess_king = Sashite::Gan.parse("CHESS:K")
418
+ shogi_pawn = Sashite::Gan.parse("shogi:p")
419
+
420
+ swapped = swap_components(chess_king, shogi_pawn)
421
+ # => [CHESS:p, shogi:K]
422
+ ```
119
423
 
120
- Check if a string is valid GAN notation:
424
+ ### Immutable Transformations
425
+ ```ruby
426
+ # All transformations return new instances
427
+ original = Sashite::Gan.parse("CHESS:P")
428
+ enhanced = original.enhance
429
+ cross_style = original.with_name(:Shogi)
430
+ enemy = original.flip
431
+
432
+ # Original actor is never modified
433
+ puts original # => "CHESS:P"
434
+ puts enhanced # => "CHESS:+P"
435
+ puts cross_style # => "SHOGI:P"
436
+ puts enemy # => "chess:p"
437
+
438
+ # Component extraction shows changes
439
+ puts enhanced.to_pin # => "+P" (state changed)
440
+ puts cross_style.to_snn # => "SHOGI" (style changed)
441
+ puts enemy.to_snn # => "chess" (case changed)
442
+ puts enemy.to_pin # => "p" (case changed)
443
+
444
+ # Transformations can be chained
445
+ result = original.flip.with_name(:Xiangqi).enhance
446
+ puts result # => "xiangqi:+p"
447
+ puts result.to_snn # => "xiangqi"
448
+ puts result.to_pin # => "+p"
449
+ ```
121
450
 
451
+ ### Multi-Style Game Management
122
452
  ```ruby
123
- require "sashite-gan"
453
+ class CrossStyleGame
454
+ def initialize
455
+ @actors = []
456
+ @style_assignments = {}
457
+ end
458
+
459
+ def assign_style(player, style)
460
+ side = player == :white ? :first : :second
461
+ @style_assignments[player] = { style: style, side: side }
462
+ end
463
+
464
+ def create_actor(player, type, state = :normal)
465
+ assignment = @style_assignments[player]
466
+ Sashite::Gan::Actor.new(assignment[:style], type, assignment[:side], state)
467
+ end
468
+
469
+ def valid_combination?
470
+ return true if @style_assignments.size < 2
471
+
472
+ sides = @style_assignments.values.map { |a| a[:side] }
473
+ sides.uniq.size == 2 # Must have different sides
474
+ end
475
+
476
+ def get_player_style_string(player)
477
+ actor = create_actor(player, :K) # Use king as reference
478
+ actor.to_snn
479
+ end
480
+ end
481
+
482
+ # Usage
483
+ game = CrossStyleGame.new
484
+ game.assign_style(:white, :Chess)
485
+ game.assign_style(:black, :Shogi)
486
+
487
+ white_king = game.create_actor(:white, :K)
488
+ black_king = game.create_actor(:black, :K)
489
+
490
+ puts white_king # => "CHESS:K"
491
+ puts white_king.to_snn # => "CHESS"
492
+ puts black_king # => "shogi:k"
493
+ puts black_king.to_snn # => "shogi"
494
+ puts game.valid_combination? # => true
495
+ ```
124
496
 
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
497
+ ### Validation and Error Handling
498
+ ```ruby
499
+ # Comprehensive validation with both module and class methods
500
+ def safe_parse(gan_string)
501
+ # You can use either method for validation
502
+ return nil unless Sashite::Gan.valid?(gan_string)
503
+
504
+ # Alternative: return nil unless Sashite::Gan::Actor.valid?(gan_string)
505
+
506
+ Sashite::Gan.parse(gan_string)
507
+ rescue ArgumentError => e
508
+ puts "Parse error: #{e.message}"
509
+ nil
510
+ end
511
+
512
+ # Batch validation with component extraction
513
+ gan_strings = ["CHESS:K", "Chess:K", "SHOGI:+p", "invalid"]
514
+ valid_actors = gan_strings.filter_map { |s| safe_parse(s) }
515
+
516
+ puts "Valid actors with components:"
517
+ valid_actors.each do |actor|
518
+ puts " #{actor} -> style: #{actor.to_snn}, piece: #{actor.to_pin}"
519
+ end
520
+
521
+ # Module-level validation
522
+ Sashite::Gan.valid?("CHESS:K") # => true
523
+ Sashite::Gan.valid?("chess:k") # => true
524
+ Sashite::Gan.valid?("Chess:K") # => false (mixed case)
525
+ Sashite::Gan.valid?("CHESS") # => false (missing piece)
526
+
527
+ # Class-level validation (equivalent to module method)
528
+ Sashite::Gan::Actor.valid?("CHESS:K") # => true
529
+ Sashite::Gan::Actor.valid?("chess:k") # => true
530
+ Sashite::Gan::Actor.valid?("Chess:K") # => false (mixed case)
531
+ Sashite::Gan::Actor.valid?("CHESS:k") # => false (case mismatch)
532
+ ```
129
533
 
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
534
+ ### Collection Operations
535
+ ```ruby
536
+ # Working with actor collections
537
+ actors = [
538
+ Sashite::Gan.parse("CHESS:K"),
539
+ Sashite::Gan.parse("CHESS:Q"),
540
+ Sashite::Gan.parse("shogi:k"),
541
+ Sashite::Gan.parse("shogi:g"),
542
+ Sashite::Gan.parse("XIANGQI:G")
543
+ ]
544
+
545
+ # Group by various attributes
546
+ by_style = actors.group_by(&:name)
547
+ by_side = actors.group_by(&:side)
548
+ by_type = actors.group_by(&:type)
549
+
550
+ # Group by string components
551
+ by_style_string = actors.group_by(&:to_snn)
552
+ by_piece_string = actors.group_by(&:to_pin)
553
+
554
+ puts "By style string: #{by_style_string.keys}" # => ["CHESS", "shogi", "XIANGQI"]
555
+ puts "By piece string: #{by_piece_string.keys}" # => ["K", "Q", "k", "g", "G"]
556
+
557
+ # Filter operations
558
+ first_player_actors = actors.select(&:first_player?)
559
+ chess_actors = actors.select { |a| a.name == :Chess }
560
+ kings = actors.select { |a| a.type == :K }
561
+ uppercase_styles = actors.select { |a| a.to_snn == a.to_snn.upcase }
562
+
563
+ # Transform collections immutably
564
+ enhanced_actors = actors.map(&:enhance)
565
+ enemy_actors = actors.map(&:flip)
566
+
567
+ # Show component changes
568
+ puts "Enhanced actors:"
569
+ enhanced_actors.each { |a| puts " #{a} (pin: #{a.to_pin})" }
570
+
571
+ puts "Enemy actors:"
572
+ enemy_actors.each { |a| puts " #{a} (snn: #{a.to_snn}, pin: #{a.to_pin})" }
573
+
574
+ # Complex queries
575
+ cross_style_pairs = actors.combination(2).select do |a1, a2|
576
+ a1.name != a2.name && a1.side != a2.side
577
+ end
578
+
579
+ puts "Cross-style pairs: #{cross_style_pairs.size}"
134
580
  ```
135
581
 
136
- ## Casing Rules
582
+ ## Protocol Mapping
583
+
584
+ GAN encodes piece attributes by combining SNN and PIN information:
585
+
586
+ | Protocol Attribute | GAN Encoding | Examples | Notes |
587
+ |-------------------|--------------|----------|-------|
588
+ | **Type** | PIN letter choice | `CHESS:K` = King, `SHOGI:P` = Pawn | Type stored as uppercase symbol (`:K`, `:P`) |
589
+ | **Side** | Unified case across components | `CHESS:K` = First player, `chess:k` = Second player | Case consistency enforced |
590
+ | **State** | PIN prefix modifier | `SHOGI:+P` = Enhanced, `CHESS:-P` = Diminished | |
591
+ | **Style** | SNN identifier | `CHESS:K` = Chess style, `SHOGI:K` = Shōgi style | Style stored with proper capitalization (`:Chess`, `:Shogi`) |
592
+
593
+ ## Properties
594
+
595
+ * **Rule-Agnostic**: Independent of specific game mechanics
596
+ * **Complete Identification**: Explicit representation of all four piece attributes
597
+ * **Cross-Style Support**: Enables multi-tradition gaming environments
598
+ * **Component Clarity**: Clear separation between style context and piece identity
599
+ * **Component Extraction**: Individual SNN and PIN components accessible via `to_snn` and `to_pin`
600
+ * **Unified Case Encoding**: Consistent case across both components for side identification
601
+ * **Protocol Compliance**: Direct implementation of Sashité piece attributes
602
+ * **Immutable Design**: All operations return new instances, ensuring thread safety
603
+ * **Compositional Architecture**: Built on independent SNN and PIN specifications
604
+ * **Modular Validation**: Delegates validation to underlying components for consistency
605
+
606
+ ## Implementation Notes
607
+
608
+ ### Validation Architecture
609
+
610
+ GAN follows a modular validation approach that leverages the underlying component libraries:
611
+
612
+ 1. **Component Splitting**: GAN strings are split on the colon separator
613
+ 2. **Individual Validation**: Each component is validated using its specific regex:
614
+ - SNN component: `Sashite::Snn::Style::SNN_PATTERN`
615
+ - PIN component: `Sashite::Pin::Piece::PIN_PATTERN`
616
+ 3. **Case Consistency**: Additional validation ensures matching case between components
137
617
 
138
- The casing of the game identifier must match the piece letter casing:
618
+ This approach:
619
+ - **Avoids Code Duplication**: No need to maintain a separate GAN regex
620
+ - **Maintains Consistency**: Automatically inherits validation improvements from SNN and PIN
621
+ - **Provides Clear Error Messages**: Component-specific validation failures are more informative
622
+ - **Enables Modularity**: Each library maintains its own validation logic
139
623
 
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
624
+ ### Component Handling Convention
142
625
 
143
- This ensures consistency with the FEEN specification's third field.
626
+ GAN follows the same internal representation conventions as its constituent libraries:
144
627
 
145
- ## Examples
628
+ 1. **Style Names**: Always stored with proper capitalization (`:Chess`, `:Shogi`)
629
+ 2. **Piece Types**: Always stored as uppercase symbols (`:K`, `:P`)
630
+ 3. **Display Logic**: Case is computed from `side` during string rendering
146
631
 
147
- ### Chess Pieces
632
+ This ensures predictable behavior and consistency across the entire Sashité ecosystem.
148
633
 
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` |
634
+ ## System Constraints
157
635
 
158
- ### Disambiguated Collisions
636
+ - **Case Consistency**: SNN and PIN components must have matching case
637
+ - **Exactly 2 players**: Distinguished through consistent case encoding
638
+ - **Style Assignment**: Fixed throughout a game (first/second player styles remain constant)
639
+ - **Component Validation**: Both SNN and PIN components must be individually valid
159
640
 
160
- These examples show how GAN resolves ambiguities between pieces that would have identical PNN representation:
641
+ ## Use Cases
161
642
 
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` |
643
+ GAN is particularly useful for:
644
+
645
+ 1. **Multi-Style Environments**: Positions involving pieces from multiple style traditions
646
+ 2. **Cross-Style Games**: Games combining elements from different piece traditions
647
+ 3. **Component Analysis**: Extracting and analyzing style and piece information separately
648
+ 4. **Game Engine Development**: Engines needing unambiguous piece identification
649
+ 5. **Database Systems**: Storing game data without naming conflicts
650
+ 6. **Hybrid Analysis**: Comparing strategic elements across different traditions
651
+ 7. **Functional Programming**: Immutable game state representations
652
+ 8. **Format Conversion**: Converting between GAN and individual SNN/PIN representations
653
+
654
+ ## Dependencies
655
+
656
+ This gem depends on:
657
+
658
+ - [sashite-snn](https://github.com/sashite/snn.rb) - Style Name Notation implementation
659
+ - [sashite-pin](https://github.com/sashite/pin.rb) - Piece Identifier Notation implementation
660
+
661
+ ## Related Specifications
662
+
663
+ - [GAN Specification v1.0.0](https://sashite.dev/specs/gan/1.0.0/)
664
+ - [GAN Examples](https://sashite.dev/specs/gan/1.0.0/examples/)
665
+ - [SNN Specification v1.0.0](https://sashite.dev/specs/snn/1.0.0/)
666
+ - [PIN Specification v1.0.0](https://sashite.dev/specs/pin/1.0.0/)
667
+ - [Game Protocol Foundation](https://sashite.dev/game-protocol/)
168
668
 
169
669
  ## Documentation
170
670
 
171
- - [Official GAN Specification](https://sashite.dev/documents/gan/1.0.0/)
172
671
  - [API Documentation](https://rubydoc.info/github/sashite/gan.rb/main)
672
+ - [SNN Documentation](https://rubydoc.info/github/sashite/snn.rb/main)
673
+ - [PIN Documentation](https://rubydoc.info/github/sashite/pin.rb/main)
674
+
675
+ ## Development
676
+
677
+ ```sh
678
+ # Clone the repository
679
+ git clone https://github.com/sashite/gan.rb.git
680
+ cd gan.rb
681
+
682
+ # Install dependencies
683
+ bundle install
684
+
685
+ # Run tests
686
+ ruby test.rb
687
+
688
+ # Generate documentation
689
+ yard doc
690
+ ```
691
+
692
+ ## Contributing
693
+
694
+ 1. Fork the repository
695
+ 2. Create a feature branch (`git checkout -b feature/new-feature`)
696
+ 3. Add tests for your changes
697
+ 4. Ensure all tests pass (`ruby test.rb`)
698
+ 5. Commit your changes (`git commit -am 'Add new feature'`)
699
+ 6. Push to the branch (`git push origin feature/new-feature`)
700
+ 7. Create a Pull Request
173
701
 
174
702
  ## License
175
703
 
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).
704
+ Available as open source under the [MIT License](https://opensource.org/licenses/MIT).
177
705
 
178
- ## About Sashité
706
+ ## About
179
707
 
180
- This project is maintained by [Sashité](https://sashite.com/) — promoting chess variants and sharing the beauty of Chinese, Japanese, and Western chess cultures.
708
+ Maintained by [Sashité](https://sashite.com/) — promoting chess variants and sharing the beauty of board game cultures.