sashite-ggn 0.3.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 +73 -339
- data/lib/sashite/ggn/move_validator.rb +208 -0
- data/lib/sashite/ggn/ruleset/source/destination/engine/transition.rb +81 -0
- data/lib/sashite/ggn/ruleset/source/destination/engine.rb +374 -0
- data/lib/sashite/ggn/ruleset/source/destination.rb +111 -0
- data/lib/sashite/ggn/{piece → ruleset}/source.rb +43 -15
- data/lib/sashite/ggn/ruleset.rb +311 -0
- data/lib/sashite/ggn/schema.rb +96 -77
- data/lib/sashite/ggn/validation_error.rb +26 -1
- data/lib/sashite/ggn.rb +36 -35
- data/lib/sashite-ggn.rb +48 -34
- metadata +13 -11
- data/lib/sashite/ggn/piece/source/destination/engine/transition.rb +0 -90
- data/lib/sashite/ggn/piece/source/destination/engine.rb +0 -407
- data/lib/sashite/ggn/piece/source/destination.rb +0 -65
- data/lib/sashite/ggn/piece.rb +0 -77
@@ -0,0 +1,111 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative File.join("destination", "engine")
|
4
|
+
|
5
|
+
module Sashite
|
6
|
+
module Ggn
|
7
|
+
class Ruleset
|
8
|
+
class Source
|
9
|
+
# Represents the possible destination squares for a piece from a specific source.
|
10
|
+
#
|
11
|
+
# A Destination instance contains all the target squares a piece can reach
|
12
|
+
# from a given starting position, along with the conditional rules that
|
13
|
+
# govern each potential move. Since GGN focuses exclusively on board-to-board
|
14
|
+
# transformations, all destinations represent squares on the game board.
|
15
|
+
#
|
16
|
+
# @example Basic usage
|
17
|
+
# destinations = source.from('e1')
|
18
|
+
# engine = destinations.to('e2')
|
19
|
+
# transitions = engine.where(board_state, 'CHESS')
|
20
|
+
#
|
21
|
+
# @example Exploring all possible destinations
|
22
|
+
# destinations = source.from('e1')
|
23
|
+
# # destinations.to('e2') - one square forward
|
24
|
+
# # destinations.to('f1') - one square right
|
25
|
+
# # destinations.to('d1') - one square left
|
26
|
+
# # Each destination has its own movement rules and conditions
|
27
|
+
class Destination
|
28
|
+
# Creates a new Destination instance from target square data.
|
29
|
+
#
|
30
|
+
# @param data [Hash] The destination data where keys are target square
|
31
|
+
# labels and values are arrays of conditional transition rules.
|
32
|
+
# @param actor [String] The GAN identifier for this piece type
|
33
|
+
# @param origin [String] The source position
|
34
|
+
#
|
35
|
+
# @raise [ArgumentError] If data is not a Hash
|
36
|
+
#
|
37
|
+
# @example Creating a Destination instance
|
38
|
+
# destination_data = {
|
39
|
+
# "e2" => [
|
40
|
+
# { "require" => { "e2" => "empty" }, "perform" => { "e1" => nil, "e2" => "CHESS:K" } }
|
41
|
+
# ],
|
42
|
+
# "f1" => [
|
43
|
+
# { "require" => { "f1" => "empty" }, "perform" => { "e1" => nil, "f1" => "CHESS:K" } }
|
44
|
+
# ]
|
45
|
+
# }
|
46
|
+
# destination = Destination.new(destination_data, actor: "CHESS:K", origin: "e1")
|
47
|
+
def initialize(data, actor:, origin:)
|
48
|
+
raise ::ArgumentError, "Expected Hash, got #{data.class}" unless data.is_a?(::Hash)
|
49
|
+
|
50
|
+
@data = data
|
51
|
+
@actor = actor
|
52
|
+
@origin = origin
|
53
|
+
|
54
|
+
freeze
|
55
|
+
end
|
56
|
+
|
57
|
+
# Retrieves the movement engine for a specific target square.
|
58
|
+
#
|
59
|
+
# This method creates an Engine instance that can evaluate whether the move
|
60
|
+
# to the specified target square is valid given the current board conditions.
|
61
|
+
# The engine encapsulates all the conditional logic (require/prevent/perform)
|
62
|
+
# for this specific source-to-destination move.
|
63
|
+
#
|
64
|
+
# @param target [String] The destination square label (e.g., 'e2', '5h', 'a8').
|
65
|
+
#
|
66
|
+
# @return [Engine] An Engine instance that can evaluate move validity
|
67
|
+
# and return all possible transition variants for this move.
|
68
|
+
#
|
69
|
+
# @raise [KeyError] If the target square is not reachable from the source
|
70
|
+
#
|
71
|
+
# @example Getting movement rules to a specific square
|
72
|
+
# engine = destinations.to('e2')
|
73
|
+
# transitions = engine.where(board_state, 'CHESS')
|
74
|
+
#
|
75
|
+
# if transitions.any?
|
76
|
+
# puts "Move is valid!"
|
77
|
+
# transitions.each { |t| puts "Result: #{t.diff}" }
|
78
|
+
# else
|
79
|
+
# puts "Move is not valid under current conditions"
|
80
|
+
# end
|
81
|
+
#
|
82
|
+
# @example Handling unreachable targets
|
83
|
+
# begin
|
84
|
+
# engine = destinations.to('invalid_square')
|
85
|
+
# rescue KeyError => e
|
86
|
+
# puts "Cannot move to this square: #{e.message}"
|
87
|
+
# end
|
88
|
+
#
|
89
|
+
# @example Testing multiple destinations
|
90
|
+
# ['e2', 'f1', 'd1'].each do |target|
|
91
|
+
# begin
|
92
|
+
# engine = destinations.to(target)
|
93
|
+
# transitions = engine.where(board_state, 'CHESS')
|
94
|
+
# puts "#{target}: #{transitions.size} possible transitions"
|
95
|
+
# rescue KeyError
|
96
|
+
# puts "#{target}: not reachable"
|
97
|
+
# end
|
98
|
+
# end
|
99
|
+
#
|
100
|
+
# @note The returned Engine handles all the complexity of move validation,
|
101
|
+
# including require/prevent conditions and multiple move variants
|
102
|
+
# (such as promotion choices).
|
103
|
+
def to(target)
|
104
|
+
transitions = @data.fetch(target)
|
105
|
+
Engine.new(*transitions, actor: @actor, origin: @origin, target: target)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -1,33 +1,52 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative File.join("..", "move_validator")
|
3
4
|
require_relative File.join("source", "destination")
|
4
5
|
|
5
6
|
module Sashite
|
6
7
|
module Ggn
|
7
|
-
class
|
8
|
+
class Ruleset
|
8
9
|
# Represents the possible source positions for a specific piece type.
|
9
10
|
#
|
10
11
|
# A Source instance contains all the starting positions from which
|
11
|
-
# a piece can move
|
12
|
-
#
|
12
|
+
# a piece can move on the board. Since GGN focuses exclusively on
|
13
|
+
# board-to-board transformations, all source positions are regular
|
14
|
+
# board squares.
|
13
15
|
#
|
14
16
|
# @example Basic usage with chess king
|
15
17
|
# piece_data = Sashite::Ggn.load_file('chess.json')
|
16
18
|
# source = piece_data.select('CHESS:K')
|
17
19
|
# destinations = source.from('e1')
|
18
20
|
#
|
19
|
-
# @example
|
20
|
-
# piece_data = Sashite::Ggn.load_file('
|
21
|
-
#
|
22
|
-
#
|
21
|
+
# @example Complete move evaluation workflow
|
22
|
+
# piece_data = Sashite::Ggn.load_file('chess.json')
|
23
|
+
# king_source = piece_data.select('CHESS:K')
|
24
|
+
# destinations = king_source.from('e1')
|
25
|
+
# engine = destinations.to('e2')
|
26
|
+
#
|
27
|
+
# board_state = { 'e1' => 'CHESS:K', 'e2' => nil }
|
28
|
+
# transitions = engine.where(board_state, 'CHESS')
|
29
|
+
#
|
30
|
+
# if transitions.any?
|
31
|
+
# puts "King can move from e1 to e2"
|
32
|
+
# end
|
23
33
|
class Source
|
34
|
+
include MoveValidator
|
35
|
+
|
24
36
|
# Creates a new Source instance from movement data.
|
25
37
|
#
|
26
38
|
# @param data [Hash] The movement data where keys are source positions
|
27
|
-
# (square labels
|
39
|
+
# (square labels) and values contain destination data.
|
28
40
|
# @param actor [String] The GAN identifier for this piece type
|
29
41
|
#
|
30
42
|
# @raise [ArgumentError] If data is not a Hash
|
43
|
+
#
|
44
|
+
# @example Creating a Source instance
|
45
|
+
# source_data = {
|
46
|
+
# "e1" => { "e2" => [...], "f1" => [...] },
|
47
|
+
# "d4" => { "d5" => [...], "e5" => [...] }
|
48
|
+
# }
|
49
|
+
# source = Source.new(source_data, actor: "CHESS:K")
|
31
50
|
def initialize(data, actor:)
|
32
51
|
raise ::ArgumentError, "Expected Hash, got #{data.class}" unless data.is_a?(::Hash)
|
33
52
|
|
@@ -39,8 +58,8 @@ module Sashite
|
|
39
58
|
|
40
59
|
# Retrieves possible destinations from a specific source position.
|
41
60
|
#
|
42
|
-
# @param origin [String] The source position label.
|
43
|
-
# square label (e.g., 'e1', '5i')
|
61
|
+
# @param origin [String] The source position label. Must be a regular
|
62
|
+
# square label (e.g., 'e1', '5i', 'a1').
|
44
63
|
#
|
45
64
|
# @return [Destination] A Destination instance containing all possible
|
46
65
|
# target squares and their movement conditions from this origin.
|
@@ -51,19 +70,28 @@ module Sashite
|
|
51
70
|
# destinations = source.from('e1')
|
52
71
|
# engine = destinations.to('e2')
|
53
72
|
#
|
54
|
-
# @example Getting drop moves (for games like Shogi)
|
55
|
-
# drop_destinations = source.from('*')
|
56
|
-
# engine = drop_destinations.to('5e')
|
57
|
-
#
|
58
73
|
# @example Handling missing origins
|
59
74
|
# begin
|
60
75
|
# destinations = source.from('invalid_square')
|
61
76
|
# rescue KeyError => e
|
62
77
|
# puts "No moves from this position: #{e.message}"
|
63
78
|
# end
|
79
|
+
#
|
80
|
+
# @example Iterating through all possible origins
|
81
|
+
# # Assuming you have access to the source data keys
|
82
|
+
# available_origins = ['e1', 'd1', 'f1'] # example origins
|
83
|
+
# available_origins.each do |pos|
|
84
|
+
# begin
|
85
|
+
# destinations = source.from(pos)
|
86
|
+
# puts "Piece can move from #{pos}"
|
87
|
+
# # Process destinations...
|
88
|
+
# rescue KeyError
|
89
|
+
# puts "No moves available from #{pos}"
|
90
|
+
# end
|
91
|
+
# end
|
64
92
|
def from(origin)
|
65
93
|
data = @data.fetch(origin)
|
66
|
-
Destination.new(data, actor: @actor, origin:)
|
94
|
+
Destination.new(data, actor: @actor, origin: origin)
|
67
95
|
end
|
68
96
|
end
|
69
97
|
end
|
@@ -0,0 +1,311 @@
|
|
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
|
+
# GGN focuses exclusively on board-to-board transformations. All moves
|
22
|
+
# represent pieces moving, capturing, or transforming on the game board.
|
23
|
+
#
|
24
|
+
# @example Basic usage
|
25
|
+
# piece_data = Sashite::Ggn.load_file('chess.json')
|
26
|
+
# chess_king = piece_data.select('CHESS:K')
|
27
|
+
# shogi_pawn = piece_data.select('SHOGI:P')
|
28
|
+
#
|
29
|
+
# @example Complete workflow
|
30
|
+
# piece_data = Sashite::Ggn.load_file('game_moves.json')
|
31
|
+
#
|
32
|
+
# # Query specific piece moves
|
33
|
+
# begin
|
34
|
+
# king_source = piece_data.select('CHESS:K')
|
35
|
+
# puts "Found chess king movement rules"
|
36
|
+
# rescue KeyError
|
37
|
+
# puts "Chess king not found in this dataset"
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# @example Finding all possible moves in a position
|
41
|
+
# board_state = { 'e1' => 'CHESS:K', 'e2' => 'CHESS:P', 'd1' => 'CHESS:Q' }
|
42
|
+
# all_moves = piece_data.pseudo_legal_transitions(board_state, 'CHESS')
|
43
|
+
# puts "Found #{all_moves.size} possible moves"
|
44
|
+
#
|
45
|
+
# @see https://sashite.dev/documents/gan/ GAN Specification
|
46
|
+
# @see https://sashite.dev/documents/ggn/ GGN Specification
|
47
|
+
class Ruleset
|
48
|
+
include MoveValidator
|
49
|
+
|
50
|
+
# Creates a new Ruleset instance from GGN data.
|
51
|
+
#
|
52
|
+
# @param data [Hash] The parsed GGN JSON data structure, where keys are
|
53
|
+
# GAN identifiers and values contain the movement definitions.
|
54
|
+
#
|
55
|
+
# @raise [ArgumentError] If data is not a Hash
|
56
|
+
#
|
57
|
+
# @example Creating from parsed JSON data
|
58
|
+
# ggn_data = JSON.parse(File.read('chess.json'))
|
59
|
+
# ruleset = Ruleset.new(ggn_data)
|
60
|
+
def initialize(data)
|
61
|
+
raise ::ArgumentError, "Expected Hash, got #{data.class}" unless data.is_a?(::Hash)
|
62
|
+
|
63
|
+
@data = data
|
64
|
+
|
65
|
+
freeze
|
66
|
+
end
|
67
|
+
|
68
|
+
# Retrieves movement rules for a specific piece type.
|
69
|
+
#
|
70
|
+
# @param actor [String] The GAN identifier for the piece type
|
71
|
+
# (e.g., 'CHESS:K', 'SHOGI:P', 'chess:q'). Must match exactly
|
72
|
+
# including case sensitivity.
|
73
|
+
#
|
74
|
+
# @return [Source] A Source instance containing all movement rules
|
75
|
+
# for this piece type from different board positions.
|
76
|
+
#
|
77
|
+
# @raise [KeyError] If the actor is not found in the GGN data
|
78
|
+
#
|
79
|
+
# @example Fetching chess king moves
|
80
|
+
# source = piece_data.select('CHESS:K')
|
81
|
+
# destinations = source.from('e1')
|
82
|
+
# engine = destinations.to('e2')
|
83
|
+
#
|
84
|
+
# @example Handling missing pieces
|
85
|
+
# begin
|
86
|
+
# moves = piece_data.select('NONEXISTENT:X')
|
87
|
+
# rescue KeyError => e
|
88
|
+
# puts "Piece not found: #{e.message}"
|
89
|
+
# end
|
90
|
+
#
|
91
|
+
# @note The actor format must follow GAN specification:
|
92
|
+
# GAME:piece (e.g., CHESS:K, shogi:p, XIANGQI:E)
|
93
|
+
def select(actor)
|
94
|
+
data = @data.fetch(actor)
|
95
|
+
Source.new(data, actor:)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Returns all pseudo-legal move transitions for the given position.
|
99
|
+
#
|
100
|
+
# This method traverses all actors defined in the GGN data using a functional
|
101
|
+
# approach with flat_map and filter_map to efficiently process and filter
|
102
|
+
# valid moves. Each result contains the complete transition information
|
103
|
+
# including all variants for moves with multiple outcomes (e.g., promotion choices).
|
104
|
+
#
|
105
|
+
# The implementation uses a three-level functional decomposition:
|
106
|
+
# 1. Process each actor (piece type) that belongs to current player
|
107
|
+
# 2. Process each valid origin position for that actor
|
108
|
+
# 3. Process each destination and evaluate transition rules
|
109
|
+
#
|
110
|
+
# @param board_state [Hash] Current board state mapping square labels
|
111
|
+
# to piece identifiers (nil for empty squares)
|
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.
|
114
|
+
#
|
115
|
+
# @return [Array<Array>] List of move transitions, where each element is:
|
116
|
+
# [actor, origin, target, transitions]
|
117
|
+
# - actor [String]: GAN identifier of the moving piece
|
118
|
+
# - origin [String]: Source square
|
119
|
+
# - target [String]: Destination square
|
120
|
+
# - transitions [Array<Transition>]: All valid transition variants
|
121
|
+
#
|
122
|
+
# @raise [ArgumentError] If any parameter is invalid or malformed
|
123
|
+
#
|
124
|
+
# @example Getting all possible transitions including promotion variants
|
125
|
+
# board_state = { 'e7' => 'CHESS:P', 'e8' => nil }
|
126
|
+
# transitions = piece_data.pseudo_legal_transitions(board_state, 'CHESS')
|
127
|
+
# # => [
|
128
|
+
# # ["CHESS:P", "e7", "e8", [
|
129
|
+
# # #<Transition diff={"e7"=>nil, "e8"=>"CHESS:Q"}>,
|
130
|
+
# # #<Transition diff={"e7"=>nil, "e8"=>"CHESS:R"}>,
|
131
|
+
# # #<Transition diff={"e7"=>nil, "e8"=>"CHESS:B"}>,
|
132
|
+
# # #<Transition diff={"e7"=>nil, "e8"=>"CHESS:N"}>
|
133
|
+
# # ]]
|
134
|
+
# # ]
|
135
|
+
#
|
136
|
+
# @example Processing grouped transitions
|
137
|
+
# transitions = piece_data.pseudo_legal_transitions(board_state, 'CHESS')
|
138
|
+
# transitions.each do |actor, origin, target, variants|
|
139
|
+
# puts "#{actor} from #{origin} to #{target}:"
|
140
|
+
# variants.each_with_index do |transition, i|
|
141
|
+
# puts " Variant #{i + 1}: #{transition.diff}"
|
142
|
+
# end
|
143
|
+
# end
|
144
|
+
#
|
145
|
+
# @example Filtering for specific move types
|
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
|
+
#
|
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
|
+
# }
|
155
|
+
#
|
156
|
+
# @example Performance considerations
|
157
|
+
# # For large datasets, consider filtering by piece type first
|
158
|
+
# specific_piece_moves = piece_data.select('CHESS:Q')
|
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)
|
162
|
+
|
163
|
+
# Use flat_map to process all actors and flatten the results in one pass
|
164
|
+
# This functional approach avoids mutation and intermediate arrays
|
165
|
+
@data.flat_map do |actor, source_data|
|
166
|
+
# Early filter: only process pieces belonging to current player
|
167
|
+
# This optimization significantly reduces processing time
|
168
|
+
next [] unless piece_belongs_to_current_player?(actor, active_game)
|
169
|
+
|
170
|
+
# Process all source positions for this actor using functional decomposition
|
171
|
+
process_actor_transitions(actor, source_data, board_state, active_game)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
private
|
176
|
+
|
177
|
+
# Processes all possible transitions for a single actor (piece type).
|
178
|
+
#
|
179
|
+
# This method represents the second level of functional decomposition,
|
180
|
+
# handling all source positions (origins) for a given piece type.
|
181
|
+
# It uses flat_map to efficiently process each origin and flatten the results.
|
182
|
+
#
|
183
|
+
# @param actor [String] GAN identifier of the piece type
|
184
|
+
# @param source_data [Hash] Movement data for this piece type, mapping
|
185
|
+
# origin squares to destination data
|
186
|
+
# @param board_state [Hash] Current board state
|
187
|
+
# @param active_game [String] Current player identifier
|
188
|
+
#
|
189
|
+
# @return [Array] Array of valid transition tuples for this actor
|
190
|
+
#
|
191
|
+
# @example Source data structure
|
192
|
+
# {
|
193
|
+
# "e1" => { "e2" => [...], "f1" => [...] } # Regular moves
|
194
|
+
# }
|
195
|
+
def process_actor_transitions(actor, source_data, board_state, active_game)
|
196
|
+
source_data.flat_map do |origin, destination_data|
|
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)
|
200
|
+
|
201
|
+
# Process all destination squares for this origin
|
202
|
+
process_origin_transitions(actor, origin, destination_data, board_state, active_game)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
# Processes all possible transitions from a single origin square.
|
207
|
+
#
|
208
|
+
# This method represents the third level of functional decomposition,
|
209
|
+
# handling all destination squares from a given origin. It creates
|
210
|
+
# engines to evaluate each move and uses filter_map to efficiently
|
211
|
+
# combine filtering and transformation operations.
|
212
|
+
#
|
213
|
+
# @param actor [String] GAN identifier of the piece
|
214
|
+
# @param origin [String] Source square
|
215
|
+
# @param destination_data [Hash] Available destinations and their transition rules
|
216
|
+
# @param board_state [Hash] Current board state
|
217
|
+
# @param active_game [String] Current player identifier
|
218
|
+
#
|
219
|
+
# @return [Array] Array of valid transition tuples for this origin
|
220
|
+
#
|
221
|
+
# @example Destination data structure
|
222
|
+
# {
|
223
|
+
# "e4" => [
|
224
|
+
# { "require" => { "e4" => "empty" }, "perform" => { "e2" => nil, "e4" => "CHESS:P" } }
|
225
|
+
# ],
|
226
|
+
# "f3" => [
|
227
|
+
# { "require" => { "f3" => "enemy" }, "perform" => { "e2" => nil, "f3" => "CHESS:P" } }
|
228
|
+
# ]
|
229
|
+
# }
|
230
|
+
def process_origin_transitions(actor, origin, destination_data, board_state, active_game)
|
231
|
+
destination_data.filter_map do |target, transition_rules|
|
232
|
+
# Create engine to evaluate this specific source-destination pair
|
233
|
+
# Each engine encapsulates the conditional logic for one move
|
234
|
+
engine = Source::Destination::Engine.new(*transition_rules, actor: actor, origin: origin, target: target)
|
235
|
+
|
236
|
+
# Get all valid transitions for this move (supports multiple variants)
|
237
|
+
# The engine handles require/prevent conditions and returns Transition objects
|
238
|
+
transitions = engine.where(board_state, active_game)
|
239
|
+
|
240
|
+
# Only return successful moves (with at least one valid transition)
|
241
|
+
# filter_map automatically filters out nil values
|
242
|
+
[actor, origin, target, transitions] unless transitions.empty?
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
# Validates parameters for pseudo_legal_transitions method.
|
247
|
+
#
|
248
|
+
# Provides comprehensive validation with clear error messages for debugging.
|
249
|
+
# This method ensures data integrity and helps catch common usage errors
|
250
|
+
# early in the processing pipeline.
|
251
|
+
#
|
252
|
+
# @param board_state [Object] Should be a Hash mapping squares to pieces
|
253
|
+
# @param active_game [Object] Should be a String representing current player's game
|
254
|
+
#
|
255
|
+
# @raise [ArgumentError] If any parameter is invalid
|
256
|
+
#
|
257
|
+
# @example Valid parameters
|
258
|
+
# validate_pseudo_legal_parameters!(
|
259
|
+
# { "e1" => "CHESS:K", "e2" => nil },
|
260
|
+
# "CHESS"
|
261
|
+
# )
|
262
|
+
#
|
263
|
+
# @example Invalid parameters (raises ArgumentError)
|
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)
|
268
|
+
# Type validation with clear, specific error messages
|
269
|
+
unless board_state.is_a?(::Hash)
|
270
|
+
raise ::ArgumentError, "board_state must be a Hash, got #{board_state.class}"
|
271
|
+
end
|
272
|
+
|
273
|
+
unless active_game.is_a?(::String)
|
274
|
+
raise ::ArgumentError, "active_game must be a String, got #{active_game.class}"
|
275
|
+
end
|
276
|
+
|
277
|
+
# Content validation - ensures meaningful data
|
278
|
+
if active_game.empty?
|
279
|
+
raise ::ArgumentError, "active_game cannot be empty"
|
280
|
+
end
|
281
|
+
|
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')."
|
284
|
+
end
|
285
|
+
|
286
|
+
# Validate board_state structure (optional deep validation)
|
287
|
+
validate_board_state_structure!(board_state) if ENV['GGN_STRICT_VALIDATION']
|
288
|
+
end
|
289
|
+
|
290
|
+
# Validates board_state structure in strict mode.
|
291
|
+
#
|
292
|
+
# This optional validation can be enabled via environment variable
|
293
|
+
# to catch malformed board states during development and testing.
|
294
|
+
#
|
295
|
+
# @param board_state [Hash] Board state to validate
|
296
|
+
#
|
297
|
+
# @raise [ArgumentError] If board_state contains invalid data
|
298
|
+
def validate_board_state_structure!(board_state)
|
299
|
+
board_state.each do |square, piece|
|
300
|
+
unless square.is_a?(::String) && !square.empty?
|
301
|
+
raise ::ArgumentError, "Invalid square label: #{square.inspect}"
|
302
|
+
end
|
303
|
+
|
304
|
+
if piece && (!piece.is_a?(::String) || piece.empty?)
|
305
|
+
raise ::ArgumentError, "Invalid piece at #{square}: #{piece.inspect}"
|
306
|
+
end
|
307
|
+
end
|
308
|
+
end
|
309
|
+
end
|
310
|
+
end
|
311
|
+
end
|