sashite-pin 1.1.0 → 2.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: 6affbdb49961967a41cadbd94ced4b32f27279d2fbd7fcc06f74fe58e400184b
4
- data.tar.gz: 5d3ea8362f3d4329cf748f36713d89456e624cc2de0dc238fb784421811e5e81
3
+ metadata.gz: a140d90c9c73aeb4773d2e94c0825cdddfe6ce5dff722325eb58bbea896ba62e
4
+ data.tar.gz: 273fbe7adfa54613f4d582feb053690724d686f2f1d966d0f6871d33e77450b3
5
5
  SHA512:
6
- metadata.gz: 71ef6fc371601e9845b667085eed4222f3bd2e03bcac2d076a653632dd535bd2eb139ca59d3e3e257a9fdb33ba6d1ce7435ea2dafe34efe7423cdbefef5d5ede
7
- data.tar.gz: 70684742e2d2406f35a6259d34788ad3545880feb5ba407e993544c78da98f4930dbaccd7a1943579061cf8ab26e0737e8e5eb5dad03b55c7b422a314bc19c8c
6
+ metadata.gz: dcdd60202c06776b8243d1330c54f93d6a38d919f9d5201660287967c987c69a119c9f1884a0c8bf3f3099d14b715b3c2d656c8725b39092df352f6ea9d5a2d8
7
+ data.tar.gz: 4e87b25c1180e9cebcf47756cce651022d5c5b3227200010f2de408744910f20f596ee80bd34d6bf0ee597982189c92c22aed88907de30fea4c6c2455ac92072
data/README.md CHANGED
@@ -32,11 +32,15 @@ gem install sashite-pin
32
32
  require "sashite/pin"
33
33
 
34
34
  # Parse PIN strings into piece objects
35
- piece = Sashite::Pin.parse("K") # => #<Pin::Piece letter="K" type="K" player=first>
35
+ piece = Sashite::Pin.parse("K") # => #<Pin::Piece type=:K side=:first state=:normal>
36
36
  piece.to_s # => "K"
37
- piece.first_player? # => true
38
- piece.type # => "K"
37
+ piece.type # => :K
39
38
  piece.side # => :first
39
+ piece.state # => :normal
40
+
41
+ # Create pieces directly
42
+ piece = Sashite::Pin.piece(:K, :first, :normal) # => #<Pin::Piece type=:K side=:first state=:normal>
43
+ piece = Sashite::Pin::Piece.new(:R, :second, :enhanced) # => #<Pin::Piece type=:R side=:second state=:enhanced>
40
44
 
41
45
  # Validate PIN strings
42
46
  Sashite::Pin.valid?("K") # => true
@@ -44,32 +48,45 @@ Sashite::Pin.valid?("+R") # => true
44
48
  Sashite::Pin.valid?("invalid") # => false
45
49
 
46
50
  # State manipulation (returns new immutable instances)
47
- enhanced = piece.enhance # => #<Pin::Piece letter="K" type="K" player=first enhanced=true>
51
+ enhanced = piece.enhance # => #<Pin::Piece type=:K side=:first state=:enhanced>
48
52
  enhanced.to_s # => "+K"
49
- diminished = piece.diminish # => #<Pin::Piece letter="K" type="K" player=first diminished=true>
53
+ diminished = piece.diminish # => #<Pin::Piece type=:K side=:first state=:diminished>
50
54
  diminished.to_s # => "-K"
51
55
 
52
- # Player manipulation
53
- flipped = piece.flip # => #<Pin::Piece letter="k" type="K" player=second>
56
+ # Side manipulation
57
+ flipped = piece.flip # => #<Pin::Piece type=:K side=:second state=:normal>
54
58
  flipped.to_s # => "k"
55
59
 
60
+ # Type manipulation
61
+ queen = piece.with_type(:Q) # => #<Pin::Piece type=:Q side=:first state=:normal>
62
+ queen.to_s # => "Q"
63
+
56
64
  # State queries
57
65
  piece.normal? # => true
58
66
  enhanced.enhanced? # => true
59
67
  diminished.diminished? # => true
60
68
 
61
- # Type and player comparison
69
+ # Side queries
70
+ piece.first_player? # => true
71
+ flipped.second_player? # => true
72
+
73
+ # Attribute access
74
+ piece.letter # => "K"
75
+ enhanced.prefix # => "+"
76
+ piece.prefix # => ""
77
+
78
+ # Type and side comparison
62
79
  king1 = Sashite::Pin.parse("K")
63
80
  king2 = Sashite::Pin.parse("k")
64
81
  queen = Sashite::Pin.parse("Q")
65
82
 
66
83
  king1.same_type?(king2) # => true (both kings)
67
- king1.same_player?(queen) # => true (both first player)
84
+ king1.same_side?(queen) # => true (both first player)
68
85
  king1.same_type?(queen) # => false (different types)
69
86
 
70
87
  # Functional transformations can be chained
71
88
  pawn = Sashite::Pin.parse("P")
