sashite-ggn 0.7.0 → 0.9.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 +313 -562
- data/lib/sashite/ggn/ruleset/source/destination/engine.rb +71 -326
- data/lib/sashite/ggn/ruleset/source/destination.rb +33 -85
- data/lib/sashite/ggn/ruleset/source.rb +33 -75
- data/lib/sashite/ggn/ruleset.rb +35 -439
- data/lib/sashite/ggn.rb +196 -324
- data/lib/sashite-ggn.rb +8 -120
- metadata +68 -20
- data/lib/sashite/ggn/move_validator.rb +0 -208
- data/lib/sashite/ggn/ruleset/source/destination/engine/transition.rb +0 -81
- data/lib/sashite/ggn/schema.rb +0 -171
- data/lib/sashite/ggn/validation_error.rb +0 -56
@@ -1,370 +1,115 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
3
|
+
require "sashite/lcn"
|
4
|
+
require "sashite/qpi"
|
5
|
+
require "sashite/stn"
|
5
6
|
|
6
7
|
module Sashite
|
7
8
|
module Ggn
|
8
9
|
class Ruleset
|
9
10
|
class Source
|
10
11
|
class Destination
|
11
|
-
# Evaluates
|
12
|
+
# Evaluates movement possibility under given position conditions
|
12
13
|
#
|
13
|
-
#
|
14
|
-
# is valid under the basic movement constraints defined in GGN. It evaluates
|
15
|
-
# require/prevent conditions and returns the resulting board transformation.
|
16
|
-
#
|
17
|
-
# Since GGN focuses exclusively on board-to-board transformations, the Engine
|
18
|
-
# only handles pieces moving, capturing, or transforming on the game board.
|
19
|
-
#
|
20
|
-
# The class uses a functional approach with filter_map for optimal performance
|
21
|
-
# and clean, readable code that avoids mutation of external variables.
|
22
|
-
#
|
23
|
-
# @example Evaluating a simple move
|
24
|
-
# engine = destinations.to('e4')
|
25
|
-
# transitions = engine.where(board_state, 'CHESS')
|
26
|
-
# puts "Move valid!" if transitions.any?
|
27
|
-
#
|
28
|
-
# @example Handling promotion choices
|
29
|
-
# engine = destinations.to('e8') # pawn promotion
|
30
|
-
# transitions = engine.where(board_state, 'CHESS')
|
31
|
-
# transitions.each_with_index do |t, i|
|
32
|
-
# puts "Choice #{i + 1}: promotes to #{t.diff['e8']}"
|
33
|
-
# end
|
14
|
+
# @see https://sashite.dev/specs/ggn/1.0.0/
|
34
15
|
class Engine
|
35
|
-
|
36
|
-
|
37
|
-
# Creates a new Engine with conditional transition rules.
|
38
|
-
#
|
39
|
-
# @param transitions [Array] Transition rules as individual arguments,
|
40
|
-
# each containing require/prevent conditions and perform actions.
|
41
|
-
# @param actor [String] GAN identifier of the piece being moved
|
42
|
-
# @param origin [String] Source square
|
43
|
-
# @param target [String] Destination square
|
16
|
+
# Create a new Engine
|
44
17
|
#
|
45
|
-
# @
|
46
|
-
|
47
|
-
|
48
|
-
# transition_rules = [
|
49
|
-
# {
|
50
|
-
# "require" => { "e4" => "empty", "e3" => "empty" },
|
51
|
-
# "perform" => { "e2" => nil, "e4" => "CHESS:P" }
|
52
|
-
# }
|
53
|
-
# ]
|
54
|
-
# engine = Engine.new(*transition_rules, actor: "CHESS:P", origin: "e2", target: "e4")
|
55
|
-
def initialize(*transitions, actor:, origin:, target:)
|
56
|
-
raise ::ArgumentError, "actor must be a String" unless actor.is_a?(::String)
|
57
|
-
raise ::ArgumentError, "origin must be a String" unless origin.is_a?(::String)
|
58
|
-
raise ::ArgumentError, "target must be a String" unless target.is_a?(::String)
|
59
|
-
|
60
|
-
@transitions = transitions
|
61
|
-
@actor = actor
|
62
|
-
@origin = origin
|
63
|
-
@target = target
|
64
|
-
|
18
|
+
# @param possibilities [Array<Hash>] Movement possibilities data
|
19
|
+
def initialize(*possibilities)
|
20
|
+
@possibilities = possibilities
|
65
21
|
freeze
|
66
22
|
end
|
67
23
|
|
68
|
-
#
|
69
|
-
#
|
70
|
-
#
|
71
|
-
#
|
72
|
-
#
|
73
|
-
#
|
74
|
-
#
|
75
|
-
# @
|
76
|
-
#
|
77
|
-
#
|
78
|
-
#
|
79
|
-
#
|
80
|
-
#
|
81
|
-
#
|
82
|
-
#
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
# transitions.first.diff # => { 'e2' => nil, 'e4' => 'CHESS:P' }
|
90
|
-
#
|
91
|
-
# @example Multiple promotion choices
|
92
|
-
# board_state = { 'e7' => 'CHESS:P', 'e8' => nil }
|
93
|
-
# transitions = engine.where(board_state, 'CHESS')
|
94
|
-
# transitions.size # => 4 (Queen, Rook, Bishop, Knight)
|
95
|
-
# transitions.map { |t| t.diff['e8'] } # => ['CHESS:Q', 'CHESS:R', 'CHESS:B', 'CHESS:N']
|
96
|
-
#
|
97
|
-
# @example Invalid move (wrong piece)
|
98
|
-
# board_state = { 'e2' => 'CHESS:Q', 'e3' => nil, 'e4' => nil }
|
99
|
-
# transitions = engine.where(board_state, 'CHESS') # => []
|
100
|
-
#
|
101
|
-
# @example Invalid move (blocked path)
|
102
|
-
# board_state = { 'e2' => 'CHESS:P', 'e3' => 'CHESS:N', 'e4' => nil }
|
103
|
-
# transitions = engine.where(board_state, 'CHESS') # => []
|
104
|
-
def where(board_state, active_game)
|
105
|
-
# Validate all input parameters before processing
|
106
|
-
validate_parameters!(board_state, active_game)
|
107
|
-
|
108
|
-
# Early return if basic move context is invalid (wrong piece, wrong player, etc.)
|
109
|
-
return [] unless valid_move_context?(board_state, active_game)
|
110
|
-
|
111
|
-
# Use filter_map for functional approach: filter valid transitions and map to Transition objects
|
112
|
-
# This avoids mutation and is more performant than select + map for large datasets
|
113
|
-
@transitions.filter_map do |transition|
|
114
|
-
# Only create Transition objects for transitions that match current board state
|
115
|
-
create_transition(transition) if transition_matches?(transition, board_state, active_game)
|
24
|
+
# Evaluate movement against position and return valid transitions
|
25
|
+
#
|
26
|
+
# @param active_side [Symbol] Active player side (:first or :second)
|
27
|
+
# @param squares [Hash{String => String, nil}] Board state where keys are CELL coordinates
|
28
|
+
# and values are QPI identifiers or nil for empty squares
|
29
|
+
# @return [Array<Sashite::Stn::Transition>] Valid state transitions (may be empty)
|
30
|
+
#
|
31
|
+
# @example
|
32
|
+
# active_side = :first
|
33
|
+
# squares = {
|
34
|
+
# "e2" => "C:P",
|
35
|
+
# "e3" => nil,
|
36
|
+
# "e4" => nil
|
37
|
+
# }
|
38
|
+
# transitions = engine.where(active_side, squares)
|
39
|
+
def where(active_side, squares)
|
40
|
+
@possibilities.select do |possibility|
|
41
|
+
satisfies_must?(possibility["must"], active_side, squares) &&
|
42
|
+
satisfies_deny?(possibility["deny"], active_side, squares)
|
43
|
+
end.map do |possibility|
|
44
|
+
Stn.parse(possibility["diff"])
|
116
45
|
end
|
117
46
|
end
|
118
47
|
|
119
48
|
private
|
120
49
|
|
121
|
-
#
|
122
|
-
# Uses the shared MoveValidator module for consistency across the codebase.
|
123
|
-
#
|
124
|
-
# This method performs essential pre-checks:
|
125
|
-
# - Ensures the piece is at the expected origin square
|
126
|
-
# - Ensures the piece belongs to the current player
|
127
|
-
#
|
128
|
-
# @param board_state [Hash] Current board state
|
129
|
-
# @param active_game [String] Current player identifier
|
50
|
+
# Check if all 'must' conditions are satisfied
|
130
51
|
#
|
131
|
-
# @
|
132
|
-
|
133
|
-
|
134
|
-
|
52
|
+
# @param conditions [Hash] LCN conditions
|
53
|
+
# @param active_side [Symbol] Active player side
|
54
|
+
# @param squares [Hash] Board state
|
55
|
+
# @return [Boolean]
|
56
|
+
def satisfies_must?(conditions, active_side, squares)
|
57
|
+
return true if conditions.empty?
|
135
58
|
|
136
|
-
|
137
|
-
piece_belongs_to_current_player?(@actor, active_game)
|
138
|
-
end
|
59
|
+
lcn_conditions = Lcn.parse(conditions)
|
139
60
|
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
# Note: GGN no longer supports gain/drop fields, so Transition creation
|
144
|
-
# is simplified to only handle board transformations.
|
145
|
-
#
|
146
|
-
# @param transition [Hash] The transition rule containing perform data
|
147
|
-
#
|
148
|
-
# @return [Transition] A new immutable Transition object
|
149
|
-
def create_transition(transition)
|
150
|
-
Transition.new(**transition["perform"])
|
151
|
-
end
|
152
|
-
|
153
|
-
# Validates all parameters in one consolidated method.
|
154
|
-
# Provides comprehensive validation with clear error messages for debugging.
|
155
|
-
#
|
156
|
-
# @param board_state [Object] Should be a Hash
|
157
|
-
# @param active_game [Object] Should be a String
|
158
|
-
#
|
159
|
-
# @raise [ArgumentError] If any parameter is invalid
|
160
|
-
def validate_parameters!(board_state, active_game)
|
161
|
-
# Type validation with clear error messages
|
162
|
-
unless board_state.is_a?(::Hash)
|
163
|
-
raise ::ArgumentError, "board_state must be a Hash, got #{board_state.class}"
|
164
|
-
end
|
165
|
-
|
166
|
-
unless active_game.is_a?(::String)
|
167
|
-
raise ::ArgumentError, "active_game must be a String, got #{active_game.class}"
|
168
|
-
end
|
169
|
-
|
170
|
-
# Content validation - ensures data integrity
|
171
|
-
validate_board_state!(board_state)
|
172
|
-
validate_active_game!(active_game)
|
173
|
-
end
|
174
|
-
|
175
|
-
# Validates board_state structure and content.
|
176
|
-
# Ensures all square labels and piece identifiers are properly formatted.
|
177
|
-
#
|
178
|
-
# @param board_state [Hash] Board state to validate
|
179
|
-
#
|
180
|
-
# @raise [ArgumentError] If board_state contains invalid data
|
181
|
-
def validate_board_state!(board_state)
|
182
|
-
board_state.each do |square, piece|
|
183
|
-
validate_square_label!(square)
|
184
|
-
validate_board_piece!(piece, square)
|
61
|
+
lcn_conditions.locations.all? do |location|
|
62
|
+
expected_state = lcn_conditions[location]
|
63
|
+
check_condition(location.to_s, expected_state, active_side, squares)
|
185
64
|
end
|
186
65
|
end
|
187
66
|
|
188
|
-
#
|
189
|
-
# Square labels must be non-empty strings.
|
67
|
+
# Check if all 'deny' conditions are not satisfied
|
190
68
|
#
|
191
|
-
# @param
|
192
|
-
#
|
193
|
-
# @
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
end
|
198
|
-
end
|
69
|
+
# @param conditions [Hash] LCN conditions
|
70
|
+
# @param active_side [Symbol] Active player side
|
71
|
+
# @param squares [Hash] Board state
|
72
|
+
# @return [Boolean]
|
73
|
+
def satisfies_deny?(conditions, active_side, squares)
|
74
|
+
return true if conditions.empty?
|
199
75
|
|
200
|
-
|
201
|
-
# Pieces can be nil (empty square) or valid GAN identifiers.
|
202
|
-
#
|
203
|
-
# @param piece [Object] Piece to validate
|
204
|
-
# @param square [String] Square where piece is located (for error context)
|
205
|
-
#
|
206
|
-
# @raise [ArgumentError] If piece is invalid
|
207
|
-
def validate_board_piece!(piece, square)
|
208
|
-
return if piece.nil? # Empty squares are valid
|
76
|
+
lcn_conditions = Lcn.parse(conditions)
|
209
77
|
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
unless valid_gan_identifier?(piece)
|
215
|
-
raise ::ArgumentError, "Invalid GAN identifier at square #{square}: #{piece.inspect}. Must follow GAN format (e.g., 'CHESS:P', 'shogi:+k')."
|
78
|
+
lcn_conditions.locations.none? do |location|
|
79
|
+
expected_state = lcn_conditions[location]
|
80
|
+
check_condition(location.to_s, expected_state, active_side, squares)
|
216
81
|
end
|
217
82
|
end
|
218
83
|
|
219
|
-
#
|
220
|
-
# Active game must be a non-empty alphabetic game identifier.
|
221
|
-
#
|
222
|
-
# @param active_game [String] Active game identifier to validate
|
84
|
+
# Check if a location satisfies a condition
|
223
85
|
#
|
224
|
-
# @
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
raise ::ArgumentError, "Invalid active_game format: #{active_game.inspect}. Must be a valid game identifier (alphabetic characters only, e.g., 'CHESS', 'shogi')."
|
232
|
-
end
|
233
|
-
end
|
234
|
-
|
235
|
-
# Validates if a string is a valid GAN identifier with casing consistency.
|
236
|
-
# Ensures game part and piece part have consistent casing (both upper or both lower).
|
237
|
-
#
|
238
|
-
# @param identifier [String] GAN identifier to validate
|
239
|
-
#
|
240
|
-
# @return [Boolean] true if valid GAN format
|
241
|
-
def valid_gan_identifier?(identifier)
|
242
|
-
return false unless identifier.include?(':')
|
243
|
-
|
244
|
-
game_part, piece_part = identifier.split(':', 2)
|
245
|
-
|
246
|
-
return false unless valid_game_identifier?(game_part)
|
247
|
-
return false if piece_part.empty?
|
248
|
-
return false unless /\A[-+]?[A-Za-z]'?\z/.match?(piece_part)
|
249
|
-
|
250
|
-
# Extract base letter and check casing consistency
|
251
|
-
base_letter = piece_part.gsub(/\A[-+]?([A-Za-z])'?\z/, '\1')
|
252
|
-
|
253
|
-
# Ensure consistent casing between game and piece parts
|
254
|
-
if game_part == game_part.upcase
|
255
|
-
base_letter == base_letter.upcase
|
256
|
-
else
|
257
|
-
base_letter == base_letter.downcase
|
258
|
-
end
|
259
|
-
end
|
260
|
-
|
261
|
-
# Checks if a transition matches the current board state.
|
262
|
-
# Evaluates both require conditions (must be true) and prevent conditions (must be false).
|
263
|
-
#
|
264
|
-
# @param transition [Hash] The transition rule to evaluate
|
265
|
-
# @param board_state [Hash] Current board state
|
266
|
-
# @param active_game [String] Current player identifier
|
267
|
-
#
|
268
|
-
# @return [Boolean] true if the transition is valid for current state
|
269
|
-
def transition_matches?(transition, board_state, active_game)
|
270
|
-
# Ensure transition is properly formatted
|
271
|
-
return false unless transition.is_a?(::Hash) && transition.key?("perform")
|
272
|
-
|
273
|
-
# Check require conditions (all must be satisfied - logical AND)
|
274
|
-
return false if has_require_conditions?(transition) && !check_require_conditions(transition["require"], board_state, active_game)
|
275
|
-
|
276
|
-
# Check prevent conditions (none must be satisfied - logical NOR)
|
277
|
-
return false if has_prevent_conditions?(transition) && !check_prevent_conditions(transition["prevent"], board_state, active_game)
|
278
|
-
|
279
|
-
true
|
280
|
-
end
|
281
|
-
|
282
|
-
# Checks if transition has require conditions that need validation.
|
283
|
-
#
|
284
|
-
# @param transition [Hash] The transition rule
|
285
|
-
#
|
286
|
-
# @return [Boolean] true if require conditions exist
|
287
|
-
def has_require_conditions?(transition)
|
288
|
-
transition["require"]&.is_a?(::Hash) && !transition["require"].empty?
|
289
|
-
end
|
290
|
-
|
291
|
-
# Checks if transition has prevent conditions that need validation.
|
292
|
-
#
|
293
|
-
# @param transition [Hash] The transition rule
|
294
|
-
#
|
295
|
-
# @return [Boolean] true if prevent conditions exist
|
296
|
-
def has_prevent_conditions?(transition)
|
297
|
-
transition["prevent"]&.is_a?(::Hash) && !transition["prevent"].empty?
|
298
|
-
end
|
299
|
-
|
300
|
-
# Verifies all require conditions are satisfied (logical AND).
|
301
|
-
# All specified conditions must be true for the move to be valid.
|
302
|
-
#
|
303
|
-
# @param require_conditions [Hash] Square -> required state mappings
|
304
|
-
# @param board_state [Hash] Current board state
|
305
|
-
# @param active_game [String] Current player identifier
|
306
|
-
#
|
307
|
-
# @return [Boolean] true if all conditions are satisfied
|
308
|
-
def check_require_conditions(require_conditions, board_state, active_game)
|
309
|
-
require_conditions.all? do |square, required_state|
|
310
|
-
actual_piece = board_state[square]
|
311
|
-
matches_state?(actual_piece, required_state, active_game)
|
312
|
-
end
|
313
|
-
end
|
314
|
-
|
315
|
-
# Verifies none of the prevent conditions are satisfied (logical NOR).
|
316
|
-
# If any prevent condition is true, the move is invalid.
|
317
|
-
#
|
318
|
-
# @param prevent_conditions [Hash] Square -> forbidden state mappings
|
319
|
-
# @param board_state [Hash] Current board state
|
320
|
-
# @param active_game [String] Current player identifier
|
321
|
-
#
|
322
|
-
# @return [Boolean] true if no forbidden conditions are satisfied
|
323
|
-
def check_prevent_conditions(prevent_conditions, board_state, active_game)
|
324
|
-
prevent_conditions.none? do |square, forbidden_state|
|
325
|
-
actual_piece = board_state[square]
|
326
|
-
matches_state?(actual_piece, forbidden_state, active_game)
|
327
|
-
end
|
328
|
-
end
|
86
|
+
# @param location [String] Location to check (CELL coordinate)
|
87
|
+
# @param expected_state [String] Expected state value
|
88
|
+
# @param active_side [Symbol] Active player side
|
89
|
+
# @param squares [Hash] Board state
|
90
|
+
# @return [Boolean]
|
91
|
+
def check_condition(location, expected_state, active_side, squares)
|
92
|
+
actual_qpi = squares[location]
|
329
93
|
|
330
|
-
# Determines if a piece matches a required/forbidden state.
|
331
|
-
# Handles special states ("empty", "enemy") and exact piece matching.
|
332
|
-
#
|
333
|
-
# @param actual_piece [String, nil] The piece currently on the square
|
334
|
-
# @param expected_state [String] The expected/forbidden state
|
335
|
-
# @param active_game [String] Current player identifier
|
336
|
-
#
|
337
|
-
# @return [Boolean] true if the piece matches the expected state
|
338
|
-
def matches_state?(actual_piece, expected_state, active_game)
|
339
94
|
case expected_state
|
340
95
|
when "empty"
|
341
|
-
|
96
|
+
actual_qpi.nil?
|
342
97
|
when "enemy"
|
343
|
-
|
98
|
+
actual_qpi && enemy?(actual_qpi, active_side)
|
344
99
|
else
|
345
|
-
#
|
346
|
-
|
100
|
+
# Expected state is a QPI identifier
|
101
|
+
actual_qpi == expected_state
|
347
102
|
end
|
348
103
|
end
|
349
104
|
|
350
|
-
#
|
351
|
-
# Uses GAN casing conventions to determine ownership based on case correspondence.
|
352
|
-
#
|
353
|
-
# @param piece [String] The piece identifier to check (must be GAN format)
|
354
|
-
# @param active_game [String] Current player identifier
|
105
|
+
# Check if a piece is an enemy relative to active side
|
355
106
|
#
|
356
|
-
# @
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
game_part = piece.split(':', 2).fetch(0)
|
363
|
-
piece_is_uppercase_player = game_part == game_part.upcase
|
364
|
-
current_is_uppercase_player = active_game == active_game.upcase
|
365
|
-
|
366
|
-
# Enemy if players have different casing
|
367
|
-
piece_is_uppercase_player != current_is_uppercase_player
|
107
|
+
# @param qpi_str [String] QPI identifier
|
108
|
+
# @param active_side [Symbol] Active player side
|
109
|
+
# @return [Boolean]
|
110
|
+
def enemy?(qpi_str, active_side)
|
111
|
+
piece_side = Qpi.parse(qpi_str).side
|
112
|
+
piece_side != active_side
|
368
113
|
end
|
369
114
|
end
|
370
115
|
end
|
@@ -1,108 +1,56 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative
|
3
|
+
require_relative "destination/engine"
|
4
4
|
|
5
5
|
module Sashite
|
6
6
|
module Ggn
|
7
7
|
class Ruleset
|
8
8
|
class Source
|
9
|
-
# Represents
|
9
|
+
# Represents movement possibilities from a specific source
|
10
10
|
#
|
11
|
-
#
|
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
|
11
|
+
# @see https://sashite.dev/specs/ggn/1.0.0/
|
27
12
|
class Destination
|
28
|
-
#
|
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
|
13
|
+
# Create a new Destination
|
36
14
|
#
|
37
|
-
# @
|
38
|
-
|
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
|
-
|
15
|
+
# @param data [Hash] Destinations data structure
|
16
|
+
def initialize(data)
|
50
17
|
@data = data
|
51
|
-
@actor = actor
|
52
|
-
@origin = origin
|
53
|
-
|
54
18
|
freeze
|
55
19
|
end
|
56
20
|
|
57
|
-
#
|
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.
|
21
|
+
# Specify the destination location
|
63
22
|
#
|
64
|
-
# @param
|
23
|
+
# @param destination [String] Destination location (CELL coordinate or HAND "*")
|
24
|
+
# @return [Engine] Movement evaluation engine
|
25
|
+
# @raise [KeyError] If destination not found from this source
|
65
26
|
#
|
66
|
-
# @
|
67
|
-
#
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
27
|
+
# @example
|
28
|
+
# engine = destination.to("e2")
|
29
|
+
def to(destination)
|
30
|
+
raise ::KeyError, "Destination not found: #{destination}" unless destination?(destination)
|
31
|
+
|
32
|
+
Engine.new(*@data.fetch(destination))
|
33
|
+
end
|
34
|
+
|
35
|
+
# Return all valid destinations from this source
|
74
36
|
#
|
75
|
-
#
|
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
|
37
|
+
# @return [Array<String>] Destination locations
|
81
38
|
#
|
82
|
-
# @example
|
83
|
-
#
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
39
|
+
# @example
|
40
|
+
# destination.destinations # => ["d1", "d2", "e2", "f2", "f1"]
|
41
|
+
def destinations
|
42
|
+
@data.keys
|
43
|
+
end
|
44
|
+
|
45
|
+
# Check if location is a valid destination from this source
|
88
46
|
#
|
89
|
-
# @
|
90
|
-
#
|
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
|
47
|
+
# @param location [String] Destination location
|
48
|
+
# @return [Boolean]
|
99
49
|
#
|
100
|
-
# @
|
101
|
-
#
|
102
|
-
|
103
|
-
|
104
|
-
transitions = @data.fetch(target)
|
105
|
-
Engine.new(*transitions, actor: @actor, origin: @origin, target: target)
|
50
|
+
# @example
|
51
|
+
# destination.destination?("e2") # => true
|
52
|
+
def destination?(location)
|
53
|
+
@data.key?(location)
|
106
54
|
end
|
107
55
|
end
|
108
56
|
end
|