sashite-pcn 0.1.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/lib/sashite/pcn.rb CHANGED
@@ -1,68 +1,58 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "sashite/pmn"
4
- require "sashite/feen"
5
- require "sashite/snn"
6
-
7
- require_relative "pcn/error"
8
- require_relative "pcn/meta"
9
- require_relative "pcn/player"
10
- require_relative "pcn/sides"
11
3
  require_relative "pcn/game"
12
4
 
13
5
  module Sashite
14
- # PCN (Portable Chess Notation) implementation.
6
+ # PCN (Portable Chess Notation) implementation for Ruby
15
7
  #
16
- # Provides a comprehensive, rule-agnostic format for representing complete
17
- # chess game records across variants, integrating PMN, FEEN, and SNN
18
- # specifications.
8
+ # Provides functionality for representing complete chess game records
9
+ # across variants using a comprehensive JSON-based format.
19
10
  #
20
- # @see https://sashite.dev/specs/pcn/1.0.0/
11
+ # This implementation is strictly compliant with PCN Specification v1.0.0
12
+ # @see https://sashite.dev/specs/pcn/1.0.0/ PCN Specification v1.0.0
21
13
  module Pcn
22
- # Parse a PCN hash into a Game object.
14
+ # Parse a PCN document from a hash structure
23
15
  #
24
- # @param hash [Hash] PCN document hash
25
- # @return [Game] Immutable game object
26
- # @raise [Error] If parsing or validation fails
16
+ # @param hash [Hash] the PCN document data
17
+ # @return [Game] new game instance
18
+ # @raise [ArgumentError] if the document is invalid
27
19
  #
28
- # @example
20
+ # @example Parse minimal PCN
21
+ # game = Sashite::Pcn.parse({
22
+ # "setup" => "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR / C/c"
23
+ # })
24
+ #
25
+ # @example Parse complete game
29
26
  # game = Sashite::Pcn.parse({
30
- # "setup" => "8/8/8/8/8/8/8/8 / C/c",
31
- # "moves" => []
27
+ # "meta" => { "event" => "World Championship" },
28
+ # "sides" => {
29
+ # "first" => { "name" => "Carlsen", "elo" => 2830, "style" => "CHESS" },
30
+ # "second" => { "name" => "Nakamura", "elo" => 2794, "style" => "chess" }
31
+ # },
32
+ # "setup" => "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR / C/c",
33
+ # "moves" => [["e2", "e4"], ["e7", "e5"]],
34
+ # "status" => "in_progress"
32
35
  # })
33
36
  def self.parse(hash)
34
- Game.parse(hash)
37
+ Game.new(**hash.transform_keys(&:to_sym))
35
38
  end
36
39
 
37
- # Validate a PCN hash without raising exceptions.
40
+ # Validate a PCN document structure
38
41
  #
39
- # @param hash [Hash] PCN document hash
40
- # @return [Boolean] true if valid, false otherwise
42
+ # @param hash [Hash] the PCN document data
43
+ # @return [Boolean] true if the document is structurally valid
41
44
  #
42
45
  # @example
43
- # Sashite::Pcn.valid?({ "setup" => "...", "moves" => [] }) # => true
44
- # Sashite::Pcn.valid?({ "setup" => "" }) # => false
46
+ # Sashite::Pcn.valid?({ "setup" => "8/8/8/8/8/8/8/8 / C/c" }) # => true
47
+ # Sashite::Pcn.valid?({ "moves" => [] }) # => false
45
48
  def self.valid?(hash)
46
- Game.valid?(hash)
47
- end
49
+ return false unless hash.is_a?(::Hash)
50
+ return false unless hash.key?("setup") || hash.key?(:setup)
48
51
 
