sashite-gan 3.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 361cb6527615fdfe0c4246c3f5f603598af97a46e6be9ec0cc300d03651dcb4a
4
- data.tar.gz: 50fea949d4a759c2a68792085e1457c16c49966adf075533eb2e5cd1da5f852a
3
+ metadata.gz: b5469e58632f24a93eea22a4dd6d48c27028f84a2137469164eae9684274a7af
4
+ data.tar.gz: fa8fe045e1f5ced019b9bdf33369bc457e94187d7703ed13c7189c040e430502
5
5
  SHA512:
6
- metadata.gz: '0298e95b598a17d558f85072321ee9006470f83e83aa20fd58235ec5550df98e1ec20f594b715962d8a063f0757ba927c0093577eb9c3c98ec9dd9b5180cdb4a'
7
- data.tar.gz: 705ad8e30abe365dedbe108d941625d6c06725d0507c2e853f622bfe9da992137109b9fd4c44a09162cfbe1aed5c1f7bb15c1689977b131123095d69897cbebf
6
+ metadata.gz: 91d7fbc665bce2be60a37921398dea166684efa68ce55e22fd740d7d85907599d8812a491dbd561b73c1bb8aa1c625d453590842adb3edfb86b686e69c1f8ed8
7
+ data.tar.gz: 7761db8067e89586009fe4c52f06e632e7c153c5a7f370c613abd3066691d3d9b3fe214f71db1de1988a4a0d02df1d841b03a70b9aadb49ad89dcf3b940ef8bd
data/README.md CHANGED
@@ -9,13 +9,9 @@
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) defines a consistent and rule-agnostic format for identifying game actors in abstract strategy board games. GAN provides unambiguous identification of pieces by combining Style Name Notation (SNN) with Piece Name Notation (PNN), eliminating collision problems when multiple piece styles are present in the same context.
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:
15
-
16
- - Serializing game actors to GAN strings
17
- - Parsing GAN strings into their component parts
18
- - Validating GAN strings according to the specification
14
+ This gem implements the [GAN Specification v1.0.0](https://sashite.dev/documents/gan/1.0.0/), providing a Ruby interface for working with game actors through a clean and modular API that builds upon the existing [sashite-snn](https://rubygems.org/gems/sashite-snn) and [pnn](https://rubygems.org/gems/pnn) gems.
19
15
 
20
16
  ## Installation
21
17
 
@@ -32,144 +28,382 @@ gem install sashite-gan
32
28
 
33
29
  ## GAN Format
34
30
 
35
- A GAN record consists of a game identifier, followed by a colon, followed by a piece identifier that follows the PNN specification:
31
+ A GAN record consists of a style identifier (SNN format), followed by a colon separator, followed by a piece identifier (PNN format):
36
32
 
37
33
  ```
38
- <game-id>:<piece-id>
34
+ <style-id>:<piece-id>
39
35
  ```
40
36
 
41
37
  Where:
38
+ - `<style-id>` is a Style Name Notation (SNN) identifier conforming to SNN specification
39
+ - `:` is a literal colon character serving as a separator
40
+ - `<piece-id>` is a Piece Name Notation (PNN) identifier conforming to PNN specification
42
41
 
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>]`.
42
+ ## Basic Usage
46
43
 
47
- The casing of the game identifier reflects the player:
44
+ ### Creating Actor Objects
48
45
 
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.
46
+ The primary interface is the `Sashite::Gan::Actor` class, which represents a game actor in GAN format:
51
47
 
52
- ## Basic Usage
48
+ ```ruby
49
+ require "sashite/gan"
50
+
51
+ # Parse a GAN string into an actor object
52
+ actor = Sashite::Gan::Actor.parse("CHESS:K")
53
+ # => #<Sashite::Gan::Actor:0x... @style="CHESS" @piece="K">
54
+
55
+ # With piece modifiers
56
+ enhanced_actor = Sashite::Gan::Actor.parse("SHOGI:+P")
57
+ # => #<Sashite::Gan::Actor:0x... @style="SHOGI" @piece="+P">
58
+
59
+ # Create directly with constructor
60
+ actor = Sashite::Gan::Actor.new("CHESS", "K")
61
+ enhanced_actor = Sashite::Gan::Actor.new("SHOGI", "+P")
62
+
63
+ # Create with style and piece objects
64
+ style = Sashite::Snn::Style.new("CHESS")
65
+ piece = Pnn::Piece.new("K")
66
+ actor = Sashite::Gan::Actor.new(style, piece)
67
+
68
+ # Convenience method
69
+ actor = Sashite::Gan.actor("CHESS", "K")
70
+ ```
71
+
72
+ ### Converting to GAN String
73
+
74
+ Convert an actor object back to its GAN string representation:
75
+
76
+ ```ruby
77
+ actor = Sashite::Gan::Actor.parse("CHESS:K")
78
+ actor.to_s
79
+ # => "CHESS:K"
80
+
81
+ enhanced_actor = Sashite::Gan::Actor.parse("SHOGI:+p'")
82
+ enhanced_actor.to_s
83
+ # => "SHOGI:+p'"
84
+ ```
85
+
86
+ ### Accessing Components
53
87
 
54
- ### Parsing GAN Strings
88
+ Access the style and piece components of an actor:
55
89
 
56
- Convert a GAN string into a structured Ruby hash:
90
+ ```ruby
91
+ actor = Sashite::Gan::Actor.parse("CHESS:K")
92
+
93
+ # Access as strings
94
+ actor.style_name # => "CHESS"
95
+ actor.piece_name # => "K"
96
+
97
+ # Access as objects
98
+ actor.style # => #<Sashite::Snn::Style:0x... @identifier="CHESS">
99
+ actor.piece # => #<Pnn::Piece:0x... @letter="K">
100
+
101
+ # Check player associations
102
+ actor.style.first_player? # => true
103
+ actor.piece.uppercase? # => true
104
+ ```
105
+
106
+ ## Casing Combinations and Player Association
107
+
108
+ GAN allows all four combinations of case between style and piece identifiers to support dynamic ownership changes:
109
+
110
+ ```ruby
111
+ # First player's style, first player's piece
112
+ actor1 = Sashite::Gan::Actor.parse("CHESS:K")
113
+ actor1.style.first_player? # => true
114
+ actor1.piece.uppercase? # => true
115
+
116
+ # First player's style, second player's piece (piece was captured and converted)
117
+ actor2 = Sashite::Gan::Actor.parse("CHESS:k")
118
+ actor2.style.first_player? # => true
119
+ actor2.piece.lowercase? # => true
120
+
121
+ # Second player's style, first player's piece (piece was captured and converted)
122
+ actor3 = Sashite::Gan::Actor.parse("chess:K")
123
+ actor3.style.second_player? # => true
124
+ actor3.piece.uppercase? # => true
125
+
126
+ # Second player's style, second player's piece
127
+ actor4 = Sashite::Gan::Actor.parse("chess:k")
128
+ actor4.style.second_player? # => true
129
+ actor4.piece.lowercase? # => true
130
+ ```
131
+
132
+ ## Dynamic Ownership Changes
133
+
134
+ While style assignment remains fixed throughout a game, piece ownership may change during gameplay:
57
135
 
58
136
  ```ruby
