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 +7 -0
- data/LICENSE.md +21 -0
- data/README.md +509 -0
- data/lib/sashite/pcn/error.rb +38 -0
- data/lib/sashite/pcn/game.rb +436 -0
- data/lib/sashite/pcn/meta.rb +275 -0
- data/lib/sashite/pcn/player.rb +186 -0
- data/lib/sashite/pcn/sides.rb +194 -0
- data/lib/sashite/pcn.rb +68 -0
- data/lib/sashite-pcn.rb +14 -0
- metadata +107 -0
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
|
+
[](https://github.com/sashite/pcn.rb/tags)
|
|
4
|
+
[](https://rubydoc.info/github/sashite/pcn.rb/main)
|
|
5
|
+

|
|
6
|
+
[](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
|