72
- enemy_promoted = pawn.flip.enhance # => "+p" (black promoted pawn)
89
+ enemy_promoted = pawn.flip.enhance # => "+p" (second player promoted pawn)
73
90
  ```
74
91
 
75
92
  ## Format Specification
@@ -81,7 +98,7 @@ enemy_promoted = pawn.flip.enhance # => "+p" (black promoted pawn)
81
98
 
82
99
  ### Components
83
100
 
84
- - **Letter** (`A-Z`, `a-z`): Represents piece type and player
101
+ - **Letter** (`A-Z`, `a-z`): Represents piece type and side
85
102
  - Uppercase: First player pieces
86
103
  - Lowercase: Second player pieces
87
104
  - **State** (optional prefix):
@@ -105,70 +122,73 @@ enemy_promoted = pawn.flip.enhance # => "+p" (black promoted pawn)
105
122
  ### Western Chess
106
123
  ```ruby
107
124
  # Standard pieces
108
- king = Sashite::Pin.parse("K") # => white king
109
- king.first_player? # => true
110
- king.type # => "K"
125
+ king = Sashite::Pin.piece(:K, :first, :normal) # => white king
126
+ king.first_player? # => true
127
+ king.type # => :K
111
128
 
112
129
  # State modifiers for special conditions
113
- castling_king = king.enhance # => castling-eligible king
114
- castling_king.to_s # => "+K"
130
+ castling_king = king.enhance # => castling-eligible king
131
+ castling_king.to_s # => "+K"
115
132
 
116
- vulnerable_pawn = Sashite::Pin.parse("P").diminish # => en passant vulnerable
117
- vulnerable_pawn.to_s # => "-P"
133
+ vulnerable_pawn = Sashite::Pin.piece(:P, :first, :diminished) # => en passant vulnerable
134
+ vulnerable_pawn.to_s # => "-P"
118
135
 
119
136
  # All piece types
120
- pieces = %w[K Q R B N P].map { |type| Sashite::Pin.parse(type) }
121
- black_pieces = pieces.map(&:flip) # Convert to black pieces
137
+ piece_types = [:K, :Q, :R, :B, :N, :P]
138
+ white_pieces = piece_types.map { |type| Sashite::Pin.piece(type, :first, :normal) }
139
+ black_pieces = white_pieces.map(&:flip) # Convert to black pieces
122
140
  ```
123
141
 
124
142
  ### Japanese Chess (Shōgi)
125
143
  ```ruby
126
144
  # Basic pieces
127
- rook = Sashite::Pin.parse("R") # => white rook
128
- bishop = Sashite::Pin.parse("B") # => white bishop
145
+ rook = Sashite::Pin.piece(:R, :first, :normal) # => white rook
146
+ bishop = Sashite::Pin.piece(:B, :first, :normal) # => white bishop
129
147
 
130
148
  # Promoted pieces (enhanced state)
131
- dragon_king = rook.enhance # => promoted rook (Dragon King)
132
- dragon_king.to_s # => "+R"
149
+ dragon_king = rook.enhance # => promoted rook (Dragon King)
150
+ dragon_king.to_s # => "+R"
133
151
 
134
- dragon_horse = bishop.enhance # => promoted bishop (Dragon Horse)
135
- dragon_horse.to_s # => "+B"
152
+ dragon_horse = bishop.enhance # => promoted bishop (Dragon Horse)
153
+ dragon_horse.to_s # => "+B"
136
154
 
137
155
  # Promoted pawn
138
- pawn = Sashite::Pin.parse("P")
139
- tokin = pawn.enhance # => promoted pawn (Tokin)
140
- tokin.to_s # => "+P"
156
+ pawn = Sashite::Pin.piece(:P, :first, :normal)
157
+ tokin = pawn.enhance # => promoted pawn (Tokin)
158
+ tokin.to_s # => "+P"
141
159
 
142
160
  # All promotable pieces can use the same pattern
143
- promotable = %w[R B S N L P].map { |type| Sashite::Pin.parse(type) }
161
+ promotable_types = [:R, :B, :S, :N, :L, :P]
162
+ promotable = promotable_types.map { |type| Sashite::Pin.piece(type, :first, :normal) }
144
163
  promoted = promotable.map(&:enhance)
145
164
  ```
146
165
 
147
166
  ### Thai Chess (Makruk)
148
167
  ```ruby
149
168
  # Basic pieces
150
- met = Sashite::Pin.parse("M") # => white Met (queen)
151
- pawn = Sashite::Pin.parse("P") # => white Bia (pawn)
169
+ met = Sashite::Pin.piece(:M, :first, :normal) # => white Met (queen)
170
+ pawn = Sashite::Pin.piece(:P, :first, :normal) # => white Bia (pawn)
152
171
 
153
172
  # Promoted pawns
154
- bia_kaew = pawn.enhance # => promoted pawn (Bia Kaew)
155
- bia_kaew.to_s # => "+P"
173
+ bia_kaew = pawn.enhance # => promoted pawn (Bia Kaew)
174
+ bia_kaew.to_s # => "+P"
156
175
 
157
176
  # Makruk pieces