59
- require "sashite-gan"
137
+ # Original piece owned by first player
138
+ original = Sashite::Gan::Actor.parse("SHOGI:P")
60
139
 
61
- # Basic actor
62
- result = Sashite::Gan.parse("CHESS:K")
63
- # => { game_id: "CHESS", letter: "K" }
140
+ # After capture by second player (modifiers preserved by default)
141
+ captured = original.change_piece_ownership
142
+ captured.to_s # => "SHOGI:p"
64
143
 
65
- # With piece prefix
66
- result = Sashite::Gan.parse("SHOGI:+P")
67
- # => { game_id: "SHOGI", letter: "P", prefix: "+" }
144
+ # Or create the captured version directly
145
+ captured = Sashite::Gan::Actor.new(original.style, "p")
68
146
 
69
- # With piece suffix
70
- result = Sashite::Gan.parse("CHESS:K'")
71
- # => { game_id: "CHESS", letter: "K", suffix: "'" }
147
+ # Example with enhanced piece - modifiers are preserved
148
+ enhanced = Sashite::Gan::Actor.parse("SHOGI:+P")
149
+ captured_enhanced = enhanced.change_piece_ownership
150
+ captured_enhanced.to_s # => "SHOGI:+p" (modifiers preserved)
72
151
 
73
- # With both piece prefix and suffix
74
- result = Sashite::Gan.parse("SHOGI:+R'")
75
- # => { game_id: "SHOGI", letter: "R", prefix: "+", suffix: "'" }
152
+ # To remove modifiers explicitly (if game rules require it):
153
+ bare_captured = enhanced.bare_piece.change_piece_ownership
154
+ bare_captured.to_s # => "SHOGI:p" (modifiers removed)
76
155
  ```
77
156
 
78
- ### Safe Parsing
157
+ ## Traditional Same-Style Games
79
158
 
80
- Parse a GAN string without raising exceptions:
159
+ In traditional games where both players use the same piece style:
81
160
 
82
161
  ```ruby
