sashite-pcn 0.2.0 → 0.3.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
@@ -9,473 +9,346 @@
9
9
 
10
10
  ## What is PCN?
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
+ PCN (Portable Chess Notation) is a comprehensive, JSON-based format for representing complete chess game records across variants. PCN provides unified, rule-agnostic game recording supporting both traditional single-variant games and cross-variant scenarios with complete metadata tracking.
13
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.
14
+ This gem implements the [PCN Specification v1.0.0](https://sashite.dev/specs/pcn/1.0.0/).
15
15
 
16
16
  ## Installation
17
-
18
17
  ```ruby
19
18
  # In your Gemfile
20
19
  gem "sashite-pcn"
21
20
  ```
22
21
 
23
22
  Or install manually:
24
-
25
23
  ```sh
26
24
  gem install sashite-pcn
27
25
  ```
28
26
 
29
27
  ## Dependencies
30
28
 
31
- PCN builds upon three foundational Sashité specifications:
32
-
29
+ PCN builds upon the Sashité ecosystem specifications:
33
30
  ```ruby
34
31
  gem "sashite-pmn" # Portable Move Notation
35
32
  gem "sashite-feen" # Forsyth-Edwards Enhanced Notation
36
33
  gem "sashite-snn" # Style Name Notation
34
+ gem "sashite-cgsn" # Chess Game Status Notation
37
35
  ```
38
36
 
39
- ## Quick Start
37
+ ## Usage
40
38
 
39
+ ### Parsing and Validation
41
40
  ```ruby
42
41
  require "sashite/pcn"
43
42
 
44
- # Parse a PCN hash
43
+ # Parse a minimal PCN document (only setup required)
45
44
  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"
45
+ "setup" => "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR / C/c"
52
46
  })
53
47
 
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
48
+ game.setup # => "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR / C/c"
49
+ game.meta # => {} (defaults to empty hash when omitted)
50
+ game.sides # => {} (defaults to empty hash when omitted)
51
+ game.moves # => [] (defaults to empty array when omitted)
52
+ game.status # => nil (defaults to nil when omitted)
59
53
 
60
- # Convert back to hash
61
- game.to_h # => { "setup" => "...", "moves" => [...], ... }
62
- ```
54
+ # Parse with explicit moves
55
+ game = Sashite::Pcn.parse({
56
+ "setup" => "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR / C/c",
57
+ "moves" => [
58
+ ["e2", "e4"],
59
+ ["e7", "e5"]
60
+ ]
61
+ })
63
62
 
64
- ## JSON Integration
63
+ game.moves.length # => 2
65
64
 
66
- This gem focuses on the core PCN data structures and does not include JSON parsing/dumping. Use your preferred JSON library:
65
+ # Validate without parsing
66
+ Sashite::Pcn.valid?({
67
+ "setup" => "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR / C/c"
68
+ }) # => true (all fields except setup are optional)
69
+ ```
67
70
 
71
+ ### Creating Games
68
72
  ```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))
73
+ # Minimal valid game (only setup required, all other fields optional)
74
+ game = Sashite::Pcn.parse(
75
+ setup: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR / C/c"
76
+ )
79
77
 
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
- ```
78
+ # Equivalent to:
79
+ game = Sashite::Pcn.parse(
80
+ setup: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR / C/c",
81
+ meta: {},
82
+ sides: {},
83
+ moves: [],
84
+ status: nil
85
+ )
85
86
 
86
- ## PCN Format
87
+ # Chess puzzle (position without moves)
88
+ puzzle = Sashite::Pcn.parse(
89
+ meta: { name: "Mate in 2" },
90
+ setup: "r1bqkb1r/pppp1ppp/2n2n2/4p2Q/2B1P3/8/PPPP1PPP/RNB1K1NR / C/c"
91
+ # sides, moves, and status omitted (use default values)
92
+ )
87
93
 
88
- A PCN document is a hash with five fields:
94
+ # Partial player information (only first player)
95
+ game = Sashite::Pcn.parse(
96
+ sides: {
97
+ first: { name: "Alice", elo: 2100 }
98
+ # second omitted (defaults to {})
99
+ },
100
+ setup: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR / C/c"
101
+ )
89
102
 
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
103
+ # Complete game with metadata
104
+ game = Sashite::Pcn.parse(
105
+ meta: {
106
+ event: "World Championship",
107
+ location: "London",
108
+ started_on: "2024-11-20"
100
109
  },
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
- }
110
+ sides: {
111
+ first: { name: "Carlsen", elo: 2830, style: "CHESS" },
112
+ second: { name: "Nakamura", elo: 2794, style: "chess" }
112
113
  },
113
- "setup" => String, # Required: FEEN position
114
- "moves" => Array, # Required: PMN arrays
115
- "status" => String # Optional: game status
116
- }
114
+ setup: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR / C/c",
115
+ moves: [
116
+ ["e2", "e4"],
117
+ ["c7", "c5"]
118
+ ],
119
+ status: "in_progress"
120
+ )
117
121
  ```