49
- # Create a new game from components.
50
- #
51
- # @param attributes [Hash] Game attributes as keyword arguments
52
- # @option attributes [Feen::Position, String] :setup Initial position (required)
53
- # @option attributes [Array<Pmn::Move, Array>] :moves Move sequence (required)
54
- # @option attributes [String, nil] :status Game status (optional)
55
- # @option attributes [Meta, Hash, nil] :meta Metadata (optional)
56
- # @option attributes [Sides, Hash, nil] :sides Player information (optional)
57
- # @return [Game] Immutable game object
58
- #
59
- # @example
60
- # game = Sashite::Pcn.new(
61
- # setup: Sashite::Feen.parse("8/8/8/8/8/8/8/8 / C/c"),
62
- # moves: []
63
- # )
64
- def self.new(**attributes)
65
- Game.new(**attributes)
52
+ parse(hash)
53
+ true
54
+ rescue ::ArgumentError, ::TypeError
55
+ false
66
56
  end
67
57
  end
68
58
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sashite-pcn
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cyril Kato
@@ -9,6 +9,20 @@ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: sashite-cgsn
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.1'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.1'
12
26
  - !ruby/object:Gem::Dependency
13
27
  name: sashite-feen
14
28
  requirement: !ruby/object:Gem::Requirement
@@ -71,11 +85,10 @@ files:
71
85
  - README.md
72
86
  - lib/sashite-pcn.rb
73
87
  - lib/sashite/pcn.rb
74
- - lib/sashite/pcn/error.rb
75
88
  - lib/sashite/pcn/game.rb
76
- - lib/sashite/pcn/meta.rb
77
- - lib/sashite/pcn/player.rb
78
- - lib/sashite/pcn/sides.rb
89
+ - lib/sashite/pcn/game/meta.rb
90
+ - lib/sashite/pcn/game/sides.rb
91
+ - lib/sashite/pcn/game/sides/player.rb
79
92
  homepage: https://github.com/sashite/pcn.rb
80
93
  licenses:
81
94
  - MIT