83
- require "sashite-gan"
162
+ # Chess pieces
163
+ white_king = Sashite::Gan::Actor.parse("CHESS:K")
164
+ black_king = Sashite::Gan::Actor.parse("chess:k")
84
165
 
85
- # Valid GAN string
86
- result = Sashite::Gan.safe_parse("CHESS:K'")
87
- # => { game_id: "CHESS", letter: "K", suffix: "'" }
166
+ white_queen = Sashite::Gan::Actor.parse("CHESS:Q")
167
+ black_queen = Sashite::Gan::Actor.parse("chess:q")
88
168
 
89
- # Invalid GAN string
90
- result = Sashite::Gan.safe_parse("invalid gan string")
91
- # => nil
169
+ # Shogi pieces
170
+ first_king = Sashite::Gan::Actor.parse("SHOGI:K")
171
+ second_king = Sashite::Gan::Actor.parse("shogi:k")
172
+
173
+ first_gold = Sashite::Gan::Actor.parse("SHOGI:G")
174
+ second_gold = Sashite::Gan::Actor.parse("shogi:g")
92
175
  ```
93
176
 
94
- ### Creating GAN Strings
177
+ ## Cross-Style Games
95
178
 
96
- Convert actor components into a GAN string:
179
+ In games where players use different piece styles:
97
180
 
98
181
  ```ruby
99
- require "sashite-gan"
182
+ # Chess vs Makruk
183
+ chess_king = Sashite::Gan::Actor.parse("CHESS:K")
184
+ makruk_king = Sashite::Gan::Actor.parse("makruk:k")
100
185
 
