sashite-ggn 0.5.0 → 0.6.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 +31 -14
- data/lib/sashite/ggn/move_validator.rb +97 -69
- data/lib/sashite/ggn/ruleset/source/destination/engine/transition.rb +41 -50
- data/lib/sashite/ggn/ruleset/source/destination/engine.rb +92 -172
- data/lib/sashite/ggn/ruleset/source/destination.rb +53 -7
- data/lib/sashite/ggn/ruleset/source.rb +42 -14
- data/lib/sashite/ggn/ruleset.rb +45 -105
- data/lib/sashite/ggn/schema.rb +96 -77
- data/lib/sashite/ggn/validation_error.rb +26 -1
- data/lib/sashite/ggn.rb +12 -11
- data/lib/sashite-ggn.rb +47 -33
- metadata +7 -6
data/lib/sashite/ggn/ruleset.rb
CHANGED
@@ -18,6 +18,9 @@ module Sashite
|
|
18
18
|
# efficient, readable, and maintainable code that avoids mutation and
|
19
19
|
# side effects.
|
20
20
|
#
|
21
|
+
# GGN focuses exclusively on board-to-board transformations. All moves
|
22
|
+
# represent pieces moving, capturing, or transforming on the game board.
|
23
|
+
#
|
21
24
|
# @example Basic usage
|
22
25
|
# piece_data = Sashite::Ggn.load_file('chess.json')
|
23
26
|
# chess_king = piece_data.select('CHESS:K')
|
@@ -36,8 +39,7 @@ module Sashite
|
|
36
39
|
#
|
37
40
|
# @example Finding all possible moves in a position
|
38
41
|
# board_state = { 'e1' => 'CHESS:K', 'e2' => 'CHESS:P', 'd1' => 'CHESS:Q' }
|
39
|
-
#
|
40
|
-
# all_moves = piece_data.pseudo_legal_transitions(board_state, captures, 'CHESS')
|
42
|
+
# all_moves = piece_data.pseudo_legal_transitions(board_state, 'CHESS')
|
41
43
|
# puts "Found #{all_moves.size} possible moves"
|
42
44
|
#
|
43
45
|
# @see https://sashite.dev/documents/gan/ GAN Specification
|
@@ -107,13 +109,13 @@ module Sashite
|
|
107
109
|
#
|
108
110
|
# @param board_state [Hash] Current board state mapping square labels
|
109
111
|
# to piece identifiers (nil for empty squares)
|
110
|
-
# @param
|
111
|
-
#
|
112
|
+
# @param active_game [String] Current player's game identifier (e.g., 'CHESS', 'shogi').
|
113
|
+
# This corresponds to the first element of the GAMES-TURN field in FEEN notation.
|
112
114
|
#
|
113
115
|
# @return [Array<Array>] List of move transitions, where each element is:
|
114
116
|
# [actor, origin, target, transitions]
|
115
117
|
# - actor [String]: GAN identifier of the moving piece
|
116
|
-
# - origin [String]: Source square
|
118
|
+
# - origin [String]: Source square
|
117
119
|
# - target [String]: Destination square
|
118
120
|
# - transitions [Array<Transition>]: All valid transition variants
|
119
121
|
#
|
@@ -121,7 +123,7 @@ module Sashite
|
|
121
123
|
#
|
122
124
|
# @example Getting all possible transitions including promotion variants
|
123
125
|
# board_state = { 'e7' => 'CHESS:P', 'e8' => nil }
|
124
|
-
# transitions = piece_data.pseudo_legal_transitions(board_state,
|
126
|
+
# transitions = piece_data.pseudo_legal_transitions(board_state, 'CHESS')
|
125
127
|
# # => [
|
126
128
|
# # ["CHESS:P", "e7", "e8", [
|
127
129
|
# # #<Transition diff={"e7"=>nil, "e8"=>"CHESS:Q"}>,
|
@@ -132,41 +134,41 @@ module Sashite
|
|
132
134
|
# # ]
|
133
135
|
#
|
134
136
|
# @example Processing grouped transitions
|
135
|
-
# transitions = piece_data.pseudo_legal_transitions(board_state,
|
137
|
+
# transitions = piece_data.pseudo_legal_transitions(board_state, 'CHESS')
|
136
138
|
# transitions.each do |actor, origin, target, variants|
|
137
139
|
# puts "#{actor} from #{origin} to #{target}:"
|
138
140
|
# variants.each_with_index do |transition, i|
|
139
141
|
# puts " Variant #{i + 1}: #{transition.diff}"
|
140
|
-
# puts " Gain: #{transition.gain}" if transition.gain?
|
141
|
-
# puts " Drop: #{transition.drop}" if transition.drop?
|
142
142
|
# end
|
143
143
|
# end
|
144
144
|
#
|
145
145
|
# @example Filtering for specific move types
|
146
|
-
# # Find all
|
147
|
-
#
|
148
|
-
# .select { |actor, origin, target, variants| variants.
|
146
|
+
# # Find all promotion moves
|
147
|
+
# promotions = piece_data.pseudo_legal_transitions(board_state, 'CHESS')
|
148
|
+
# .select { |actor, origin, target, variants| variants.size > 1 }
|
149
149
|
#
|
150
|
-
# # Find all
|
151
|
-
#
|
152
|
-
# .select { |actor, origin, target, variants|
|
150
|
+
# # Find all multi-square moves (like castling)
|
151
|
+
# complex_moves = piece_data.pseudo_legal_transitions(board_state, 'CHESS')
|
152
|
+
# .select { |actor, origin, target, variants|
|
153
|
+
# variants.any? { |t| t.diff.keys.size > 2 }
|
154
|
+
# }
|
153
155
|
#
|
154
156
|
# @example Performance considerations
|
155
157
|
# # For large datasets, consider filtering by piece type first
|
156
158
|
# specific_piece_moves = piece_data.select('CHESS:Q')
|
157
|
-
# .from('d1').to('d8').where(board_state,
|
158
|
-
def pseudo_legal_transitions(board_state,
|
159
|
-
validate_pseudo_legal_parameters!(board_state,
|
159
|
+
# .from('d1').to('d8').where(board_state, 'CHESS')
|
160
|
+
def pseudo_legal_transitions(board_state, active_game)
|
161
|
+
validate_pseudo_legal_parameters!(board_state, active_game)
|
160
162
|
|
161
163
|
# Use flat_map to process all actors and flatten the results in one pass
|
162
164
|
# This functional approach avoids mutation and intermediate arrays
|
163
165
|
@data.flat_map do |actor, source_data|
|
164
166
|
# Early filter: only process pieces belonging to current player
|
165
167
|
# This optimization significantly reduces processing time
|
166
|
-
next [] unless piece_belongs_to_current_player?(actor,
|
168
|
+
next [] unless piece_belongs_to_current_player?(actor, active_game)
|
167
169
|
|
168
170
|
# Process all source positions for this actor using functional decomposition
|
169
|
-
process_actor_transitions(actor, source_data, board_state,
|
171
|
+
process_actor_transitions(actor, source_data, board_state, active_game)
|
170
172
|
end
|
171
173
|
end
|
172
174
|
|
@@ -182,25 +184,22 @@ module Sashite
|
|
182
184
|
# @param source_data [Hash] Movement data for this piece type, mapping
|
183
185
|
# origin squares to destination data
|
184
186
|
# @param board_state [Hash] Current board state
|
185
|
-
# @param
|
186
|
-
# @param turn [String] Current player identifier
|
187
|
+
# @param active_game [String] Current player identifier
|
187
188
|
#
|
188
189
|
# @return [Array] Array of valid transition tuples for this actor
|
189
190
|
#
|
190
191
|
# @example Source data structure
|
191
192
|
# {
|
192
|
-
# "e1" => { "e2" => [...], "f1" => [...] }
|
193
|
-
# "*" => { "e4" => [...], "f5" => [...] } # Drop moves
|
193
|
+
# "e1" => { "e2" => [...], "f1" => [...] } # Regular moves
|
194
194
|
# }
|
195
|
-
def process_actor_transitions(actor, source_data, board_state,
|
195
|
+
def process_actor_transitions(actor, source_data, board_state, active_game)
|
196
196
|
source_data.flat_map do |origin, destination_data|
|
197
|
-
# Early filter: check
|
198
|
-
#
|
199
|
-
|
200
|
-
next [] unless valid_movement_context?(actor, origin, board_state, captures)
|
197
|
+
# Early filter: check piece presence at origin square
|
198
|
+
# Piece must be present at origin square for the move to be valid
|
199
|
+
next [] unless piece_on_board_at_origin?(actor, origin, board_state)
|
201
200
|
|
202
201
|
# Process all destination squares for this origin
|
203
|
-
process_origin_transitions(actor, origin, destination_data, board_state,
|
202
|
+
process_origin_transitions(actor, origin, destination_data, board_state, active_game)
|
204
203
|
end
|
205
204
|
end
|
206
205
|
|
@@ -212,11 +211,10 @@ module Sashite
|
|
212
211
|
# combine filtering and transformation operations.
|
213
212
|
#
|
214
213
|
# @param actor [String] GAN identifier of the piece
|
215
|
-
# @param origin [String] Source square
|
214
|
+
# @param origin [String] Source square
|
216
215
|
# @param destination_data [Hash] Available destinations and their transition rules
|
217
216
|
# @param board_state [Hash] Current board state
|
218
|
-
# @param
|
219
|
-
# @param turn [String] Current player identifier
|
217
|
+
# @param active_game [String] Current player identifier
|
220
218
|
#
|
221
219
|
# @return [Array] Array of valid transition tuples for this origin
|
222
220
|
#
|
@@ -229,7 +227,7 @@ module Sashite
|
|
229
227
|
# { "require" => { "f3" => "enemy" }, "perform" => { "e2" => nil, "f3" => "CHESS:P" } }
|
230
228
|
# ]
|
231
229
|
# }
|
232
|
-
def process_origin_transitions(actor, origin, destination_data, board_state,
|
230
|
+
def process_origin_transitions(actor, origin, destination_data, board_state, active_game)
|
233
231
|
destination_data.filter_map do |target, transition_rules|
|
234
232
|
# Create engine to evaluate this specific source-destination pair
|
235
233
|
# Each engine encapsulates the conditional logic for one move
|
@@ -237,7 +235,7 @@ module Sashite
|
|
237
235
|
|
238
236
|
# Get all valid transitions for this move (supports multiple variants)
|
239
237
|
# The engine handles require/prevent conditions and returns Transition objects
|
240
|
-
transitions = engine.where(board_state,
|
238
|
+
transitions = engine.where(board_state, active_game)
|
241
239
|
|
242
240
|
# Only return successful moves (with at least one valid transition)
|
243
241
|
# filter_map automatically filters out nil values
|
@@ -245,38 +243,6 @@ module Sashite
|
|
245
243
|
end
|
246
244
|
end
|
247
245
|
|
248
|
-
# Validates movement context based on origin type.
|
249
|
-
#
|
250
|
-
# This method centralizes the logic for checking piece availability and position,
|
251
|
-
# providing a clean abstraction over the different requirements for drops vs moves.
|
252
|
-
# Uses the shared MoveValidator module for consistency across the codebase.
|
253
|
-
#
|
254
|
-
# @param actor [String] GAN identifier of the piece
|
255
|
-
# @param origin [String] Source square or "*" for drops
|
256
|
-
# @param board_state [Hash] Current board state
|
257
|
-
# @param captures [Hash] Available pieces in hand
|
258
|
-
#
|
259
|
-
# @return [Boolean] true if the movement context is valid
|
260
|
-
#
|
261
|
-
# @example Drop move validation
|
262
|
-
# valid_movement_context?("SHOGI:P", "*", board_state, {"SHOGI:P" => 1})
|
263
|
-
# # => true (pawn available in hand)
|
264
|
-
#
|
265
|
-
# @example Regular move validation
|
266
|
-
# valid_movement_context?("CHESS:K", "e1", {"e1" => "CHESS:K"}, {})
|
267
|
-
# # => true (king present at e1)
|
268
|
-
def valid_movement_context?(actor, origin, board_state, captures)
|
269
|
-
if origin == DROP_ORIGIN
|
270
|
-
# For drops: piece must be available in hand
|
271
|
-
# Uses base form of piece identifier (without modifiers)
|
272
|
-
piece_available_in_hand?(actor, captures)
|
273
|
-
else
|
274
|
-
# For regular moves: piece must be on board at origin
|
275
|
-
# Ensures the exact piece is at the expected position
|
276
|
-
piece_on_board_at_origin?(actor, origin, board_state)
|
277
|
-
end
|
278
|
-
end
|
279
|
-
|
280
246
|
# Validates parameters for pseudo_legal_transitions method.
|
281
247
|
#
|
282
248
|
# Provides comprehensive validation with clear error messages for debugging.
|
@@ -284,47 +250,41 @@ module Sashite
|
|
284
250
|
# early in the processing pipeline.
|
285
251
|
#
|
286
252
|
# @param board_state [Object] Should be a Hash mapping squares to pieces
|
287
|
-
# @param
|
288
|
-
# @param turn [Object] Should be a String representing current player
|
253
|
+
# @param active_game [Object] Should be a String representing current player's game
|
289
254
|
#
|
290
255
|
# @raise [ArgumentError] If any parameter is invalid
|
291
256
|
#
|
292
257
|
# @example Valid parameters
|
293
258
|
# validate_pseudo_legal_parameters!(
|
294
259
|
# { "e1" => "CHESS:K", "e2" => nil },
|
295
|
-
# { "CHESS:P" => 2 },
|
296
260
|
# "CHESS"
|
297
261
|
# )
|
298
262
|
#
|
299
263
|
# @example Invalid parameters (raises ArgumentError)
|
300
|
-
# validate_pseudo_legal_parameters!("invalid",
|
301
|
-
# validate_pseudo_legal_parameters!({},
|
302
|
-
# validate_pseudo_legal_parameters!({},
|
303
|
-
|
304
|
-
def validate_pseudo_legal_parameters!(board_state, captures, turn)
|
264
|
+
# validate_pseudo_legal_parameters!("invalid", "CHESS")
|
265
|
+
# validate_pseudo_legal_parameters!({}, 123)
|
266
|
+
# validate_pseudo_legal_parameters!({}, "")
|
267
|
+
def validate_pseudo_legal_parameters!(board_state, active_game)
|
305
268
|
# Type validation with clear, specific error messages
|
306
269
|
unless board_state.is_a?(::Hash)
|
307
270
|
raise ::ArgumentError, "board_state must be a Hash, got #{board_state.class}"
|
308
271
|
end
|
309
272
|
|
310
|
-
unless
|
311
|
-
raise ::ArgumentError, "
|
273
|
+
unless active_game.is_a?(::String)
|
274
|
+
raise ::ArgumentError, "active_game must be a String, got #{active_game.class}"
|
312
275
|
end
|
313
276
|
|
314
|
-
|
315
|
-
|
277
|
+
# Content validation - ensures meaningful data
|
278
|
+
if active_game.empty?
|
279
|
+
raise ::ArgumentError, "active_game cannot be empty"
|
316
280
|
end
|
317
281
|
|
318
|
-
|
319
|
-
|
320
|
-
raise ::ArgumentError, "turn cannot be empty"
|
282
|
+
unless valid_game_identifier?(active_game)
|
283
|
+
raise ::ArgumentError, "Invalid active_game format: #{active_game.inspect}. Must be a valid game identifier (alphabetic characters only, e.g., 'CHESS', 'shogi')."
|
321
284
|
end
|
322
285
|
|
323
286
|
# Validate board_state structure (optional deep validation)
|
324
287
|
validate_board_state_structure!(board_state) if ENV['GGN_STRICT_VALIDATION']
|
325
|
-
|
326
|
-
# Validate captures structure (optional deep validation)
|
327
|
-
validate_captures_structure!(captures) if ENV['GGN_STRICT_VALIDATION']
|
328
288
|
end
|
329
289
|
|
330
290
|
# Validates board_state structure in strict mode.
|
@@ -346,26 +306,6 @@ module Sashite
|
|
346
306
|
end
|
347
307
|
end
|
348
308
|
end
|
349
|
-
|
350
|
-
# Validates captures structure in strict mode.
|
351
|
-
#
|
352
|
-
# This optional validation ensures that capture data follows
|
353
|
-
# the expected format with proper piece identifiers and counts.
|
354
|
-
#
|
355
|
-
# @param captures [Hash] Captures to validate
|
356
|
-
#
|
357
|
-
# @raise [ArgumentError] If captures contains invalid data
|
358
|
-
def validate_captures_structure!(captures)
|
359
|
-
captures.each do |piece, count|
|
360
|
-
unless piece.is_a?(::String) && !piece.empty?
|
361
|
-
raise ::ArgumentError, "Invalid piece in captures: #{piece.inspect}"
|
362
|
-
end
|
363
|
-
|
364
|
-
unless count.is_a?(::Integer) && count >= 0
|
365
|
-
raise ::ArgumentError, "Invalid count for #{piece}: #{count.inspect}"
|
366
|
-
end
|
367
|
-
end
|
368
|
-
end
|
369
309
|
end
|
370
310
|
end
|
371
311
|
end
|
data/lib/sashite/ggn/schema.rb
CHANGED
@@ -6,8 +6,11 @@ module Sashite
|
|
6
6
|
#
|
7
7
|
# This schema defines the structure and constraints for GGN documents,
|
8
8
|
# which describe pseudo-legal moves in abstract strategy board games.
|
9
|
-
# GGN is rule-agnostic and focuses on
|
10
|
-
#
|
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.
|
11
14
|
#
|
12
15
|
# @example Basic GGN document structure
|
13
16
|
# {
|
@@ -23,15 +26,28 @@ module Sashite
|
|
23
26
|
# }
|
24
27
|
# }
|
25
28
|
#
|
26
|
-
# @example Complex move with
|
29
|
+
# @example Complex move with multiple conditions
|
27
30
|
# {
|
28
|
-
# "
|
29
|
-
# "
|
30
|
-
# "
|
31
|
+
# "CHESS:P": {
|
32
|
+
# "d5": {
|
33
|
+
# "e6": [
|
31
34
|
# {
|
32
|
-
# "require": { "e5": "
|
33
|
-
# "perform": { "
|
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 }
|
35
51
|
# }
|
36
52
|
# ]
|
37
53
|
# }
|
@@ -45,7 +61,7 @@ module Sashite
|
|
45
61
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
46
62
|
"$id": "https://sashite.dev/schemas/ggn/1.0.0/schema.json",
|
47
63
|
"title": "General Gameplay Notation (GGN)",
|
48
|
-
"description": "JSON Schema for pseudo-legal moves in abstract board games using the GGN format.",
|
64
|
+
"description": "JSON Schema for pseudo-legal moves in abstract board games using the GGN format. GGN focuses exclusively on board-to-board transformations.",
|
49
65
|
"type": "object",
|
50
66
|
|
51
67
|
# Optional schema reference property
|
@@ -66,82 +82,85 @@ module Sashite
|
|
66
82
|
"type": "object",
|
67
83
|
"minProperties": 1,
|
68
84
|
|
69
|
-
# Source squares: where the piece starts (
|
70
|
-
"
|
71
|
-
"
|
72
|
-
|
85
|
+
# Source squares: where the piece starts (regular board squares only)
|
86
|
+
"patternProperties": {
|
87
|
+
".+": {
|
88
|
+
"type": "object",
|
89
|
+
"minProperties": 1,
|
73
90
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
# Array of conditional transitions for this source->destination pair
|
80
|
-
"items": {
|
81
|
-
"type": "object",
|
82
|
-
"properties": {
|
83
|
-
# Conditions that MUST be satisfied before the move (logical AND)
|
84
|
-
"require": {
|
85
|
-
"type": "object",
|
86
|
-
"minProperties": 1,
|
87
|
-
"additionalProperties": {
|
88
|
-
"type": "string",
|
89
|
-
# Occupation states: "empty", "enemy", or exact GAN identifier
|
90
|
-
"pattern": "^empty$|^enemy$|([A-Z]+:[-+]?[A-Z][']?|[a-z]+:[-+]?[a-z][']?)$"
|
91
|
-
}
|
92
|
-
},
|
91
|
+
# Destination squares: where the piece can move to (regular board squares only)
|
92
|
+
"patternProperties": {
|
93
|
+
".+": {
|
94
|
+
"type": "array",
|
95
|
+
"minItems": 1,
|
93
96
|
|
94
|
-
#
|
95
|
-
"
|
97
|
+
# Array of conditional transitions for this source->destination pair
|
98
|
+
"items": {
|
96
99
|
"type": "object",
|
97
|
-
"
|
98
|
-
|
99
|
-
"
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
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
|
+
},
|
104
114
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
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
|
+
}
|
115
125
|
},
|
116
|
-
|
117
|
-
|
118
|
-
"type": "null"
|
119
|
-
}
|
120
|
-
]
|
121
|
-
}
|
122
|
-
},
|
126
|
+
"additionalProperties": false
|
127
|
+
},
|
123
128
|
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
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
|
+
},
|
130
152
|
|
131
|
-
|
132
|
-
|
133
|
-
"
|
134
|
-
|
135
|
-
"pattern": "^([A-Z]+:[A-Z]|[a-z]+:[a-z])$"
|
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
|
136
157
|
}
|
137
|
-
}
|
138
|
-
|
139
|
-
|
140
|
-
"required": ["perform"],
|
141
|
-
"additionalProperties": false
|
142
|
-
}
|
158
|
+
}
|
159
|
+
},
|
160
|
+
"additionalProperties": false
|
143
161
|
}
|
144
|
-
}
|
162
|
+
},
|
163
|
+
"additionalProperties": false
|
145
164
|
}
|
146
165
|
},
|
147
166
|
|
@@ -8,22 +8,47 @@ module Sashite
|
|
8
8
|
# the JSON Schema, contain malformed data, or encounter processing errors
|
9
9
|
# during parsing and evaluation of pseudo-legal moves.
|
10
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
|
+
#
|
11
18
|
# Common scenarios that raise ValidationError:
|
12
19
|
# - Invalid JSON syntax in GGN files
|
13
20
|
# - Schema validation failures (missing required fields, invalid patterns)
|
14
21
|
# - File system errors (file not found, permission denied)
|
15
22
|
# - Malformed GAN identifiers or square labels
|
16
23
|
# - Logical contradictions in require/prevent conditions
|
24
|
+
# - Invalid board transformation specifications
|
17
25
|
#
|
18
26
|
# @example Handling validation errors during file loading
|
19
27
|
# begin
|
20
|
-
#
|
28
|
+
# piece_data = Sashite::Ggn.load_file('invalid_moves.json')
|
21
29
|
# rescue Sashite::Ggn::ValidationError => e
|
22
30
|
# puts "GGN validation failed: #{e.message}"
|
23
31
|
# # Handle the error appropriately
|
24
32
|
# end
|
25
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
|
+
#
|
26
50
|
# @see Sashite::Ggn.load_file Main method that can raise this exception
|
51
|
+
# @see Sashite::Ggn.validate! Schema validation method
|
27
52
|
# @see Sashite::Ggn::Schema JSON Schema used for validation
|
28
53
|
class ValidationError < ::StandardError
|
29
54
|
end
|
data/lib/sashite/ggn.rb
CHANGED
@@ -12,14 +12,15 @@ module Sashite
|
|
12
12
|
# General Gameplay Notation (GGN) module for parsing, validating, and working with
|
13
13
|
# JSON documents that describe pseudo-legal moves in abstract strategy board games.
|
14
14
|
#
|
15
|
-
# GGN is a rule-agnostic format that focuses on
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
15
|
+
# GGN is a rule-agnostic format that focuses exclusively on board-to-board transformations.
|
16
|
+
# It answers the fundamental question: "Can this piece, currently on this square, reach
|
17
|
+
# that square?" while remaining neutral about higher-level game rules like check, ko,
|
18
|
+
# repetition, or castling paths.
|
19
19
|
#
|
20
20
|
# = Key Features
|
21
21
|
#
|
22
22
|
# - **Rule-agnostic**: Works with any abstract strategy board game
|
23
|
+
# - **Board-focused**: Describes only board transformations, no hand management
|
23
24
|
# - **Pseudo-legal** focus: Describes basic movement constraints only
|
24
25
|
# - **JSON-based**: Structured, machine-readable format
|
25
26
|
# - **Validation** support: Built-in schema validation
|
@@ -80,8 +81,8 @@ module Sashite
|
|
80
81
|
# engine = destinations.to('e2')
|
81
82
|
#
|
82
83
|
# board_state = { 'e1' => 'CHESS:K', 'e2' => nil }
|
83
|
-
#
|
84
|
-
# puts "King can move from e1 to e2" if
|
84
|
+
# transitions = engine.where(board_state, 'CHESS')
|
85
|
+
# puts "King can move from e1 to e2" if transitions.any?
|
85
86
|
# rescue Sashite::Ggn::ValidationError => e
|
86
87
|
# puts "Failed to process move: #{e.message}"
|
87
88
|
# end
|
@@ -181,16 +182,16 @@ module Sashite
|
|
181
182
|
#
|
182
183
|
# @example Creating from existing Hash data
|
183
184
|
# ggn_data = {
|
184
|
-
# "
|
185
|
-
# "
|
186
|
-
# "
|
187
|
-
# "
|
185
|
+
# "CHESS:K" => {
|
186
|
+
# "e1" => {
|
187
|
+
# "e2" => [{ "require" => { "e2" => "empty" }, "perform" => { "e1" => nil, "e2" => "CHESS:K" } }],
|
188
|
+
# "f1" => [{ "require" => { "f1" => "empty" }, "perform" => { "e1" => nil, "f1" => "CHESS:K" } }]
|
188
189
|
# }
|
189
190
|
# }
|
190
191
|
# }
|
191
192
|
#
|
192
193
|
# piece_data = Sashite::Ggn.load_hash(ggn_data)
|
193
|
-
#
|
194
|
+
# chess_king = piece_data.select('CHESS:K')
|
194
195
|
def load_hash(data, validate: true)
|
195
196
|
unless data.is_a?(Hash)
|
196
197
|
raise ValidationError, "Expected Hash, got #{data.class}"
|