158
- makruk_pieces = %w[K M R B N P].map { |type| Sashite::Pin.parse(type) }
177
+ makruk_types = [:K, :M, :R, :B, :N, :P]
178
+ makruk_pieces = makruk_types.map { |type| Sashite::Pin.piece(type, :first, :normal) }
159
179
  ```
160
180
 
161
181
  ### Chinese Chess (Xiangqi)
162
182
  ```ruby
163
183
  # Pieces with positional states
164
- general = Sashite::Pin.parse("G") # => red general
165
- flying_general = general.enhance # => flying general (special state)
166
- flying_general.to_s # => "+G"
184
+ general = Sashite::Pin.piece(:G, :first, :normal) # => red general
185
+ flying_general = general.enhance # => flying general (special state)
186
+ flying_general.to_s # => "+G"
167
187
 
168
188
  # Soldiers that crossed the river
169
- soldier = Sashite::Pin.parse("P")
170
- crossed_soldier = soldier.enhance # => soldier with enhanced movement
171
- crossed_soldier.to_s # => "+P"
189
+ soldier = Sashite::Pin.piece(:P, :first, :normal)
190
+ crossed_soldier = soldier.enhance # => soldier with enhanced movement
191
+ crossed_soldier.to_s # => "+P"
172
192
  ```
173
193
 
174
194
  ## API Reference
@@ -177,26 +197,28 @@ crossed_soldier.to_s # => "+P"
177
197
 
178
198
  - `Sashite::Pin.valid?(pin_string)` - Check if string is valid PIN notation
179
199
  - `Sashite::Pin.parse(pin_string)` - Parse PIN string into Piece object
200
+ - `Sashite::Pin.piece(type, side, state = :normal)` - Create piece instance directly
180
201
 
181
202
  ### Piece Class
182
203
 
183
204
  #### Creation and Parsing
184
- - `Sashite::Pin::Piece.new(letter, enhanced: false, diminished: false)` - Create piece instance
205
+ - `Sashite::Pin::Piece.new(type, side, state = :normal)` - Create piece instance
185
206
  - `Sashite::Pin::Piece.parse(pin_string)` - Parse PIN string (same as module method)
186
207
 
187
- #### String Representation
208
+ #### Attribute Access
209
+ - `#type` - Get piece type (symbol :A to :Z)
210
+ - `#side` - Get player side (:first or :second)
211
+ - `#state` - Get state (:normal, :enhanced, or :diminished)
212
+ - `#letter` - Get letter representation (string)
213
+ - `#prefix` - Get state prefix (string: "+", "-", or "")
188
214
  - `#to_s` - Convert to PIN string representation
189
- - `#letter` - Get the letter (type + side)
190
- - `#type` - Get piece type (uppercase letter)
191
- - `#side` - Get player side (`:first` or `:second`)
192
- - `#state` - Get state (`:normal`, `:enhanced`, or `:diminished`)
193
215
 
194
216
  #### State Queries
195
217
  - `#normal?` - Check if normal state (no modifiers)
196
218
  - `#enhanced?` - Check if enhanced state
197
219
  - `#diminished?` - Check if diminished state
198
220
 
199
- #### Player Queries
221
+ #### Side Queries
200
222
  - `#first_player?` - Check if first player piece
201
223
  - `#second_player?` - Check if second player piece
202
224
 
@@ -206,11 +228,17 @@ crossed_soldier.to_s # => "+P"
206
228
  - `#diminish` - Create diminished version
207
229
  - `#undiminish` - Remove diminished state
208
230
  - `#normalize` - Remove all state modifiers
209
- - `#flip` - Switch player (change case)
231
+ - `#flip` - Switch player (change side)
232
+
233
+ #### Attribute Transformations (immutable - return new instances)
234
+ - `#with_type(new_type)` - Create piece with different type
235
+ - `#with_side(new_side)` - Create piece with different side
236
+ - `#with_state(new_state)` - Create piece with different state
210
237
 
211
238
  #### Comparison Methods
212
239
  - `#same_type?(other)` - Check if same piece type
213
- - `#same_player?(other)` - Check if same player
240
+ - `#same_side?(other)` - Check if same side
241
+ - `#same_state?(other)` - Check if same state
214
242
  - `#==(other)` - Full equality comparison
215
243
 
216
244
  ### Constants
@@ -221,7 +249,7 @@ crossed_soldier.to_s # => "+P"
221
249
  ### Immutable Transformations
222
250
  ```ruby
223
251
  # All transformations return new instances
224
- original = Sashite::Pin.parse("K")
252
+ original = Sashite::Pin.piece(:K, :first, :normal)
225
253
  enhanced = original.enhance
226
254
  diminished = original.diminish
227
255
 
@@ -231,8 +259,8 @@ puts enhanced.to_s # => "+K"
231
259
  puts diminished.to_s # => "-K"
232
260
 
233
261
  # Transformations can be chained
234
- result = original.flip.enhance.flip.diminish
235
- puts result.to_s # => "-K"
262
+ result = original.flip.enhance.with_type(:Q)
263
+ puts result.to_s # => "+q"
236
264
  ```