101
- # Basic actor
102
- Sashite::Gan.dump(game_id: "CHESS", letter: "K")
103
- # => "CHESS:K"
186
+ chess_queen = Sashite::Gan::Actor.parse("CHESS:Q")
187
+ makruk_queen = Sashite::Gan::Actor.parse("makruk:q")
188
+
189
+ # Shogi vs Xiangqi
190
+ shogi_king = Sashite::Gan::Actor.parse("SHOGI:K")
191
+ xiangqi_general = Sashite::Gan::Actor.parse("xiangqi:g")
192
+
193
+ shogi_gold = Sashite::Gan::Actor.parse("SHOGI:G")
194
+ xiangqi_advisor = Sashite::Gan::Actor.parse("xiangqi:a")
195
+ ```
196
+
197
+ ## Pieces with States and Ownership Changes
104
198
 
105
- # With piece prefix
106
- Sashite::Gan.dump(game_id: "SHOGI", letter: "P", prefix: "+")
107
- # => "SHOGI:+P"
199
+ ```ruby
200
+ # Original enhanced piece
201
+ original = Sashite::Gan::Actor.parse("CHESS:R'")
202
+
203
+ # After capture (modifiers preserved by default)
204
+ captured = original.change_piece_ownership
205
+ captured.to_s # => "chess:R'"
206
+
207
+ # If game rules require modifier removal during capture:
208
+ captured_bare = original.bare_piece.change_piece_ownership
209
+ captured_bare.to_s # => "chess:R"
210
+
211
+ # Promoted shogi piece captured
212
+ promoted_pawn = Sashite::Gan::Actor.parse("shogi:+p")
213
+ captured_promoted = promoted_pawn.change_piece_ownership
214
+ captured_promoted.to_s # => "SHOGI:+p" (modifiers preserved)
215
+
216
+ # With explicit modifier removal:
217
+ captured_demoted = promoted_pawn.bare_piece.change_piece_ownership
218
+ captured_demoted.to_s # => "SHOGI:p"
219
+ ```
220
+
221
+ ## Collision Resolution
222
+
223
+ GAN resolves naming conflicts between different styles:
224
+
225
+ ```ruby
226
+ # All different actors despite similar piece types
227
+ chess_rook = Sashite::Gan::Actor.parse("CHESS:R")
228
+ shogi_rook = Sashite::Gan::Actor.parse("SHOGI:R")
229
+ makruk_rook = Sashite::Gan::Actor.parse("MAKRUK:R")
230
+ xiangqi_chariot = Sashite::Gan::Actor.parse("xiangqi:r")
231
+
232
+ # They can all coexist in the same context
233
+ pieces = [chess_rook, shogi_rook, makruk_rook, xiangqi_chariot]
234
+ puts pieces.map(&:to_s)
235
+ # => ["CHESS:R", "SHOGI:R", "MAKRUK:R", "xiangqi:r"]
236
+ ```
237
+
238
+ ## Advanced Usage
239
+
240
+ ### Working with Collections
241
+
242
+ ```ruby
243
+ # Group actors by style
244
+ actors = [
245
+ Sashite::Gan::Actor.parse("CHESS:K"),
246
+ Sashite::Gan::Actor.parse("CHESS:Q"),
247
+ Sashite::Gan::Actor.parse("shogi:k"),
248
+ Sashite::Gan::Actor.parse("shogi:g")
249
+ ]
250
+
251
+ grouped = actors.group_by { |actor| actor.style_name.downcase }
252
+ # => {"chess" => [...], "shogi" => [...]}
253
+
254
+ # Filter by player
255
+ first_player_actors = actors.select { |actor| actor.style.first_player? }
256
+ second_player_actors = actors.select { |actor| actor.style.second_player? }
257
+
258
+ # Find actors by piece type
259
+ kings = actors.select { |actor| actor.piece_name.downcase == "k" }
260
+ ```
261
+
262
+ ### State Manipulation
263
+
264
+ ```ruby
265
+ actor = Sashite::Gan::Actor.parse("SHOGI:P")
266
+
267
+ # Enhance the piece
268
+ enhanced = actor.enhance_piece
269
+ enhanced.to_s # => "SHOGI:+P"
270
+
271
+ # Add intermediate state
272
+ intermediate = actor.set_piece_intermediate
273
+ intermediate.to_s # => "SHOGI:P'"
108
274
 
109
- # With piece suffix
110
- Sashite::Gan.dump(game_id: "CHESS", letter: "K", suffix: "'")
111
- # => "CHESS:K'"
275
+ # Chain operations
276
+ complex = actor.enhance_piece.set_piece_intermediate
277
+ complex.to_s # => "SHOGI:+P'"
112
278
 
113
- # With both piece prefix and suffix
114
- Sashite::Gan.dump(game_id: "SHOGI", letter: "R", prefix: "+", suffix: "'")
115
- # => "SHOGI:+R'"
279
+ # Remove all modifiers
280
+ bare = complex.bare_piece
281
+ bare.to_s # => "SHOGI:P"
116
282
  ```
117
283
 
118
284
  ### Validation
119
285
 
120
- Check if a string is valid GAN notation:
286
+ All parsing automatically validates input according to the GAN specification:
121
287
 
122
288
  ```ruby
