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.
@@ -0,0 +1,186 @@
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
@@ -0,0 +1,194 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sashite
4
+ module Pcn
5
+ # Immutable representation of player information for both sides.
6
+ #
7
+ # Contains player information for first and second player.
8
+ # At least one player must be defined when sides are present.
9
+ #
10
+ # @see https://sashite.dev/specs/pcn/1.0.0/
11
+ class Sides
12
+ # @return [Player, nil] First player information
13
+ attr_reader :first
14
+
15
+ # @return [Player, nil] Second player information
16
+ attr_reader :second
17
+
18
+ # Parse a sides hash into a Sides object.
19
+ #
20
+ # @param hash [Hash] Sides hash
21
+ # @return [Sides] Immutable sides object
22
+ # @raise [Error::Validation] If validation fails
23
+ #
24
+ # @example
25
+ # sides = Sides.parse({
26
+ # "first" => { "name" => "Alice", "elo" => 2800 },
27
+ # "second" => { "name" => "Bob", "elo" => 2750 }
28
+ # })
29
+ def self.parse(hash)
30
+ raise Error::Validation, "Sides must be a Hash, got #{hash.class}" unless hash.is_a?(::Hash)
31
+
32
+ first = parse_player(hash["first"], "first")
33
+ second = parse_player(hash["second"], "second")
34
+
35
+ new(first:, second:)
36
+ end
37
+
38
+ # Validate a sides hash without raising exceptions.
39
+ #
40
+ # @param hash [Hash] Sides hash
41
+ # @return [Boolean] true if valid, false otherwise
42
+ #
43
+ # @example
44
+ # Sides.valid?({ "first" => { "name" => "Alice" } }) # => true
45
+ def self.valid?(hash)
46
+ parse(hash)
47
+ true
48
+ rescue Error
49
+ false
50
+ end
51
+
52
+ # Create a new Sides.
53
+ #
54
+ # @param first [Player, Hash, nil] First player information
55
+ # @param second [Player, Hash, nil] Second player information
56
+ # @raise [Error::Validation] If validation fails
57
+ #
58
+ # @example
59
+ # sides = Sides.new(
60
+ # first: Player.new(name: "Alice", elo: 2800),
61
+ # second: Player.new(name: "Bob", elo: 2750)
62
+ # )
63
+ def initialize(first: nil, second: nil)
64
+ @first = normalize_player(first, "first")
65
+ @second = normalize_player(second, "second")
66
+
67
+ validate!
68
+
69
+ freeze
70
+ end
71
+
72
+ # Check if the sides are valid.
73
+ #
74
+ # @return [Boolean] true if valid
75
+ def valid?
76
+ validate!
77
+ true
78
+ rescue Error
79
+ false
80
+ end
81
+
82
+ # Check if both sides are empty.
83
+ #
84
+ # @return [Boolean] true if both players are nil
85
+ def empty?
86
+ first.nil? && second.nil?
87
+ end
88
+
89
+ # Convert to hash representation.
90
+ #
91
+ # @return [Hash] Sides hash (excludes nil values)
92
+ #
93
+ # @example
94
+ # sides.to_h # => { "first" => {...}, "second" => {...} }
95
+ def to_h
96
+ hash = {}
97
+
98
+ hash["first"] = first.to_h unless first.nil?
99
+ hash["second"] = second.to_h unless second.nil?
100
+
101
+ hash
102
+ end
103
+
104
+ # String representation.
105
+ #
106
+ # @return [String] Inspectable representation
107
+ def to_s
108
+ fields = []
109
+ fields << "first=#{first.name.inspect}" if first && first.name
110
+ fields << "second=#{second.name.inspect}" if second && second.name
111
+
112
+ "#<#{self.class} #{fields.join(' ')}>"
113
+ end
114
+ alias inspect to_s
115
+
116
+ # Equality comparison.
117
+ #
118
+ # @param other [Sides] Other sides
119
+ # @return [Boolean] true if equal
120
+ def ==(other)
121
+ other.is_a?(self.class) &&
122
+ other.first == first &&
123
+ other.second == second
124
+ end
125
+ alias eql? ==
126
+
127
+ # Hash code for equality.
128
+ #
129
+ # @return [Integer] Hash code
130
+ def hash
131
+ [self.class, first, second].hash
132
+ end
133
+
134
+ private
135
+
136
+ # Parse player field.
137
+ def self.parse_player(value, field_name)
138
+ return nil if value.nil?
139
+
140
+ Player.parse(value)
141
+ rescue Error => e
142
+ raise Error::Validation, "Invalid '#{field_name}' player: #{e.message}"
143
+ end
144
+
145
+ # Normalize player to Player object.
146
+ def normalize_player(value, field_name)
147
+ return nil if value.nil?
148
+ return value if value.is_a?(Player)
149
+
150
+ Player.parse(value)
151
+ rescue Error => e
152
+ raise Error::Validation, "Invalid '#{field_name}' player: #{e.message}"
153
+ end
154
+
155
+ # Validate all fields.
156
+ def validate!
157
+ validate_structure!
158
+ validate_first!
159
+ validate_second!
160
+ end
161
+
162
+ # Validate that at least one player is defined.
163
+ def validate_structure!
164
+ return unless first.nil? && second.nil?
165
+
166
+ raise Error::Validation, "Sides must have at least one player defined"
167
+ end
168
+
169
+ # Validate first player.
170
+ def validate_first!
171
+ return if first.nil?
172
+
173
+ raise Error::Validation, "Sides 'first' must be a Player object, got #{first.class}" unless first.is_a?(Player)
174
+
175
+ return if first.valid?
176
+
177
+ raise Error::Validation, "Sides 'first' player validation failed"
178
+ end
179
+
180
+ # Validate second player.
181
+ def validate_second!
182
+ return if second.nil?
183
+
184
+ unless second.is_a?(Player)
185
+ raise Error::Validation, "Sides 'second' must be a Player object, got #{second.class}"
186
+ end
187
+
188
+ return if second.valid?
189
+
190
+ raise Error::Validation, "Sides 'second' player validation failed"
191
+ end
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
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
+ require_relative "pcn/game"
12
+
13
+ module Sashite
14
+ # PCN (Portable Chess Notation) implementation.
15
+ #
16
+ # Provides a comprehensive, rule-agnostic format for representing complete
17
+ # chess game records across variants, integrating PMN, FEEN, and SNN
18
+ # specifications.
19
+ #
20
+ # @see https://sashite.dev/specs/pcn/1.0.0/
21
+ module Pcn
22
+ # Parse a PCN hash into a Game object.
23
+ #
24
+ # @param hash [Hash] PCN document hash
25
+ # @return [Game] Immutable game object
26
+ # @raise [Error] If parsing or validation fails
27
+ #
28
+ # @example
29
+ # game = Sashite::Pcn.parse({
30
+ # "setup" => "8/8/8/8/8/8/8/8 / C/c",
31
+ # "moves" => []
32
+ # })
33
+ def self.parse(hash)
34
+ Game.parse(hash)
35
+ end
36
+
37
+ # Validate a PCN hash without raising exceptions.
38
+ #
39
+ # @param hash [Hash] PCN document hash
40
+ # @return [Boolean] true if valid, false otherwise
41
+ #
42
+ # @example
43
+ # Sashite::Pcn.valid?({ "setup" => "...", "moves" => [] }) # => true
44
+ # Sashite::Pcn.valid?({ "setup" => "" }) # => false
45
+ def self.valid?(hash)
46
+ Game.valid?(hash)
47
+ end
48
+
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)
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "sashite/pcn"
4
+
5
+ # Sashité namespace for board game notation libraries
6
+ #
7
+ # Sashité provides a collection of libraries for representing and manipulating
8
+ # board game concepts according to the Sashité Protocol specifications.
9
+ #
10
+ # @see https://sashite.dev/protocol/ Sashité Protocol
11
+ # @see https://sashite.dev/specs/ Sashité Specifications
12
+ # @author Sashité
13
+ module Sashite
14
+ end
metadata ADDED
@@ -0,0 +1,107 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sashite-pcn
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Cyril Kato
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: sashite-feen
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.3'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.3'
26
+ - !ruby/object:Gem::Dependency
27
+ name: sashite-pmn
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '1.1'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.1'
40
+ - !ruby/object:Gem::Dependency
41
+ name: sashite-snn
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.1'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3.1'
54
+ description: |
55
+ PCN (Portable Chess Notation) provides a comprehensive, JSON-based format for representing
56
+ complete chess game records across variants. This gem implements the PCN Specification v1.0.0
57
+ with a modern Ruby interface featuring immutable game objects and functional programming
58
+ principles. PCN integrates the Sashité ecosystem specifications (PMN for moves, FEEN for
59
+ positions, and SNN for style identification) to create a unified, rule-agnostic game recording
60
+ system. Supports traditional single-variant games and cross-variant scenarios where players
61
+ use different game systems, with complete metadata tracking including player information,
62
+ tournament context, and game status. Perfect for game engines, database storage, game analysis
63
+ tools, and archival systems requiring comprehensive game record management across diverse
64
+ abstract strategy board games.
65
+ email: contact@cyril.email
66
+ executables: []
67
+ extensions: []
68
+ extra_rdoc_files: []
69
+ files:
70
+ - LICENSE.md
71
+ - README.md
72
+ - lib/sashite-pcn.rb
73
+ - lib/sashite/pcn.rb
74
+ - lib/sashite/pcn/error.rb
75
+ - lib/sashite/pcn/game.rb
76
+ - lib/sashite/pcn/meta.rb
77
+ - lib/sashite/pcn/player.rb
78
+ - lib/sashite/pcn/sides.rb
79
+ homepage: https://github.com/sashite/pcn.rb
80
+ licenses:
81
+ - MIT
82
+ metadata:
83
+ bug_tracker_uri: https://github.com/sashite/pcn.rb/issues
84
+ documentation_uri: https://rubydoc.info/github/sashite/pcn.rb/main
85
+ homepage_uri: https://github.com/sashite/pcn.rb
86
+ source_code_uri: https://github.com/sashite/pcn.rb
87
+ specification_uri: https://sashite.dev/specs/pcn/1.0.0/
88
+ rubygems_mfa_required: 'true'
89
+ rdoc_options: []
90
+ require_paths:
91
+ - lib
92
+ required_ruby_version: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: 3.2.0
97
+ required_rubygems_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ requirements: []
103
+ rubygems_version: 3.6.9
104
+ specification_version: 4
105
+ summary: PCN (Portable Chess Notation) implementation for Ruby with comprehensive
106
+ game record representation
107
+ test_files: []