118
122
 
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
123
+ ### Common Use Cases
124
+ ```ruby
125
+ # Position without moves (puzzle, endgame study, analysis)
126
+ puzzle = Sashite::Pcn.parse(
127
+ meta: { name: "Lucena Position" },
128
+ setup: "1K6/1P6/8/8/8/8/r7/2k5 / C/c"
129
+ # moves omitted (defaults to [])
130
+ )
122
131
 
123
- ### Parsing Game Records
132
+ # Terminal position with status
133
+ terminal = Sashite::Pcn.parse(
134
+ setup: "7k/5Q2/6K1/8/8/8/8/8 / C/c",
135
+ status: "stalemate"
136
+ # moves omitted (defaults to [])
137
+ )
124
138
 
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
- ```
139
+ # Game template (starting position)
140
+ template = Sashite::Pcn.parse(
141
+ sides: {
142
+ first: { style: "CHESS" },
143
+ second: { style: "chess" }
144
+ },
145
+ setup: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR / C/c"
146
+ # meta, moves, and status omitted (use default values)
147
+ )
138
148
 
139
- ### Creating Game Records
149
+ # Position with inferable status (checkmate can be inferred from position)
150
+ game = Sashite::Pcn.parse(
151
+ setup: "r1bqkb1r/pppp1ppp/2n2n2/4p2Q/2B1P3/8/PPPP1PPP/RNB1K1NR / c/C",
152
+ moves: [
153
+ ["f1", "c4"],
154
+ ["g8", "f6"],
155
+ ["d1", "h5"],
156
+ ["f6", "h5"]
157
+ ]
158
+ # status omitted (defaults to nil, can be inferred as "checkmate")
159
+ )
140
160
 
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"),
161
+ # Game with explicit-only status (must be declared)
162
+ game = Sashite::Pcn.parse(
163
+ setup: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR / C/c",
145
164
  moves: [
146
- Sashite::Pmn.parse(["e2", "e4", "C:P"]),
147
- Sashite::Pmn.parse(["e7", "e5", "c:p"])
165
+ ["e2", "e4"],
166
+ ["c7", "c5"]
148
167
  ],
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
- )
168
+ status: "resignation" # Cannot be inferred, must be explicit
158
169
  )
159
170
  ```
160
171
 
161
- ### Accessing Game Data
162
-
172
+ ### Immutability and Transformations
163
173
  ```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
- ```
174
+ # All objects are frozen
175
+ game.frozen? # => true
176
+ game.meta.frozen? # => true
190
177
 
191
- ### Validation
178
+ # Transformations return new instances
179
+ new_game = game.add_move(["g1", "f3"])
180
+ new_game.moves.length # => 3
181
+ game.moves.length # => 2 (unchanged)
192
182
 
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
183
+ # Update metadata
184
+ updated = game.with_status("resignation")
185
+ updated.status # => "resignation"
186
+ game.status # => "in_progress" (unchanged)
209
187
  ```
210
188
 
211
- ### Working with Moves
212
-
189
+ ### Accessing Game Data
213
190
  ```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
191
+ # Metadata access (empty hash if omitted)
192
+ game.meta # => {} or { event: "...", location: "...", ... }
193
+ game.meta[:event] # => "World Championship" or nil
194
+ game.started_on # => "2024-11-20" or nil
195
+
196
+ # Player information (empty hash if omitted)
197
+ game.sides # => {} or { first: {...}, second: {...} }
198
+ game.first_player # => { name: "Carlsen", elo: 2830, style: "CHESS" } or {}
199
+ game.second_player # => { name: "Nakamura", elo: 2794, style: "chess" } or {}
200
+
201
+ # Move access (always returns array, empty if omitted)
202
+ game.moves # => [[...], [...]] or []
203
+ game.move_at(0) # => ["e2", "e4"] or nil
204
+ game.move_count # => 2 or 0
205
+
206
+ # Status (nil if omitted)
207
+ game.status # => "in_progress" or nil
208
+ game.finished? # => false
209
+ game.in_progress? # => true
228
210
  ```
