sashite-pin 1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a65bc46335f587c689efc712bc26efc23c236ecf4aeaf26ab6fe2a47fc6420a6
4
+ data.tar.gz: 5c1cde47d08f270ab0594917001b0427792d5b931a7de60327e59fc6764f53d8
5
+ SHA512:
6
+ metadata.gz: 0c115d775f83ad50597f66478139720d9b6d1dfe970384d615ea1c25e96427b803dde10581a4349de1c0707064974d6f138ede38de175ed7168c5982482be9ea
7
+ data.tar.gz: ed8959e794d0d62ec73697b95a0ed18b19e150f9493e0db57d78c17d7bce1b122723847ef92470f528606eedec10900f3fce32a75360732e628b156da771f0ad
data/LICENSE.md ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2025 Cyril Kato
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,403 @@
1
+ # Pin.rb
2
+
3
+ [![Version](https://img.shields.io/github/v/tag/sashite/pin.rb?label=Version&logo=github)](https://github.com/sashite/pin.rb/tags)
4
+ [![Yard documentation](https://img.shields.io/badge/Yard-documentation-blue.svg?logo=github)](https://rubydoc.info/github/sashite/pin.rb/main)
5
+ ![Ruby](https://github.com/sashite/pin.rb/actions/workflows/main.yml/badge.svg?branch=main)
6
+ [![License](https://img.shields.io/github/license/sashite/pin.rb?label=License&logo=github)](https://github.com/sashite/pin.rb/raw/main/LICENSE.md)
7
+
8
+ > **PIN** (Piece Identifier Notation) implementation for the Ruby language.
9
+
10
+ ## What is PIN?
11
+
12
+ PIN (Piece Identifier Notation) provides an ASCII-based format for representing pieces in abstract strategy board games. PIN translates piece attributes from the [Game Protocol](https://sashite.dev/game-protocol/) into a compact, portable notation system.
13
+
14
+ This gem implements the [PIN Specification v1.0.0](https://sashite.dev/specs/pin/1.0.0/), providing a modern Ruby interface with immutable piece objects and functional programming principles.
15
+
16
+ ## Installation
17
+
18
+ ```ruby
19
+ # In your Gemfile
20
+ gem "sashite-pin"
21
+ ```
22
+
23
+ Or install manually:
24
+
25
+ ```sh
26
+ gem install sashite-pin
27
+ ```
28
+
29
+ ## Usage
30
+
31
+ ```ruby
32
+ require "sashite-pin"
33
+
34
+ # Parse PIN strings into piece objects
35
+ piece = Sashite::Pin.parse("K") # => #<Pin::Piece letter="K" type="K" player=first>
36
+ piece.to_s # => "K"
37
+ piece.first_player? # => true
38
+ piece.type # => "K"
39
+ piece.side # => :first
40
+
41
+ # Validate PIN strings
42
+ Sashite::Pin.valid?("K") # => true
43
+ Sashite::Pin.valid?("+R") # => true
44
+ Sashite::Pin.valid?("invalid") # => false
45
+
46
+ # State manipulation (returns new immutable instances)
47
+ enhanced = piece.enhance # => #<Pin::Piece letter="K" type="K" player=first enhanced=true>
48
+ enhanced.to_s # => "+K"
49
+ diminished = piece.diminish # => #<Pin::Piece letter="K" type="K" player=first diminished=true>
50
+ diminished.to_s # => "-K"
51
+
52
+ # Player manipulation
53
+ flipped = piece.flip # => #<Pin::Piece letter="k" type="K" player=second>
54
+ flipped.to_s # => "k"
55
+
56
+ # State queries
57
+ piece.normal? # => true
58
+ enhanced.enhanced? # => true
59
+ diminished.diminished? # => true
60
+
61
+ # Type and player comparison
62
+ king1 = Sashite::Pin.parse("K")
63
+ king2 = Sashite::Pin.parse("k")
64
+ queen = Sashite::Pin.parse("Q")
65
+
66
+ king1.same_type?(king2) # => true (both kings)
67
+ king1.same_player?(queen) # => true (both first player)
68
+ king1.same_type?(queen) # => false (different types)
69
+
70
+ # Functional transformations can be chained
71
+ pawn = Sashite::Pin.parse("P")
72
+ enemy_promoted = pawn.flip.enhance # => "+p" (black promoted pawn)
73
+ ```
74
+
75
+ ## Format Specification
76
+
77
+ ### Structure
78
+ ```
79
+ [<state>]<letter>
80
+ ```
81
+
82
+ ### Components
83
+
84
+ - **Letter** (`A-Z`, `a-z`): Represents piece type and player
85
+ - Uppercase: First player pieces
86
+ - Lowercase: Second player pieces
87
+ - **State** (optional prefix):
88
+ - `+`: Enhanced state (promoted, upgraded, empowered)
89
+ - `-`: Diminished state (weakened, restricted, temporary)
90
+ - No prefix: Normal state
91
+
92
+ ### Regular Expression
93
+ ```ruby
94
+ /\A[-+]?[A-Za-z]\z/
95
+ ```
96
+
97
+ ### Examples
98
+ - `K` - First player king (normal state)
99
+ - `k` - Second player king (normal state)
100
+ - `+R` - First player rook (enhanced state)
101
+ - `-p` - Second player pawn (diminished state)
102
+
103
+ ## Game Examples
104
+
105
+ ### Western Chess
106
+ ```ruby
107
+ # Standard pieces
108
+ king = Sashite::Pin.parse("K") # => white king
109
+ king.first_player? # => true
110
+ king.type # => "K"
111
+
112
+ # State modifiers for special conditions
113
+ castling_king = king.enhance # => castling-eligible king
114
+ castling_king.to_s # => "+K"
115
+
116
+ vulnerable_pawn = Sashite::Pin.parse("P").diminish # => en passant vulnerable
117
+ vulnerable_pawn.to_s # => "-P"
118
+
119
+ # 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
122
+ ```
123
+
124
+ ### Japanese Chess (Shōgi)
125
+ ```ruby
126
+ # Basic pieces
127
+ rook = Sashite::Pin.parse("R") # => white rook
128
+ bishop = Sashite::Pin.parse("B") # => white bishop
129
+
130
+ # Promoted pieces (enhanced state)
131
+ dragon_king = rook.enhance # => promoted rook (Dragon King)
132
+ dragon_king.to_s # => "+R"
133
+
134
+ dragon_horse = bishop.enhance # => promoted bishop (Dragon Horse)
135
+ dragon_horse.to_s # => "+B"
136
+
137
+ # Promoted pawn
138
+ pawn = Sashite::Pin.parse("P")
139
+ tokin = pawn.enhance # => promoted pawn (Tokin)
140
+ tokin.to_s # => "+P"
141
+
142
+ # All promotable pieces can use the same pattern
143
+ promotable = %w[R B S N L P].map { |type| Sashite::Pin.parse(type) }
144
+ promoted = promotable.map(&:enhance)
145
+ ```
146
+
147
+ ### Thai Chess (Makruk)
148
+ ```ruby
149
+ # Basic pieces
150
+ met = Sashite::Pin.parse("M") # => white Met (queen)
151
+ pawn = Sashite::Pin.parse("P") # => white Bia (pawn)
152
+
153
+ # Promoted pawns
154
+ bia_kaew = pawn.enhance # => promoted pawn (Bia Kaew)
155
+ bia_kaew.to_s # => "+P"
156
+
157
+ # Makruk pieces
158
+ makruk_pieces = %w[K M R B N P].map { |type| Sashite::Pin.parse(type) }
159
+ ```
160
+
161
+ ### Chinese Chess (Xiangqi)
162
+ ```ruby
163
+ # 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"
167
+
168
+ # 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"
172
+ ```
173
+
174
+ ## API Reference
175
+
176
+ ### Main Module Methods
177
+
178
+ - `Sashite::Pin.valid?(pin_string)` - Check if string is valid PIN notation
179
+ - `Sashite::Pin.parse(pin_string)` - Parse PIN string into Piece object
180
+
181
+ ### Piece Class
182
+
183
+ #### Creation and Parsing
184
+ - `Sashite::Pin::Piece.new(letter, enhanced: false, diminished: false)` - Create piece instance
185
+ - `Sashite::Pin::Piece.parse(pin_string)` - Parse PIN string (same as module method)
186
+
187
+ #### String Representation
188
+ - `#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
+
194
+ #### State Queries
195
+ - `#normal?` - Check if normal state (no modifiers)
196
+ - `#enhanced?` - Check if enhanced state
197
+ - `#diminished?` - Check if diminished state
198
+
199
+ #### Player Queries
200
+ - `#first_player?` - Check if first player piece
201
+ - `#second_player?` - Check if second player piece
202
+
203
+ #### State Transformations (immutable - return new instances)
204
+ - `#enhance` - Create enhanced version
205
+ - `#unenhance` - Remove enhanced state
206
+ - `#diminish` - Create diminished version
207
+ - `#undiminish` - Remove diminished state
208
+ - `#normalize` - Remove all state modifiers
209
+ - `#flip` - Switch player (change case)
210
+
211
+ #### Comparison Methods
212
+ - `#same_type?(other)` - Check if same piece type
213
+ - `#same_player?(other)` - Check if same player
214
+ - `#==(other)` - Full equality comparison
215
+
216
+ ### Constants
217
+ - `Sashite::Pin::PIN_REGEX` - Regular expression for PIN validation
218
+
219
+ ## Advanced Usage
220
+
221
+ ### Immutable Transformations
222
+ ```ruby
223
+ # All transformations return new instances
224
+ original = Sashite::Pin.parse("K")
225
+ enhanced = original.enhance
226
+ diminished = original.diminish
227
+
228
+ # Original piece is never modified
229
+ puts original.to_s # => "K"
230
+ puts enhanced.to_s # => "+K"
231
+ puts diminished.to_s # => "-K"
232
+
233
+ # Transformations can be chained
234
+ result = original.flip.enhance.flip.diminish
235
+ puts result.to_s # => "-K"
236
+ ```
237
+
238
+ ### Game State Management
239
+ ```ruby
240
+ class GameBoard
241
+ def initialize
242
+ @pieces = {}
243
+ end
244
+
245
+ def place(square, pin_string)
246
+ @pieces[square] = Sashite::Pin.parse(pin_string)
247
+ end
248
+
249
+ def promote(square)
250
+ piece = @pieces[square]
251
+ return nil unless piece&.normal? # Can only promote normal pieces
252
+
253
+ @pieces[square] = piece.enhance
254
+ end
255
+
256
+ def capture(from_square, to_square)
257
+ captured = @pieces[to_square]
258
+ @pieces[to_square] = @pieces.delete(from_square)
259
+ captured
260
+ end
261
+
262
+ def pieces_by_player(first_player: true)
263
+ @pieces.select { |_, piece| piece.first_player? == first_player }
264
+ end
265
+
266
+ def promoted_pieces
267
+ @pieces.select { |_, piece| piece.enhanced? }
268
+ end
269
+ end
270
+
271
+ # Usage
272
+ board = GameBoard.new
273
+ board.place("e1", "K")
274
+ board.place("e8", "k")
275
+ board.place("a7", "P")
276
+
277
+ # Promote pawn
278
+ board.promote("a7")
279
+ promoted = board.promoted_pieces
280
+ puts promoted.values.first.to_s # => "+P"
281
+ ```
282
+
283
+ ### Piece Analysis
284
+ ```ruby
285
+ def analyze_pieces(pin_strings)
286
+ pieces = pin_strings.map { |pin| Sashite::Pin.parse(pin) }
287
+
288
+ {
289
+ total: pieces.size,
290
+ by_player: pieces.group_by(&:side),
291
+ by_type: pieces.group_by(&:type),
292
+ by_state: pieces.group_by(&:state),
293
+ promoted: pieces.count(&:enhanced?),
294
+ weakened: pieces.count(&:diminished?)
295
+ }
296
+ end
297
+
298
+ pins = %w[K Q +R B N P k q r +b n -p]
299
+ analysis = analyze_pieces(pins)
300
+ puts analysis[:by_player][:first].size # => 6
301
+ puts analysis[:promoted] # => 2
302
+ ```
303
+
304
+ ### Move Validation Example
305
+ ```ruby
306
+ def can_promote?(piece, target_rank)
307
+ return false unless piece.normal? # Already promoted pieces can't promote again
308
+
309
+ case piece.type
310
+ when "P" # Pawn
311
+ (piece.first_player? && target_rank == 8) ||
312
+ (piece.second_player? && target_rank == 1)
313
+ when "R", "B", "S", "N", "L" # Shōgi pieces that can promote
314
+ true
315
+ else
316
+ false
317
+ end
318
+ end
319
+
320
+ pawn = Sashite::Pin.parse("P")
321
+ puts can_promote?(pawn, 8) # => true
322
+
323
+ promoted_pawn = pawn.enhance
324
+ puts can_promote?(promoted_pawn, 8) # => false (already promoted)
325
+ ```
326
+
327
+ ## Protocol Mapping
328
+
329
+ Following the [Game Protocol](https://sashite.dev/game-protocol/):
330
+
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 |
336
+
337
+ **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
+
339
+ ## Properties
340
+
341
+ * **ASCII Compatible**: Maximum portability across systems
342
+ * **Rule-Agnostic**: Independent of specific game mechanics
343
+ * **Compact Format**: 1-2 characters per piece
344
+ * **Visual Distinction**: Clear player differentiation through case
345
+ * **Protocol Compliant**: Direct implementation of Sashité piece attributes
346
+ * **Immutable**: All piece instances are frozen and transformations return new objects
347
+ * **Functional**: Pure functions with no side effects
348
+
349
+ ## System Constraints
350
+
351
+ - **Maximum 26 piece types** per game system (one per ASCII letter)
352
+ - **Exactly 2 players** (uppercase/lowercase distinction)
353
+ - **3 state levels** (enhanced, normal, diminished)
354
+
355
+ ## Related Specifications
356
+
357
+ - [Game Protocol](https://sashite.dev/game-protocol/) - Conceptual foundation for abstract strategy board games
358
+ - [PNN](https://sashite.dev/specs/pnn/) - Piece Name Notation (style-aware piece representation)
359
+ - [CELL](https://sashite.dev/specs/cell/) - Board position coordinates
360
+ - [HAND](https://sashite.dev/specs/hand/) - Reserve location notation
361
+ - [PMN](https://sashite.dev/specs/pmn/) - Portable Move Notation
362
+
363
+ ## Documentation
364
+
365
+ - [Official PIN Specification v1.0.0](https://sashite.dev/specs/pin/1.0.0/)
366
+ - [PIN Examples Documentation](https://sashite.dev/specs/pin/1.0.0/examples/)
367
+ - [Game Protocol Foundation](https://sashite.dev/game-protocol/)
368
+ - [API Documentation](https://rubydoc.info/github/sashite/pin.rb/main)
369
+
370
+ ## Development
371
+
372
+ ```sh
373
+ # Clone the repository
374
+ git clone https://github.com/sashite/pin.rb.git
375
+ cd pin.rb
376
+
377
+ # Install dependencies
378
+ bundle install
379
+
380
+ # Run tests
381
+ ruby test.rb
382
+
383
+ # Generate documentation
384
+ yard doc
385
+ ```
386
+
387
+ ## Contributing
388
+
389
+ 1. Fork the repository
390
+ 2. Create a feature branch (`git checkout -b feature/new-feature`)
391
+ 3. Add tests for your changes
392
+ 4. Ensure all tests pass (`ruby test.rb`)
393
+ 5. Commit your changes (`git commit -am 'Add new feature'`)
394
+ 6. Push to the branch (`git push origin feature/new-feature`)
395
+ 7. Create a Pull Request
396
+
397
+ ## License
398
+
399
+ Available as open source under the [MIT License](https://opensource.org/licenses/MIT).
400
+
401
+ ## About
402
+
403
+ Maintained by [Sashité](https://sashite.com/) — promoting chess variants and sharing the beauty of board game cultures.
@@ -0,0 +1,329 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sashite
4
+ module Pin
5
+ # Represents a piece in PIN (Piece Identifier Notation) format.
6
+ #
7
+ # A piece consists of a single ASCII letter with optional state modifiers:
8
+ # - Enhanced state: prefix '+'
9
+ # - Diminished state: prefix '-'
10
+ # - Normal state: no modifier
11
+ #
12
+ # The case of the letter determines ownership:
13
+ # - Uppercase (A-Z): first player
14
+ # - Lowercase (a-z): second player
15
+ #
16
+ # All instances are immutable - state manipulation methods return new instances.
17
+ # This follows the Game Protocol's piece model with Type, Side, and State attributes.
18
+ class Piece
19
+ # PIN validation pattern matching the specification
20
+ PIN_PATTERN = /\A(?<prefix>[-+])?(?<letter>[a-zA-Z])\z/
21
+
22
+ # Valid state modifiers
23
+ ENHANCED_PREFIX = "+"
24
+ DIMINISHED_PREFIX = "-"
25
+
26
+ # Error messages
27
+ ERROR_INVALID_PIN = "Invalid PIN string: %s"
28
+ ERROR_INVALID_LETTER = "Letter must be a single ASCII letter (a-z or A-Z): %s"
29
+
30
+ # @return [String] the base letter identifier (type + side)
31
+ attr_reader :letter
32
+
33
+ # Create a new piece instance
34
+ #
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
38
+ # @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)
42
+
43
+ @letter = letter.freeze
44
+ @enhanced = enhanced
45
+ @diminished = diminished
46
+
47
+ freeze
48
+ end
49
+
50
+ # Parse a PIN string into a Piece object
51
+ #
52
+ # @param pin_string [String] PIN notation string
53
+ # @return [Piece] new piece instance
54
+ # @raise [ArgumentError] if the PIN string is invalid
55
+ # @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>
59
+ def self.parse(pin_string)
60
+ string_value = String(pin_string)
61
+ matches = match_pattern(string_value)
62
+
63
+ letter = matches[:letter]
64
+ enhanced = matches[:prefix] == ENHANCED_PREFIX
65
+ diminished = matches[:prefix] == DIMINISHED_PREFIX
66
+
67
+ new(
68
+ letter,
69
+ enhanced: enhanced,
70
+ diminished: diminished
71
+ )
72
+ end
73
+
74
+ # Convert the piece to its PIN string representation
75
+ #
76
+ # @return [String] PIN notation string
77
+ # @example
78
+ # piece.to_s # => "+R"
79
+ # piece.to_s # => "-p"
80
+ # piece.to_s # => "K"
81
+ def to_s
82
+ prefix = if @enhanced
83
+ ENHANCED_PREFIX
84
+ else
85
+ (@diminished ? DIMINISHED_PREFIX : "")
86
+ end
87
+
88
+ "#{prefix}#{letter}"
89
+ end
90
+
91
+ # Create a new piece with enhanced state
92
+ #
93
+ # @return [Piece] new piece instance with enhanced state
94
+ # @example
95
+ # piece.enhance # k => +k
96
+ def enhance
97
+ return self if enhanced?
98
+
99
+ self.class.new(
100
+ letter,
101
+ enhanced: true,
102
+ diminished: false
103
+ )
104
+ end
105
+
106
+ # Create a new piece without enhanced state
107
+ #
108
+ # @return [Piece] new piece instance without enhanced state
109
+ # @example
110
+ # piece.unenhance # +k => k
111
+ def unenhance
112
+ return self unless enhanced?
113
+
114
+ self.class.new(
115
+ letter,
116
+ enhanced: false,
117
+ diminished: @diminished
118
+ )
119
+ end
120
+
121
+ # Create a new piece with diminished state
122
+ #
123
+ # @return [Piece] new piece instance with diminished state
124
+ # @example
125
+ # piece.diminish # k => -k
126
+ def diminish
127
+ return self if diminished?
128
+
129
+ self.class.new(
130
+ letter,
131
+ enhanced: false,
132
+ diminished: true
133
+ )
134
+ end
135
+
136
+ # Create a new piece without diminished state
137
+ #
138
+ # @return [Piece] new piece instance without diminished state
139
+ # @example
140
+ # piece.undiminish # -k => k
141
+ def undiminish
142
+ return self unless diminished?
143
+
144
+ self.class.new(
145
+ letter,
146
+ enhanced: @enhanced,
147
+ diminished: false
148
+ )
149
+ end
150
+
151
+ # Create a new piece with normal state (no modifiers)
152
+ #
153
+ # @return [Piece] new piece instance with normal state
154
+ # @example
155
+ # piece.normalize # +k => k, -k => k
156
+ def normalize
157
+ return self if normal?
158
+
159
+ self.class.new(letter)
160
+ end
161
+
162
+ # Create a new piece with opposite ownership (case)
163
+ #
164
+ # @return [Piece] new piece instance with flipped case
165
+ # @example
166
+ # piece.flip # K => k, k => K
167
+ def flip
168
+ flipped_letter = letter.swapcase
169
+
170
+ self.class.new(
171
+ flipped_letter,
172
+ enhanced: @enhanced,
173
+ diminished: @diminished
174
+ )
175
+ end
176
+
177
+ # Check if the piece has enhanced state
178
+ #
179
+ # @return [Boolean] true if enhanced
180
+ def enhanced?
181
+ @enhanced
182
+ end
183
+
184
+ # Check if the piece has diminished state
185
+ #
186
+ # @return [Boolean] true if diminished
187
+ def diminished?
188
+ @diminished
189
+ end
190
+
191
+ # Check if the piece has normal state (no modifiers)
192
+ #
193
+ # @return [Boolean] true if no modifiers are present
194
+ def normal?
195
+ !enhanced? && !diminished?
196
+ end
197
+
198
+ # Check if the piece belongs to the first player (uppercase)
199
+ #
200
+ # @return [Boolean] true if uppercase letter
201
+ def first_player?
202
+ letter == letter.upcase
203
+ end
204
+
205
+ # Check if the piece belongs to the second player (lowercase)
206
+ #
207
+ # @return [Boolean] true if lowercase letter
208
+ def second_player?
209
+ letter == letter.downcase
210
+ end
211
+
212
+ # Get the piece type (uppercase letter regardless of player)
213
+ #
214
+ # @return [String] uppercase letter representing the piece type
215
+ # @example
216
+ # piece.type # "k" => "K", "R" => "R", "+p" => "P"
217
+ def type
218
+ letter.upcase
219
+ end
220
+
221
+ # Get the player side based on letter case
222
+ #
223
+ # @return [Symbol] :first or :second
224
+ def side
225
+ first_player? ? :first : :second
226
+ end
227
+
228
+ # Get the state as a symbol
229
+ #
230
+ # @return [Symbol] :enhanced, :diminished, or :normal
231
+ def state
232
+ return :enhanced if enhanced?
233
+ return :diminished if diminished?
234
+
235
+ :normal
236
+ end
237
+
238
+ # Check if this piece is the same type as another (ignoring player and state)
239
+ #
240
+ # @param other [Piece] piece to compare with
241
+ # @return [Boolean] true if same type
242
+ # @example
243
+ # king1.same_type?(king2) # K and k => true, K and Q => false
244
+ def same_type?(other)
245
+ return false unless other.is_a?(self.class)
246
+
247
+ type == other.type
248
+ end
249
+
250
+ # Check if this piece belongs to the same player as another
251
+ #
252
+ # @param other [Piece] piece to compare with
253
+ # @return [Boolean] true if same player
254
+ def same_player?(other)
255
+ return false unless other.is_a?(self.class)
256
+
257
+ side == other.side
258
+ end
259
+
260
+ # Custom equality comparison
261
+ #
262
+ # @param other [Object] object to compare with
263
+ # @return [Boolean] true if pieces are equal
264
+ def ==(other)
265
+ return false unless other.is_a?(self.class)
266
+
267
+ letter == other.letter &&
268
+ enhanced? == other.enhanced? &&
269
+ diminished? == other.diminished?
270
+ end
271
+
272
+ # Custom hash implementation for use in collections
273
+ #
274
+ # @return [Integer] hash value
275
+ def hash
276
+ [letter, @enhanced, @diminished].hash
277
+ end
278
+
279
+ # Custom inspection for debugging
280
+ #
281
+ # @return [String] detailed string representation
282
+ def inspect
283
+ modifiers = []
284
+ modifiers << "enhanced=true" if enhanced?
285
+ modifiers << "diminished=true" if diminished?
286
+
287
+ modifier_str = modifiers.empty? ? "" : " #{modifiers.join(' ')}"
288
+ player = first_player? ? "first" : "second"
289
+ "#<#{self.class.name}:0x#{object_id.to_s(16)} letter='#{letter}' type='#{type}' player=#{player}#{modifier_str}>"
290
+ end
291
+
292
+ # Validate that the letter is a single ASCII letter
293
+ #
294
+ # @param letter [String] the letter to validate
295
+ # @raise [ArgumentError] if invalid
296
+ def self.validate_letter(letter)
297
+ letter_str = String(letter)
298
+ return if letter_str.match?(/\A[a-zA-Z]\z/)
299
+
300
+ raise ::ArgumentError, format(ERROR_INVALID_LETTER, letter_str)
301
+ end
302
+
303
+ # Validate that enhanced and diminished states are not both true
304
+ #
305
+ # @param enhanced [Boolean] enhanced state
306
+ # @param diminished [Boolean] diminished state
307
+ # @raise [ArgumentError] if both are true
308
+ def self.validate_state_combination(enhanced, diminished)
309
+ return unless enhanced && diminished
310
+
311
+ raise ::ArgumentError, "A piece cannot be both enhanced and diminished"
312
+ end
313
+
314
+ # Match PIN pattern against string
315
+ #
316
+ # @param string [String] string to match
317
+ # @return [MatchData] match data
318
+ # @raise [ArgumentError] if string doesn't match
319
+ def self.match_pattern(string)
320
+ matches = PIN_PATTERN.match(string)
321
+ return matches if matches
322
+
323
+ raise ::ArgumentError, format(ERROR_INVALID_PIN, string)
324
+ end
325
+
326
+ private_class_method :match_pattern
327
+ end
328
+ end
329
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "pin/piece"
4
+
5
+ module Sashite
6
+ # PIN (Piece Identifier Notation) implementation for Ruby
7
+ #
8
+ # Provides ASCII-based format for representing pieces in abstract strategy board games.
9
+ # PIN translates piece attributes from the Game Protocol into a compact, portable notation system.
10
+ #
11
+ # Format: [<state>]<letter>
12
+ # - State modifier: "+" (enhanced), "-" (diminished), or none (normal)
13
+ # - Letter: A-Z (first player), a-z (second player)
14
+ #
15
+ # Examples:
16
+ # "K" - First player king (normal state)
17
+ # "k" - Second player king (normal state)
18
+ # "+R" - First player rook (enhanced state)
19
+ # "-p" - Second player pawn (diminished state)
20
+ #
21
+ # See: https://sashite.dev/specs/pin/1.0.0/
22
+ module Pin
23
+ # Regular expression for PIN validation
24
+ # Matches: optional state modifier followed by a single letter
25
+ PIN_REGEX = /\A[-+]?[A-Za-z]\z/
26
+
27
+ # Check if a string is a valid PIN notation
28
+ #
29
+ # @param pin [String] The string to validate
30
+ # @return [Boolean] true if valid PIN, false otherwise
31
+ #
32
+ # @example
33
+ # Sashite::Pin.valid?("K") # => true
34
+ # Sashite::Pin.valid?("+R") # => true
35
+ # Sashite::Pin.valid?("-p") # => true
36
+ # Sashite::Pin.valid?("KK") # => false
37
+ # Sashite::Pin.valid?("++K") # => false
38
+ def self.valid?(pin)
39
+ return false unless pin.is_a?(::String)
40
+
41
+ pin.match?(PIN_REGEX)
42
+ end
43
+
44
+ # Parse a PIN string into a Piece object
45
+ #
46
+ # @param pin_string [String] PIN notation string
47
+ # @return [Pin::Piece] new piece instance
48
+ # @raise [ArgumentError] if the PIN string is invalid
49
+ # @example
50
+ # Sashite::Pin.parse("K") # => #<Pin::Piece letter="K">
51
+ # Sashite::Pin.parse("+R") # => #<Pin::Piece letter="R" enhanced=true>
52
+ def self.parse(pin_string)
53
+ Piece.parse(pin_string)
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Sashité namespace for board game notation libraries
4
+ 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
+ end
19
+
20
+ require_relative "sashite/pin"
metadata ADDED
@@ -0,0 +1,57 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sashite-pin
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Cyril Kato
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: |
13
+ PIN (Piece Identifier Notation) provides an ASCII-based format for representing pieces
14
+ in abstract strategy board games. This gem implements the PIN Specification v1.0.0 with
15
+ a modern Ruby interface featuring immutable piece objects and functional programming
16
+ principles. PIN translates piece attributes from the Game Protocol into a compact,
17
+ portable notation system using ASCII letters with optional state modifiers and
18
+ case-based player group classification.
19
+ email: contact@cyril.email
20
+ executables: []
21
+ extensions: []
22
+ extra_rdoc_files: []
23
+ files:
24
+ - LICENSE.md
25
+ - README.md
26
+ - lib/sashite-pin.rb
27
+ - lib/sashite/pin.rb
28
+ - lib/sashite/pin/piece.rb
29
+ homepage: https://github.com/sashite/pin.rb
30
+ licenses:
31
+ - MIT
32
+ metadata:
33
+ bug_tracker_uri: https://github.com/sashite/pin.rb/issues
34
+ documentation_uri: https://rubydoc.info/github/sashite/pin.rb/main
35
+ homepage_uri: https://github.com/sashite/pin.rb
36
+ source_code_uri: https://github.com/sashite/pin.rb
37
+ specification_uri: https://sashite.dev/specs/pin/1.0.0/
38
+ rubygems_mfa_required: 'true'
39
+ rdoc_options: []
40
+ require_paths:
41
+ - lib
42
+ required_ruby_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 3.2.0
47
+ required_rubygems_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: '0'
52
+ requirements: []
53
+ rubygems_version: 3.6.9
54
+ specification_version: 4
55
+ summary: PIN (Piece Identifier Notation) implementation for Ruby with immutable piece
56
+ objects
57
+ test_files: []