237
265
 
238
266
  ### Game State Management
@@ -242,15 +270,15 @@ class GameBoard
242
270
  @pieces = {}
243
271
  end
244
272
 
245
- def place(square, pin_string)
246
- @pieces[square] = Sashite::Pin.parse(pin_string)
273
+ def place(square, piece)
274
+ @pieces[square] = piece
247
275
  end
248
276
 
249
- def promote(square)
277
+ def promote(square, new_type = :Q)
250
278
  piece = @pieces[square]
251
279
  return nil unless piece&.normal? # Can only promote normal pieces
252
280
 
253
- @pieces[square] = piece.enhance
281
+ @pieces[square] = piece.with_type(new_type).enhance
254
282
  end
255
283
 
256
284
  def capture(from_square, to_square)
@@ -259,8 +287,8 @@ class GameBoard
259
287
  captured
260
288
  end
261
289
 
262
- def pieces_by_player(first_player: true)
263
- @pieces.select { |_, piece| piece.first_player? == first_player }
290
+ def pieces_by_side(side)
291
+ @pieces.select { |_, piece| piece.side == side }
264
292
  end
265
293
 
266
294
  def promoted_pieces
@@ -270,24 +298,24 @@ end
270
298
 
271
299
  # Usage
272
300
  board = GameBoard.new
273
- board.place("e1", "K")
274
- board.place("e8", "k")
275
- board.place("a7", "P")
301
+ board.place("e1", Sashite::Pin.piece(:K, :first, :normal))
302
+ board.place("e8", Sashite::Pin.piece(:K, :second, :normal))
303
+ board.place("a7", Sashite::Pin.piece(:P, :first, :normal))
276
304
 
277
305
  # Promote pawn
278
- board.promote("a7")
306
+ board.promote("a7", :Q)
279
307
  promoted = board.promoted_pieces
280
- puts promoted.values.first.to_s # => "+P"
308
+ puts promoted.values.first.to_s # => "+Q"
281
309
  ```
282
310
 
283
311
  ### Piece Analysis
284
312
  ```ruby
285
- def analyze_pieces(pin_strings)
286
- pieces = pin_strings.map { |pin| Sashite::Pin.parse(pin) }
313
+ def analyze_pieces(pins)
314
+ pieces = pins.map { |pin| Sashite::Pin.parse(pin) }
287
315
 
