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.
@@ -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
- # captures = { 'CHESS:P' => 2 }
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 captures [Hash] Available pieces in hand for drops
111
- # @param turn [String] Current player's game identifier (e.g., 'CHESS', 'shogi')
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 or "*" for drops
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, {}, 'CHESS')
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, captures, 'CHESS')
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 capture moves
147
- # captures_only = piece_data.pseudo_legal_transitions(board_state, captures, turn)
148
- # .select { |actor, origin, target, variants| variants.any?(&:gain?) }
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 drop moves
151
- # drops_only = piece_data.pseudo_legal_transitions(board_state, captures, turn)
152
- # .select { |actor, origin, target, variants| origin == "*" }
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, captures, turn)
158
- def pseudo_legal_transitions(board_state, captures, turn)
159
- validate_pseudo_legal_parameters!(board_state, captures, turn)
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, turn)
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, captures, turn)
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 captures [Hash] Available pieces in hand
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" => [...] }, # Regular moves
193
- # "*" => { "e4" => [...], "f5" => [...] } # Drop moves
193
+ # "e1" => { "e2" => [...], "f1" => [...] } # Regular moves
194
194
  # }
195
- def process_actor_transitions(actor, source_data, board_state, captures, turn)
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 movement context (piece availability/position)
198
- # For drops: piece must be available in hand
199
- # For moves: piece must be present at origin square
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, captures, turn)
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 or "*" for drops
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 captures [Hash] Available pieces in hand
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, captures, turn)
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, captures, turn)
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 captures [Object] Should be a Hash mapping piece types to counts
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", {}, "CHESS")
301
- # validate_pseudo_legal_parameters!({}, "invalid", "CHESS")
302
- # validate_pseudo_legal_parameters!({}, {}, 123)
303
- # validate_pseudo_legal_parameters!({}, {}, "")
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 captures.is_a?(::Hash)
311
- raise ::ArgumentError, "captures must be a Hash, got #{captures.class}"
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
- unless turn.is_a?(::String)
315
- raise ::ArgumentError, "turn must be a String, got #{turn.class}"
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
- # Content validation - ensures meaningful data
319
- if turn.empty?
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
@@ -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 basic movement constraints rather
10
- # than game-specific legality (e.g., check, ko, repetition).
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 capture and piece gain
29
+ # @example Complex move with multiple conditions
27
30
  # {
28
- # "OGI:P": {
29
- # "e4": {
30
- # "e5": [
31
+ # "CHESS:P": {
32
+ # "d5": {
33
+ # "e6": [
31
34
  # {
32
- # "require": { "e5": "enemy" },
33
- # "perform": { "e4": null, "e5": "OGI:P" },
34
- # "gain": "OGI:P"
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 (or "*" for drops)
70
- "additionalProperties": {
71
- "type": "object",
72
- "minProperties": 1,
85
+ # Source squares: where the piece starts (regular board squares only)
86
+ "patternProperties": {
87
+ ".+": {
88
+ "type": "object",
89
+ "minProperties": 1,
73
90
 
74
- # Destination squares: where the piece can move to
75
- "additionalProperties": {
76
- "type": "array",
77
- "minItems": 0,
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
- # Conditions that MUST NOT be satisfied before the move (logical OR)
95
- "prevent": {
97
+ # Array of conditional transitions for this source->destination pair
98
+ "items": {
96
99
  "type": "object",
97
- "minProperties": 1,
98
- "additionalProperties": {
99
- "type": "string",
100
- # Same occupation states as require
101
- "pattern": "^empty$|^enemy$|([A-Z]+:[-+]?[A-Z][']?|[a-z]+:[-+]?[a-z][']?)$"
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
- # Board state changes after the move (REQUIRED field)
106
- "perform": {
107
- "type": "object",
108
- "minProperties": 1,
109
- "additionalProperties": {
110
- "anyOf": [
111
- {
112
- # Square contains a piece (GAN identifier)
113
- "type": "string",
114
- "pattern": "^([A-Z]+:[-+]?[A-Z][']?|[a-z]+:[-+]?[a-z][']?)$"
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
- # Square becomes empty (null)
118
- "type": "null"
119
- }
120
- ]
121
- }
122
- },
126
+ "additionalProperties": false
127
+ },
123
128
 
124
- # Piece added to player's hand (base GAN only, no modifiers)
125
- "gain": {
126
- "type": ["string", "null"],
127
- # Base form GAN pattern (no prefixes/suffixes for hand pieces)
128
- "pattern": "^([A-Z]+:[A-Z]|[a-z]+:[a-z])$"
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
- # Piece removed from player's hand (base GAN only, no modifiers)
132
- "drop": {
133
- "type": ["string", "null"],
134
- # Base form GAN pattern (no prefixes/suffixes for hand pieces)
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
- # Only "perform" is mandatory; other fields are optional
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
- # piece = Sashite::Ggn.load_file('invalid_moves.json')
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 basic movement constraints rather
16
- # than game-specific legality rules. It answers the fundamental question: "Can this
17
- # piece, currently on this square, reach that square?" while remaining neutral about
18
- # higher-level game rules like check, ko, repetition, or castling paths.
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
- # result = engine.evaluate(board_state, {}, 'CHESS')
84
- # puts "King can move from e1 to e2" if result
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
- # "SHOGI:K" => {
185
- # "5i" => {
186
- # "4i" => [{ "require" => { "4i" => "empty" }, "perform" => { "5i" => nil, "4i" => "SHOGI:K" } }],
187
- # "6i" => [{ "require" => { "6i" => "empty" }, "perform" => { "5i" => nil, "6i" => "SHOGI:K" } }]
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
- # shogi_king = piece_data.select('SHOGI:K')
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}"