@@ -1,38 +0,0 @@
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
@@ -1,275 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Sashite
4
- module Pcn
5
- # Immutable representation of game metadata.
6
- #
7
- # All fields are optional. Metadata provides contextual information
8
- # about the game session.
9
- #
10
- # @see https://sashite.dev/specs/pcn/1.0.0/
11
- class Meta
12
- # ISO 8601 date format: YYYY-MM-DD
13
- DATE_PATTERN = /\A\d{4}-\d{2}-\d{2}\z/
14
-
15
- # ISO 8601 datetime format with UTC timezone: YYYY-MM-DDTHH:MM:SSZ
16
- DATETIME_PATTERN = /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z\z/
17
-
18
- # Absolute URL pattern (http:// or https://)
19
- URL_PATTERN = %r{\Ahttps?://.+\z}
20
-
21
- # @return [String, nil] Game name or opening identification
22
- attr_reader :name
23
-
24
- # @return [String, nil] Tournament or event name
25
- attr_reader :event
26
-
27
- # @return [String, nil] Physical or virtual venue
28
- attr_reader :location
29
-
30
- # @return [Integer, nil] Round number in tournament context
31
- attr_reader :round
32
-
33
- # @return [String, nil] Game start date (ISO 8601: YYYY-MM-DD)
34
- attr_reader :started_on
35
-
36
- # @return [String, nil] Game completion timestamp (ISO 8601 UTC: YYYY-MM-DDTHH:MM:SSZ)
37
- attr_reader :finished_at
38
-
39
- # @return [String, nil] Reference link to external resource
40
- attr_reader :href
41
-
42
- # Parse a meta hash into a Meta object.
43
- #
44
- # @param hash [Hash] Metadata hash
45
- # @return [Meta] Immutable meta object
46
- # @raise [Error::Validation] If validation fails
47
- #
48
- # @example
49
- # meta = Meta.parse({
50
- # "event" => "World Championship",
51
- # "round" => 5
52
- # })
53
- def self.parse(hash)
54
- raise Error::Validation, "Meta must be a Hash, got #{hash.class}" unless hash.is_a?(::Hash)
55
-
56
- new(
57
- name: hash["name"],
58
- event: hash["event"],
59
- location: hash["location"],
60
- round: hash["round"],
61
- started_on: hash["started_on"],
62
- finished_at: hash["finished_at"],
63
- href: hash["href"]
64
- )
65
- end
66
-
67
- # Validate a meta hash without raising exceptions.
68
- #
69
- # @param hash [Hash] Metadata hash
70
- # @return [Boolean] true if valid, false otherwise
71
- #
72
- # @example
73
- # Meta.valid?({ "event" => "Tournament" }) # => true
74
- def self.valid?(hash)
75
- parse(hash)
76
- true
77
- rescue Error
78
- false
79
- end
80
-
81
- # Create a new Meta.
82
- #
83
- # @param name [String, nil] Game name
84
- # @param event [String, nil] Event name
85
- # @param location [String, nil] Location
86
- # @param round [Integer, nil] Round number
87
- # @param started_on [String, nil] Start date (YYYY-MM-DD)
88
- # @param finished_at [String, nil] Finish timestamp (YYYY-MM-DDTHH:MM:SSZ)
89
- # @param href [String, nil] Reference URL
90
- # @raise [Error::Validation] If validation fails
91
- #
92
- # @example
93
- # meta = Meta.new(
94
- # event: "World Championship",
95
- # round: 5,
96
- # started_on: "2025-11-15"
97
- # )
98
- def initialize(name: nil, event: nil, location: nil, round: nil, started_on: nil, finished_at: nil, href: nil)
99
- @name = name
100
- @event = event
101
- @location = location
102
- @round = round
103
- @started_on = started_on
104
- @finished_at = finished_at
105
- @href = href
106
-
107
- validate!
108
-
109
- freeze
110
- end
111
-
112
- # Check if the meta is valid.
113
- #
114
- # @return [Boolean] true if valid
115
- def valid?
116
- validate!
117
- true
118
- rescue Error
119
- false
120
- end
121
-
122
- # Check if metadata is empty (all fields nil).
123
- #
124
- # @return [Boolean] true if all fields are nil
125
- def empty?
126
- name.nil? && event.nil? && location.nil? && round.nil? &&
127
- started_on.nil? && finished_at.nil? && href.nil?
128
- end
129
-
130
- # Convert to hash representation.
131
- #
132
- # @return [Hash] Metadata hash (excludes nil values)
133
- #
134
- # @example
135
- # meta.to_h # => { "event" => "Tournament", "round" => 5 }
136
- def to_h
137
- hash = {}
138
-
139
- hash["name"] = name unless name.nil?
140
- hash["event"] = event unless event.nil?
141
- hash["location"] = location unless location.nil?
142
- hash["round"] = round unless round.nil?
143
- hash["started_on"] = started_on unless started_on.nil?
144
- hash["finished_at"] = finished_at unless finished_at.nil?
145
- hash["href"] = href unless href.nil?
146
-
147
- hash
148
- end
149
-
150
- # String representation.
151
- #
152
- # @return [String] Inspectable representation
153
- def to_s
154
- fields = []
155
- fields << "event=#{event.inspect}" unless event.nil?
156
- fields << "round=#{round}" unless round.nil?
157
- fields << "location=#{location.inspect}" unless location.nil?
158
-
159
- "#<#{self.class} #{fields.join(' ')}>"
160
- end
161
- alias inspect to_s
162
-
163
- # Equality comparison.
164
- #
165
- # @param other [Meta] Other meta
166
- # @return [Boolean] true if equal
167
- def ==(other)
168
- other.is_a?(self.class) &&
169
- other.name == name &&
170
- other.event == event &&
171
- other.location == location &&
172
- other.round == round &&
173
- other.started_on == started_on &&
174
- other.finished_at == finished_at &&
175
- other.href == href
176
- end
177
- alias eql? ==
178
-
179
- # Hash code for equality.
180
- #
181
- # @return [Integer] Hash code
182
- def hash
183
- [self.class, name, event, location, round, started_on, finished_at, href].hash
184
- end
185
-
186
- private
187
-
188
- # Validate all fields.
189
- def validate!
190
- validate_name!
191
- validate_event!
192
- validate_location!
193
- validate_round!
194
- validate_started_on!
195
- validate_finished_at!
196
- validate_href!
197
- end
198
-
199
- # Validate name field.
200
- def validate_name!
201
- return if name.nil?
202
-
203
- return if name.is_a?(::String)
204
-
205
- raise Error::Validation, "Meta 'name' must be a String, got #{name.class}"
206
- end
207
-
208
- # Validate event field.
209
- def validate_event!
210
- return if event.nil?
211
-
212
- return if event.is_a?(::String)
213
-
214
- raise Error::Validation, "Meta 'event' must be a String, got #{event.class}"
215
- end
216
-
217
- # Validate location field.
218
- def validate_location!
219
- return if location.nil?
220
-
221
- return if location.is_a?(::String)
222
-
223
- raise Error::Validation, "Meta 'location' must be a String, got #{location.class}"
224
- end
225
-
226
- # Validate round field.
227
- def validate_round!
228
- return if round.nil?
229
-
230
- raise Error::Validation, "Meta 'round' must be an Integer, got #{round.class}" unless round.is_a?(::Integer)
231
-
232
- return unless round < 1
233
-
234
- raise Error::Validation, "Meta 'round' must be >= 1, got #{round}"
235
- end
236
-
237
- # Validate started_on field.
238
- def validate_started_on!
239
- return if started_on.nil?
240
-
241
- unless started_on.is_a?(::String)
242
- raise Error::Validation, "Meta 'started_on' must be a String, got #{started_on.class}"
243
- end
244
-
245
- return if DATE_PATTERN.match?(started_on)
246
-
247
- raise Error::Validation, "Meta 'started_on' must match format YYYY-MM-DD, got #{started_on.inspect}"
248
- end
249
-
250
- # Validate finished_at field.
251
- def validate_finished_at!
252
- return if finished_at.nil?
253
-
254
- unless finished_at.is_a?(::String)
255
- raise Error::Validation, "Meta 'finished_at' must be a String, got #{finished_at.class}"
256
- end
257
-
258
- return if DATETIME_PATTERN.match?(finished_at)
259
-
260
- raise Error::Validation, "Meta 'finished_at' must match format YYYY-MM-DDTHH:MM:SSZ, got #{finished_at.inspect}"
261
- end
262
-
263
- # Validate href field.
264
- def validate_href!
265
- return if href.nil?
266
-
267
- raise Error::Validation, "Meta 'href' must be a String, got #{href.class}" unless href.is_a?(::String)
268
-
269
- return if URL_PATTERN.match?(href)
270
-
271
- raise Error::Validation, "Meta 'href' must be an absolute URL (http:// or https://), got #{href.inspect}"
272
- end
273
- end
274
- end
275
- end
@@ -1,186 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Sashite
4
- module Pcn
5
- # Immutable representation of player information.
6
- #
7
- # All fields are optional. Player provides identification and
8
- # rating information for game participants.
9
- #
10
- # @see https://sashite.dev/specs/pcn/1.0.0/
11
- class Player
12
- # @return [String, nil] Style name in SNN format
13
- attr_reader :style
14
-
15
- # @return [String, nil] Player name or identifier
16
- attr_reader :name
17
-
18
- # @return [Integer, nil] Elo rating
19
- attr_reader :elo
20
-
21
- # Parse a player hash into a Player object.
22
- #
23
- # @param hash [Hash] Player hash
24
- # @return [Player] Immutable player object
25
- # @raise [Error::Validation] If validation fails
26
- #
27
- # @example
28
- # player = Player.parse({
29
- # "name" => "Magnus Carlsen",
30
- # "elo" => 2830,
31
- # "style" => "CHESS"
32
- # })
33
- def self.parse(hash)
34
- raise Error::Validation, "Player must be a Hash, got #{hash.class}" unless hash.is_a?(::Hash)
35
-
36
- new(
37
- style: hash["style"],
38
- name: hash["name"],
39
- elo: hash["elo"]
40
- )
41
- end
42
-
43
- # Validate a player hash without raising exceptions.
44
- #
45
- # @param hash [Hash] Player hash
46
- # @return [Boolean] true if valid, false otherwise
47
- #
48
- # @example
49
- # Player.valid?({ "name" => "Alice" }) # => true
50
- def self.valid?(hash)
51
- parse(hash)
52
- true
53
- rescue Error
54
- false
55
- end
56
-
57
- # Create a new Player.
58
- #
59
- # @param style [String, nil] Style name (SNN format)
60
- # @param name [String, nil] Player name
61
- # @param elo [Integer, nil] Elo rating
62
- # @raise [Error::Validation] If validation fails
63
- #
64
- # @example
65
- # player = Player.new(
66
- # name: "Magnus Carlsen",
67
- # elo: 2830,
68
- # style: "CHESS"
69
- # )
70
- def initialize(style: nil, name: nil, elo: nil)
71
- @style = style
72
- @name = name
73
- @elo = elo
74
-
75
- validate!
76
-
77
- freeze
78
- end
79
-
80
- # Check if the player is valid.
81
- #
82
- # @return [Boolean] true if valid
83
- def valid?
84
- validate!
85
- true
86
- rescue Error
87
- false
88
- end
89
-
90
- # Check if player is empty (all fields nil).
91
- #
92
- # @return [Boolean] true if all fields are nil
93
- def empty?
94
- style.nil? && name.nil? && elo.nil?
95
- end
96
-
97
- # Convert to hash representation.
98
- #
99
- # @return [Hash] Player hash (excludes nil values)
100
- #
101
- # @example
102
- # player.to_h # => { "name" => "Alice", "elo" => 2800 }
103
- def to_h
104
- hash = {}
105
-
106
- hash["style"] = style unless style.nil?
107
- hash["name"] = name unless name.nil?
108
- hash["elo"] = elo unless elo.nil?
109
-
110
- hash
111
- end
112
-
113
- # String representation.
114
- #
115
- # @return [String] Inspectable representation
116
- def to_s
117
- fields = []
118
- fields << "name=#{name.inspect}" unless name.nil?
119
- fields << "elo=#{elo}" unless elo.nil?
120
- fields << "style=#{style.inspect}" unless style.nil?
121
-
122
- "#<#{self.class} #{fields.join(' ')}>"
123
- end
124
- alias inspect to_s
125
-
126
- # Equality comparison.
127
- #
128
- # @param other [Player] Other player
129
- # @return [Boolean] true if equal
130
- def ==(other)
131
- other.is_a?(self.class) &&
132
- other.style == style &&
133
- other.name == name &&
134
- other.elo == elo
135
- end
136
- alias eql? ==
137
-
138
- # Hash code for equality.
139
- #
140
- # @return [Integer] Hash code
141
- def hash
142
- [self.class, style, name, elo].hash
143
- end
144
-
145
- private
146
-
147
- # Validate all fields.
148
- def validate!
149
- validate_style!
150
- validate_name!
151
- validate_elo!
152
- end
153
-
154
- # Validate style field.
155
- def validate_style!
156
- return if style.nil?
157
-
158
- raise Error::Validation, "Player 'style' must be a String, got #{style.class}" unless style.is_a?(::String)
159
-
160
- return if ::Sashite::Snn.valid?(style)
161
-
162
- raise Error::Validation, "Player 'style' must be valid SNN format, got #{style.inspect}"
163
- end
164
-
165
- # Validate name field.
166
- def validate_name!
167
- return if name.nil?
168
-
169
- return if name.is_a?(::String)
170
-
171
- raise Error::Validation, "Player 'name' must be a String, got #{name.class}"
172
- end
173
-
174
- # Validate elo field.
175
- def validate_elo!
176
- return if elo.nil?
177
-
178
- raise Error::Validation, "Player 'elo' must be an Integer, got #{elo.class}" unless elo.is_a?(::Integer)
179
-
180
- return unless elo < 0
181
-
182
- raise Error::Validation, "Player 'elo' must be >= 0, got #{elo}"
183
- end
184
- end
185
- end
186
- end