123
- require "sashite-gan"
289
+ # Valid GAN strings
290
+ Sashite::Gan::Actor.parse("CHESS:K") # ✓
291
+ Sashite::Gan::Actor.parse("shogi:+p") # ✓
292
+ Sashite::Gan::Actor.parse("XIANGQI:r'") # ✓
293
+
294
+ # Valid constructor calls
295
+ Sashite::Gan::Actor.new("CHESS", "K") # ✓
296
+ Sashite::Gan::Actor.new("shogi", "+p") # ✓
297
+
298
+ # Convenience method
299
+ Sashite::Gan.actor("MAKRUK", "Q") # ✓
300
+
301
+ # Check validity
302
+ Sashite::Gan.valid?("CHESS:K") # => true
303
+ Sashite::Gan.valid?("Chess:K") # => false (mixed case in style)
304
+ Sashite::Gan.valid?("CHESS") # => false (missing piece)
305
+ Sashite::Gan.valid?("") # => false (empty string)
306
+
307
+ # Invalid GAN strings raise ArgumentError
308
+ Sashite::Gan::Actor.parse("") # ✗ ArgumentError
309
+ Sashite::Gan::Actor.parse("Chess:K") # ✗ ArgumentError (mixed case)
310
+ Sashite::Gan::Actor.parse("CHESS") # ✗ ArgumentError (missing piece)
311
+ Sashite::Gan::Actor.parse("CHESS:++K") # ✗ ArgumentError (invalid piece)
312
+ ```
124
313
 
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
314
+ ### Inspection and Debugging
129
315
 
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
316
+ ```ruby
317
+ actor = Sashite::Gan::Actor.parse("SHOGI:+p'")
318
+
319
+ # Get detailed information
320
+ actor.inspect
321
+ # => "#<Sashite::Gan::Actor:0x... style=\"SHOGI\" piece=\"+p'\">"
322
+
323
+ # Check components
324
+ actor.style_name # => "SHOGI"
325
+ actor.piece_name # => "+p'"
326
+ actor.piece.enhanced? # => true
327
+ actor.piece.intermediate? # => true
134
328
  ```
135
329
 
136
- ## Casing Rules
330
+ ## API Reference
331
+
332
+ ### Module Methods
333
+
334
+ - `Sashite::Gan.valid?(gan_string)` - Check if a string is valid GAN notation
335
+ - `Sashite::Gan.actor(style, piece)` - Convenience method to create actors
336
+
337
+ ### Sashite::Gan::Actor Class Methods
338
+
339
+ - `Sashite::Gan::Actor.parse(gan_string)` - Parse a GAN string into an actor object
340
+ - `Sashite::Gan::Actor.new(style, piece)` - Create a new actor instance
341
+
342
+ ### Instance Methods
343
+
344
+ #### Component Access
345
+ - `#style` - Get the style object (Sashite::Snn::Style)
346
+ - `#piece` - Get the piece object (Pnn::Piece)
347
+ - `#style_name` - Get the style name as string
348
+ - `#piece_name` - Get the piece name as string
349
+
350
+ #### Piece State Manipulation
351
+ - `#enhance_piece` - Create actor with enhanced piece
352
+ - `#diminish_piece` - Create actor with diminished piece
353
+ - `#set_piece_intermediate` - Create actor with intermediate piece state
354
+ - `#bare_piece` - Create actor with piece without modifiers
355
+ - `#change_piece_ownership` - Create actor with piece ownership flipped
356
+
357
+ #### Conversion
358
+ - `#to_s` - Convert to GAN string representation
359
+ - `#inspect` - Detailed string representation for debugging
360
+
361
+ ## Properties of GAN
362
+
363
+ * **Rule-agnostic**: GAN does not encode game states, legality, validity, or game-specific conditions
364
+ * **Unambiguous identification**: Different piece styles can coexist without naming conflicts
365
+ * **Canonical representation**: Equivalent actors yield identical strings
366
+ * **Cross-style support**: Enables games where pieces from multiple traditions may be present
367
+ * **Dynamic ownership**: Supports games where piece ownership can change during gameplay
368
+ * **Compositional architecture**: Built on independent SNN and PNN specifications
369
+
370
+ ## Constraints
137
371
 
138
- The casing of the game identifier must match the piece letter casing:
372
+ * GAN supports exactly **two players**
373
+ * Players are distinguished through the combination of SNN and PNN casing
374
+ * Style assignment to players remains **fixed throughout a game**
375
+ * Piece ownership may change during gameplay through casing changes
376
+ * Both style and piece identifiers must conform to their respective specifications
139
377
 
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
378
+ ## Use Cases
142
379
 
143
- This ensures consistency with the FEEN specification's third field.
380
+ GAN is particularly useful in the following scenarios:
144
381
 
