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.
- checksums.yaml +4 -4
- data/README.md +255 -382
- data/lib/sashite/pcn/game/meta.rb +170 -0
- data/lib/sashite/pcn/game/sides/player.rb +129 -0
- data/lib/sashite/pcn/game/sides.rb +96 -0
- data/lib/sashite/pcn/game.rb +275 -344
- data/lib/sashite/pcn.rb +35 -45
- metadata +18 -5
- data/lib/sashite/pcn/error.rb +0 -38
- data/lib/sashite/pcn/meta.rb +0 -275
- data/lib/sashite/pcn/player.rb +0 -186
- data/lib/sashite/pcn/sides.rb +0 -194
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
|
|
17
|
-
#
|
|
18
|
-
# specifications.
|
|
8
|
+
# Provides functionality for representing complete chess game records
|
|
9
|
+
# across variants using a comprehensive JSON-based format.
|
|
19
10
|
#
|
|
20
|
-
#
|
|
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
|
|
14
|
+
# Parse a PCN document from a hash structure
|
|
23
15
|
#
|
|
24
|
-
# @param hash [Hash] PCN document
|
|
25
|
-
# @return [Game]
|
|
26
|
-
# @raise [
|
|
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
|
-
# "
|
|
31
|
-
# "
|
|
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.
|
|
37
|
+
Game.new(**hash.transform_keys(&:to_sym))
|
|
35
38
|
end
|
|
36
39
|
|
|
37
|
-
# Validate a PCN
|
|
40
|
+
# Validate a PCN document structure
|
|
38
41
|
#
|
|
39
|
-
# @param hash [Hash] PCN document
|
|
40
|
-
# @return [Boolean] true if
|
|
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" => "
|
|
44
|
-
# Sashite::Pcn.valid?({ "
|
|
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
|
-
|
|
47
|
-
|
|
49
|
+
return false unless hash.is_a?(::Hash)
|
|
50
|
+
return false unless hash.key?("setup") || hash.key?(:setup)
|
|
48
51
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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.
|
|
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/
|
|
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
|
data/lib/sashite/pcn/error.rb
DELETED
|
@@ -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
|
data/lib/sashite/pcn/meta.rb
DELETED
|
@@ -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
|
data/lib/sashite/pcn/player.rb
DELETED
|
@@ -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
|