229
211
 
230
- ### Immutable Transformations
231
-
212
+ ### JSON Serialization
232
213
  ```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")
214
+ # Convert to hash (ready for JSON)
215
+ game.to_h
216
+ # => {
217
+ # "meta" => { "event" => "...", ... },
218
+ # "sides" => { "first" => {...}, "second" => {...} },
219
+ # "setup" => "...",
220
+ # "moves" => [[...], [...]],
221
+ # "status" => "in_progress"
222
+ # }
223
+
224
+ # Minimal game (only required field + moves array)
225
+ minimal = Sashite::Pcn.parse(setup: "8/8/8/8/8/8/8/8 / U/u")
226
+ minimal.to_h
227
+ # => {
228
+ # "setup" => "8/8/8/8/8/8/8/8 / U/u",
229
+ # "moves" => [] # Always included in serialization
230
+ # }
231
+ # Note: meta, sides, and status omitted when at default values
232
+
233
+ # Game with some fields at default values
234
+ partial = Sashite::Pcn.parse(
235
+ meta: { name: "Study" },
236
+ setup: "8/8/8/8/8/8/8/8 / U/u"
244
237
  )
238
+ partial.to_h
239
+ # => {
240
+ # "meta" => { "name" => "Study" },
241
+ # "setup" => "8/8/8/8/8/8/8/8 / U/u",
242
+ # "moves" => []
243
+ # }
244
+ # Note: sides and status omitted (at default values)
245
+
246
+ # Use with any JSON library
247
+ require "json"
248
+ json_string = JSON.generate(game.to_h)
245
249
 
246
- # Chain transformations
247
- result = original
248
- .with_status("checkmate")
249
- .add_move(new_move)
250
- .with_meta(updated_meta)
250
+ # Parse from JSON
251
+ parsed = Sashite::Pcn.parse(JSON.parse(json_string))
251
252
  ```
252
253
 
253
- ## Format Specification
254
-
255
- ### Document Structure
256
-
254
+ ### Functional Operations
257
255
  ```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
- }
256
+ # Chain transformations (starting from minimal game)
257
+ game = Sashite::Pcn.parse(setup: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR / C/c")
258
+ .add_move(["e2", "e4"])
259
+ .add_move(["e7", "e5"])
260
+ .with_meta(event: "Casual Game")
261
+ .with_status("in_progress")
262
+
263
+ # Map over moves
264
+ move_notations = game.moves.map { |move| move.join("-") }
265
+
266
+ # Filter and select
267
+ queens_moves = game.moves.select { |move| move[2]&.include?("Q") }
284
268
  ```
285
269
 
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
270
+ ## Properties
271
+
272
+ * **Rule-agnostic**: Independent of specific game mechanics
273
+ * **Comprehensive**: Complete game records with metadata
274
+ * **Minimal requirements**: Only `setup` field required
275
+ * **Smart defaults**: Optional fields (`meta`, `sides`, `moves`, `status`) have sensible defaults
276
+ * **Immutable**: All objects frozen, transformations return new instances
277
+ * **Functional**: Pure functions without side effects
278
+ * **Flexible**: Supports positions without moves (puzzles, analysis, templates)
279
+ * **Composable**: Built on PMN, FEEN, SNN, and CGSN specifications
280
+ * **Type-safe**: Strong validation at all levels
281
+ * **JSON-compatible**: Native Ruby hash structure ready for JSON serialization
282
+ * **Minimal API**: Small, focused public interface
283
+ * **Library-agnostic**: No JSON parser dependency, use your preferred library
284
+
285
+ ## Default Values
286
+
287
+ When fields are omitted in initialization or parsing:
288
+
289
+ | Field | Default Value | Description |
290
+ |-------|---------------|-------------|
291
+ | `meta` | `{}` | No metadata provided |
292
+ | `sides` | `{}` | No player information |
293
+ | `sides[:first]` | `{}` | No first player information |
294
+ | `sides[:second]` | `{}` | No second player information |
295
+ | `moves` | `[]` | No moves played |
296
+ | `status` | `nil` | No explicit status declaration |
297
+ | `setup` | *required* | Must be explicitly provided |
300
298
 
301
299
  ## API Reference
302
300
 
303
- ### Main Module Methods
304
-
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
308
-
309
- ### Game Class
310
-
311
- #### Creation
312
- - `Sashite::Pcn::Game.new(setup:, moves:, status: nil, meta: nil, sides: nil)`
313
-
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)
320
-
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
328
-
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)
334
-
335
- #### Conversion
336
- - `#to_h` - Convert to hash
337
- - `#to_s` - Alias for pretty-printed hash representation
301
+ ### Class Methods
338
302
 