145
- ## Examples
382
+ 1. **Multi-style environments**: When positions or analyses involve pieces from multiple style traditions
383
+ 2. **Game engine development**: When implementing engines that need to distinguish between similar pieces from different styles while tracking ownership changes
384
+ 3. **Hybrid games**: When creating or analyzing positions from games that combine elements from different piece traditions
385
+ 4. **Database systems**: When storing game data that must avoid naming conflicts between similar pieces from different styles
386
+ 5. **Cross-style analysis**: When comparing or analyzing strategic elements across different piece traditions
387
+ 6. **Capture-conversion games**: When implementing games like shōgi where pieces change ownership and require clear ownership tracking
146
388
 
147
- ### Chess Pieces
389
+ ## Dependencies
148
390
 
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` |
391
+ This gem depends on:
157
392
 
158
- ### Disambiguated Collisions
393
+ - [sashite-snn](https://github.com/sashite/snn.rb) (~> 1.0.0) - Style Name Notation implementation
394
+ - [pnn](https://github.com/sashite/pnn.rb) (~> 2.0.0) - Piece Name Notation implementation
159
395
 
160
- These examples show how GAN resolves ambiguities between pieces that would have identical PNN representation:
396
+ ## Specification
161
397
 
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` |
398
+ - [GAN Specification](https://sashite.dev/documents/gan/1.0.0/)
399
+ - [SNN Specification](https://sashite.dev/documents/snn/1.0.0/)
400
+ - [PNN Specification](https://sashite.dev/documents/pnn/1.0.0/)
168
401
 
169
402
  ## Documentation
170
403
 
171
- - [Official GAN Specification](https://sashite.dev/documents/gan/1.0.0/)
172
- - [API Documentation](https://rubydoc.info/github/sashite/gan.rb/main)
404
+ - [GAN Documentation](https://rubydoc.info/github/sashite/gan.rb/main)
405
+ - [SNN Documentation](https://rubydoc.info/github/sashite/snn.rb/main)
406
+ - [PNN Documentation](https://rubydoc.info/github/sashite/pnn.rb/main)
173
407
 
174
408
  ## License
175
409
 
@@ -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,76 +1,88 @@
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 "sashite/snn"
4
+ require "pnn"
5
+ require_relative "gan/actor"
6
6
 
7
7
  module Sashite
8
- # This module provides a Ruby interface for serialization and
9
- # deserialization of game actors in GAN format.
8
+ # General Actor Notation (GAN) module
10
9
  #
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).
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
14
  #
15
- # @see https://sashite.dev/documents/gan/1.0.0/
15
+ # @see https://sashite.dev/documents/gan/1.0.0/ GAN Specification v1.0.0
16
16
  module Gan
17
- # Serializes an actor into a GAN string.
18
- #
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
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/
32
20
 
33
- # Parses a GAN string into its component parts.
21
+ # Check if a string is valid GAN notation
22
+ #
23
+ # @param gan_string [String] The string to validate
24
+ # @return [Boolean] true if the string is valid GAN notation, false otherwise
34
25
  #
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
26
  # @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)
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
47
  end
48
48
 
49
- # Safely parses a GAN string into its component parts without raising exceptions.
49
+ # Convenience method to create an actor object
50
+ #
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
50
55
  #
51
- # @param gan_string [String] GAN notation string
52
- # @return [Hash, nil] Hash containing the parsed actor data or nil if parsing fails
53
56
  # @example
54
- # # Valid GAN string
55
- # Sashite::Gan.safe_parse("CHESS:K'")
56
- # # => { game_id: "CHESS", letter: "K", suffix: "'" }
57
+ # actor = Sashite::Gan.actor("CHESS", "K")
58
+ # # => #<Sashite::Gan::Actor:0x... style="CHESS" piece="K">
57
59
  #
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)
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)
63
66
  end
64
67
 
65
- # Validates if the given string is a valid GAN string
68
+ # Parse a GAN string into component parts
69
+ #
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
66
73
  #
67
- # @param gan_string [String] GAN string to validate
68
- # @return [Boolean] True if the string is a valid GAN string
69
74
  # @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)
75
+ # Sashite::Gan.parse_components("CHESS:K")
76
+ # # => ["CHESS", "K"]
77
+ #
78
+ # Sashite::Gan.parse_components("shogi:+p'")
79
+ # # => ["shogi", "+p'"]
80
+ #
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)
74
86
  end
75
87
  end
76
88
  end
data/lib/sashite-gan.rb CHANGED
@@ -1,7 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Sashité namespace
3
+ # Sashité namespace for board game notation libraries
4
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
5
16
  end
6
17
 
7
18
  require_relative "sashite/gan"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sashite-gan
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0
4
+ version: 4.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cyril Kato
@@ -15,14 +15,28 @@ dependencies:
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: 1.1.0
18
+ version: 2.0.0
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: 1.1.0
25
+ version: 2.0.0
26
+ - !ruby/object:Gem::Dependency
27
+ name: sashite-snn
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: 1.0.0
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: 1.0.0
26
40
  description: A Ruby interface for serialization and deserialization of game actors
27
41
  in GAN format. GAN is a consistent and rule-agnostic format for representing game
28
42
  actors in abstract strategy board games, providing a standardized way to identify
@@ -36,9 +50,7 @@ files:
36
50
  - README.md
37
51
  - lib/sashite-gan.rb
38
52
  - lib/sashite/gan.rb
39
- - lib/sashite/gan/dumper.rb
40
- - lib/sashite/gan/parser.rb
41
- - lib/sashite/gan/validator.rb
53
+ - lib/sashite/gan/actor.rb
42
54
  homepage: https://github.com/sashite/gan.rb
43
55
  licenses:
44
56
  - MIT
@@ -1,94 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "pnn"
4
-
5
- module Sashite
6
- module Gan
7
- # Serializes actor components into GAN (General Actor Notation) strings.
8
- #
9
- # The dumper transforms piece data and game identifiers into properly
10
- # formatted GAN strings, ensuring consistency between game ID casing
11
- # and piece letter casing according to the GAN specification.
12
- #
13
- # According to the specification, game IDs must be either all uppercase
14
- # or all lowercase, and their casing must match the casing of the piece letter.
15
- class Dumper
16
- # Pattern for validating game identifiers - must be all uppercase OR all lowercase
17
- GAME_ID_PATTERN = /\A([A-Z]+|[a-z]+)\z/
18
-
19
- # Error message templates
20
- INVALID_GAME_ID_ERROR = "Game ID must be a non-empty string containing only ASCII letters and must be either all uppercase or all lowercase: %s"
21
- CASING_MISMATCH_ERROR = "Game ID casing (%s) must match piece letter casing (%s)"
22
-
23
- # Serializes actor components into a GAN string
24
- #
25
- # @param game_id [String] The game identifier (e.g., "CHESS", "shogi")
26
- # @param piece_params [Hash] Piece parameters as accepted by Pnn.dump:
27
- # @option piece_params [String] :letter The single ASCII letter identifier (required)
28
- # @option piece_params [String, nil] :prefix Optional prefix modifier for the piece ("+", "-")
29
- # @option piece_params [String, nil] :suffix Optional suffix modifier for the piece ("'")
30
- # @return [String] A properly formatted GAN notation string (e.g., "CHESS:K'")
31
- # @raise [ArgumentError] If game_id is invalid or casing is inconsistent with piece letter
32
- # @example Create a GAN string for a white chess king with castling rights
33
- # Dumper.dump(game_id: "CHESS", letter: "K", suffix: "'")
34
- # # => "CHESS:K'"
35
- # @example Create a GAN string for a promoted shogi pawn
36
- # Dumper.dump(game_id: "SHOGI", letter: "P", prefix: "+")
37
- # # => "SHOGI:+P"
38
- def self.dump(game_id:, **piece_params)
39
- game_id = String(game_id)
40
- validate_game_id!(game_id)
41
-
42
- # Build the piece string using the PNN gem
43
- pnn_string = ::Pnn.dump(**piece_params)
44
-
45
- # Verify casing consistency
46
- validate_casing_consistency!(game_id, pnn_string)
47
-
48
- "#{game_id}:#{pnn_string}"
49
- end
50
-
51
- # @api private
52
- # Validates that the game_id contains only ASCII letters
53
- #
54
- # @param game_id [String] The game identifier to validate
55
- # @return [void]
56
- # @raise [ArgumentError] If game_id contains non-letter characters
57
- def self.validate_game_id!(game_id)
58
- return if game_id.match?(GAME_ID_PATTERN)
59
-
60
- raise ::ArgumentError, format(INVALID_GAME_ID_ERROR, game_id)
61
- end
62
- private_class_method :validate_game_id!
63
-
64
- # @api private
65
- # Validates that the casing of the game_id is consistent with the piece letter
66
- #
67
- # According to GAN specification, if game_id is uppercase, piece letter must be uppercase,
68
- # and if game_id is lowercase, piece letter must be lowercase.
69
- #
70
- # @param game_id [String] The game identifier
71
- # @param pnn_string [String] The PNN string
72
- # @return [void]
73
- # @raise [ArgumentError] If casing is inconsistent
74
- def self.validate_casing_consistency!(game_id, pnn_string)
75
- return if casing_consistent?(game_id, pnn_string)
76
-
77
- raise ::ArgumentError, format(CASING_MISMATCH_ERROR, game_id, pnn_string)
78
- end
79
- private_class_method :validate_casing_consistency!
80
-
81
- # @api private
82
- # Verifies that the casing of the game_id matches the casing of the piece letter
83
- #
84
- # @param game_id [String] The game identifier
85
- # @param pnn_string [String] The PNN string
86
- # @return [Boolean] True if casing is consistent
87
- def self.casing_consistent?(game_id, pnn_string)
88
- # Both must be uppercase or both must be lowercase
89
- (game_id == game_id.upcase) == (pnn_string == pnn_string.upcase)
90
- end
91
- private_class_method :casing_consistent?
92
- end
93
- end
94
- end
@@ -1,58 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "pnn"
4
-
5
- module Sashite
6
- module Gan
7
- # Parses GAN strings into their component parts
8
- class Parser
9
- # GAN regex pattern for parsing
10
- PATTERN = /\A(?<game_id>[a-zA-Z]+):(?<pnn_part>[-+]?[a-zA-Z][']?)\z/
11
-
12
- # Parse a GAN string into its components
13
- #
14
- # @param gan_string [String] The GAN string to parse
15
- # @return [Hash] Hash containing the parsed components
16
- # @raise [ArgumentError] If the GAN string is invalid
17
- def self.parse(gan_string)
18
- gan_string = String(gan_string)
19
-
20
- matches = PATTERN.match(gan_string)
21
- raise ArgumentError, "Invalid GAN string: #{gan_string}" if matches.nil?
22
-
23
- game_id = matches[:game_id]
24
- pnn_part = matches[:pnn_part]
25
-
26
- # Parse the PNN part using the PNN gem
27
- pnn_result = Pnn.parse(pnn_part)
28
-
29
- # Verify casing consistency
30
- unless casing_consistent?(game_id, pnn_result[:letter])
31
- raise ArgumentError, "Game ID casing (#{game_id}) must match piece letter casing (#{pnn_result[:letter]})"
32
- end
33
-
34
- # Merge the game_id with the piece parameters for a flatter structure
35
- { game_id: game_id }.merge(pnn_result)
36
- end
37
-
38
- # Safely parse a GAN string without raising exceptions
39
- #
40
- # @param gan_string [String] The GAN string to parse
41
- # @return [Hash, nil] Hash containing the parsed components or nil if invalid
42
- def self.safe_parse(gan_string)
43
- parse(gan_string)
44
- rescue ArgumentError
45
- nil
46
- end
47
-
48
- # Verifies that the casing of the game_id matches the casing of the piece letter
49
- #
50
- # @param game_id [String] The game identifier
51
- # @param letter [String] The piece letter
52
- # @return [Boolean] True if casing is consistent
53
- def self.casing_consistent?(game_id, letter)
54
- (game_id == game_id.upcase) == (letter == letter.upcase)
55
- end
56
- end
57
- end
58
- end
@@ -1,23 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "pnn"
4
-
5
- module Sashite
6
- module Gan
7
- # Validates GAN strings
8
- class Validator
9
- # GAN regex pattern for validation
10
- PATTERN = /\A([A-Z]+:[-+]?[A-Z][']?|[a-z]+:[-+]?[a-z][']?)\z/
11
-
12
- # Validates if the given string is a valid GAN string
13
- #
14
- # @param gan_string [String] The GAN string to validate
15
- # @return [Boolean] True if the string is a valid GAN string
16
- def self.valid?(gan_string)
17
- return false unless gan_string.is_a?(String)
18
-
19
- PATTERN.match?(gan_string)
20
- end
21
- end
22
- end
23
- end