sashite-pcn 0.2.0 → 0.4.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.
data/README.md CHANGED
@@ -5,35 +5,55 @@
5
5
  ![Ruby](https://github.com/sashite/pcn.rb/actions/workflows/main.yml/badge.svg?branch=main)
6
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
7
 
8
- > **PCN** (Portable Chess Notation) implementation for the Ruby language.
8
+ > **PCN** (Portable Chess Notation) - Complete Ruby implementation for game record management
9
9
 
10
- ## What is PCN?
10
+ ## Table of Contents
11
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.
12
+ - [Overview](#overview)
13
+ - [Installation](#installation)
14
+ - [Quick Start](#quick-start)
15
+ - [API Documentation](#api-documentation)
16
+ - [Format Specifications](#format-specifications)
17
+ - [Time Control Examples](#time-control-examples)
18
+ - [Error Handling](#error-handling)
19
+ - [Complete Examples](#complete-examples)
20
+ - [JSON Interoperability](#json-interoperability)
13
21
 
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.
22
+ ## Overview
23
+
24
+ PCN (Portable Chess Notation) is a comprehensive, JSON-based format for representing complete chess game records across variants. This Ruby implementation provides:
25
+
26
+ - **Complete game records** with positions, moves, time tracking, and metadata
27
+ - **Time control support** for Fischer, Classical, Byōyomi, Canadian, and more
28
+ - **Rule-agnostic design** supporting all abstract strategy board games
29
+ - **Immutable objects** with functional transformations
30
+ - **Full validation** of all data formats
31
+ - **JSON compatibility** for easy serialization and storage
32
+
33
+ Implements [PCN Specification v1.0.0](https://sashite.dev/specs/pcn/1.0.0/).
15
34
 
16
35
  ## Installation
17
36
 
18
37
  ```ruby
19
- # In your Gemfile
38
+ # Gemfile
20
39
  gem "sashite-pcn"
21
40
  ```
22
41
 
23
- Or install manually:
42
+ Or install directly:
24
43
 
25
44
  ```sh
26
45
  gem install sashite-pcn
27
46
  ```
28
47
 
29
- ## Dependencies
48
+ ### Dependencies
30
49
 
31
- PCN builds upon three foundational Sashité specifications:
50
+ PCN integrates these Sashité specifications (installed automatically):
32
51
 
33
52
  ```ruby
34
- gem "sashite-pmn" # Portable Move Notation
35
- gem "sashite-feen" # Forsyth-Edwards Enhanced Notation
36
- gem "sashite-snn" # Style Name Notation
53
+ gem "sashite-pan" # Portable Action Notation (moves)
54
+ gem "sashite-feen" # Forsyth-Edwards Enhanced Notation (positions)
55
+ gem "sashite-snn" # Style Name Notation (game variants)
56
+ gem "sashite-cgsn" # Chess Game Status Notation (game states)
37
57
  ```
38
58
 
39
59
  ## Quick Start
@@ -41,470 +61,532 @@ gem "sashite-snn" # Style Name Notation
41
61
  ```ruby
42
62
  require "sashite/pcn"
43
63
 
44
- # Parse a PCN hash
64
+ # Parse a complete game
45
65
  game = Sashite::Pcn.parse({
46
66
  "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
67
  "moves" => [
48
- ["e2", "e4", "C:P"],
49
- ["e7", "e5", "c:p"]
68
+ ["e2-e4", 2.5], # Each move: [PAN notation, seconds spent]
69
+ ["e7-e5", 3.1]
50
70
  ],
51
71
  "status" => "in_progress"
52
72
  })
53
73
 
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
74
+ # Access game data
75
+ game.setup # => FEEN position object
76
+ game.moves # => [["e2-e4", 2.5], ["e7-e5", 3.1]]
77
+ game.move_count # => 2
78
+ game.status # => CGSN status object
59
79
 
60
- # Convert back to hash
61
- game.to_h # => { "setup" => "...", "moves" => [...], ... }
80
+ # Transform immutably
81
+ new_game = game.add_move(["g1-f3", 1.8])
82
+ final_game = new_game.with_status("checkmate")
62
83
  ```
63
84
 
64
- ## JSON Integration
85
+ ## API Documentation
65
86
 
66
- This gem focuses on the core PCN data structures and does not include JSON parsing/dumping. Use your preferred JSON library:
87
+ For complete API documentation, see [API.md](API.md).
67
88
 
68
- ```ruby
69
- require "json"
70
- require "sashite/pcn"
89
+ The API documentation includes:
90
+ - All classes and methods
91
+ - Type signatures and parameters
92
+ - Return values and exceptions
93
+ - Code examples for every method
94
+ - Common usage patterns
95
+ - Time control formats
96
+ - Error handling
71
97
 
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)
98
+ ## Format Specifications
76
99
 
77
- # Save to JSON file
78
- File.write("game.json", JSON.pretty_generate(game.to_h))
100
+ ### FEEN (Position)
79
101
 
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
- ```
102
+ ```ruby
103
+ # Standard chess starting position
104
+ "+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"
105
+ # └─ board ─────────────────────────────────────────────────────┘ └┘ └─┘
106
+ # turn styles
85
107
 
86
- ## PCN Format
108
+ # Empty board
109
+ "8/8/8/8/8/8/8/8 / U/u"
87
110
 
88
- A PCN document is a hash with five fields:
111
+ # With piece attributes (+ for light, - for dark)
112
+ "+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"
113
+ ```
114
+
115
+ ### PAN (Moves)
89
116
 
90
117
  ```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
+ # Basic movement
119
+ "e2-e4" # Move from e2 to e4
120
+ "g1-f3" # Knight from g1 to f3
118
121
 
119
- Only `setup` and `moves` are required. See the [PCN Specification](https://sashite.dev/specs/pcn/1.0.0/) for complete format details.
122
+ # Special moves
123
+ "e1~g1" # Castling (special path ~)
124
+ "e5~f6" # En passant (special path ~)
120
125
 
121
- ## Usage
126
+ # Captures
127
+ "d1+f3" # Movement with capture
128
+ "+e5" # Static capture at e5
122
129
 
123
- ### Parsing Game Records
130
+ # Promotions
131
+ "e7-e8=Q" # Pawn promotion to Queen
132
+ "e4=+P" # In-place transformation
124
133
 
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)
134
+ # Drops (shogi-style)
135
+ "P*e4" # Drop piece P at e4
133
136
 
134
- # Validation without exception
135
- Sashite::Pcn.valid?(game_hash) # => true
136
- Sashite::Pcn.valid?({ "setup" => "" }) # => false
137
+ # Pass move
138
+ "..." # Pass (no action)
137
139
  ```
138
140
 
139
- ### Creating Game Records
141
+ ### CGSN (Status)
140
142
 
141
143
  ```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
- )
144
+ # Inferable (can be derived from position)
145
+ "checkmate" # King under inescapable attack
146
+ "stalemate" # No legal moves, not in check
147
+ "insufficient" # Neither side can force checkmate
148
+ "in_progress" # Game continues
149
+
150
+ # Explicit only (must be declared)
151
+ "resignation" # Player resigned
152
+ "time_limit" # Time expired
153
+ "agreement" # Mutual agreement
154
+ "illegal_move" # Invalid move played
159
155
  ```
160
156
 
161
- ### Accessing Game Data
157
+ ### SNN (Styles)
162
158
 
163
159
  ```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"
160
+ # Common styles
161
+ "CHESS" # Western Chess
162
+ "shogi" # Japanese Chess
163
+ "xiangqi" # Chinese Chess
164
+ "makruk" # Thai Chess
165
+
166
+ # Case indicates piece set
167
+ "CHESS" # Uppercase = Western pieces
168
+ "chess" # Lowercase = alternative representation
189
169
  ```
190
170
 
191
- ### Validation
192
-
193
- ```ruby
194
- # Structural validation
195
- game.valid? # => true/false
171
+ ## Time Control Examples
196
172
 
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
173
+ ### Fischer/Increment
204
174
 
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
175
+ ```ruby
176
+ # Blitz 5+3 (5 minutes + 3 seconds per move)
177
+ periods: [
178
+ { time: 300, moves: nil, inc: 3 }
179
+ ]
180
+
181
+ # Rapid 15+10
182
+ periods: [
183
+ { time: 900, moves: nil, inc: 10 }
184
+ ]
185
+
186
+ # No increment
187
+ periods: [
188
+ { time: 600, moves: nil, inc: 0 } # 10 minutes, no increment
189
+ ]
209
190
  ```
210
191
 
211
- ### Working with Moves
192
+ ### Classical (Multiple Periods)
212
193
 
213
194
  ```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
195
+ # Tournament time control
196
+ periods: [
197
+ { time: 5400, moves: 40, inc: 0 }, # 90 min for first 40 moves
198
+ { time: 1800, moves: 20, inc: 0 }, # 30 min for next 20 moves
199
+ { time: 900, moves: nil, inc: 30 } # 15 min + 30s/move for rest
200
+ ]
228
201
  ```
229
202
 
230
- ### Immutable Transformations
203
+ ### Byōyomi (Japanese)
231
204
 
232
205
  ```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)
206
+ # 1 hour main + 60 seconds per move (5 periods)
207
+ periods: [
208
+ { time: 3600, moves: nil, inc: 0 }, # Main time
209
+ { time: 60, moves: 1, inc: 0 }, # Byōyomi period 1
210
+ { time: 60, moves: 1, inc: 0 }, # Byōyomi period 2
211
+ { time: 60, moves: 1, inc: 0 }, # Byōyomi period 3
212
+ { time: 60, moves: 1, inc: 0 }, # Byōyomi period 4
213
+ { time: 60, moves: 1, inc: 0 } # Byōyomi period 5
214
+ ]
251
215
  ```
252
216
 
253
- ## Format Specification
254
-
255
- ### Document Structure
217
+ ### Canadian Overtime
256
218
 
257
219
  ```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
- }
220
+ # 1 hour + 5 minutes for every 10 moves
221
+ periods: [
222
+ { time: 3600, moves: nil, inc: 0 }, # Main time: 1 hour
223
+ { time: 300, moves: 10, inc: 0 } # Overtime: 5 min/10 moves
224
+ ]
284
225
  ```
285
226
 
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
- - `"agreement"` - Players agreed to end
299
- - `"insufficient"` - Neither player has sufficient material to force a win
300
-
301
- ## API Reference
302
-
303
- ### Main Module Methods
227
+ ### No Time Control
304
228
 
305
- - `Sashite::Pcn.parse(hash)` - Parse hash into Game object
306
- - `Sashite::Pcn.valid?(hash)` - Validate without raising exceptions
307
- - `Sashite::Pcn.new(**attributes)` - Create game from components
229
+ ```ruby
230
+ # Casual/correspondence game
231
+ periods: [] # Empty array
232
+ periods: nil # Or omit entirely
233
+ ```
308
234
 
309
- ### Game Class
235
+ ## Error Handling
310
236
 
311
- #### Creation
312
- - `Sashite::Pcn::Game.new(setup:, moves:, status: nil, meta: nil, sides: nil)`
237
+ ```ruby
238
+ # Setup validation
239
+ begin
240
+ game = Sashite::Pcn::Game.new(setup: "invalid")
241
+ rescue ArgumentError => e
242
+ puts e.message # => "Invalid FEEN format"
243
+ end
313
244
 
314
- #### Attributes (read-only)
315
- - `#setup` - Feen::Position object (required)
316
- - `#moves` - Array of Pmn::Move objects (required)
317
- - `#status` - String or nil (optional)
318
- - `#meta` - Meta object or nil (optional)
319
- - `#sides` - Sides object or nil (optional)
245
+ # Move validation
246
+ begin
247
+ game.add_move(["invalid", -5])
248
+ rescue ArgumentError => e
249
+ puts e.message # => "Invalid PAN notation"
250
+ end
320
251
 
321
- #### Queries
322
- - `#valid?` - Check overall validity
323
- - `#move_count` / `#size` - Number of moves
324
- - `#empty?` - No moves played
325
- - `#has_status?` - Status field present
326
- - `#has_meta?` - Metadata present
327
- - `#has_sides?` - Player information present
252
+ # Move format validation
253
+ begin
254
+ game.add_move("e2-e4") # Wrong: should be array
255
+ rescue ArgumentError => e
256
+ puts e.message # => "Each move must be [PAN string, seconds float] tuple"
257
+ end
328
258
 
329
- #### Transformations (immutable)
330
- - `#add_move(pmn_move)` - Add move (returns new game)
331
- - `#with_status(status)` - Update status (returns new game)
332
- - `#with_meta(meta)` - Update metadata (returns new game)
333
- - `#with_sides(sides)` - Update player info (returns new game)
259
+ # Metadata validation
260
+ begin
261
+ Sashite::Pcn::Game.new(
262
+ setup: "8/8/8/8/8/8/8/8 / U/u",
263
+ meta: { round: -1 } # Invalid: must be >= 1
264
+ )
265
+ rescue ArgumentError => e
266
+ puts e.message # => "round must be a positive integer (>= 1)"
267
+ end
334
268
 
335
- #### Conversion
336
- - `#to_h` - Convert to hash
337
- - `#to_s` - Alias for pretty-printed hash representation
269
+ # Time control validation
270
+ begin
271
+ sides = {
272
+ first: {
273
+ periods: [
274
+ { time: -100 } # Invalid: negative time
275
+ ]
276
+ }
277
+ }
278
+ rescue ArgumentError => e
279
+ puts e.message # => "time must be a non-negative integer (>= 0)"
280
+ end
281
+ ```
338
282
 
339
- ### Meta Class
283
+ ## Complete Examples
340
284
 
341
- - `Sashite::Pcn::Meta.new(**attributes)`
342
- - `#name`, `#event`, `#location`, `#round`
343
- - `#started_on`, `#finished_at`, `#href`
344
- - `#to_h` - Convert to hash
345
- - `Meta.valid?(hash)` - Validate metadata
285
+ ### Minimal Valid Game
346
286
 
347
- ### Sides Class
287
+ ```ruby
288
+ # Absolute minimum (only setup required)
289
+ game = Sashite::Pcn::Game.new(
290
+ setup: "8/8/8/8/8/8/8/8 / U/u"
291
+ )
292
+ ```
348
293
 
349
- - `Sashite::Pcn::Sides.new(first:, second:)`
350
- - `#first` - First player (Player object or nil)
351
- - `#second` - Second player (Player object or nil)
352
- - `#to_h` - Convert to hash
353
- - `Sides.valid?(hash)` - Validate sides
294
+ ### Standard Chess Game
354
295
 
355
- ### Player Class
296
+ ```ruby
297
+ game = Sashite::Pcn::Game.new(
298
+ meta: {
299
+ name: "Italian Game",
300
+ event: "Online Tournament",
301
+ round: 3,
302
+ started_at: "2025-01-27T19:30:00Z"
303
+ },
304
+ sides: {
305
+ first: {
306
+ name: "Alice",
307
+ elo: 2100,
308
+ style: "CHESS",
309
+ periods: [{ time: 300, moves: nil, inc: 3 }] # 5+3 blitz
310
+ },
311
+ second: {
312
+ name: "Bob",
313
+ elo: 2050,
314
+ style: "chess",
315
+ periods: [{ time: 300, moves: nil, inc: 3 }]
316
+ }
317
+ },
318
+ 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",
319
+ moves: [
320
+ ["e2-e4", 2.3],
321
+ ["c7-c5", 3.1],
322
+ ["g1-f3", 1.8],
323
+ ["d7-d6", 2.5],
324
+ ["d2-d4", 4.2],
325
+ ["c5+d4", 1.0],
326
+ ["f3+d4", 0.8]
327
+ ],
328
+ status: "in_progress"
329
+ )
330
+ ```
356
331
 
357
- - `Sashite::Pcn::Player.new(style: nil, name: nil, elo: nil)`
358
- - `#style` - SNN style name or nil
359
- - `#name` - Player name or nil
360
- - `#elo` - Elo rating or nil
361
- - `#to_h` - Convert to hash
362
- - `Player.valid?(hash)` - Validate player
332
+ ### Building a Game Progressively
363
333
 
364
- ### Exceptions
334
+ ```ruby
335
+ # Start with minimal game
336
+ game = Sashite::Pcn::Game.new(
337
+ 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"
338
+ )
365
339
 
366
- - `Sashite::Pcn::Error` - Base error class
367
- - `Sashite::Pcn::ParseError` - Structure parsing failed
368
- - `Sashite::Pcn::ValidationError` - Format validation failed
369
- - `Sashite::Pcn::SemanticError` - Semantic consistency violation
340
+ # Add metadata
341
+ game = game.with_meta(
342
+ event: "Friendly Match",
343
+ started_at: Time.now.utc.iso8601
344
+ )
370
345
 
371
- ## Examples
346
+ # Play moves
347
+ game = game.add_move(["e2-e4", 5.2])
348
+ game = game.add_move(["e7-e5", 3.8])
349
+ game = game.add_move(["g1-f3", 2.1])
350
+
351
+ # Check progress
352
+ puts "Moves played: #{game.move_count}"
353
+ puts "White time: #{game.first_player_time}s"
354
+ puts "Black time: #{game.second_player_time}s"
355
+
356
+ # Finish game
357
+ if some_condition
358
+ game = game.with_status("checkmate")
359
+ elsif another_condition
360
+ game = game.with_status("resignation")
361
+ end
362
+ ```
372
363
 
373
- ### Traditional Chess Game
364
+ ### Complex Tournament Game
374
365
 
375
366
  ```ruby
376
- chess_game = Sashite::Pcn.parse({
367
+ require "sashite/pcn"
368
+ require "json"
369
+
370
+ # Full tournament game with all features
371
+ game_data = {
377
372
  "meta" => {
378
- "event" => "World Championship",
379
- "round" => 5,
380
- "started_on" => "2025-11-15"
373
+ "name" => "Sicilian Defense, Najdorf Variation",
374
+ "event" => "FIDE World Championship",
375
+ "location" => "Dubai, UAE",
376
+ "round" => 11,
377
+ "started_at" => "2025-11-20T15:00:00+04:00",
378
+ "href" => "https://worldchess.com/match/2025/round11",
379
+
380
+ # Custom metadata
381
+ "arbiter" => "John Smith",
382
+ "opening_eco" => "B90",
383
+ "opening_name" => "Sicilian Najdorf",
384
+ "board_number" => 1,
385
+ "section" => "Open",
386
+ "live_url" => "https://chess24.com/watch/live"
381
387
  },
388
+
382
389
  "sides" => {
383
390
  "first" => {
384
391
  "name" => "Magnus Carlsen",
385
392
  "elo" => 2830,
386
- "style" => "CHESS"
393
+ "style" => "CHESS",
394
+ "title" => "GM", # Custom field
395
+ "federation" => "NOR", # Custom field
396
+ "periods" => [
397
+ { "time" => 5400, "moves" => 40, "inc" => 0 },
398
+ { "time" => 1800, "moves" => 20, "inc" => 0 },
399
+ { "time" => 900, "moves" => nil, "inc" => 30 }
400
+ ]
387
401
  },
388
402
  "second" => {
389
403
  "name" => "Fabiano Caruana",
390
404
  "elo" => 2820,
391
- "style" => "chess"
405
+ "style" => "chess",
406
+ "title" => "GM",
407
+ "federation" => "USA",
408
+ "periods" => [
409
+ { "time" => 5400, "moves" => 40, "inc" => 0 },
410
+ { "time" => 1800, "moves" => 20, "inc" => 0 },
411
+ { "time" => 900, "moves" => nil, "inc" => 30 }
412
+ ]
392
413
  }
393
414
  },
415
+
394
416
  "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",
417
+
395
418
  "moves" => [
396
- ["e2", "e4", "C:P"],
397
- ["e7", "e5", "c:p"],
398
- ["g1", "f3", "C:N"],
399
- ["b8", "c6", "c:n"]
419
+ ["e2-e4", 32.1], ["c7-c5", 28.5],
420
+ ["g1-f3", 45.2], ["d7-d6", 31.0],
421
+ ["d2-d4", 38.9], ["c5+d4", 29.8],
422
+ ["f3+d4", 15.5], ["g8-f6", 35.2],
423
+ ["b1-c3", 62.3], ["a7-a6", 44.1],
424
+ # ... many more moves
400
425
  ],
401
- "status" => "in_progress"
402
- })
403
426
 
404
- chess_game.move_count # => 4
405
- chess_game.sides.first.name # => "Magnus Carlsen"
406
- ```
427
+ "status" => "resignation"
428
+ }
407
429
 
408
- ### Cross-Style Game
430
+ # Parse and use
431
+ game = Sashite::Pcn.parse(game_data)
409
432
 
410
- ```ruby
411
- hybrid_game = Sashite::Pcn.parse({
412
- "sides" => {
413
- "first" => { "style" => "CHESS" },
414
- "second" => { "style" => "makruk" }
415
- },
416
- "setup" => "rnsmksnr/8/pppppppp/8/8/8/+P+P+P+P+P+P+P+P/+RNBQ+KBN+R / C/m",
417
- "moves" => [
418
- ["e2", "e4", "C:P"],
419
- ["d6", "d5", "m:p"]
420
- ],
421
- "status" => "in_progress"
422
- })
433
+ # Analysis
434
+ puts "Game: #{game.meta[:name]}"
435
+ puts "Duration: #{(game.first_player_time + game.second_player_time) / 60} minutes"
436
+ puts "Winner: #{game.status == 'resignation' ? 'First player (White)' : 'Unknown'}"
437
+ puts "Total moves: #{game.move_count}"
438
+
439
+ # Export to JSON file
440
+ File.write("game.json", JSON.generate(game.to_h))
423
441
  ```
424
442
 
425
- ### Shōgi Game
443
+ ## JSON Interoperability
444
+
445
+ ### Reading PCN Files
426
446
 
427
447
  ```ruby
428
- shogi_game = Sashite::Pcn.parse({
429
- "setup" => "lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL / S/s",
430
- "moves" => [
431
- ["e1", "e2", "S:P"],
432
- ["*", "e5", "s:p"] # Drop from hand
433
- ],
434
- "status" => "in_progress"
435
- })
448
+ require "json"
449
+ require "sashite/pcn"
450
+
451
+ # From file
452
+ json_data = File.read("game.pcn.json")
453
+ hash = JSON.parse(json_data)
454
+ game = Sashite::Pcn.parse(hash)
455
+
456
+ # From URL
457
+ require "net/http"
458
+ require "uri"
459
+
460
+ uri = URI("https://api.example.com/game/123")
461
+ response = Net::HTTP.get(uri)
462
+ hash = JSON.parse(response)
463
+ game = Sashite::Pcn.parse(hash)
436
464
  ```
437
465
 
438
- ### Minimal Valid Game
466
+ ### Writing PCN Files
439
467
 
440
468
  ```ruby
441
- minimal = Sashite::Pcn.parse({
442
- "setup" => "8/8/8/8/8/8/8/8 / C/c",
443
- "moves" => []
444
- })
469
+ # Save to file
470
+ game_hash = game.to_h
471
+ json = JSON.pretty_generate(game_hash)
472
+ File.write("game.pcn.json", json)
445
473
 
446
- minimal.valid? # => true
447
- minimal.empty? # => true
448
- minimal.has_status? # => false
474
+ # Send to API
475
+ require "net/http"
476
+
477
+ uri = URI("https://api.example.com/games")
478
+ http = Net::HTTP.new(uri.host, uri.port)
479
+ http.use_ssl = true
480
+
481
+ request = Net::HTTP::Post.new(uri)
482
+ request["Content-Type"] = "application/json"
483
+ request.body = JSON.generate(game.to_h)
484
+
485
+ response = http.request(request)
449
486
  ```
450
487
 
451
- ## Design Properties
488
+ ### Database Storage
489
+
490
+ ```ruby
491
+ # Store in database (e.g., PostgreSQL with JSON column)
492
+ class GameRecord < ActiveRecord::Base
493
+ # Assumes: t.json :pcn_data
494
+
495
+ def game
496
+ @game ||= Sashite::Pcn.parse(pcn_data)
497
+ end
498
+
499
+ def game=(game_object)
500
+ self.pcn_data = game_object.to_h
501
+ @game = game_object
502
+ end
503
+ end
452
504
 
453
- - **Rule-agnostic**: Independent of specific game mechanics
454
- - **Comprehensive**: Complete game records with metadata
455
- - **Immutable**: All objects frozen, transformations return new instances
456
- - **Functional**: Pure functions without side effects
457
- - **Composable**: Built on PMN, FEEN, and SNN specifications
458
- - **Type-safe**: Strong validation at all levels
459
- - **JSON-compatible**: Native Ruby hash structure ready for JSON serialization
460
- - **Minimal API**: Small, focused public interface
461
- - **Library-agnostic**: No JSON parser dependency, use your preferred library
505
+ # Usage
506
+ record = GameRecord.new
507
+ record.game = Sashite::Pcn::Game.new(setup: "...")
508
+ record.save!
509
+
510
+ # Retrieve
511
+ record = GameRecord.find(id)
512
+ game = record.game
513
+ puts game.move_count
514
+ ```
462
515
 
463
- ## Related Specifications
516
+ ## Properties
464
517
 
465
- - [PCN Specification v1.0.0](https://sashite.dev/specs/pcn/1.0.0/) - Complete technical specification
466
- - [PCN Examples](https://sashite.dev/specs/pcn/1.0.0/examples/) - Comprehensive examples
467
- - [PMN](https://sashite.dev/specs/pmn/) - Portable Move Notation
468
- - [FEEN](https://sashite.dev/specs/feen/) - Forsyth-Edwards Enhanced Notation
469
- - [SNN](https://sashite.dev/specs/snn/) - Style Name Notation
518
+ - **Immutable**: All objects are frozen; transformations return new instances
519
+ - **Validated**: All data is validated on creation
520
+ - **Type-safe**: Strong type checking throughout
521
+ - **Rule-agnostic**: Independent of specific game rules
522
+ - **JSON-native**: Direct serialization to/from JSON
523
+ - **Comprehensive**: Complete game information including time tracking
524
+ - **Extensible**: Custom metadata and player fields supported
470
525
 
471
526
  ## Documentation
472
527
 
473
528
  - [Official PCN Specification v1.0.0](https://sashite.dev/specs/pcn/1.0.0/)
474
- - [PCN Examples Documentation](https://sashite.dev/specs/pcn/1.0.0/examples/)
529
+ - [PCN Examples](https://sashite.dev/specs/pcn/1.0.0/examples/)
475
530
  - [API Documentation](https://rubydoc.info/github/sashite/pcn.rb/main)
531
+ - [PAN Specification](https://sashite.dev/specs/pan/) (moves)
532
+ - [FEEN Specification](https://sashite.dev/specs/feen/) (positions)
533
+ - [SNN Specification](https://sashite.dev/specs/snn/) (styles)
534
+ - [CGSN Specification](https://sashite.dev/specs/cgsn/) (statuses)
476
535
 
477
536
  ## Development
478
537
 
479
538
  ```sh
480
- # Clone the repository
539
+ # Setup
481
540
  git clone https://github.com/sashite/pcn.rb.git
482
541
  cd pcn.rb
483
-
484
- # Install dependencies
485
542
  bundle install
486
543
 
487
544
  # Run tests
545
+ bundle exec rake test
546
+ # or
488
547
  ruby test.rb
489
548
 
549
+ # Run linter
550
+ bundle exec rubocop
551
+
490
552
  # Generate documentation
491
- yard doc
553
+ bundle exec yard doc
554
+
555
+ # Console for experimentation
556
+ bundle exec irb -r ./lib/sashite/pcn
492
557
  ```
493
558
 
494
559
  ## Contributing
495
560
 
496
561
  1. Fork the repository
497
- 2. Create a feature branch (`git checkout -b feature/new-feature`)
498
- 3. Add tests for your changes
499
- 4. Ensure all tests pass (`ruby test.rb`)
500
- 5. Commit your changes (`git commit -am 'Add new feature'`)
501
- 6. Push to the branch (`git push origin feature/new-feature`)
502
- 7. Create a Pull Request
562
+ 2. Create a feature branch (`git checkout -b feature/amazing-feature`)
563
+ 3. Write tests for your changes
564
+ 4. Implement your feature
565
+ 5. Ensure all tests pass (`ruby test.rb`)
566
+ 6. Check code style (`rubocop`)
567
+ 7. Commit your changes (`git commit -am 'Add amazing feature'`)
568
+ 8. Push to the branch (`git push origin feature/amazing-feature`)
569
+ 9. Open a Pull Request
503
570
 
504
571
  ## License
505
572
 
506
- Available as open source under the [MIT License](https://opensource.org/licenses/MIT).
573
+ Released under the [MIT License](https://opensource.org/licenses/MIT).
507
574
 
508
575
  ## About
509
576
 
510
- Maintained by [Sashité](https://sashite.com/) — promoting chess variants and sharing the beauty of board game cultures.
577
+ Maintained by [Sashité](https://sashite.com/)
578
+
579
+ > Sashité is a community initiative promoting chess variants and sharing the beauty of traditional board game cultures from around the world.
580
+
581
+ ### Contact
582
+
583
+ - Website: https://sashite.com
584
+ - GitHub: https://github.com/sashite
585
+ - Email: contact@sashite.com
586
+
587
+ ### Related Projects
588
+
589
+ - [Pan.rb](https://github.com/sashite/pan.rb) - Portable Action Notation
590
+ - [Feen.rb](https://github.com/sashite/feen.rb) - Forsyth-Edwards Enhanced Notation
591
+ - [Snn.rb](https://github.com/sashite/snn.rb) - Style Name Notation
592
+ - [Cgsn.rb](https://github.com/sashite/cgsn.rb) - Chess Game Status Notation