sashite-ggn 0.7.0 → 0.8.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 +300 -562
- data/lib/sashite/ggn/ruleset/source/destination/engine.rb +120 -309
- data/lib/sashite/ggn/ruleset/source/destination.rb +46 -84
- data/lib/sashite/ggn/ruleset/source.rb +40 -73
- data/lib/sashite/ggn/ruleset.rb +183 -403
- data/lib/sashite/ggn.rb +47 -334
- data/lib/sashite-ggn.rb +8 -120
- metadata +96 -20
- data/lib/sashite/ggn/move_validator.rb +0 -208
- data/lib/sashite/ggn/ruleset/source/destination/engine/transition.rb +0 -81
- data/lib/sashite/ggn/schema.rb +0 -171
- data/lib/sashite/ggn/validation_error.rb +0 -56
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sashite-ggn
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Cyril Kato
|
@@ -10,26 +10,109 @@ cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
|
-
name:
|
13
|
+
name: sashite-cell
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
15
15
|
requirements:
|
16
16
|
- - "~>"
|
17
17
|
- !ruby/object:Gem::Version
|
18
|
-
version: 2.
|
18
|
+
version: '2.0'
|
19
19
|
type: :runtime
|
20
20
|
prerelease: false
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
22
22
|
requirements:
|
23
23
|
- - "~>"
|
24
24
|
- !ruby/object:Gem::Version
|
25
|
-
version: 2.
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
25
|
+
version: '2.0'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: sashite-epin
|
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-feen
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0.3'
|
47
|
+
type: :runtime
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0.3'
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: sashite-hand
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '1.0'
|
61
|
+
type: :runtime
|
62
|
+
prerelease: false
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '1.0'
|
68
|
+
- !ruby/object:Gem::Dependency
|
69
|
+
name: sashite-lcn
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0.1'
|
75
|
+
type: :runtime
|
76
|
+
prerelease: false
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - "~>"
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0.1'
|
82
|
+
- !ruby/object:Gem::Dependency
|
83
|
+
name: sashite-qpi
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - "~>"
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '1.0'
|
89
|
+
type: :runtime
|
90
|
+
prerelease: false
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - "~>"
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '1.0'
|
96
|
+
- !ruby/object:Gem::Dependency
|
97
|
+
name: sashite-stn
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - "~>"
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '1.0'
|
103
|
+
type: :runtime
|
104
|
+
prerelease: false
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - "~>"
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '1.0'
|
110
|
+
description: A pure functional Ruby implementation of the General Gameplay Notation
|
111
|
+
(GGN) specification v1.0.0. Provides a movement possibility oracle for evaluating
|
112
|
+
pseudo-legal moves in abstract strategy board games. Features include hierarchical
|
113
|
+
move navigation (piece → source → destination → transitions), pre-condition evaluation
|
114
|
+
(must/deny), and state transition support via STN format. Works with Chess, Shogi,
|
115
|
+
Xiangqi, and custom variants.
|
33
116
|
email: contact@cyril.email
|
34
117
|
executables: []
|
35
118
|
extensions: []
|
@@ -39,14 +122,10 @@ files:
|
|
39
122
|
- README.md
|
40
123
|
- lib/sashite-ggn.rb
|
41
124
|
- lib/sashite/ggn.rb
|
42
|
-
- lib/sashite/ggn/move_validator.rb
|
43
125
|
- lib/sashite/ggn/ruleset.rb
|
44
126
|
- lib/sashite/ggn/ruleset/source.rb
|
45
127
|
- lib/sashite/ggn/ruleset/source/destination.rb
|
46
128
|
- lib/sashite/ggn/ruleset/source/destination/engine.rb
|
47
|
-
- lib/sashite/ggn/ruleset/source/destination/engine/transition.rb
|
48
|
-
- lib/sashite/ggn/schema.rb
|
49
|
-
- lib/sashite/ggn/validation_error.rb
|
50
129
|
homepage: https://github.com/sashite/ggn.rb
|
51
130
|
licenses:
|
52
131
|
- MIT
|
@@ -55,11 +134,8 @@ metadata:
|
|
55
134
|
documentation_uri: https://rubydoc.info/github/sashite/ggn.rb/main
|
56
135
|
homepage_uri: https://github.com/sashite/ggn.rb
|
57
136
|
source_code_uri: https://github.com/sashite/ggn.rb
|
58
|
-
specification_uri: https://sashite.dev/
|
137
|
+
specification_uri: https://sashite.dev/specs/ggn/1.0.0/
|
59
138
|
rubygems_mfa_required: 'true'
|
60
|
-
keywords: board-game, chess, game, gameplay, json, makruk, notation, performance,
|
61
|
-
pseudo-legal-move, rule-agnostic, serialization, shogi, strategy, validation,
|
62
|
-
xiangqi
|
63
139
|
rdoc_options: []
|
64
140
|
require_paths:
|
65
141
|
- lib
|
@@ -76,5 +152,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
76
152
|
requirements: []
|
77
153
|
rubygems_version: 3.6.9
|
78
154
|
specification_version: 4
|
79
|
-
summary: General Gameplay Notation (GGN)
|
155
|
+
summary: General Gameplay Notation (GGN) - movement possibilities for board games
|
80
156
|
test_files: []
|
@@ -1,208 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Sashite
|
4
|
-
module Ggn
|
5
|
-
# Centralized module for move condition validation.
|
6
|
-
# Contains shared logic for validating piece ownership and board positions
|
7
|
-
# in GGN move evaluation.
|
8
|
-
#
|
9
|
-
# This module focuses exclusively on board-based validation since GGN
|
10
|
-
# only handles board-to-board transformations. All methods work with
|
11
|
-
# pieces on the board and use GAN (General Actor Notation) identifiers.
|
12
|
-
module MoveValidator
|
13
|
-
# Separator in GAN (General Actor Notation) identifiers.
|
14
|
-
# Used to split game identifiers from piece identifiers.
|
15
|
-
#
|
16
|
-
# @example GAN format
|
17
|
-
# "CHESS:K" # game: "CHESS", piece: "K"
|
18
|
-
# "shogi:+p" # game: "shogi", piece: "+p"
|
19
|
-
GAN_SEPARATOR = ":"
|
20
|
-
|
21
|
-
private
|
22
|
-
|
23
|
-
# Checks if the correct piece is present at the origin square on the board.
|
24
|
-
#
|
25
|
-
# This method validates that the expected piece is actually present at the
|
26
|
-
# specified origin square, which is a fundamental requirement for any move.
|
27
|
-
#
|
28
|
-
# @param actor [String] GAN identifier of the piece
|
29
|
-
# @param origin [String] Origin square
|
30
|
-
# @param board_state [Hash] Current board state
|
31
|
-
#
|
32
|
-
# @return [Boolean] true if the piece is at the correct position
|
33
|
-
#
|
34
|
-
# @example Valid piece placement
|
35
|
-
# board_state = { "e1" => "CHESS:K", "e2" => "CHESS:P" }
|
36
|
-
# piece_on_board_at_origin?("CHESS:K", "e1", board_state)
|
37
|
-
# # => true
|
38
|
-
#
|
39
|
-
# @example Invalid piece placement
|
40
|
-
# board_state = { "e1" => "CHESS:Q", "e2" => "CHESS:P" }
|
41
|
-
# piece_on_board_at_origin?("CHESS:K", "e1", board_state)
|
42
|
-
# # => false (wrong piece at e1)
|
43
|
-
#
|
44
|
-
# @example Empty square
|
45
|
-
# board_state = { "e1" => nil, "e2" => "CHESS:P" }
|
46
|
-
# piece_on_board_at_origin?("CHESS:K", "e1", board_state)
|
47
|
-
# # => false (no piece at e1)
|
48
|
-
def piece_on_board_at_origin?(actor, origin, board_state)
|
49
|
-
return false unless valid_gan_format?(actor)
|
50
|
-
return false unless origin.is_a?(String) && !origin.empty?
|
51
|
-
return false unless board_state.is_a?(Hash)
|
52
|
-
|
53
|
-
board_state[origin] == actor
|
54
|
-
end
|
55
|
-
|
56
|
-
# Checks if the piece belongs to the current player based on case matching.
|
57
|
-
#
|
58
|
-
# This method implements the corrected ownership logic based on FEEN specification:
|
59
|
-
# - Ownership is determined by case correspondence, not exact string matching
|
60
|
-
# - If active_game is uppercase, the player owns uppercase-cased pieces
|
61
|
-
# - If active_game is lowercase, the player owns lowercase-cased pieces
|
62
|
-
# - This allows for hybrid games where a player may control pieces from different games
|
63
|
-
#
|
64
|
-
# @param actor [String] GAN identifier of the piece
|
65
|
-
# @param active_game [String] Current player's game identifier
|
66
|
-
#
|
67
|
-
# @return [Boolean] true if the piece belongs to the current player
|
68
|
-
#
|
69
|
-
# @example Same game, same case (typical scenario)
|
70
|
-
# piece_belongs_to_current_player?("CHESS:K", "CHESS")
|
71
|
-
# # => true (both uppercase)
|
72
|
-
#
|
73
|
-
# @example Different games, same case (hybrid scenario)
|
74
|
-
# piece_belongs_to_current_player?("MAKRUK:K", "CHESS")
|
75
|
-
# # => true (both uppercase, player controls both)
|
76
|
-
#
|
77
|
-
# @example Same game, different case
|
78
|
-
# piece_belongs_to_current_player?("chess:k", "CHESS")
|
79
|
-
# # => false (different players)
|
80
|
-
#
|
81
|
-
# @example Mixed case active_game (invalid)
|
82
|
-
# piece_belongs_to_current_player?("CHESS:K", "Chess")
|
83
|
-
# # => false (invalid active_game format)
|
84
|
-
def piece_belongs_to_current_player?(actor, active_game)
|
85
|
-
return false unless valid_gan_format?(actor)
|
86
|
-
return false unless valid_game_identifier?(active_game)
|
87
|
-
|
88
|
-
game_part, piece_part = actor.split(GAN_SEPARATOR, 2)
|
89
|
-
|
90
|
-
# Determine player ownership based on case correspondence
|
91
|
-
# If active_game is uppercase, player owns uppercase pieces
|
92
|
-
# If active_game is lowercase, player owns lowercase pieces
|
93
|
-
case active_game
|
94
|
-
when active_game.upcase
|
95
|
-
# Current player is the uppercase one
|
96
|
-
game_part == game_part.upcase && piece_part.match?(/\A[-+]?[A-Z]'?\z/)
|
97
|
-
when active_game.downcase
|
98
|
-
# Current player is the lowercase one
|
99
|
-
game_part == game_part.downcase && piece_part.match?(/\A[-+]?[a-z]'?\z/)
|
100
|
-
else
|
101
|
-
# active_game is neither entirely uppercase nor lowercase
|
102
|
-
false
|
103
|
-
end
|
104
|
-
end
|
105
|
-
|
106
|
-
# Validates the GAN format of an identifier.
|
107
|
-
#
|
108
|
-
# A valid GAN identifier must:
|
109
|
-
# - Be a string containing exactly one colon separator
|
110
|
-
# - Have a valid game identifier before the colon
|
111
|
-
# - Have a valid piece identifier after the colon
|
112
|
-
# - Maintain case consistency between game and piece parts
|
113
|
-
#
|
114
|
-
# @param actor [String] Identifier to validate
|
115
|
-
#
|
116
|
-
# @return [Boolean] true if the format is valid
|
117
|
-
#
|
118
|
-
# @example Valid GAN identifiers
|
119
|
-
# valid_gan_format?("CHESS:K") # => true
|
120
|
-
# valid_gan_format?("shogi:+p") # => true
|
121
|
-
# valid_gan_format?("MAKRUK:R'") # => true
|
122
|
-
#
|
123
|
-
# @example Invalid GAN identifiers
|
124
|
-
# valid_gan_format?("CHESS") # => false (no colon)
|
125
|
-
# valid_gan_format?("chess:K") # => false (case mismatch)
|
126
|
-
# valid_gan_format?("CHESS:") # => false (no piece part)
|
127
|
-
def valid_gan_format?(actor)
|
128
|
-
return false unless actor.is_a?(String)
|
129
|
-
return false unless actor.include?(GAN_SEPARATOR)
|
130
|
-
|
131
|
-
parts = actor.split(GAN_SEPARATOR, 2)
|
132
|
-
return false unless parts.length == 2
|
133
|
-
|
134
|
-
game_part, piece_part = parts
|
135
|
-
|
136
|
-
return false unless valid_game_identifier?(game_part)
|
137
|
-
return false unless valid_piece_identifier?(piece_part)
|
138
|
-
|
139
|
-
# Case consistency verification between game and piece
|
140
|
-
game_is_upper = game_part == game_part.upcase
|
141
|
-
piece_match = piece_part.match(/\A[-+]?([A-Za-z])'?\z/)
|
142
|
-
return false unless piece_match
|
143
|
-
|
144
|
-
piece_char = piece_match[1]
|
145
|
-
piece_is_upper = piece_char == piece_char.upcase
|
146
|
-
|
147
|
-
game_is_upper == piece_is_upper
|
148
|
-
end
|
149
|
-
|
150
|
-
# Validates a game identifier.
|
151
|
-
#
|
152
|
-
# Game identifiers must be non-empty strings containing only
|
153
|
-
# alphabetic characters, either all uppercase or all lowercase.
|
154
|
-
# Mixed case is not allowed as it breaks the player distinction.
|
155
|
-
#
|
156
|
-
# @param game_id [String] Game identifier to validate
|
157
|
-
#
|
158
|
-
# @return [Boolean] true if the identifier is valid
|
159
|
-
#
|
160
|
-
# @example Valid game identifiers
|
161
|
-
# valid_game_identifier?("CHESS") # => true
|
162
|
-
# valid_game_identifier?("shogi") # => true
|
163
|
-
# valid_game_identifier?("XIANGQI") # => true
|
164
|
-
#
|
165
|
-
# @example Invalid game identifiers
|
166
|
-
# valid_game_identifier?("Chess") # => false (mixed case)
|
167
|
-
# valid_game_identifier?("") # => false (empty)
|
168
|
-
# valid_game_identifier?("CHESS1") # => false (contains digit)
|
169
|
-
def valid_game_identifier?(game_id)
|
170
|
-
return false unless game_id.is_a?(String)
|
171
|
-
return false if game_id.empty?
|
172
|
-
|
173
|
-
# Must be either entirely uppercase or entirely lowercase
|
174
|
-
game_id.match?(/\A[A-Z]+\z/) || game_id.match?(/\A[a-z]+\z/)
|
175
|
-
end
|
176
|
-
|
177
|
-
# Validates a piece identifier (part after the colon).
|
178
|
-
#
|
179
|
-
# Piece identifiers follow the pattern: [optional prefix][letter][optional suffix]
|
180
|
-
# Where:
|
181
|
-
# - Optional prefix: + or -
|
182
|
-
# - Letter: A-Z or a-z (must match game part case)
|
183
|
-
# - Optional suffix: ' (apostrophe)
|
184
|
-
#
|
185
|
-
# @param piece_id [String] Piece identifier to validate
|
186
|
-
#
|
187
|
-
# @return [Boolean] true if the identifier is valid
|
188
|
-
#
|
189
|
-
# @example Valid piece identifiers
|
190
|
-
# valid_piece_identifier?("K") # => true
|
191
|
-
# valid_piece_identifier?("+p") # => true
|
192
|
-
# valid_piece_identifier?("R'") # => true
|
193
|
-
# valid_piece_identifier?("-Q'") # => true
|
194
|
-
#
|
195
|
-
# @example Invalid piece identifiers
|
196
|
-
# valid_piece_identifier?("") # => false (empty)
|
197
|
-
# valid_piece_identifier?("++K") # => false (double prefix)
|
198
|
-
# valid_piece_identifier?("K''") # => false (double suffix)
|
199
|
-
def valid_piece_identifier?(piece_id)
|
200
|
-
return false unless piece_id.is_a?(String)
|
201
|
-
return false if piece_id.empty?
|
202
|
-
|
203
|
-
# Format: [optional prefix][letter][optional suffix]
|
204
|
-
piece_id.match?(/\A[-+]?[A-Za-z]'?\z/)
|
205
|
-
end
|
206
|
-
end
|
207
|
-
end
|
208
|
-
end
|
@@ -1,81 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Sashite
|
4
|
-
module Ggn
|
5
|
-
class Ruleset
|
6
|
-
class Source
|
7
|
-
class Destination
|
8
|
-
class Engine
|
9
|
-
# Represents the result of a valid pseudo-legal move evaluation.
|
10
|
-
#
|
11
|
-
# A Transition encapsulates the changes that occur when a move is executed
|
12
|
-
# on the game board. Since GGN focuses exclusively on board-to-board
|
13
|
-
# transformations, a Transition only contains board state changes: pieces
|
14
|
-
# moving, appearing, or disappearing on the board.
|
15
|
-
#
|
16
|
-
# @example Basic move (pawn advance)
|
17
|
-
# transition = Transition.new("e2" => nil, "e4" => "CHESS:P")
|
18
|
-
# transition.diff # => { "e2" => nil, "e4" => "CHESS:P" }
|
19
|
-
#
|
20
|
-
# @example Capture (piece takes enemy piece)
|
21
|
-
# transition = Transition.new("d4" => nil, "e5" => "CHESS:P")
|
22
|
-
# transition.diff # => { "d4" => nil, "e5" => "CHESS:P" }
|
23
|
-
#
|
24
|
-
# @example Complex move (castling with king and rook)
|
25
|
-
# transition = Transition.new(
|
26
|
-
# "e1" => nil, "f1" => "CHESS:R", "g1" => "CHESS:K", "h1" => nil
|
27
|
-
# )
|
28
|
-
# transition.diff # => { "e1" => nil, "f1" => "CHESS:R", "g1" => "CHESS:K", "h1" => nil }
|
29
|
-
#
|
30
|
-
# @example Promotion (pawn becomes queen)
|
31
|
-
# transition = Transition.new("e7" => nil, "e8" => "CHESS:Q")
|
32
|
-
# transition.diff # => { "e7" => nil, "e8" => "CHESS:Q" }
|
33
|
-
class Transition
|
34
|
-
# @return [Hash<String, String|nil>] Board state changes after the move.
|
35
|
-
# Keys are square labels, values are piece identifiers or nil for empty squares.
|
36
|
-
attr_reader :diff
|
37
|
-
|
38
|
-
# Creates a new Transition with the specified board changes.
|
39
|
-
#
|
40
|
-
# @param diff [Hash] Board state changes as keyword arguments.
|
41
|
-
# Keys should be square labels, values should be piece identifiers or nil.
|
42
|
-
#
|
43
|
-
# @example Creating a simple move transition
|
44
|
-
# Transition.new("e2" => nil, "e4" => "CHESS:P")
|
45
|
-
#
|
46
|
-
# @example Creating a capture transition
|
47
|
-
# Transition.new("d4" => nil, "e5" => "CHESS:P")
|
48
|
-
#
|
49
|
-
# @example Creating a complex multi-square transition (castling)
|
50
|
-
# Transition.new(
|
51
|
-
# "e1" => nil, # King leaves e1
|
52
|
-
# "f1" => "CHESS:R", # Rook moves to f1
|
53
|
-
# "g1" => "CHESS:K", # King moves to g1
|
54
|
-
# "h1" => nil # Rook leaves h1
|
55
|
-
# )
|
56
|
-
#
|
57
|
-
# @example Creating a promotion transition
|
58
|
-
# Transition.new("e7" => nil, "e8" => "CHESS:Q")
|
59
|
-
#
|
60
|
-
# @example Creating an en passant capture
|
61
|
-
# Transition.new(
|
62
|
-
# "d5" => nil, # Attacking pawn leaves d5
|
63
|
-
# "e5" => nil, # Captured pawn removed from e5
|
64
|
-
# "e6" => "CHESS:P" # Attacking pawn lands on e6
|
65
|
-
# )
|
66
|
-
def initialize(**diff)
|
67
|
-
@diff = diff
|
68
|
-
|
69
|
-
freeze
|
70
|
-
end
|
71
|
-
|
72
|
-
# This class remains intentionally simple and rule-agnostic.
|
73
|
-
# Any interpretation of what constitutes a "capture" or "promotion"
|
74
|
-
# is left to higher-level game logic, maintaining GGN's neutrality.
|
75
|
-
end
|
76
|
-
end
|
77
|
-
end
|
78
|
-
end
|
79
|
-
end
|
80
|
-
end
|
81
|
-
end
|
data/lib/sashite/ggn/schema.rb
DELETED
@@ -1,171 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Sashite
|
4
|
-
module Ggn
|
5
|
-
# JSON Schema for General Gameplay Notation (GGN) validation.
|
6
|
-
#
|
7
|
-
# This schema defines the structure and constraints for GGN documents,
|
8
|
-
# which describe pseudo-legal moves in abstract strategy board games.
|
9
|
-
# GGN is rule-agnostic and focuses exclusively on board-to-board transformations:
|
10
|
-
# pieces moving, capturing, or transforming on the game board.
|
11
|
-
#
|
12
|
-
# The schema has been updated to reflect GGN's focus on board transformations only.
|
13
|
-
# Hand management, piece drops, and captures-to-hand are outside the scope of GGN.
|
14
|
-
#
|
15
|
-
# @example Basic GGN document structure
|
16
|
-
# {
|
17
|
-
# "CHESS:K": {
|
18
|
-
# "e1": {
|
19
|
-
# "e2": [
|
20
|
-
# {
|
21
|
-
# "require": { "e2": "empty" },
|
22
|
-
# "perform": { "e1": null, "e2": "CHESS:K" }
|
23
|
-
# }
|
24
|
-
# ]
|
25
|
-
# }
|
26
|
-
# }
|
27
|
-
# }
|
28
|
-
#
|
29
|
-
# @example Complex move with multiple conditions
|
30
|
-
# {
|
31
|
-
# "CHESS:P": {
|
32
|
-
# "d5": {
|
33
|
-
# "e6": [
|
34
|
-
# {
|
35
|
-
# "require": { "e5": "chess:p", "e6": "empty" },
|
36
|
-
# "perform": { "d5": null, "e5": null, "e6": "CHESS:P" }
|
37
|
-
# }
|
38
|
-
# ]
|
39
|
-
# }
|
40
|
-
# }
|
41
|
-
# }
|
42
|
-
#
|
43
|
-
# @example Multi-square move (castling)
|
44
|
-
# {
|
45
|
-
# "CHESS:K": {
|
46
|
-
# "e1": {
|
47
|
-
# "g1": [
|
48
|
-
# {
|
49
|
-
# "require": { "f1": "empty", "g1": "empty", "h1": "CHESS:R" },
|
50
|
-
# "perform": { "e1": null, "f1": "CHESS:R", "g1": "CHESS:K", "h1": null }
|
51
|
-
# }
|
52
|
-
# ]
|
53
|
-
# }
|
54
|
-
# }
|
55
|
-
# }
|
56
|
-
#
|
57
|
-
# @see https://sashite.dev/documents/ggn/1.0.0/ GGN Specification
|
58
|
-
# @see https://sashite.dev/schemas/ggn/1.0.0/schema.json JSON Schema URL
|
59
|
-
Schema = {
|
60
|
-
# JSON Schema meta-information
|
61
|
-
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
62
|
-
"$id": "https://sashite.dev/schemas/ggn/1.0.0/schema.json",
|
63
|
-
"title": "General Gameplay Notation (GGN)",
|
64
|
-
"description": "JSON Schema for pseudo-legal moves in abstract board games using the GGN format. GGN focuses exclusively on board-to-board transformations.",
|
65
|
-
"type": "object",
|
66
|
-
|
67
|
-
# Optional schema reference property
|
68
|
-
"properties": {
|
69
|
-
# Allows documents to self-reference the schema
|
70
|
-
"$schema": {
|
71
|
-
"type": "string",
|
72
|
-
"format": "uri"
|
73
|
-
}
|
74
|
-
},
|
75
|
-
|
76
|
-
# Pattern-based validation for GAN (General Actor Notation) identifiers
|
77
|
-
# Matches format: GAME:piece_char (e.g., "CHESS:K'", "shogi:+p", "XIANGQI:E")
|
78
|
-
"patternProperties": {
|
79
|
-
# GAN pattern: game identifier (with casing) + colon + piece identifier
|
80
|
-
# Supports prefixes (-/+), suffixes ('), and both uppercase/lowercase games
|
81
|
-
"^([A-Z]+:[-+]?[A-Z][']?|[a-z]+:[-+]?[a-z][']?)$": {
|
82
|
-
"type": "object",
|
83
|
-
"minProperties": 1,
|
84
|
-
|
85
|
-
# Source squares: where the piece starts (regular board squares only)
|
86
|
-
"patternProperties": {
|
87
|
-
".+": {
|
88
|
-
"type": "object",
|
89
|
-
"minProperties": 1,
|
90
|
-
|
91
|
-
# Destination squares: where the piece can move to (regular board squares only)
|
92
|
-
"patternProperties": {
|
93
|
-
".+": {
|
94
|
-
"type": "array",
|
95
|
-
"minItems": 1,
|
96
|
-
|
97
|
-
# Array of conditional transitions for this source->destination pair
|
98
|
-
"items": {
|
99
|
-
"type": "object",
|
100
|
-
"properties": {
|
101
|
-
# Conditions that MUST be satisfied before the move (logical AND)
|
102
|
-
"require": {
|
103
|
-
"type": "object",
|
104
|
-
"minProperties": 1,
|
105
|
-
"patternProperties": {
|
106
|
-
".+": {
|
107
|
-
"type": "string",
|
108
|
-
# Occupation states: "empty", "enemy", or exact GAN identifier
|
109
|
-
"pattern": "^(empty|enemy|[A-Z]+:[-+]?[A-Z][']?|[a-z]+:[-+]?[a-z][']?)$"
|
110
|
-
}
|
111
|
-
},
|
112
|
-
"additionalProperties": false
|
113
|
-
},
|
114
|
-
|
115
|
-
# Conditions that MUST NOT be satisfied before the move (logical OR)
|
116
|
-
"prevent": {
|
117
|
-
"type": "object",
|
118
|
-
"minProperties": 1,
|
119
|
-
"patternProperties": {
|
120
|
-
".+": {
|
121
|
-
"type": "string",
|
122
|
-
# Same occupation states as require
|
123
|
-
"pattern": "^(empty|enemy|[A-Z]+:[-+]?[A-Z][']?|[a-z]+:[-+]?[a-z][']?)$"
|
124
|
-
}
|
125
|
-
},
|
126
|
-
"additionalProperties": false
|
127
|
-
},
|
128
|
-
|
129
|
-
# Board state changes after the move (REQUIRED field)
|
130
|
-
# This is the core of GGN: describing board transformations
|
131
|
-
"perform": {
|
132
|
-
"type": "object",
|
133
|
-
"minProperties": 1,
|
134
|
-
"patternProperties": {
|
135
|
-
".+": {
|
136
|
-
"anyOf": [
|
137
|
-
{
|
138
|
-
# Square contains a piece (GAN identifier)
|
139
|
-
"type": "string",
|
140
|
-
"pattern": "^([A-Z]+:[-+]?[A-Z][']?|[a-z]+:[-+]?[a-z][']?)$"
|
141
|
-
},
|
142
|
-
{
|
143
|
-
# Square becomes empty (null)
|
144
|
-
"type": "null"
|
145
|
-
}
|
146
|
-
]
|
147
|
-
}
|
148
|
-
},
|
149
|
-
"additionalProperties": false
|
150
|
-
}
|
151
|
-
},
|
152
|
-
|
153
|
-
# Only "perform" is mandatory; "require" and "prevent" are optional
|
154
|
-
# NOTE: "gain" and "drop" fields are no longer supported in GGN
|
155
|
-
"required": ["perform"],
|
156
|
-
"additionalProperties": false
|
157
|
-
}
|
158
|
-
}
|
159
|
-
},
|
160
|
-
"additionalProperties": false
|
161
|
-
}
|
162
|
-
},
|
163
|
-
"additionalProperties": false
|
164
|
-
}
|
165
|
-
},
|
166
|
-
|
167
|
-
# No additional properties allowed at root level (strict validation)
|
168
|
-
"additionalProperties": false
|
169
|
-
}.freeze
|
170
|
-
end
|
171
|
-
end
|
@@ -1,56 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Sashite
|
4
|
-
module Ggn
|
5
|
-
# Custom exception class for GGN validation and processing errors.
|
6
|
-
#
|
7
|
-
# This exception is raised when GGN documents fail validation against
|
8
|
-
# the JSON Schema, contain malformed data, or encounter processing errors
|
9
|
-
# during parsing and evaluation of pseudo-legal moves.
|
10
|
-
#
|
11
|
-
# Since GGN focuses exclusively on board-to-board transformations, validation
|
12
|
-
# errors typically relate to:
|
13
|
-
# - Invalid board position representations
|
14
|
-
# - Malformed GAN identifiers or square labels
|
15
|
-
# - Logical contradictions in require/prevent conditions
|
16
|
-
# - Missing or invalid perform actions
|
17
|
-
#
|
18
|
-
# Common scenarios that raise ValidationError:
|
19
|
-
# - Invalid JSON syntax in GGN files
|
20
|
-
# - Schema validation failures (missing required fields, invalid patterns)
|
21
|
-
# - File system errors (file not found, permission denied)
|
22
|
-
# - Malformed GAN identifiers or square labels
|
23
|
-
# - Logical contradictions in require/prevent conditions
|
24
|
-
# - Invalid board transformation specifications
|
25
|
-
#
|
26
|
-
# @example Handling validation errors during file loading
|
27
|
-
# begin
|
28
|
-
# piece_data = Sashite::Ggn.load_file('invalid_moves.json')
|
29
|
-
# rescue Sashite::Ggn::ValidationError => e
|
30
|
-
# puts "GGN validation failed: #{e.message}"
|
31
|
-
# # Handle the error appropriately
|
32
|
-
# end
|
33
|
-
#
|
34
|
-
# @example Handling validation errors during move evaluation
|
35
|
-
# begin
|
36
|
-
# transitions = engine.where(board_state, 'CHESS')
|
37
|
-
# rescue Sashite::Ggn::ValidationError => e
|
38
|
-
# puts "Move evaluation failed: #{e.message}"
|
39
|
-
# # Handle invalid board state or parameters
|
40
|
-
# end
|
41
|
-
#
|
42
|
-
# @example Handling schema validation errors
|
43
|
-
# begin
|
44
|
-
# Sashite::Ggn.validate!(ggn_data)
|
45
|
-
# rescue Sashite::Ggn::ValidationError => e
|
46
|
-
# puts "Schema validation failed: #{e.message}"
|
47
|
-
# # The data doesn't conform to GGN specification
|
48
|
-
# end
|
49
|
-
#
|
50
|
-
# @see Sashite::Ggn.load_file Main method that can raise this exception
|
51
|
-
# @see Sashite::Ggn.validate! Schema validation method
|
52
|
-
# @see Sashite::Ggn::Schema JSON Schema used for validation
|
53
|
-
class ValidationError < ::StandardError
|
54
|
-
end
|
55
|
-
end
|
56
|
-
end
|