339
- ### Meta Class
303
+ - `Sashite::Pcn.parse(hash)` - Parse PCN from hash structure
304
+ - `Sashite::Pcn.valid?(hash)` - Validate PCN structure
340
305
 
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
306
+ ### Instance Methods
346
307
 
347
- ### Sides Class
308
+ #### Core Data Access
309
+ - `#setup` - Initial position (FEEN string) **[required]**
310
+ - `#meta` - Metadata hash (defaults to `{}`)
311
+ - `#sides` - Player information hash (defaults to `{}`)
312
+ - `#moves` - Move sequence array (defaults to `[]`)
313
+ - `#status` - Game status (CGSN value or `nil`, defaults to `nil`)
348
314
 
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
315
+ #### Player Access
316
+ - `#first_player` - First player data (defaults to `{}`)
317
+ - `#second_player` - Second player data (defaults to `{}`)
354
318
 
355
- ### Player Class
319
+ #### Move Operations
320
+ - `#move_at(index)` - Get move at index
321
+ - `#move_count` - Total number of moves
322
+ - `#add_move(move)` - Return new game with added move
356
323
 
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
324
+ #### Metadata Shortcuts
325
+ - `#started_on` - Game start date
326
+ - `#finished_at` - Game completion timestamp
327
+ - `#event` - Event name
328
+ - `#location` - Event location
329
+ - `#round` - Round number
363
330
 
364
- ### Exceptions
331
+ #### Transformations
332
+ - `#with_status(status)` - Return new game with status
333
+ - `#with_meta(**meta)` - Return new game with updated metadata
334
+ - `#with_moves(moves)` - Return new game with move sequence
365
335
 
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
336
+ #### Predicates
337
+ - `#finished?` - Check if game is finished
338
+ - `#in_progress?` - Check if game is in progress
370
339
 
371
- ## Examples
372
-
373
- ### Traditional Chess Game
374
-
375
- ```ruby
376
- chess_game = Sashite::Pcn.parse({
377
- "meta" => {
378
- "event" => "World Championship",
379
- "round" => 5,
380
- "started_on" => "2025-11-15"
381
- },
382
- "sides" => {
383
- "first" => {
384
- "name" => "Magnus Carlsen",
385
- "elo" => 2830,
386
- "style" => "CHESS"
387
- },
388
- "second" => {
389
- "name" => "Fabiano Caruana",
390
- "elo" => 2820,
391
- "style" => "chess"
392
- }
393
- },
394
- "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",
395
- "moves" => [
396
- ["e2", "e4", "C:P"],
397
- ["e7", "e5", "c:p"],
398
- ["g1", "f3", "C:N"],
399
- ["b8", "c6", "c:n"]
400
- ],
401
- "status" => "in_progress"
402
- })
403
-
404
- chess_game.move_count # => 4
405
- chess_game.sides.first.name # => "Magnus Carlsen"
406
- ```
407
-
408
- ### Cross-Style Game
409
-
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
- })
423
- ```
424
-
425
- ### Shōgi Game
426
-
427
- ```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
- })
436
- ```
437
-
438
- ### Minimal Valid Game
439
-
440
- ```ruby
441
- minimal = Sashite::Pcn.parse({
442
- "setup" => "8/8/8/8/8/8/8/8 / C/c",
443
- "moves" => []
444
- })
445
-
446
- minimal.valid? # => true
447
- minimal.empty? # => true
448
- minimal.has_status? # => false
449
- ```
450
-
451
- ## Design Properties
452
-
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
462
-
463
- ## Related Specifications
464
-
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
340
+ #### Serialization
341
+ - `#to_h` - Convert to hash (always includes `moves` array, omits fields at default values)
342
+ - `#to_json(*args)` - Convert to JSON (if JSON library loaded)
343
+ - `#frozen?` - Always returns true
470
344
 
471
345
  ## Documentation
472
346
 
473
347
  - [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/)
348
+ - [PCN Examples](https://sashite.dev/specs/pcn/1.0.0/examples/)
475
349
  - [API Documentation](https://rubydoc.info/github/sashite/pcn.rb/main)
476
350
 
477
351
  ## Development
478
-
479
352
  ```sh
480
353
  # Clone the repository
481
354
  git clone https://github.com/sashite/pcn.rb.git