sashite-ggn 0.3.0 → 0.5.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 +63 -346
- data/lib/sashite/ggn/move_validator.rb +180 -0
- data/lib/sashite/ggn/{piece → ruleset}/source/destination/engine/transition.rb +1 -1
- data/lib/sashite/ggn/{piece → ruleset}/source/destination/engine.rb +143 -96
- data/lib/sashite/ggn/{piece → ruleset}/source/destination.rb +1 -1
- data/lib/sashite/ggn/{piece → ruleset}/source.rb +1 -1
- data/lib/sashite/ggn/ruleset.rb +371 -0
- data/lib/sashite/ggn.rb +25 -25
- data/lib/sashite-ggn.rb +3 -3
- metadata +7 -6
- data/lib/sashite/ggn/piece.rb +0 -77
@@ -0,0 +1,371 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "move_validator"
|
4
|
+
require_relative File.join("ruleset", "source")
|
5
|
+
|
6
|
+
module Sashite
|
7
|
+
module Ggn
|
8
|
+
# Represents a collection of piece definitions from a GGN document.
|
9
|
+
#
|
10
|
+
# A Ruleset instance contains all the pseudo-legal move definitions for
|
11
|
+
# various game pieces, organized by their GAN (General Actor Notation)
|
12
|
+
# identifiers. This class provides the entry point for querying specific
|
13
|
+
# piece movement rules and generating all possible transitions for a given
|
14
|
+
# game state.
|
15
|
+
#
|
16
|
+
# The class uses functional programming principles throughout, leveraging
|
17
|
+
# Ruby's Enumerable methods (flat_map, filter_map, select) to create
|
18
|
+
# efficient, readable, and maintainable code that avoids mutation and
|
19
|
+
# side effects.
|
20
|
+
#
|
21
|
+
# @example Basic usage
|
22
|
+
# piece_data = Sashite::Ggn.load_file('chess.json')
|
23
|
+
# chess_king = piece_data.select('CHESS:K')
|
24
|
+
# shogi_pawn = piece_data.select('SHOGI:P')
|
25
|
+
#
|
26
|
+
# @example Complete workflow
|
27
|
+
# piece_data = Sashite::Ggn.load_file('game_moves.json')
|
28
|
+
#
|
29
|
+
# # Query specific piece moves
|
30
|
+
# begin
|
31
|
+
# king_source = piece_data.select('CHESS:K')
|
32
|
+
# puts "Found chess king movement rules"
|
33
|
+
# rescue KeyError
|
34
|
+
# puts "Chess king not found in this dataset"
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# @example Finding all possible moves in a position
|
38
|
+
# 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')
|
41
|
+
# puts "Found #{all_moves.size} possible moves"
|
42
|
+
#
|
43
|
+
# @see https://sashite.dev/documents/gan/ GAN Specification
|
44
|
+
# @see https://sashite.dev/documents/ggn/ GGN Specification
|
45
|
+
class Ruleset
|
46
|
+
include MoveValidator
|
47
|
+
|
48
|
+
# Creates a new Ruleset instance from GGN data.
|
49
|
+
#
|
50
|
+
# @param data [Hash] The parsed GGN JSON data structure, where keys are
|
51
|
+
# GAN identifiers and values contain the movement definitions.
|
52
|
+
#
|
53
|
+
# @raise [ArgumentError] If data is not a Hash
|
54
|
+
#
|
55
|
+
# @example Creating from parsed JSON data
|
56
|
+
# ggn_data = JSON.parse(File.read('chess.json'))
|
57
|
+
# ruleset = Ruleset.new(ggn_data)
|
58
|
+
def initialize(data)
|
59
|
+
raise ::ArgumentError, "Expected Hash, got #{data.class}" unless data.is_a?(::Hash)
|
60
|
+
|
61
|
+
@data = data
|
62
|
+
|
63
|
+
freeze
|
64
|
+
end
|
65
|
+
|
66
|
+
# Retrieves movement rules for a specific piece type.
|
67
|
+
#
|
68
|
+
# @param actor [String] The GAN identifier for the piece type
|
69
|
+
# (e.g., 'CHESS:K', 'SHOGI:P', 'chess:q'). Must match exactly
|
70
|
+
# including case sensitivity.
|
71
|
+
#
|
72
|
+
# @return [Source] A Source instance containing all movement rules
|
73
|
+
# for this piece type from different board positions.
|
74
|
+
#
|
75
|
+
# @raise [KeyError] If the actor is not found in the GGN data
|
76
|
+
#
|
77
|
+
# @example Fetching chess king moves
|
78
|
+
# source = piece_data.select('CHESS:K')
|
79
|
+
# destinations = source.from('e1')
|
80
|
+
# engine = destinations.to('e2')
|
81
|
+
#
|
82
|
+
# @example Handling missing pieces
|
83
|
+
# begin
|
84
|
+
# moves = piece_data.select('NONEXISTENT:X')
|
85
|
+
# rescue KeyError => e
|
86
|
+
# puts "Piece not found: #{e.message}"
|
87
|
+
# end
|
88
|
+
#
|
89
|
+
# @note The actor format must follow GAN specification:
|
90
|
+
# GAME:piece (e.g., CHESS:K, shogi:p, XIANGQI:E)
|
91
|
+
def select(actor)
|
92
|
+
data = @data.fetch(actor)
|
93
|
+
Source.new(data, actor:)
|
94
|
+
end
|
95
|
+
|
96
|
+
# Returns all pseudo-legal move transitions for the given position.
|
97
|
+
#
|
98
|
+
# This method traverses all actors defined in the GGN data using a functional
|
99
|
+
# approach with flat_map and filter_map to efficiently process and filter
|
100
|
+
# valid moves. Each result contains the complete transition information
|
101
|
+
# including all variants for moves with multiple outcomes (e.g., promotion choices).
|
102
|
+
#
|
103
|
+
# The implementation uses a three-level functional decomposition:
|
104
|
+
# 1. Process each actor (piece type) that belongs to current player
|
105
|
+
# 2. Process each valid origin position for that actor
|
106
|
+
# 3. Process each destination and evaluate transition rules
|
107
|
+
#
|
108
|
+
# @param board_state [Hash] Current board state mapping square labels
|
109
|
+
# 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
|
+
#
|
113
|
+
# @return [Array<Array>] List of move transitions, where each element is:
|
114
|
+
# [actor, origin, target, transitions]
|
115
|
+
# - actor [String]: GAN identifier of the moving piece
|
116
|
+
# - origin [String]: Source square or "*" for drops
|
117
|
+
# - target [String]: Destination square
|
118
|
+
# - transitions [Array<Transition>]: All valid transition variants
|
119
|
+
#
|
120
|
+
# @raise [ArgumentError] If any parameter is invalid or malformed
|
121
|
+
#
|
122
|
+
# @example Getting all possible transitions including promotion variants
|
123
|
+
# board_state = { 'e7' => 'CHESS:P', 'e8' => nil }
|
124
|
+
# transitions = piece_data.pseudo_legal_transitions(board_state, {}, 'CHESS')
|
125
|
+
# # => [
|
126
|
+
# # ["CHESS:P", "e7", "e8", [
|
127
|
+
# # #<Transition diff={"e7"=>nil, "e8"=>"CHESS:Q"}>,
|
128
|
+
# # #<Transition diff={"e7"=>nil, "e8"=>"CHESS:R"}>,
|
129
|
+
# # #<Transition diff={"e7"=>nil, "e8"=>"CHESS:B"}>,
|
130
|
+
# # #<Transition diff={"e7"=>nil, "e8"=>"CHESS:N"}>
|
131
|
+
# # ]]
|
132
|
+
# # ]
|
133
|
+
#
|
134
|
+
# @example Processing grouped transitions
|
135
|
+
# transitions = piece_data.pseudo_legal_transitions(board_state, captures, 'CHESS')
|
136
|
+
# transitions.each do |actor, origin, target, variants|
|
137
|
+
# puts "#{actor} from #{origin} to #{target}:"
|
138
|
+
# variants.each_with_index do |transition, i|
|
139
|
+
# puts " Variant #{i + 1}: #{transition.diff}"
|
140
|
+
# puts " Gain: #{transition.gain}" if transition.gain?
|
141
|
+
# puts " Drop: #{transition.drop}" if transition.drop?
|
142
|
+
# end
|
143
|
+
# end
|
144
|
+
#
|
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?) }
|
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 == "*" }
|
153
|
+
#
|
154
|
+
# @example Performance considerations
|
155
|
+
# # For large datasets, consider filtering by piece type first
|
156
|
+
# 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)
|
160
|
+
|
161
|
+
# Use flat_map to process all actors and flatten the results in one pass
|
162
|
+
# This functional approach avoids mutation and intermediate arrays
|
163
|
+
@data.flat_map do |actor, source_data|
|
164
|
+
# Early filter: only process pieces belonging to current player
|
165
|
+
# This optimization significantly reduces processing time
|
166
|
+
next [] unless piece_belongs_to_current_player?(actor, turn)
|
167
|
+
|
168
|
+
# Process all source positions for this actor using functional decomposition
|
169
|
+
process_actor_transitions(actor, source_data, board_state, captures, turn)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
private
|
174
|
+
|
175
|
+
# Processes all possible transitions for a single actor (piece type).
|
176
|
+
#
|
177
|
+
# This method represents the second level of functional decomposition,
|
178
|
+
# handling all source positions (origins) for a given piece type.
|
179
|
+
# It uses flat_map to efficiently process each origin and flatten the results.
|
180
|
+
#
|
181
|
+
# @param actor [String] GAN identifier of the piece type
|
182
|
+
# @param source_data [Hash] Movement data for this piece type, mapping
|
183
|
+
# origin squares to destination data
|
184
|
+
# @param board_state [Hash] Current board state
|
185
|
+
# @param captures [Hash] Available pieces in hand
|
186
|
+
# @param turn [String] Current player identifier
|
187
|
+
#
|
188
|
+
# @return [Array] Array of valid transition tuples for this actor
|
189
|
+
#
|
190
|
+
# @example Source data structure
|
191
|
+
# {
|
192
|
+
# "e1" => { "e2" => [...], "f1" => [...] }, # Regular moves
|
193
|
+
# "*" => { "e4" => [...], "f5" => [...] } # Drop moves
|
194
|
+
# }
|
195
|
+
def process_actor_transitions(actor, source_data, board_state, captures, turn)
|
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)
|
201
|
+
|
202
|
+
# Process all destination squares for this origin
|
203
|
+
process_origin_transitions(actor, origin, destination_data, board_state, captures, turn)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
# Processes all possible transitions from a single origin square.
|
208
|
+
#
|
209
|
+
# This method represents the third level of functional decomposition,
|
210
|
+
# handling all destination squares from a given origin. It creates
|
211
|
+
# engines to evaluate each move and uses filter_map to efficiently
|
212
|
+
# combine filtering and transformation operations.
|
213
|
+
#
|
214
|
+
# @param actor [String] GAN identifier of the piece
|
215
|
+
# @param origin [String] Source square or "*" for drops
|
216
|
+
# @param destination_data [Hash] Available destinations and their transition rules
|
217
|
+
# @param board_state [Hash] Current board state
|
218
|
+
# @param captures [Hash] Available pieces in hand
|
219
|
+
# @param turn [String] Current player identifier
|
220
|
+
#
|
221
|
+
# @return [Array] Array of valid transition tuples for this origin
|
222
|
+
#
|
223
|
+
# @example Destination data structure
|
224
|
+
# {
|
225
|
+
# "e4" => [
|
226
|
+
# { "require" => { "e4" => "empty" }, "perform" => { "e2" => nil, "e4" => "CHESS:P" } }
|
227
|
+
# ],
|
228
|
+
# "f3" => [
|
229
|
+
# { "require" => { "f3" => "enemy" }, "perform" => { "e2" => nil, "f3" => "CHESS:P" } }
|
230
|
+
# ]
|
231
|
+
# }
|
232
|
+
def process_origin_transitions(actor, origin, destination_data, board_state, captures, turn)
|
233
|
+
destination_data.filter_map do |target, transition_rules|
|
234
|
+
# Create engine to evaluate this specific source-destination pair
|
235
|
+
# Each engine encapsulates the conditional logic for one move
|
236
|
+
engine = Source::Destination::Engine.new(*transition_rules, actor: actor, origin: origin, target: target)
|
237
|
+
|
238
|
+
# Get all valid transitions for this move (supports multiple variants)
|
239
|
+
# The engine handles require/prevent conditions and returns Transition objects
|
240
|
+
transitions = engine.where(board_state, captures, turn)
|
241
|
+
|
242
|
+
# Only return successful moves (with at least one valid transition)
|
243
|
+
# filter_map automatically filters out nil values
|
244
|
+
[actor, origin, target, transitions] unless transitions.empty?
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
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
|
+
# Validates parameters for pseudo_legal_transitions method.
|
281
|
+
#
|
282
|
+
# Provides comprehensive validation with clear error messages for debugging.
|
283
|
+
# This method ensures data integrity and helps catch common usage errors
|
284
|
+
# early in the processing pipeline.
|
285
|
+
#
|
286
|
+
# @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
|
289
|
+
#
|
290
|
+
# @raise [ArgumentError] If any parameter is invalid
|
291
|
+
#
|
292
|
+
# @example Valid parameters
|
293
|
+
# validate_pseudo_legal_parameters!(
|
294
|
+
# { "e1" => "CHESS:K", "e2" => nil },
|
295
|
+
# { "CHESS:P" => 2 },
|
296
|
+
# "CHESS"
|
297
|
+
# )
|
298
|
+
#
|
299
|
+
# @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)
|
305
|
+
# Type validation with clear, specific error messages
|
306
|
+
unless board_state.is_a?(::Hash)
|
307
|
+
raise ::ArgumentError, "board_state must be a Hash, got #{board_state.class}"
|
308
|
+
end
|
309
|
+
|
310
|
+
unless captures.is_a?(::Hash)
|
311
|
+
raise ::ArgumentError, "captures must be a Hash, got #{captures.class}"
|
312
|
+
end
|
313
|
+
|
314
|
+
unless turn.is_a?(::String)
|
315
|
+
raise ::ArgumentError, "turn must be a String, got #{turn.class}"
|
316
|
+
end
|
317
|
+
|
318
|
+
# Content validation - ensures meaningful data
|
319
|
+
if turn.empty?
|
320
|
+
raise ::ArgumentError, "turn cannot be empty"
|
321
|
+
end
|
322
|
+
|
323
|
+
# Validate board_state structure (optional deep validation)
|
324
|
+
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
|
+
end
|
329
|
+
|
330
|
+
# Validates board_state structure in strict mode.
|
331
|
+
#
|
332
|
+
# This optional validation can be enabled via environment variable
|
333
|
+
# to catch malformed board states during development and testing.
|
334
|
+
#
|
335
|
+
# @param board_state [Hash] Board state to validate
|
336
|
+
#
|
337
|
+
# @raise [ArgumentError] If board_state contains invalid data
|
338
|
+
def validate_board_state_structure!(board_state)
|
339
|
+
board_state.each do |square, piece|
|
340
|
+
unless square.is_a?(::String) && !square.empty?
|
341
|
+
raise ::ArgumentError, "Invalid square label: #{square.inspect}"
|
342
|
+
end
|
343
|
+
|
344
|
+
if piece && (!piece.is_a?(::String) || piece.empty?)
|
345
|
+
raise ::ArgumentError, "Invalid piece at #{square}: #{piece.inspect}"
|
346
|
+
end
|
347
|
+
end
|
348
|
+
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
|
+
end
|
370
|
+
end
|
371
|
+
end
|
data/lib/sashite/ggn.rb
CHANGED
@@ -4,7 +4,7 @@ require 'json'
|
|
4
4
|
require 'json_schemer'
|
5
5
|
require 'pathname'
|
6
6
|
|
7
|
-
require_relative File.join("ggn", "
|
7
|
+
require_relative File.join("ggn", "ruleset")
|
8
8
|
require_relative File.join("ggn", "schema")
|
9
9
|
require_relative File.join("ggn", "validation_error")
|
10
10
|
|
@@ -17,16 +17,16 @@ module Sashite
|
|
17
17
|
# piece, currently on this square, reach that square?" while remaining neutral about
|
18
18
|
# higher-level game rules like check, ko, repetition, or castling paths.
|
19
19
|
#
|
20
|
-
#
|
20
|
+
# = Key Features
|
21
21
|
#
|
22
22
|
# - **Rule-agnostic**: Works with any abstract strategy board game
|
23
|
-
# - **Pseudo-legal focus
|
23
|
+
# - **Pseudo-legal** focus: Describes basic movement constraints only
|
24
24
|
# - **JSON-based**: Structured, machine-readable format
|
25
|
-
# - **Validation support
|
26
|
-
# - **Performance optimized
|
27
|
-
# - **Cross-game compatible
|
25
|
+
# - **Validation** support: Built-in schema validation
|
26
|
+
# - **Performance** optimized: Optional validation for large datasets
|
27
|
+
# - **Cross-game** compatible: Supports hybrid games and variants
|
28
28
|
#
|
29
|
-
#
|
29
|
+
# = Related Specifications
|
30
30
|
#
|
31
31
|
# GGN works alongside other Sashité specifications:
|
32
32
|
# - **GAN** (General Actor Notation): Unique piece identifiers
|
@@ -45,7 +45,7 @@ module Sashite
|
|
45
45
|
# 1. Reads the JSON file from the filesystem with proper encoding
|
46
46
|
# 2. Parses the JSON content into a Ruby Hash with error handling
|
47
47
|
# 3. Optionally validates the structure against the GGN JSON Schema
|
48
|
-
# 4. Creates and returns a
|
48
|
+
# 4. Creates and returns a Ruleset instance for querying moves
|
49
49
|
#
|
50
50
|
# @param filepath [String, Pathname] Path to the GGN JSON file to load.
|
51
51
|
# Supports both relative and absolute paths.
|
@@ -54,7 +54,7 @@ module Sashite
|
|
54
54
|
# @param encoding [String] File encoding to use when reading (default: 'UTF-8').
|
55
55
|
# Most GGN files should use UTF-8 encoding.
|
56
56
|
#
|
57
|
-
# @return [
|
57
|
+
# @return [Ruleset] A Ruleset instance containing the parsed and validated GGN data.
|
58
58
|
# Use this instance to query pseudo-legal moves for specific pieces and positions.
|
59
59
|
#
|
60
60
|
# @raise [ValidationError] If any of the following conditions occur:
|
@@ -66,7 +66,7 @@ module Sashite
|
|
66
66
|
# @example Loading a chess piece definition with full validation
|
67
67
|
# begin
|
68
68
|
# piece_data = Sashite::Ggn.load_file('data/chess_pieces.json')
|
69
|
-
# chess_king_source = piece_data.
|
69
|
+
# chess_king_source = piece_data.select('CHESS:K')
|
70
70
|
# puts "Loaded chess king movement rules successfully"
|
71
71
|
# rescue Sashite::Ggn::ValidationError => e
|
72
72
|
# puts "Failed to load chess pieces: #{e.message}"
|
@@ -75,9 +75,9 @@ module Sashite
|
|
75
75
|
# @example Complete workflow with move evaluation
|
76
76
|
# begin
|
77
77
|
# piece_data = Sashite::Ggn.load_file('data/chess.json')
|
78
|
-
# source = piece_data.
|
79
|
-
# destinations = source.
|
80
|
-
# engine = destinations.
|
78
|
+
# source = piece_data.select('CHESS:K')
|
79
|
+
# destinations = source.from('e1')
|
80
|
+
# engine = destinations.to('e2')
|
81
81
|
#
|
82
82
|
# board_state = { 'e1' => 'CHESS:K', 'e2' => nil }
|
83
83
|
# result = engine.evaluate(board_state, {}, 'CHESS')
|
@@ -122,8 +122,8 @@ module Sashite
|
|
122
122
|
# Validate against GGN schema if requested
|
123
123
|
validate_schema(data, file_path) if validate
|
124
124
|
|
125
|
-
# Create and return
|
126
|
-
|
125
|
+
# Create and return Ruleset instance
|
126
|
+
Ruleset.new(data)
|
127
127
|
end
|
128
128
|
|
129
129
|
# Loads GGN data directly from a JSON string.
|
@@ -134,7 +134,7 @@ module Sashite
|
|
134
134
|
# @param json_string [String] JSON string containing GGN data
|
135
135
|
# @param validate [Boolean] Whether to validate against GGN schema (default: true)
|
136
136
|
#
|
137
|
-
# @return [
|
137
|
+
# @return [Ruleset] A Ruleset instance containing the parsed GGN data
|
138
138
|
#
|
139
139
|
# @raise [ValidationError] If the JSON is invalid or doesn't conform to GGN schema
|
140
140
|
#
|
@@ -143,7 +143,7 @@ module Sashite
|
|
143
143
|
#
|
144
144
|
# begin
|
145
145
|
# piece_data = Sashite::Ggn.load_string(ggn_json)
|
146
|
-
# pawn_source = piece_data.
|
146
|
+
# pawn_source = piece_data.select('CHESS:P')
|
147
147
|
# puts "Loaded pawn with move from e2 to e4"
|
148
148
|
# rescue Sashite::Ggn::ValidationError => e
|
149
149
|
# puts "Invalid GGN data: #{e.message}"
|
@@ -163,19 +163,19 @@ module Sashite
|
|
163
163
|
# Validate against GGN schema if requested
|
164
164
|
validate_schema(data, "<string>") if validate
|
165
165
|
|
166
|
-
# Create and return
|
167
|
-
|
166
|
+
# Create and return Ruleset instance
|
167
|
+
Ruleset.new(data)
|
168
168
|
end
|
169
169
|
|
170
170
|
# Loads GGN data from a Ruby Hash.
|
171
171
|
#
|
172
172
|
# This method is useful when you already have parsed JSON data as a Hash
|
173
|
-
# and want to create a GGN
|
173
|
+
# and want to create a GGN Ruleset instance with optional validation.
|
174
174
|
#
|
175
175
|
# @param data [Hash] Ruby Hash containing GGN data structure
|
176
176
|
# @param validate [Boolean] Whether to validate against GGN schema (default: true)
|
177
177
|
#
|
178
|
-
# @return [
|
178
|
+
# @return [Ruleset] A Ruleset instance containing the GGN data
|
179
179
|
#
|
180
180
|
# @raise [ValidationError] If the data doesn't conform to GGN schema (when validation enabled)
|
181
181
|
#
|
@@ -190,7 +190,7 @@ module Sashite
|
|
190
190
|
# }
|
191
191
|
#
|
192
192
|
# piece_data = Sashite::Ggn.load_hash(ggn_data)
|
193
|
-
# shogi_king = piece_data.
|
193
|
+
# shogi_king = piece_data.select('SHOGI:K')
|
194
194
|
def load_hash(data, validate: true)
|
195
195
|
unless data.is_a?(Hash)
|
196
196
|
raise ValidationError, "Expected Hash, got #{data.class}"
|
@@ -199,14 +199,14 @@ module Sashite
|
|
199
199
|
# Validate against GGN schema if requested
|
200
200
|
validate_schema(data, "<hash>") if validate
|
201
201
|
|
202
|
-
# Create and return
|
203
|
-
|
202
|
+
# Create and return Ruleset instance
|
203
|
+
Ruleset.new(data)
|
204
204
|
end
|
205
205
|
|
206
206
|
# Validates a data structure against the GGN JSON Schema.
|
207
207
|
#
|
208
208
|
# This method can be used independently to validate GGN data without
|
209
|
-
# creating a
|
209
|
+
# creating a Ruleset instance. Useful for pre-validation or testing.
|
210
210
|
#
|
211
211
|
# @param data [Hash] The data structure to validate
|
212
212
|
# @param context [String] Context information for error messages (default: "<data>")
|
data/lib/sashite-ggn.rb
CHANGED
@@ -21,7 +21,7 @@
|
|
21
21
|
# require "sashite/ggn"
|
22
22
|
#
|
23
23
|
# piece_data = Sashite::Ggn.load_file("chess_moves.json")
|
24
|
-
# engine = piece_data.
|
24
|
+
# engine = piece_data.select("CHESS:P").from("e2").to("e4")
|
25
25
|
#
|
26
26
|
# # Check if the move is valid given current board state
|
27
27
|
# board_state = {
|
@@ -45,7 +45,7 @@
|
|
45
45
|
# @example Piece drops in Shogi
|
46
46
|
# # Shogi allows captured pieces to be dropped back onto the board
|
47
47
|
# piece_data = Sashite::Ggn.load_file("shogi_moves.json")
|
48
|
-
# engine = piece_data.
|
48
|
+
# engine = piece_data.select("SHOGI:P").from("*").to("5e")
|
49
49
|
#
|
50
50
|
# # Player has captured pawns available
|
51
51
|
# captures = { "SHOGI:P" => 2 }
|
@@ -68,7 +68,7 @@
|
|
68
68
|
# @example Captures with piece promotion
|
69
69
|
# # A chess pawn capturing and promoting to queen
|
70
70
|
# piece_data = Sashite::Ggn.load_file("chess_moves.json")
|
71
|
-
# engine = piece_data.
|
71
|
+
# engine = piece_data.select("CHESS:P").from("g7").to("h8")
|
72
72
|
#
|
73
73
|
# # Board with enemy piece on h8
|
74
74
|
# board_state = {
|
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.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Cyril Kato
|
@@ -37,11 +37,12 @@ files:
|
|
37
37
|
- README.md
|
38
38
|
- lib/sashite-ggn.rb
|
39
39
|
- lib/sashite/ggn.rb
|
40
|
-
- lib/sashite/ggn/
|
41
|
-
- lib/sashite/ggn/
|
42
|
-
- lib/sashite/ggn/
|
43
|
-
- lib/sashite/ggn/
|
44
|
-
- lib/sashite/ggn/
|
40
|
+
- lib/sashite/ggn/move_validator.rb
|
41
|
+
- lib/sashite/ggn/ruleset.rb
|
42
|
+
- lib/sashite/ggn/ruleset/source.rb
|
43
|
+
- lib/sashite/ggn/ruleset/source/destination.rb
|
44
|
+
- lib/sashite/ggn/ruleset/source/destination/engine.rb
|
45
|
+
- lib/sashite/ggn/ruleset/source/destination/engine/transition.rb
|
45
46
|
- lib/sashite/ggn/schema.rb
|
46
47
|
- lib/sashite/ggn/validation_error.rb
|
47
48
|
homepage: https://github.com/sashite/ggn.rb
|
data/lib/sashite/ggn/piece.rb
DELETED
@@ -1,77 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative File.join("piece", "source")
|
4
|
-
|
5
|
-
module Sashite
|
6
|
-
module Ggn
|
7
|
-
# Represents a collection of piece definitions from a GGN document.
|
8
|
-
#
|
9
|
-
# A Piece instance contains all the pseudo-legal move definitions for
|
10
|
-
# various game pieces, organized by their GAN (General Actor Notation)
|
11
|
-
# identifiers. This class provides the entry point for querying specific
|
12
|
-
# piece movement rules.
|
13
|
-
#
|
14
|
-
# @example Basic usage
|
15
|
-
# piece_data = Sashite::Ggn.load_file('chess.json')
|
16
|
-
# chess_king = piece_data.select('CHESS:K')
|
17
|
-
# shogi_pawn = piece_data.select('SHOGI:P')
|
18
|
-
#
|
19
|
-
# @example Complete workflow
|
20
|
-
# piece_data = Sashite::Ggn.load_file('game_moves.json')
|
21
|
-
#
|
22
|
-
# # Query specific piece moves
|
23
|
-
# begin
|
24
|
-
# king_source = piece_data.select('CHESS:K')
|
25
|
-
# puts "Found chess king movement rules"
|
26
|
-
# rescue KeyError
|
27
|
-
# puts "Chess king not found in this dataset"
|
28
|
-
# end
|
29
|
-
#
|
30
|
-
# @see https://sashite.dev/documents/gan/ GAN Specification
|
31
|
-
class Piece
|
32
|
-
# Creates a new Piece instance from GGN data.
|
33
|
-
#
|
34
|
-
# @param data [Hash] The parsed GGN JSON data structure, where keys are
|
35
|
-
# GAN identifiers and values contain the movement definitions.
|
36
|
-
#
|
37
|
-
# @raise [ArgumentError] If data is not a Hash
|
38
|
-
def initialize(data)
|
39
|
-
raise ::ArgumentError, "Expected Hash, got #{data.class}" unless data.is_a?(::Hash)
|
40
|
-
|
41
|
-
@data = data
|
42
|
-
|
43
|
-
freeze
|
44
|
-
end
|
45
|
-
|
46
|
-
# Retrieves movement rules for a specific piece type.
|
47
|
-
#
|
48
|
-
# @param actor [String] The GAN identifier for the piece type
|
49
|
-
# (e.g., 'CHESS:K', 'SHOGI:P', 'chess:q'). Must match exactly
|
50
|
-
# including case sensitivity.
|
51
|
-
#
|
52
|
-
# @return [Source] A Source instance containing all movement rules
|
53
|
-
# for this piece type from different board positions.
|
54
|
-
#
|
55
|
-
# @raise [KeyError] If the actor is not found in the GGN data
|
56
|
-
#
|
57
|
-
# @example Fetching chess king moves
|
58
|
-
# source = piece_data.select('CHESS:K')
|
59
|
-
# destinations = source.from('e1')
|
60
|
-
# engine = destinations.to('e2')
|
61
|
-
#
|
62
|
-
# @example Handling missing pieces
|
63
|
-
# begin
|
64
|
-
# moves = piece_data.select('NONEXISTENT:X')
|
65
|
-
# rescue KeyError => e
|
66
|
-
# puts "Piece not found: #{e.message}"
|
67
|
-
# end
|
68
|
-
#
|
69
|
-
# @note The actor format must follow GAN specification:
|
70
|
-
# GAME:piece (e.g., CHESS:K, shogi:p, XIANGQI:E)
|
71
|
-
def select(actor)
|
72
|
-
data = @data.fetch(actor)
|
73
|
-
Source.new(data, actor:)
|
74
|
-
end
|
75
|
-
end
|
76
|
-
end
|
77
|
-
end
|