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.
@@ -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", "piece")
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
- # ## Key Features
20
+ # = Key Features
21
21
  #
22
22
  # - **Rule-agnostic**: Works with any abstract strategy board game
23
- # - **Pseudo-legal focus**: Describes basic movement constraints only
23
+ # - **Pseudo-legal** focus: Describes basic movement constraints only
24
24
  # - **JSON-based**: Structured, machine-readable format
25
- # - **Validation support**: Built-in schema validation
26
- # - **Performance optimized**: Optional validation for large datasets
27
- # - **Cross-game compatible**: Supports hybrid games and variants
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
- # ## Related Specifications
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 Piece instance for querying moves
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 [Piece] A Piece instance containing the parsed and validated GGN data.
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.fetch('CHESS:K')
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.fetch('CHESS:K')
79
- # destinations = source.fetch('e1')
80
- # engine = destinations.fetch('e2')
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 Piece instance
126
- Piece.new(data)
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 [Piece] A Piece instance containing the parsed GGN data
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.fetch('CHESS:P')
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 Piece instance
167
- Piece.new(data)
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 Piece instance with optional validation.
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 [Piece] A Piece instance containing the GGN data
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.fetch('SHOGI:K')
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 Piece instance
203
- Piece.new(data)
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 Piece instance. Useful for pre-validation or testing.
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.fetch("CHESS:P").fetch("e2").fetch("e4")
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.fetch("SHOGI:P").fetch("*").fetch("5e")
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.fetch("CHESS:P").fetch("g7").fetch("h8")
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.3.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/piece.rb
41
- - lib/sashite/ggn/piece/source.rb
42
- - lib/sashite/ggn/piece/source/destination.rb
43
- - lib/sashite/ggn/piece/source/destination/engine.rb
44
- - lib/sashite/ggn/piece/source/destination/engine/transition.rb
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
@@ -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