sashite-pin 1.1.0 → 2.0.1
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 +4 -4
- data/README.md +180 -77
- data/lib/sashite/pin/piece.rb +181 -114
- data/lib/sashite/pin.rb +18 -2
- data/lib/sashite-pin.rb +9 -15
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0335d119541a7fa271689c6235a9c785a4c46f6c70e9577d4cddcc597d520dd5
|
4
|
+
data.tar.gz: 7d95e04c803457156561bfe0fd8e270db5df1929b58721b9ed8266646f60e7e1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6904c80bee041595400f4074ed48a975ad6337c3c5a8c66f9d18ad7864f19e23d04e34b30c725d37ce9c4bfde58667328a82bbd1c38208c123cb44a9da00b283
|
7
|
+
data.tar.gz: 32289d6453d81ff2e1920aeb7d69eedfac6bc61f9acdd1929e7ca4bc09c6988bb2a64bc3620130cfb63024977e0508b7c5ffce695590382eac3420d85454c67a
|
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
|
35
|
+
piece = Sashite::Pin.parse("K") # => #<Pin::Piece type=:K side=:first state=:normal>
|
36
36
|
piece.to_s # => "K"
|
37
|
-
piece.
|
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
|
51
|
+
enhanced = piece.enhance # => #<Pin::Piece type=:K side=:first state=:enhanced>
|
48
52
|
enhanced.to_s # => "+K"
|
49
|
-
diminished = piece.diminish # => #<Pin::Piece
|
53
|
+
diminished = piece.diminish # => #<Pin::Piece type=:K side=:first state=:diminished>
|
50
54
|
diminished.to_s # => "-K"
|
51
55
|
|
52
|
-
#
|
53
|
-
flipped = piece.flip # => #<Pin::Piece
|
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
|
-
#
|
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.
|
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" (
|
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
|
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.
|
109
|
-
king.first_player?
|
110
|
-
king.type
|
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
|
114
|
-
castling_king.to_s
|
130
|
+
castling_king = king.enhance # => castling-eligible king
|
131
|
+
castling_king.to_s # => "+K"
|
115
132
|
|
116
|
-
vulnerable_pawn = Sashite::Pin.
|
117
|
-
vulnerable_pawn.to_s
|
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
|
-
|
121
|
-
|
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.
|
128
|
-
bishop = Sashite::Pin.
|
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
|
132
|
-
dragon_king.to_s
|
149
|
+
dragon_king = rook.enhance # => promoted rook (Dragon King)
|
150
|
+
dragon_king.to_s # => "+R"
|
133
151
|
|
134
|
-
dragon_horse = bishop.enhance
|
135
|
-
dragon_horse.to_s
|
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.
|
139
|
-
tokin = pawn.enhance
|
140
|
-
tokin.to_s
|
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
|
-
|
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.
|
151
|
-
pawn = Sashite::Pin.
|
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
|
155
|
-
bia_kaew.to_s
|
173
|
+
bia_kaew = pawn.enhance # => promoted pawn (Bia Kaew)
|
174
|
+
bia_kaew.to_s # => "+P"
|
156
175
|
|
157
176
|
# Makruk pieces
|
158
|
-
|
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.
|
165
|
-
flying_general = general.enhance
|
166
|
-
flying_general.to_s
|
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.
|
170
|
-
crossed_soldier = soldier.enhance
|
171
|
-
crossed_soldier.to_s
|
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,44 @@ 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(
|
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
|
-
####
|
208
|
+
#### Attribute Access
|
209
|
+
- `#type` - Get piece type (symbol :A to :Z, always uppercase)
|
210
|
+
- `#side` - Get player side (:first or :second)
|
211
|
+
- `#state` - Get state (:normal, :enhanced, or :diminished)
|
212
|
+
- `#letter` - Get letter representation (string, case determined by side)
|
213
|
+
- `#prefix` - Get state prefix (string: "+", "-", or "")
|
188
214
|
- `#to_s` - Convert to PIN string representation
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
215
|
+
|
216
|
+
#### Type and Case Handling
|
217
|
+
|
218
|
+
**Important**: The `type` attribute is always stored as an uppercase symbol (`:A` to `:Z`), regardless of the input case when parsing. The display case in `#letter` and `#to_s` is determined by the `side` attribute:
|
219
|
+
|
220
|
+
```ruby
|
221
|
+
# Both create the same internal type representation
|
222
|
+
piece1 = Sashite::Pin.parse("K") # type: :K, side: :first
|
223
|
+
piece2 = Sashite::Pin.parse("k") # type: :K, side: :second
|
224
|
+
|
225
|
+
piece1.type # => :K (uppercase symbol)
|
226
|
+
piece2.type # => :K (same uppercase symbol)
|
227
|
+
|
228
|
+
piece1.letter # => "K" (uppercase display)
|
229
|
+
piece2.letter # => "k" (lowercase display)
|
230
|
+
```
|
193
231
|
|
194
232
|
#### State Queries
|
195
233
|
- `#normal?` - Check if normal state (no modifiers)
|
196
234
|
- `#enhanced?` - Check if enhanced state
|
197
235
|
- `#diminished?` - Check if diminished state
|
198
236
|
|
199
|
-
####
|
237
|
+
#### Side Queries
|
200
238
|
- `#first_player?` - Check if first player piece
|
201
239
|
- `#second_player?` - Check if second player piece
|
202
240
|
|
@@ -206,11 +244,17 @@ crossed_soldier.to_s # => "+P"
|
|
206
244
|
- `#diminish` - Create diminished version
|
207
245
|
- `#undiminish` - Remove diminished state
|
208
246
|
- `#normalize` - Remove all state modifiers
|
209
|
-
- `#flip` - Switch player (change
|
247
|
+
- `#flip` - Switch player (change side)
|
248
|
+
|
249
|
+
#### Attribute Transformations (immutable - return new instances)
|
250
|
+
- `#with_type(new_type)` - Create piece with different type
|
251
|
+
- `#with_side(new_side)` - Create piece with different side
|
252
|
+
- `#with_state(new_state)` - Create piece with different state
|
210
253
|
|
211
254
|
#### Comparison Methods
|
212
255
|
- `#same_type?(other)` - Check if same piece type
|
213
|
-
- `#
|
256
|
+
- `#same_side?(other)` - Check if same side
|
257
|
+
- `#same_state?(other)` - Check if same state
|
214
258
|
- `#==(other)` - Full equality comparison
|
215
259
|
|
216
260
|
### Constants
|
@@ -218,10 +262,34 @@ crossed_soldier.to_s # => "+P"
|
|
218
262
|
|
219
263
|
## Advanced Usage
|
220
264
|
|
265
|
+
### Type Normalization Examples
|
266
|
+
|
267
|
+
```ruby
|
268
|
+
# Parsing different cases results in same type
|
269
|
+
white_king = Sashite::Pin.parse("K")
|
270
|
+
black_king = Sashite::Pin.parse("k")
|
271
|
+
|
272
|
+
# Types are normalized to uppercase
|
273
|
+
white_king.type # => :K
|
274
|
+
black_king.type # => :K (same type!)
|
275
|
+
|
276
|
+
# Sides are different
|
277
|
+
white_king.side # => :first
|
278
|
+
black_king.side # => :second
|
279
|
+
|
280
|
+
# Display follows side convention
|
281
|
+
white_king.letter # => "K"
|
282
|
+
black_king.letter # => "k"
|
283
|
+
|
284
|
+
# Same type, different sides
|
285
|
+
white_king.same_type?(black_king) # => true
|
286
|
+
white_king.same_side?(black_king) # => false
|
287
|
+
```
|
288
|
+
|
221
289
|
### Immutable Transformations
|
222
290
|
```ruby
|
223
291
|
# All transformations return new instances
|
224
|
-
original = Sashite::Pin.
|
292
|
+
original = Sashite::Pin.piece(:K, :first, :normal)
|
225
293
|
enhanced = original.enhance
|
226
294
|
diminished = original.diminish
|
227
295
|
|
@@ -231,8 +299,8 @@ puts enhanced.to_s # => "+K"
|
|
231
299
|
puts diminished.to_s # => "-K"
|
232
300
|
|
233
301
|
# Transformations can be chained
|
234
|
-
result = original.flip.enhance.
|
235
|
-
puts result.to_s # => "
|
302
|
+
result = original.flip.enhance.with_type(:Q)
|
303
|
+
puts result.to_s # => "+q"
|
236
304
|
```
|
237
305
|
|
238
306
|
### Game State Management
|
@@ -242,15 +310,15 @@ class GameBoard
|
|
242
310
|
@pieces = {}
|
243
311
|
end
|
244
312
|
|
245
|
-
def place(square,
|
246
|
-
@pieces[square] =
|
313
|
+
def place(square, piece)
|
314
|
+
@pieces[square] = piece
|
247
315
|
end
|
248
316
|
|
249
|
-
def promote(square)
|
317
|
+
def promote(square, new_type = :Q)
|
250
318
|
piece = @pieces[square]
|
251
319
|
return nil unless piece&.normal? # Can only promote normal pieces
|
252
320
|
|
253
|
-
@pieces[square] = piece.enhance
|
321
|
+
@pieces[square] = piece.with_type(new_type).enhance
|
254
322
|
end
|
255
323
|
|
256
324
|
def capture(from_square, to_square)
|
@@ -259,8 +327,8 @@ class GameBoard
|
|
259
327
|
captured
|
260
328
|
end
|
261
329
|
|
262
|
-
def
|
263
|
-
@pieces.select { |_, piece| piece.
|
330
|
+
def pieces_by_side(side)
|
331
|
+
@pieces.select { |_, piece| piece.side == side }
|
264
332
|
end
|
265
333
|
|
266
334
|
def promoted_pieces
|
@@ -270,24 +338,24 @@ end
|
|
270
338
|
|
271
339
|
# Usage
|
272
340
|
board = GameBoard.new
|
273
|
-
board.place("e1",
|
274
|
-
board.place("e8",
|
275
|
-
board.place("a7",
|
341
|
+
board.place("e1", Sashite::Pin.piece(:K, :first, :normal))
|
342
|
+
board.place("e8", Sashite::Pin.piece(:K, :second, :normal))
|
343
|
+
board.place("a7", Sashite::Pin.piece(:P, :first, :normal))
|
276
344
|
|
277
345
|
# Promote pawn
|
278
|
-
board.promote("a7")
|
346
|
+
board.promote("a7", :Q)
|
279
347
|
promoted = board.promoted_pieces
|
280
|
-
puts promoted.values.first.to_s # => "+
|
348
|
+
puts promoted.values.first.to_s # => "+Q"
|
281
349
|
```
|
282
350
|
|
283
351
|
### Piece Analysis
|
284
352
|
```ruby
|
285
|
-
def analyze_pieces(
|
286
|
-
pieces =
|
353
|
+
def analyze_pieces(pins)
|
354
|
+
pieces = pins.map { |pin| Sashite::Pin.parse(pin) }
|
287
355
|
|
288
356
|
{
|
289
357
|
total: pieces.size,
|
290
|
-
|
358
|
+
by_side: pieces.group_by(&:side),
|
291
359
|
by_type: pieces.group_by(&:type),
|
292
360
|
by_state: pieces.group_by(&:state),
|
293
361
|
promoted: pieces.count(&:enhanced?),
|
@@ -297,8 +365,8 @@ end
|
|
297
365
|
|
298
366
|
pins = %w[K Q +R B N P k q r +b n -p]
|
299
367
|
analysis = analyze_pieces(pins)
|
300
|
-
puts analysis[:
|
301
|
-
puts analysis[:promoted]
|
368
|
+
puts analysis[:by_side][:first].size # => 6
|
369
|
+
puts analysis[:promoted] # => 2
|
302
370
|
```
|
303
371
|
|
304
372
|
### Move Validation Example
|
@@ -307,17 +375,17 @@ def can_promote?(piece, target_rank)
|
|
307
375
|
return false unless piece.normal? # Already promoted pieces can't promote again
|
308
376
|
|
309
377
|
case piece.type
|
310
|
-
when
|
378
|
+
when :P # Pawn
|
311
379
|
(piece.first_player? && target_rank == 8) ||
|
312
380
|
(piece.second_player? && target_rank == 1)
|
313
|
-
when
|
381
|
+
when :R, :B, :S, :N, :L # Shōgi pieces that can promote
|
314
382
|
true
|
315
383
|
else
|
316
384
|
false
|
317
385
|
end
|
318
386
|
end
|
319
387
|
|
320
|
-
pawn = Sashite::Pin.
|
388
|
+
pawn = Sashite::Pin.piece(:P, :first, :normal)
|
321
389
|
puts can_promote?(pawn, 8) # => true
|
322
390
|
|
323
391
|
promoted_pawn = pawn.enhance
|
@@ -328,11 +396,15 @@ puts can_promote?(promoted_pawn, 8) # => false (already promoted)
|
|
328
396
|
|
329
397
|
Following the [Game Protocol](https://sashite.dev/game-protocol/):
|
330
398
|
|
331
|
-
| Protocol Attribute | PIN Encoding | Examples |
|
332
|
-
|
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 |
|
399
|
+
| Protocol Attribute | PIN Encoding | Examples | Notes |
|
400
|
+
|-------------------|--------------|----------|-------|
|
401
|
+
| **Type** | ASCII letter choice | `K`/`k` = King, `P`/`p` = Pawn | Type is always stored as uppercase symbol (`:K`, `:P`) |
|
402
|
+
| **Side** | Letter case in display | `K` = First player, `k` = Second player | Case is determined by side during rendering |
|
403
|
+
| **State** | Optional prefix | `+K` = Enhanced, `-K` = Diminished, `K` = Normal | |
|
404
|
+
|
405
|
+
**Type Convention**: All piece types are internally represented as uppercase symbols (`:A` to `:Z`). The display case is determined by the `side` attribute: first player pieces display as uppercase, second player pieces as lowercase.
|
406
|
+
|
407
|
+
**Canonical principle**: Identical pieces must have identical PIN representations.
|
336
408
|
|
337
409
|
**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
410
|
|
@@ -342,10 +414,41 @@ Following the [Game Protocol](https://sashite.dev/game-protocol/):
|
|
342
414
|
* **Rule-Agnostic**: Independent of specific game mechanics
|
343
415
|
* **Compact Format**: 1-2 characters per piece
|
344
416
|
* **Visual Distinction**: Clear player differentiation through case
|
417
|
+
* **Type Normalization**: Consistent uppercase type representation internally
|
345
418
|
* **Protocol Compliant**: Direct implementation of Sashité piece attributes
|
346
419
|
* **Immutable**: All piece instances are frozen and transformations return new objects
|
347
420
|
* **Functional**: Pure functions with no side effects
|
348
421
|
|
422
|
+
## Implementation Notes
|
423
|
+
|
424
|
+
### Type Normalization Convention
|
425
|
+
|
426
|
+
PIN follows a strict type normalization convention:
|
427
|
+
|
428
|
+
1. **Internal Storage**: All piece types are stored as uppercase symbols (`:A` to `:Z`)
|
429
|
+
2. **Input Flexibility**: Both `"K"` and `"k"` are valid input during parsing
|
430
|
+
3. **Case Semantics**: Input case determines the `side` attribute, not the `type`
|
431
|
+
4. **Display Logic**: Output case is computed from `side` during rendering
|
432
|
+
|
433
|
+
This design ensures:
|
434
|
+
- Consistent internal representation regardless of input format
|
435
|
+
- Clear separation between piece identity (type) and ownership (side)
|
436
|
+
- Predictable behavior when comparing pieces of the same type
|
437
|
+
|
438
|
+
### Example Flow
|
439
|
+
|
440
|
+
```ruby
|
441
|
+
# Input: "k" (lowercase)
|
442
|
+
# ↓ Parsing
|
443
|
+
# type: :K (normalized to uppercase)
|
444
|
+
# side: :second (inferred from lowercase input)
|
445
|
+
# ↓ Display
|
446
|
+
# letter: "k" (computed from type + side)
|
447
|
+
# PIN: "k" (final representation)
|
448
|
+
```
|
449
|
+
|
450
|
+
This ensures that `parse(pin).to_s == pin` for all valid PIN strings while maintaining internal consistency.
|
451
|
+
|
349
452
|
## System Constraints
|
350
453
|
|
351
454
|
- **Maximum 26 piece types** per game system (one per ASCII letter)
|
data/lib/sashite/pin/piece.rb
CHANGED
@@ -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
|
-
|
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 [
|
31
|
-
attr_reader :
|
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
|
36
|
-
# @param
|
37
|
-
# @param
|
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(
|
40
|
-
self.class.
|
41
|
-
self.class.
|
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
|
-
@
|
44
|
-
@
|
45
|
-
@
|
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
|
57
|
-
# Pin::Piece.parse("+R") # => #<Pin::Piece
|
58
|
-
# Pin::Piece.parse("-p") # => #<Pin::Piece
|
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
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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,197 @@ 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 #
|
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 #
|
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 #
|
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 #
|
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 #
|
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(
|
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
|
197
|
+
# piece.flip # (:K, :first, :normal) => (:K, :second, :normal)
|
166
198
|
def flip
|
167
|
-
|
199
|
+
self.class.new(type, opposite_side, state)
|
200
|
+
end
|
168
201
|
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
202
|
+
# Create a new piece with a different type (keeping same side and state)
|
203
|
+
#
|
204
|
+
# @param new_type [Symbol] new type (:A to :Z)
|
205
|
+
# @return [Piece] new piece instance with different type
|
206
|
+
# @example
|
207
|
+
# piece.with_type(:Q) # (:K, :first, :normal) => (:Q, :first, :normal)
|
208
|
+
def with_type(new_type)
|
209
|
+
self.class.validate_type(new_type)
|
210
|
+
return self if type == new_type
|
211
|
+
|
212
|
+
self.class.new(new_type, side, state)
|
213
|
+
end
|
214
|
+
|
215
|
+
# Create a new piece with a different side (keeping same type and state)
|
216
|
+
#
|
217
|
+
# @param new_side [Symbol] :first or :second
|
218
|
+
# @return [Piece] new piece instance with different side
|
219
|
+
# @example
|
220
|
+
# piece.with_side(:second) # (:K, :first, :normal) => (:K, :second, :normal)
|
221
|
+
def with_side(new_side)
|
222
|
+
self.class.validate_side(new_side)
|
223
|
+
return self if side == new_side
|
224
|
+
|
225
|
+
self.class.new(type, new_side, state)
|
226
|
+
end
|
227
|
+
|
228
|
+
# Create a new piece with a different state (keeping same type and side)
|
229
|
+
#
|
230
|
+
# @param new_state [Symbol] :normal, :enhanced, or :diminished
|
231
|
+
# @return [Piece] new piece instance with different state
|
232
|
+
# @example
|
233
|
+
# piece.with_state(:enhanced) # (:K, :first, :normal) => (:K, :first, :enhanced)
|
234
|
+
def with_state(new_state)
|
235
|
+
self.class.validate_state(new_state)
|
236
|
+
return self if state == new_state
|
237
|
+
|
238
|
+
self.class.new(type, side, new_state)
|
174
239
|
end
|
175
240
|
|
176
241
|
# Check if the piece has enhanced state
|
177
242
|
#
|
178
243
|
# @return [Boolean] true if enhanced
|
179
244
|
def enhanced?
|
180
|
-
|
245
|
+
state == ENHANCED_STATE
|
181
246
|
end
|
182
247
|
|
183
248
|
# Check if the piece has diminished state
|
184
249
|
#
|
185
250
|
# @return [Boolean] true if diminished
|
186
251
|
def diminished?
|
187
|
-
|
252
|
+
state == DIMINISHED_STATE
|
188
253
|
end
|
189
254
|
|
190
255
|
# Check if the piece has normal state (no modifiers)
|
191
256
|
#
|
192
257
|
# @return [Boolean] true if no modifiers are present
|
193
258
|
def normal?
|
194
|
-
|
259
|
+
state == NORMAL_STATE
|
195
260
|
end
|
196
261
|
|
197
|
-
# Check if the piece belongs to the first player
|
262
|
+
# Check if the piece belongs to the first player
|
198
263
|
#
|
199
|
-
# @return [Boolean] true if
|
264
|
+
# @return [Boolean] true if first player
|
200
265
|
def first_player?
|
201
|
-
|
266
|
+
side == FIRST_PLAYER
|
202
267
|
end
|
203
268
|
|
204
|
-
# Check if the piece belongs to the second player
|
269
|
+
# Check if the piece belongs to the second player
|
205
270
|
#
|
206
|
-
# @return [Boolean] true if
|
271
|
+
# @return [Boolean] true if second player
|
207
272
|
def second_player?
|
208
|
-
|
273
|
+
side == SECOND_PLAYER
|
209
274
|
end
|
210
275
|
|
211
|
-
#
|
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)
|
276
|
+
# Check if this piece is the same type as another (ignoring side and state)
|
237
277
|
#
|
238
278
|
# @param other [Piece] piece to compare with
|
239
279
|
# @return [Boolean] true if same type
|
240
280
|
# @example
|
241
|
-
# king1.same_type?(king2) # K
|
281
|
+
# king1.same_type?(king2) # (:K, :first, :normal) and (:K, :second, :enhanced) => true
|
242
282
|
def same_type?(other)
|
243
283
|
return false unless other.is_a?(self.class)
|
284
|
+
|
244
285
|
type == other.type
|
245
286
|
end
|
246
287
|
|
247
|
-
# Check if this piece belongs to the same
|
288
|
+
# Check if this piece belongs to the same side as another
|
248
289
|
#
|
249
290
|
# @param other [Piece] piece to compare with
|
250
|
-
# @return [Boolean] true if same
|
251
|
-
def
|
291
|
+
# @return [Boolean] true if same side
|
292
|
+
def same_side?(other)
|
252
293
|
return false unless other.is_a?(self.class)
|
294
|
+
|
253
295
|
side == other.side
|
254
296
|
end
|
255
297
|
|
298
|
+
# Check if this piece has the same state as another
|
299
|
+
#
|
300
|
+
# @param other [Piece] piece to compare with
|
301
|
+
# @return [Boolean] true if same state
|
302
|
+
def same_state?(other)
|
303
|
+
return false unless other.is_a?(self.class)
|
304
|
+
|
305
|
+
state == other.state
|
306
|
+
end
|
307
|
+
|
256
308
|
# Custom equality comparison
|
257
309
|
#
|
258
310
|
# @param other [Object] object to compare with
|
@@ -260,9 +312,7 @@ module Sashite
|
|
260
312
|
def ==(other)
|
261
313
|
return false unless other.is_a?(self.class)
|
262
314
|
|
263
|
-
|
264
|
-
enhanced? == other.enhanced? &&
|
265
|
-
diminished? == other.diminished?
|
315
|
+
type == other.type && side == other.side && state == other.state
|
266
316
|
end
|
267
317
|
|
268
318
|
# Alias for == to ensure Set functionality works correctly
|
@@ -272,29 +322,37 @@ module Sashite
|
|
272
322
|
#
|
273
323
|
# @return [Integer] hash value
|
274
324
|
def hash
|
275
|
-
[self.class,
|
325
|
+
[self.class, type, side, state].hash
|
276
326
|
end
|
277
327
|
|
278
|
-
# Validate that the
|
328
|
+
# Validate that the type is a valid symbol
|
279
329
|
#
|
280
|
-
# @param
|
330
|
+
# @param type [Symbol] the type to validate
|
281
331
|
# @raise [ArgumentError] if invalid
|
282
|
-
def self.
|
283
|
-
|
284
|
-
return if letter_str.match?(/\A[a-zA-Z]\z/)
|
332
|
+
def self.validate_type(type)
|
333
|
+
return if VALID_TYPES.include?(type)
|
285
334
|
|
286
|
-
raise ::ArgumentError, format(
|
335
|
+
raise ::ArgumentError, format(ERROR_INVALID_TYPE, type.inspect)
|
287
336
|
end
|
288
337
|
|
289
|
-
# Validate that
|
338
|
+
# Validate that the side is a valid symbol
|
290
339
|
#
|
291
|
-
# @param
|
292
|
-
# @
|
293
|
-
|
294
|
-
|
295
|
-
return unless enhanced && diminished
|
340
|
+
# @param side [Symbol] the side to validate
|
341
|
+
# @raise [ArgumentError] if invalid
|
342
|
+
def self.validate_side(side)
|
343
|
+
return if VALID_SIDES.include?(side)
|
296
344
|
|
297
|
-
raise ::ArgumentError,
|
345
|
+
raise ::ArgumentError, format(ERROR_INVALID_SIDE, side.inspect)
|
346
|
+
end
|
347
|
+
|
348
|
+
# Validate that the state is a valid symbol
|
349
|
+
#
|
350
|
+
# @param state [Symbol] the state to validate
|
351
|
+
# @raise [ArgumentError] if invalid
|
352
|
+
def self.validate_state(state)
|
353
|
+
return if VALID_STATES.include?(state)
|
354
|
+
|
355
|
+
raise ::ArgumentError, format(ERROR_INVALID_STATE, state.inspect)
|
298
356
|
end
|
299
357
|
|
300
358
|
# Match PIN pattern against string
|
@@ -310,6 +368,15 @@ module Sashite
|
|
310
368
|
end
|
311
369
|
|
312
370
|
private_class_method :match_pattern
|
371
|
+
|
372
|
+
private
|
373
|
+
|
374
|
+
# Get the opposite side of the current piece
|
375
|
+
#
|
376
|
+
# @return [Symbol] :first if current side is :second, :second if current side is :first
|
377
|
+
def opposite_side
|
378
|
+
first_player? ? SECOND_PLAYER : FIRST_PLAYER
|
379
|
+
end
|
313
380
|
end
|
314
381
|
end
|
315
382
|
end
|
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
|
51
|
-
# Sashite::Pin.parse("+R") # => #<Pin::Piece
|
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:
|
4
|
+
version: 2.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Cyril Kato
|
@@ -15,7 +15,8 @@ description: |
|
|
15
15
|
a modern Ruby interface featuring immutable piece objects and functional programming
|
16
16
|
principles. PIN translates piece attributes from the Game Protocol into a compact,
|
17
17
|
portable notation system using ASCII letters with optional state modifiers and
|
18
|
-
case-based
|
18
|
+
case-based side encoding. Perfect for game engines, board game notation systems,
|
19
|
+
and multi-game environments.
|
19
20
|
email: contact@cyril.email
|
20
21
|
executables: []
|
21
22
|
extensions: []
|