sashite-pcn 0.1.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: 6bf9f1d06c41969fe2ed053fa57dfec15e639751070b1a6811f4a09f5c23e455
4
+ data.tar.gz: c2294034b9529836b9675dd37e572eb7dd883b6e52ba81f1bf401d8a9da9d3b1
5
+ SHA512:
6
+ metadata.gz: 2e520a3bc564b5e3d3d566ce7f9504cfab01b9094f5c8bcab9bcef95a6b519ecaa242a3d2ad582e17d7415f58fc0d585b4448c4bd521cc9b27559898ae5da6f3
7
+ data.tar.gz: 24f763a02d7777e14f289cdc20c8587d2bac0a233be822562ce0caba4d2b1b067b924a508b5b57fd3c311e30ffc83fefd5ea7cd7b767680590d02b2dbb85c024
data/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ # The MIT License
2
+
3
+ Copyright (c) 2014-2025 Sashité
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,509 @@
1
+ # Pcn.rb
2
+
3
+ [![Version](https://img.shields.io/github/v/tag/sashite/pcn.rb?label=Version&logo=github)](https://github.com/sashite/pcn.rb/tags)
4
+ [![Yard documentation](https://img.shields.io/badge/Yard-documentation-blue.svg?logo=github)](https://rubydoc.info/github/sashite/pcn.rb/main)
5
+ ![Ruby](https://github.com/sashite/pcn.rb/actions/workflows/main.yml/badge.svg?branch=main)
6
+ [![License](https://img.shields.io/github/license/sashite/pcn.rb?label=License&logo=github)](https://github.com/sashite/pcn.rb/raw/main/LICENSE.md)
7
+
8
+ > **PCN** (Portable Chess Notation) implementation for the Ruby language.
9
+
10
+ ## What is PCN?
11
+
12
+ PCN (Portable Chess Notation) is a comprehensive, JSON-based format for representing complete chess game records across variants. PCN integrates the Sashité ecosystem specifications (PMN, FEEN, and SNN) to create a unified, rule-agnostic game recording system that supports both traditional single-variant games and cross-variant scenarios.
13
+
14
+ This gem implements the [PCN Specification v1.0.0](https://sashite.dev/specs/pcn/1.0.0/) as a pure functional library with immutable data structures, providing a clean Ruby interface for parsing, validating, and generating PCN game records.
15
+
16
+ ## Installation
17
+
18
+ ```ruby
19
+ # In your Gemfile
20
+ gem "sashite-pcn"
21
+ ```
22
+
23
+ Or install manually:
24
+
25
+ ```sh
26
+ gem install sashite-pcn
27
+ ```
28
+
29
+ ## Dependencies
30
+
31
+ PCN builds upon three foundational Sashité specifications:
32
+
33
+ ```ruby
34
+ gem "sashite-pmn" # Portable Move Notation
35
+ gem "sashite-feen" # Forsyth-Edwards Enhanced Notation
36
+ gem "sashite-snn" # Style Name Notation
37
+ ```
38
+
39
+ ## Quick Start
40
+
41
+ ```ruby
42
+ require "sashite/pcn"
43
+
44
+ # Parse a PCN hash
45
+ game = Sashite::Pcn.parse({
46
+ "setup" => "+rnbq+kbn+r/+p+p+p+p+p+p+p+p/8/8/8/8/+P+P+P+P+P+P+P+P/+RNBQ+KBN+R / C/c",
47
+ "moves" => [
48
+ ["e2", "e4", "C:P"],
49
+ ["e7", "e5", "c:p"]
50
+ ],
51
+ "status" => "in_progress"
52
+ })
53
+
54
+ # Access game components
55
+ game.setup # => #<Sashite::Feen::Position ...>
56
+ game.moves # => [#<Sashite::Pmn::Move ...>, ...]
57
+ game.status # => "in_progress"
58
+ game.valid? # => true
59
+
60
+ # Convert back to hash
61
+ game.to_h # => { "setup" => "...", "moves" => [...], ... }
62
+ ```
63
+
64
+ ## JSON Integration
65
+
66
+ This gem focuses on the core PCN data structures and does not include JSON parsing/dumping. Use your preferred JSON library:
67
+
68
+ ```ruby
69
+ require "json"
70
+ require "sashite/pcn"
71
+
72
+ # Load from JSON file
73
+ json_string = File.read("game.json")
74
+ pcn_hash = JSON.parse(json_string)
75
+ game = Sashite::Pcn.parse(pcn_hash)
76
+
77
+ # Save to JSON file
78
+ File.write("game.json", JSON.pretty_generate(game.to_h))
79
+
80
+ # Or with Oj for better performance
81
+ require "oj"
82
+ game = Sashite::Pcn.parse(Oj.load_file("game.json"))
83
+ Oj.to_file("game.json", game.to_h)
84
+ ```
85
+
86
+ ## PCN Format
87
+
88
+ A PCN document is a hash with five fields:
89
+
90
+ ```ruby
91
+ {
92
+ "meta" => { # Optional metadata
93
+ "name" => String,
94
+ "event" => String,
95
+ "location" => String,
96
+ "round" => Integer,
97
+ "started_on" => "YYYY-MM-DD",
98
+ "finished_at" => "YYYY-MM-DDTHH:MM:SSZ",
99
+ "href" => String
100
+ },
101
+ "sides" => { # Optional player information
102
+ "first" => {
103
+ "style" => String, # SNN format
104
+ "name" => String,
105
+ "elo" => Integer
106
+ },
107
+ "second" => {
108
+ "style" => String,
109
+ "name" => String,
110
+ "elo" => Integer
111
+ }
112
+ },
113
+ "setup" => String, # Required: FEEN position
114
+ "moves" => Array, # Required: PMN arrays
115
+ "status" => String # Optional: game status
116
+ }
117
+ ```
118
+
119
+ Only `setup` and `moves` are required. See the [PCN Specification](https://sashite.dev/specs/pcn/1.0.0/) for complete format details.
120
+
121
+ ## Usage
122
+
123
+ ### Parsing Game Records
124
+
125
+ ```ruby
126
+ # From hash
127
+ game_hash = {
128
+ "setup" => "+rnbq+kbn+r/+p+p+p+p+p+p+p+p/8/8/8/8/+P+P+P+P+P+P+P+P/+RNBQ+KBN+R / C/c",
129
+ "moves" => [["e2", "e4", "C:P"]],
130
+ "status" => "in_progress"
131
+ }
132
+ game = Sashite::Pcn.parse(game_hash)
133
+
134
+ # Validation without exception
135
+ Sashite::Pcn.valid?(game_hash) # => true
136
+ Sashite::Pcn.valid?({ "setup" => "" }) # => false
137
+ ```
138
+
139
+ ### Creating Game Records
140
+
141
+ ```ruby
142
+ # Create a new game from components
143
+ game = Sashite::Pcn.new(
144
+ setup: Sashite::Feen.parse("+rnbq+kbn+r/+p+p+p+p+p+p+p+p/8/8/8/8/+P+P+P+P+P+P+P+P/+RNBQ+KBN+R / C/c"),
145
+ moves: [
146
+ Sashite::Pmn.parse(["e2", "e4", "C:P"]),
147
+ Sashite::Pmn.parse(["e7", "e5", "c:p"])
148
+ ],
149
+ status: "in_progress",
150
+ meta: Sashite::Pcn::Meta.new(
151
+ event: "World Championship",
152
+ round: 5
153
+ ),
154
+ sides: Sashite::Pcn::Sides.new(
155
+ first: Sashite::Pcn::Player.new(name: "Alice", elo: 2800, style: "CHESS"),
156
+ second: Sashite::Pcn::Player.new(name: "Bob", elo: 2750, style: "chess")
157
+ )
158
+ )
159
+ ```
160
+
161
+ ### Accessing Game Data
162
+
163
+ ```ruby
164
+ game = Sashite::Pcn.parse(pcn_hash)
165
+
166
+ # Required fields
167
+ game.setup # => Feen::Position
168
+ game.setup.to_s # => FEEN string
169
+ game.moves # => Array of Pmn::Move
170
+ game.moves.size # => Number of moves
171
+ game.moves.first.to_a # => PMN array
172
+
173
+ # Optional fields (may be nil)
174
+ game.status # => "in_progress" or nil
175
+ game.meta # => Meta object or nil
176
+ game.sides # => Sides object or nil
177
+
178
+ # Metadata access (when present)
179
+ game.meta&.event # => "World Championship"
180
+ game.meta&.round # => 5
181
+ game.meta&.started_on # => "2025-11-15"
182
+ game.meta&.finished_at # => "2025-11-15T18:45:00Z"
183
+
184
+ # Player information (when present)
185
+ game.sides&.first&.name # => "Alice"
186
+ game.sides&.first&.elo # => 2800
187
+ game.sides&.first&.style # => "CHESS"
188
+ game.sides&.second&.name # => "Bob"
189
+ ```
190
+
191
+ ### Validation
192
+
193
+ ```ruby
194
+ # Structural validation
195
+ game.valid? # => true/false
196
+
197
+ # Detailed validation with errors
198
+ begin
199
+ Sashite::Pcn.parse(invalid_hash)
200
+ rescue Sashite::Pcn::Error => e
201
+ puts e.message
202
+ puts e.class # Specific error type
203
+ end
204
+
205
+ # Validate individual components
206
+ Sashite::Pcn::Meta.valid?(meta_hash) # => true/false
207
+ Sashite::Pcn::Sides.valid?(sides_hash) # => true/false
208
+ Sashite::Pcn::Player.valid?(player_hash) # => true/false
209
+ ```
210
+
211
+ ### Working with Moves
212
+
213
+ ```ruby
214
+ # Add moves to a game (returns new game)
215
+ game = Sashite::Pcn.parse(pcn_hash)
216
+ new_move = Sashite::Pmn.parse(["g1", "f3", "C:N"])
217
+ updated_game = game.add_move(new_move)
218
+
219
+ # Iterate over moves
220
+ game.moves.each_with_index do |move, index|
221
+ player = index.even? ? "First player" : "Second player"
222
+ puts "#{player}: #{move.to_a.inspect}"
223
+ end
224
+
225
+ # Get move count
226
+ game.move_count # => 2
227
+ game.empty? # => false
228
+ ```
229
+
230
+ ### Immutable Transformations
231
+
232
+ ```ruby
233
+ # All transformations return new instances
234
+ original = Sashite::Pcn.parse(pcn_hash)
235
+
236
+ # Update status
237
+ finished = original.with_status("checkmate")
238
+ finished.status # => "checkmate"
239
+ original.status # => "in_progress" (unchanged)
240
+
241
+ # Update metadata
242
+ with_meta = original.with_meta(
243
+ Sashite::Pcn::Meta.new(event: "Tournament")
244
+ )
245
+
246
+ # Chain transformations
247
+ result = original
248
+ .with_status("checkmate")
249
+ .add_move(new_move)
250
+ .with_meta(updated_meta)
251
+ ```
252
+
253
+ ## Format Specification
254
+
255
+ ### Document Structure
256
+
257
+ ```ruby
258
+ {
259
+ "meta" => { # Optional metadata
260
+ "name" => String,
261
+ "event" => String,
262
+ "location" => String,
263
+ "round" => Integer,
264
+ "started_on" => "YYYY-MM-DD",
265
+ "finished_at" => "YYYY-MM-DDTHH:MM:SSZ",
266
+ "href" => String
267
+ },
268
+ "sides" => { # Optional player information
269
+ "first" => {
270
+ "style" => String, # SNN format
271
+ "name" => String,
272
+ "elo" => Integer
273
+ },
274
+ "second" => {
275
+ "style" => String,
276
+ "name" => String,
277
+ "elo" => Integer
278
+ }
279
+ },
280
+ "setup" => String, # Required: FEEN position
281
+ "moves" => Array, # Required: PMN arrays
282
+ "status" => String # Optional: game status
283
+ }
284
+ ```
285
+
286
+ ### Valid Status Values
287
+
288
+ - `"in_progress"` - Game ongoing
289
+ - `"checkmate"` - Terminal piece checkmated
290
+ - `"stalemate"` - No legal moves available
291
+ - `"bare_king"` - Only terminal piece remains
292
+ - `"mare_king"` - Terminal piece captured
293
+ - `"resignation"` - Player resigned
294
+ - `"illegal_move"` - Illegal move performed
295
+ - `"time_limit"` - Time exceeded
296
+ - `"move_limit"` - Move limit reached
297
+ - `"repetition"` - Position repetition
298
+ - `"mutual_agreement"` - Players agreed to end
299
+
300
+ ## API Reference
301
+
302
+ ### Main Module Methods
303
+
304
+ - `Sashite::Pcn.parse(hash)` - Parse hash into Game object
305
+ - `Sashite::Pcn.valid?(hash)` - Validate without raising exceptions
306
+ - `Sashite::Pcn.new(**attributes)` - Create game from components
307
+
308
+ ### Game Class
309
+
310
+ #### Creation
311
+ - `Sashite::Pcn::Game.new(setup:, moves:, status: nil, meta: nil, sides: nil)`
312
+
313
+ #### Attributes (read-only)
314
+ - `#setup` - Feen::Position object (required)
315
+ - `#moves` - Array of Pmn::Move objects (required)
316
+ - `#status` - String or nil (optional)
317
+ - `#meta` - Meta object or nil (optional)
318
+ - `#sides` - Sides object or nil (optional)
319
+
320
+ #### Queries
321
+ - `#valid?` - Check overall validity
322
+ - `#move_count` / `#size` - Number of moves
323
+ - `#empty?` - No moves played
324
+ - `#has_status?` - Status field present
325
+ - `#has_meta?` - Metadata present
326
+ - `#has_sides?` - Player information present
327
+
328
+ #### Transformations (immutable)
329
+ - `#add_move(pmn_move)` - Add move (returns new game)
330
+ - `#with_status(status)` - Update status (returns new game)
331
+ - `#with_meta(meta)` - Update metadata (returns new game)
332
+ - `#with_sides(sides)` - Update player info (returns new game)
333
+
334
+ #### Conversion
335
+ - `#to_h` - Convert to hash
336
+ - `#to_s` - Alias for pretty-printed hash representation
337
+
338
+ ### Meta Class
339
+
340
+ - `Sashite::Pcn::Meta.new(**attributes)`
341
+ - `#name`, `#event`, `#location`, `#round`
342
+ - `#started_on`, `#finished_at`, `#href`
343
+ - `#to_h` - Convert to hash
344
+ - `Meta.valid?(hash)` - Validate metadata
345
+
346
+ ### Sides Class
347
+
348
+ - `Sashite::Pcn::Sides.new(first:, second:)`
349
+ - `#first` - First player (Player object or nil)
350
+ - `#second` - Second player (Player object or nil)
351
+ - `#to_h` - Convert to hash
352
+ - `Sides.valid?(hash)` - Validate sides
353
+
354
+ ### Player Class
355
+
356
+ - `Sashite::Pcn::Player.new(style: nil, name: nil, elo: nil)`
357
+ - `#style` - SNN style name or nil
358
+ - `#name` - Player name or nil
359
+ - `#elo` - Elo rating or nil
360
+ - `#to_h` - Convert to hash
361
+ - `Player.valid?(hash)` - Validate player
362
+
363
+ ### Exceptions
364
+
365
+ - `Sashite::Pcn::Error` - Base error class
366
+ - `Sashite::Pcn::ParseError` - Structure parsing failed
367
+ - `Sashite::Pcn::ValidationError` - Format validation failed
368
+ - `Sashite::Pcn::SemanticError` - Semantic consistency violation
369
+
370
+ ## Examples
371
+
372
+ ### Traditional Chess Game
373
+
374
+ ```ruby
375
+ chess_game = Sashite::Pcn.parse({
376
+ "meta" => {
377
+ "event" => "World Championship",
378
+ "round" => 5,
379
+ "started_on" => "2025-11-15"
380
+ },
381
+ "sides" => {
382
+ "first" => {
383
+ "name" => "Magnus Carlsen",
384
+ "elo" => 2830,
385
+ "style" => "CHESS"
386
+ },
387
+ "second" => {
388
+ "name" => "Fabiano Caruana",
389
+ "elo" => 2820,
390
+ "style" => "chess"
391
+ }
392
+ },
393
+ "setup" => "+rnbq+kbn+r/+p+p+p+p+p+p+p+p/8/8/8/8/+P+P+P+P+P+P+P+P/+RNBQ+KBN+R / C/c",
394
+ "moves" => [
395
+ ["e2", "e4", "C:P"],
396
+ ["e7", "e5", "c:p"],
397
+ ["g1", "f3", "C:N"],
398
+ ["b8", "c6", "c:n"]
399
+ ],
400
+ "status" => "in_progress"
401
+ })
402
+
403
+ chess_game.move_count # => 4
404
+ chess_game.sides.first.name # => "Magnus Carlsen"
405
+ ```
406
+
407
+ ### Cross-Style Game
408
+
409
+ ```ruby
410
+ hybrid_game = Sashite::Pcn.parse({
411
+ "sides" => {
412
+ "first" => { "style" => "CHESS" },
413
+ "second" => { "style" => "makruk" }
414
+ },
415
+ "setup" => "rnsmksnr/8/pppppppp/8/8/8/+P+P+P+P+P+P+P+P/+RNBQ+KBN+R / C/m",
416
+ "moves" => [
417
+ ["e2", "e4", "C:P"],
418
+ ["d6", "d5", "m:p"]
419
+ ],
420
+ "status" => "in_progress"
421
+ })
422
+ ```
423
+
424
+ ### Shōgi Game
425
+
426
+ ```ruby
427
+ shogi_game = Sashite::Pcn.parse({
428
+ "setup" => "lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL / S/s",
429
+ "moves" => [
430
+ ["e1", "e2", "S:P"],
431
+ ["*", "e5", "s:p"] # Drop from hand
432
+ ],
433
+ "status" => "in_progress"
434
+ })
435
+ ```
436
+
437
+ ### Minimal Valid Game
438
+
439
+ ```ruby
440
+ minimal = Sashite::Pcn.parse({
441
+ "setup" => "8/8/8/8/8/8/8/8 / C/c",
442
+ "moves" => []
443
+ })
444
+
445
+ minimal.valid? # => true
446
+ minimal.empty? # => true
447
+ minimal.has_status? # => false
448
+ ```
449
+
450
+ ## Design Properties
451
+
452
+ - **Rule-agnostic**: Independent of specific game mechanics
453
+ - **Comprehensive**: Complete game records with metadata
454
+ - **Immutable**: All objects frozen, transformations return new instances
455
+ - **Functional**: Pure functions without side effects
456
+ - **Composable**: Built on PMN, FEEN, and SNN specifications
457
+ - **Type-safe**: Strong validation at all levels
458
+ - **JSON-compatible**: Native Ruby hash structure ready for JSON serialization
459
+ - **Minimal API**: Small, focused public interface
460
+ - **Library-agnostic**: No JSON parser dependency, use your preferred library
461
+
462
+ ## Related Specifications
463
+
464
+ - [PCN Specification v1.0.0](https://sashite.dev/specs/pcn/1.0.0/) - Complete technical specification
465
+ - [PCN Examples](https://sashite.dev/specs/pcn/1.0.0/examples/) - Comprehensive examples
466
+ - [PMN](https://sashite.dev/specs/pmn/) - Portable Move Notation
467
+ - [FEEN](https://sashite.dev/specs/feen/) - Forsyth-Edwards Enhanced Notation
468
+ - [SNN](https://sashite.dev/specs/snn/) - Style Name Notation
469
+
470
+ ## Documentation
471
+
472
+ - [Official PCN Specification v1.0.0](https://sashite.dev/specs/pcn/1.0.0/)
473
+ - [PCN Examples Documentation](https://sashite.dev/specs/pcn/1.0.0/examples/)
474
+ - [API Documentation](https://rubydoc.info/github/sashite/pcn.rb/main)
475
+
476
+ ## Development
477
+
478
+ ```sh
479
+ # Clone the repository
480
+ git clone https://github.com/sashite/pcn.rb.git
481
+ cd pcn.rb
482
+
483
+ # Install dependencies
484
+ bundle install
485
+
486
+ # Run tests
487
+ ruby test.rb
488
+
489
+ # Generate documentation
490
+ yard doc
491
+ ```
492
+
493
+ ## Contributing
494
+
495
+ 1. Fork the repository
496
+ 2. Create a feature branch (`git checkout -b feature/new-feature`)
497
+ 3. Add tests for your changes
498
+ 4. Ensure all tests pass (`ruby test.rb`)
499
+ 5. Commit your changes (`git commit -am 'Add new feature'`)
500
+ 6. Push to the branch (`git push origin feature/new-feature`)
501
+ 7. Create a Pull Request
502
+
503
+ ## License
504
+
505
+ Available as open source under the [MIT License](https://opensource.org/licenses/MIT).
506
+
507
+ ## About
508
+
509
+ Maintained by [Sashité](https://sashite.com/) — promoting chess variants and sharing the beauty of board game cultures.
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sashite
4
+ module Pcn
5
+ # Base error class for all PCN-related errors.
6
+ #
7
+ # @see https://sashite.dev/specs/pcn/1.0.0/
8
+ class Error < ::StandardError
9
+ # Error raised when PCN structure parsing fails.
10
+ #
11
+ # This occurs when the PCN hash structure is malformed or missing
12
+ # required fields.
13
+ #
14
+ # @example
15
+ # raise Error::Parse, "Missing required field 'setup'"
16
+ class Parse < Error; end
17
+
18
+ # Error raised when PCN format validation fails.
19
+ #
20
+ # This occurs when field values do not conform to their expected
21
+ # formats (e.g., invalid FEEN string, invalid PMN array, invalid
22
+ # status value).
23
+ #
24
+ # @example
25
+ # raise Error::Validation, "Invalid status value: 'unknown'"
26
+ class Validation < Error; end
27
+
28
+ # Error raised when PCN semantic consistency validation fails.
29
+ #
30
+ # This occurs when field combinations violate semantic rules
31
+ # (e.g., SNN/SIN case consistency, invalid player object structure).
32
+ #
33
+ # @example
34
+ # raise Error::Semantic, "SNN 'CHESS' does not match SIN 'c' in FEEN"
35
+ class Semantic < Error; end
36
+ end
37
+ end
38
+ end