288
316
  {
289
317
  total: pieces.size,
290
- by_player: pieces.group_by(&:side),
318
+ by_side: pieces.group_by(&:side),
291
319
  by_type: pieces.group_by(&:type),
292
320
  by_state: pieces.group_by(&:state),
293
321
  promoted: pieces.count(&:enhanced?),
@@ -297,8 +325,8 @@ end
297
325
 
298
326
  pins = %w[K Q +R B N P k q r +b n -p]
299
327
  analysis = analyze_pieces(pins)
300
- puts analysis[:by_player][:first].size # => 6
301
- puts analysis[:promoted] # => 2
328
+ puts analysis[:by_side][:first].size # => 6
329
+ puts analysis[:promoted] # => 2
302
330
  ```
303
331
 
304
332
  ### Move Validation Example
@@ -307,17 +335,17 @@ def can_promote?(piece, target_rank)
307
335
  return false unless piece.normal? # Already promoted pieces can't promote again
308
336
 
309
337
  case piece.type
310
- when "P" # Pawn
338
+ when :P # Pawn
311
339
  (piece.first_player? && target_rank == 8) ||
312
340
  (piece.second_player? && target_rank == 1)
313
- when "R", "B", "S", "N", "L" # Shōgi pieces that can promote
341
+ when :R, :B, :S, :N, :L # Shōgi pieces that can promote
314
342
  true
315
343
  else
316
344
  false
317
345
  end
318
346
  end
319
347
 
320
- pawn = Sashite::Pin.parse("P")
348
+ pawn = Sashite::Pin.piece(:P, :first, :normal)
321
349
  puts can_promote?(pawn, 8) # => true
322
350
 
323
351
  promoted_pawn = pawn.enhance
@@ -330,9 +358,9 @@ Following the [Game Protocol](https://sashite.dev/game-protocol/):
330
358
 
331
359
  | Protocol Attribute | PIN Encoding | Examples |
332
360
  |-------------------|--------------|----------|
333
- | **Type** | ASCII letter choice | `K`/`k` = King, `P`/`p` = Pawn |
334
- | **Side** | Letter case | `K` = First player, `k` = Second player |
335
- | **State** | Optional prefix | `+K` = Enhanced, `-K` = Diminished, `K` = Normal |
361
+ | **Type** | Symbol choice | `:K`/`:k` = King, `:P`/`:p` = Pawn |
362
+ | **Side** | Symbol value | `:first` = First player, `:second` = Second player |
363
+ | **State** | Symbol value | `:enhanced` = Enhanced, `:diminished` = Diminished, `:normal` = Normal |
336
364
 
337
365
  **Note**: PIN does not represent the **Style** attribute from the Game Protocol. For style-aware piece notation, see [Piece Name Notation (PNN)](https://sashite.dev/specs/pnn/).
338
366
 
@@ -22,27 +22,55 @@ module Sashite
22
22
  # Valid state modifiers
23
23
  ENHANCED_PREFIX = "+"
24
24
  DIMINISHED_PREFIX = "-"
25
+ NORMAL_PREFIX = ""
26
+
27
+ # State constants
28
+ ENHANCED_STATE = :enhanced
29
+ DIMINISHED_STATE = :diminished
30
+ NORMAL_STATE = :normal
31
+
32
+ # Player side constants
33
+ FIRST_PLAYER = :first
34
+ SECOND_PLAYER = :second
35
+
36
+ # Valid types (A-Z)
37
+ VALID_TYPES = (:A..:Z).to_a.freeze
38
+
39
+ # Valid sides
40
+ VALID_SIDES = [FIRST_PLAYER, SECOND_PLAYER].freeze
41
+
42
+ # Valid states
43
+ VALID_STATES = [NORMAL_STATE, ENHANCED_STATE, DIMINISHED_STATE].freeze
25
44
 
26
45
  # Error messages
27
46
  ERROR_INVALID_PIN = "Invalid PIN string: %s"
28
- ERROR_INVALID_LETTER = "Letter must be a single ASCII letter (a-z or A-Z): %s"
47
+ ERROR_INVALID_TYPE = "Type must be a symbol from :A to :Z, got: %s"
48
+ ERROR_INVALID_SIDE = "Side must be :first or :second, got: %s"
49
+ ERROR_INVALID_STATE = "State must be :normal, :enhanced, or :diminished, got: %s"
50
+
51
+ # @return [Symbol] the piece type (:A to :Z)
52
+ attr_reader :type
53
+
54
+ # @return [Symbol] the player side (:first or :second)
55
+ attr_reader :side
29
56
 
30
- # @return [String] the base letter identifier (type + side)
31
- attr_reader :letter
57
+ # @return [Symbol] the piece state (:normal, :enhanced, or :diminished)
58
+ attr_reader :state
32
59
 
33
60
  # Create a new piece instance
34
61
  #
35
- # @param letter [String] single ASCII letter (a-z or A-Z)
36
- # @param enhanced [Boolean] whether the piece has enhanced state
37
- # @param diminished [Boolean] whether the piece has diminished state
62
+ # @param type [Symbol] piece type (:A to :Z)
63
+ # @param side [Symbol] player side (:first or :second)
64
+ # @param state [Symbol] piece state (:normal, :enhanced, or :diminished)
38
65
  # @raise [ArgumentError] if parameters are invalid
39
- def initialize(letter, enhanced: false, diminished: false)
40
- self.class.validate_letter(letter)
41
- self.class.validate_state_combination(enhanced, diminished)
66
+ def initialize(type, side, state = NORMAL_STATE)
67
+ self.class.validate_type(type)
68
+ self.class.validate_side(side)
69
+ self.class.validate_state(state)
42
70
 
43
- @letter = letter.freeze
44
- @enhanced = enhanced
45
- @diminished = diminished
71
+ @type = type
72
+ @side = side
73
+ @state = state
46
74
 
47
75
  freeze
48
76
  end
@@ -53,9 +81,9 @@ module Sashite
53
81
  # @return [Piece] new piece instance
54
82
  # @raise [ArgumentError] if the PIN string is invalid
55
83
  # @example
56
- # Pin::Piece.parse("k") # => #<Pin::Piece letter="k">
57
- # Pin::Piece.parse("+R") # => #<Pin::Piece letter="R" enhanced=true>
58
- # Pin::Piece.parse("-p") # => #<Pin::Piece letter="p" diminished=true>
84
+ # Pin::Piece.parse("k") # => #<Pin::Piece type=:K side=:second state=:normal>
85
+ # Pin::Piece.parse("+R") # => #<Pin::Piece type=:R side=:first state=:enhanced>
86
+ # Pin::Piece.parse("-p") # => #<Pin::Piece type=:P side=:second state=:diminished>
59
87
  def self.parse(pin_string)
60
88
  string_value = String(pin_string)
61
89
  matches = match_pattern(string_value)
@@ -64,11 +92,18 @@ module Sashite
64
92
  enhanced = matches[:prefix] == ENHANCED_PREFIX
65
93
  diminished = matches[:prefix] == DIMINISHED_PREFIX
66
94
 
67
- new(
68
- letter,
69
- enhanced: enhanced,
70
- diminished: diminished
71
- )
95
+ # Extract type and side from letter
96
+ piece_type = letter.upcase.to_sym
97
+ piece_side = letter == letter.upcase ? FIRST_PLAYER : SECOND_PLAYER
98
+ piece_state = if enhanced
99
+ ENHANCED_STATE
100
+ elsif diminished
101
+ DIMINISHED_STATE
102
+ else
103
+ NORMAL_STATE
104
+ end
105
+
106
+ new(piece_type, piece_side, piece_state)
72
107
  end
73
108
 
74
109
  # Convert the piece to its PIN string representation
@@ -79,180 +114,198 @@ module Sashite
79
114
  # piece.to_s # => "-p"
80
115
  # piece.to_s # => "K"
81
116
  def to_s
82
- prefix = if @enhanced
83
- ENHANCED_PREFIX
84
- else
85
- (@diminished ? DIMINISHED_PREFIX : "")
86
- end
87
117
  "#{prefix}#{letter}"
88
118
  end
89
119
 
120
+ # Get the letter representation
121
+ #
122
+ # @return [String] letter representation combining type and side
123
+ def letter
124
+ first_player? ? type.to_s.upcase : type.to_s.downcase
125
+ end
126
+
127
+ # Get the prefix representation
128
+ #
129
+ # @return [String] prefix representing the state
130
+ def prefix
131
+ case state
132
+ when ENHANCED_STATE then ENHANCED_PREFIX
133
+ when DIMINISHED_STATE then DIMINISHED_PREFIX
134
+ else NORMAL_PREFIX
135
+ end
136
+ end
137
+
90
138
  # Create a new piece with enhanced state
91
139
  #
92
140
  # @return [Piece] new piece instance with enhanced state
93
141
  # @example
94
- # piece.enhance # k => +k
142
+ # piece.enhance # (:K, :first, :normal) => (:K, :first, :enhanced)
95
143
  def enhance
96
144
  return self if enhanced?
97
145
 
98
- self.class.new(
99
- letter,
100
- enhanced: true,
101
- diminished: false
102
- )
146
+ self.class.new(type, side, ENHANCED_STATE)
103
147
  end
104
148
 
105
149
  # Create a new piece without enhanced state
106
150
  #
107
151
  # @return [Piece] new piece instance without enhanced state
108
152
  # @example
109
- # piece.unenhance # +k => k
153
+ # piece.unenhance # (:K, :first, :enhanced) => (:K, :first, :normal)
110
154
  def unenhance
111
155
  return self unless enhanced?
112
156
 
113
- self.class.new(
114
- letter,
115
- enhanced: false,
116
- diminished: @diminished
117
- )
157
+ self.class.new(type, side, NORMAL_STATE)
118
158
  end
119
159
 
120
160
  # Create a new piece with diminished state
121
161
  #
122
162
  # @return [Piece] new piece instance with diminished state
123
163
  # @example
124
- # piece.diminish # k => -k
164
+ # piece.diminish # (:K, :first, :normal) => (:K, :first, :diminished)
125
165
  def diminish
126
166
  return self if diminished?
127
167
 
128
- self.class.new(
129
- letter,
130
- enhanced: false,
131
- diminished: true
132
- )
168
+ self.class.new(type, side, DIMINISHED_STATE)
133
169
  end
134
170
 
135
171
  # Create a new piece without diminished state
136
172
  #
137
173
  # @return [Piece] new piece instance without diminished state
138
174
  # @example
139
- # piece.undiminish # -k => k
175
+ # piece.undiminish # (:K, :first, :diminished) => (:K, :first, :normal)
140
176
  def undiminish
141
177
  return self unless diminished?
142
178
 
143
- self.class.new(
144
- letter,
145
- enhanced: @enhanced,
146
- diminished: false
147
- )
179
+ self.class.new(type, side, NORMAL_STATE)
148
180
  end
149
181
 
150
182
  # Create a new piece with normal state (no modifiers)
151
183
  #
152
184
  # @return [Piece] new piece instance with normal state
153
185
  # @example
154
- # piece.normalize # +k => k, -k => k
186
+ # piece.normalize # (:K, :first, :enhanced) => (:K, :first, :normal)
155
187
  def normalize
156
188
  return self if normal?
157
189
 
158
- self.class.new(letter)
190
+ self.class.new(type, side, NORMAL_STATE)
159
191
  end
160
192
 
161
193
  # Create a new piece with opposite ownership (case)
162
194
  #
163
195
  # @return [Piece] new piece instance with flipped case
164
196
  # @example
165
- # piece.flip # K => k, k => K
197
+ # piece.flip # (:K, :first, :normal) => (:K, :second, :normal)
166
198
  def flip
167
- flipped_letter = letter.swapcase
199
+ new_side = first_player? ? SECOND_PLAYER : FIRST_PLAYER
200
+ self.class.new(type, new_side, state)
201
+ end
202
+
203
+ # Create a new piece with a different type (keeping same side and state)
204
+ #
205
+ # @param new_type [Symbol] new type (:A to :Z)
206
+ # @return [Piece] new piece instance with different type
207
+ # @example
208
+ # piece.with_type(:Q) # (:K, :first, :normal) => (:Q, :first, :normal)
209
+ def with_type(new_type)
210
+ self.class.validate_type(new_type)
211
+ return self if type == new_type
168
212
 
169
- self.class.new(
170
- flipped_letter,
171
- enhanced: @enhanced,
172
- diminished: @diminished
173
- )
213
+ self.class.new(new_type, side, state)
214
+ end
215
+
216
+ # Create a new piece with a different side (keeping same type and state)
217
+ #
218
+ # @param new_side [Symbol] :first or :second
219
+ # @return [Piece] new piece instance with different side
220
+ # @example
221
+ # piece.with_side(:second) # (:K, :first, :normal) => (:K, :second, :normal)
222
+ def with_side(new_side)
223
+ self.class.validate_side(new_side)
224
+ return self if side == new_side
225
+
226
+ self.class.new(type, new_side, state)
227
+ end
228
+
229
+ # Create a new piece with a different state (keeping same type and side)
230
+ #
231
+ # @param new_state [Symbol] :normal, :enhanced, or :diminished
232
+ # @return [Piece] new piece instance with different state
233
+ # @example
234
+ # piece.with_state(:enhanced) # (:K, :first, :normal) => (:K, :first, :enhanced)
235
+ def with_state(new_state)
236
+ self.class.validate_state(new_state)
237
+ return self if state == new_state
238
+
239
+ self.class.new(type, side, new_state)
174
240
  end
175
241
 
176
242
  # Check if the piece has enhanced state
177
243
  #
178
244
  # @return [Boolean] true if enhanced
179
245
  def enhanced?
180
- @enhanced
246
+ state == ENHANCED_STATE
181
247
  end
182
248
 
183
249
  # Check if the piece has diminished state
184
250
  #
185
251
  # @return [Boolean] true if diminished
186
252
  def diminished?
187
- @diminished
253
+ state == DIMINISHED_STATE
188
254
  end
189
255
 
190
256
  # Check if the piece has normal state (no modifiers)
191
257
  #
192
258
  # @return [Boolean] true if no modifiers are present
193
259
  def normal?
194
- !enhanced? && !diminished?
260
+ state == NORMAL_STATE
195
261
  end
196
262
 
197
- # Check if the piece belongs to the first player (uppercase)
263
+ # Check if the piece belongs to the first player
198
264
  #
199
- # @return [Boolean] true if uppercase letter
265
+ # @return [Boolean] true if first player
200
266
  def first_player?
201
- letter == letter.upcase
267
+ side == FIRST_PLAYER
202
268
  end
203
269
 
204
- # Check if the piece belongs to the second player (lowercase)
270
+ # Check if the piece belongs to the second player
205
271
  #
206
- # @return [Boolean] true if lowercase letter
272
+ # @return [Boolean] true if second player
207
273
  def second_player?
208
- letter == letter.downcase
274
+ side == SECOND_PLAYER
209
275
  end
210
276
 
211
- # Get the piece type (uppercase letter regardless of player)
212
- #
213
- # @return [String] uppercase letter representing the piece type
214
- # @example
215
- # piece.type # "k" => "K", "R" => "R", "+p" => "P"
216
- def type
217
- letter.upcase
218
- end
219
-
220
- # Get the player side based on letter case
221
- #
222
- # @return [Symbol] :first or :second
223
- def side
224
- first_player? ? :first : :second
225
- end
226
-
227
- # Get the state as a symbol
228
- #
229
- # @return [Symbol] :enhanced, :diminished, or :normal
230
- def state
231
- return :enhanced if enhanced?
232
- return :diminished if diminished?
233
- :normal
234
- end
235
-
236
- # Check if this piece is the same type as another (ignoring player and state)
277
+ # Check if this piece is the same type as another (ignoring side and state)
237
278
  #
238
279
  # @param other [Piece] piece to compare with
239
280
  # @return [Boolean] true if same type
240
281
  # @example
241
- # king1.same_type?(king2) # K and k => true, K and Q => false
282
+ # king1.same_type?(king2) # (:K, :first, :normal) and (:K, :second, :enhanced) => true
242
283
  def same_type?(other)
243
284
  return false unless other.is_a?(self.class)
285
+
244
286
  type == other.type
245
287
  end
246
288
 
247
- # Check if this piece belongs to the same player as another
289
+ # Check if this piece belongs to the same side as another
248
290
  #
249
291
  # @param other [Piece] piece to compare with
250
- # @return [Boolean] true if same player
251
- def same_player?(other)
292
+ # @return [Boolean] true if same side
293
+ def same_side?(other)
252
294
  return false unless other.is_a?(self.class)
295
+
253
296
  side == other.side
254
297
  end
255
298
 
299
+ # Check if this piece has the same state as another
300
+ #
301
+ # @param other [Piece] piece to compare with
302
+ # @return [Boolean] true if same state
303
+ def same_state?(other)
304
+ return false unless other.is_a?(self.class)
305
+
306
+ state == other.state
307
+ end
308
+
256
309
  # Custom equality comparison
257
310
  #
258
311
  # @param other [Object] object to compare with
@@ -260,9 +313,7 @@ module Sashite
260
313
  def ==(other)
261
314
  return false unless other.is_a?(self.class)
262
315
 
263
- letter == other.letter &&
264
- enhanced? == other.enhanced? &&
265
- diminished? == other.diminished?
316
+ type == other.type && side == other.side && state == other.state
266
317
  end
267
318
 
268
319
  # Alias for == to ensure Set functionality works correctly
@@ -272,29 +323,37 @@ module Sashite
272
323
  #
273
324
  # @return [Integer] hash value
274
325
  def hash
275
- [self.class, @letter, @enhanced, @diminished].hash
326
+ [self.class, type, side, state].hash
276
327
  end
277
328
 
278
- # Validate that the letter is a single ASCII letter
329
+ # Validate that the type is a valid symbol
279
330
  #
280
- # @param letter [String] the letter to validate
331
+ # @param type [Symbol] the type to validate
281
332
  # @raise [ArgumentError] if invalid
282
- def self.validate_letter(letter)
283
- letter_str = String(letter)
284
- return if letter_str.match?(/\A[a-zA-Z]\z/)
333
+ def self.validate_type(type)
334
+ return if VALID_TYPES.include?(type)
285
335
 
286
- raise ::ArgumentError, format(ERROR_INVALID_LETTER, letter_str)
336
+ raise ::ArgumentError, format(ERROR_INVALID_TYPE, type.inspect)
287
337
  end
288
338
 
289
- # Validate that enhanced and diminished states are not both true
339
+ # Validate that the side is a valid symbol
290
340
  #
291
- # @param enhanced [Boolean] enhanced state
292
- # @param diminished [Boolean] diminished state
293
- # @raise [ArgumentError] if both are true
294
- def self.validate_state_combination(enhanced, diminished)
295
- return unless enhanced && diminished
341
+ # @param side [Symbol] the side to validate
342
+ # @raise [ArgumentError] if invalid
343
+ def self.validate_side(side)
344
+ return if VALID_SIDES.include?(side)
345
+
346
+ raise ::ArgumentError, format(ERROR_INVALID_SIDE, side.inspect)
347
+ end
348
+
349
+ # Validate that the state is a valid symbol
350
+ #
351
+ # @param state [Symbol] the state to validate
352
+ # @raise [ArgumentError] if invalid
353
+ def self.validate_state(state)
354
+ return if VALID_STATES.include?(state)
296
355
 
297
- raise ::ArgumentError, "A piece cannot be both enhanced and diminished"
356
+ raise ::ArgumentError, format(ERROR_INVALID_STATE, state.inspect)
298
357
  end
299
358
 
300
359
  # Match PIN pattern against string
data/lib/sashite/pin.rb CHANGED
@@ -47,10 +47,26 @@ module Sashite
47
47
  # @return [Pin::Piece] new piece instance
48
48
  # @raise [ArgumentError] if the PIN string is invalid
49
49
  # @example
50
- # Sashite::Pin.parse("K") # => #<Pin::Piece letter="K">
51
- # Sashite::Pin.parse("+R") # => #<Pin::Piece letter="R" enhanced=true>
50
+ # Sashite::Pin.parse("K") # => #<Pin::Piece type=:K side=:first state=:normal>
51
+ # Sashite::Pin.parse("+R") # => #<Pin::Piece type=:R side=:first state=:enhanced>
52
+ # Sashite::Pin.parse("-p") # => #<Pin::Piece type=:P side=:second state=:diminished>
52
53
  def self.parse(pin_string)
53
54
  Piece.parse(pin_string)
54
55
  end
56
+
57
+ # Create a new piece instance
58
+ #
59
+ # @param type [Symbol] piece type (:A to :Z)
60
+ # @param side [Symbol] player side (:first or :second)
61
+ # @param state [Symbol] piece state (:normal, :enhanced, or :diminished)
62
+ # @return [Pin::Piece] new piece instance
63
+ # @raise [ArgumentError] if parameters are invalid
64
+ # @example
65
+ # Sashite::Pin.piece(:K, :first, :normal) # => #<Pin::Piece type=:K side=:first state=:normal>
66
+ # Sashite::Pin.piece(:R, :first, :enhanced) # => #<Pin::Piece type=:R side=:first state=:enhanced>
67
+ # Sashite::Pin.piece(:P, :second, :diminished) # => #<Pin::Piece type=:P side=:second state=:diminished>
68
+ def self.piece(type, side, state = :normal)
69
+ Piece.new(type, side, state)
70
+ end
55
71
  end
56
72
  end
data/lib/sashite-pin.rb CHANGED
@@ -1,20 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "sashite/pin"
4
+
3
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
- # Piece Identifier Notation (PIN) implementation for Ruby
6
- #
7
- # PIN provides an ASCII-based format for representing pieces in abstract
8
- # strategy board games. PIN translates piece attributes from the Game Protocol
9
- # into a compact, portable notation system using ASCII letters with optional
10
- # state modifiers and case-based player group classification.
11
- #
12
- # Format: [<state>]<letter>
13
- # - State modifier: "+" (enhanced), "-" (diminished), or none (normal)
14
- # - Letter: A-Z (first player), a-z (second player)
15
- #
16
- # @see https://sashite.dev/specs/pin/1.0.0/ PIN Specification v1.0.0
17
- # @author Sashité
18
14
  end
19
-
20
- require_relative "sashite/pin"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sashite-pin
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cyril Kato