Class: Sashite::Stn::Transition
- Inherits:
-
Object
- Object
- Sashite::Stn::Transition
- Defined in:
- lib/sashite/stn/transition.rb
Overview
Immutable representation of an STN delta.
A Transition encodes the net differences between two positions:
-
board changes (CELL => QPI or nil)
-
hand/reserve deltas (QPI => non-zero Integer)
-
active player toggle (Boolean)
All instances are frozen; any “mutation” returns a new Transition.
Instance Attribute Summary collapse
-
#board_changes ⇒ Hash{String=>String,nil}
readonly
Board final states by CELL.
-
#hand_changes ⇒ Hash{String=>Integer}
readonly
Hand deltas by QPI (non-zero).
-
#toggle ⇒ Boolean
readonly
True if active player should switch.
Class Method Summary collapse
-
.parse(data) ⇒ Transition
Parse a Ruby Hash (with “board”, “hands”, “toggle”) into a Transition.
-
.valid?(data) ⇒ Boolean
Predicate wrapper for
parse
that traps validation errors.
Instance Method Summary collapse
-
#==(other) ⇒ Boolean
(also: #eql?)
Structural equality.
-
#board_change(cell) ⇒ String?
Read a single board change for a given CELL.
-
#combine(other) ⇒ Transition
Combine this transition with another one, left-to-right.
-
#empty? ⇒ Boolean
True when no changes at all and no toggle.
-
#hand_change(qpi) ⇒ Integer?
Read a single hand delta for a given QPI key.
-
#has_board_change?(cell) ⇒ Boolean
Whether a CELL is present in the board delta.
-
#has_hand_change?(qpi) ⇒ Boolean
Whether a QPI key is present in the hand delta.
-
#hash ⇒ Integer
Hash code consistent with #==.
-
#initialize(board: {}, hands: {}, toggle: false) ⇒ Transition
constructor
Build an immutable transition.
-
#invert ⇒ Transition
Compute an inverse transition for hands and toggle only.
-
#invert_board_against(previous_board:) ⇒ Transition
Build a board inverse against a known before position snapshot.
-
#pass_move? ⇒ Boolean
True when there is a toggle only (no board/hands).
-
#to_h ⇒ Hash
Produce a Ruby Hash representation suitable for serialization.
-
#toggle? ⇒ Boolean
True if
toggle
is set. -
#with_board_change(cell, value) ⇒ Transition
Replace or add a board entry (CELL => value) and return a new Transition.
-
#with_hand_change(qpi, delta) ⇒ Transition
Replace or add a single hand delta and return a new Transition.
-
#with_toggle(value) ⇒ Transition
Return a new Transition with the given toggle flag.
-
#without_board_change(cell) ⇒ Transition
Remove a board entry (if present) and return a new Transition.
-
#without_hand_change(qpi) ⇒ Transition
Remove a hand entry (if present) and return a new Transition.
Constructor Details
#initialize(board: {}, hands: {}, toggle: false) ⇒ Transition
Build an immutable transition.
Keys for board
and hands
are stringified and values are validated. Inputs are never mutated.
54 55 56 57 58 59 60 61 |
# File 'lib/sashite/stn/transition.rb', line 54 def initialize(board: {}, hands: {}, toggle: false) @board_changes = _stringify_map(board).freeze @hand_changes = _stringify_map(hands).freeze @toggle = toggle _validate! freeze end |
Instance Attribute Details
#board_changes ⇒ Hash{String=>String,nil} (readonly)
Returns board final states by CELL.
33 34 35 |
# File 'lib/sashite/stn/transition.rb', line 33 def board_changes @board_changes end |
#hand_changes ⇒ Hash{String=>Integer} (readonly)
Returns hand deltas by QPI (non-zero).
35 36 37 |
# File 'lib/sashite/stn/transition.rb', line 35 def hand_changes @hand_changes end |
#toggle ⇒ Boolean (readonly)
Returns true if active player should switch.
37 38 39 |
# File 'lib/sashite/stn/transition.rb', line 37 def toggle @toggle end |
Class Method Details
.parse(data) ⇒ Transition
Parse a Ruby Hash (with “board”, “hands”, “toggle”) into a Transition. Keys inside “board”/“hands” may be symbols or strings.
73 74 75 76 77 78 79 80 81 |
# File 'lib/sashite/stn/transition.rb', line 73 def self.parse(data) raise Error::Validation, "STN must be a Hash" unless data.is_a?(::Hash) board = data.key?("board") ? data["board"] : (data[:board] if data.key?(:board)) hands = data.key?("hands") ? data["hands"] : (data[:hands] if data.key?(:hands)) toggle = data.key?("toggle") ? data["toggle"] : (data[:toggle] if data.key?(:toggle)) new(board: board || {}, hands: hands || {}, toggle: !!toggle) end |
Instance Method Details
#==(other) ⇒ Boolean Also known as: eql?
Structural equality.
269 270 271 272 273 274 |
# File 'lib/sashite/stn/transition.rb', line 269 def ==(other) other.is_a?(Transition) && board_changes == other.board_changes && hand_changes == other.hand_changes && toggle? == other.toggle? end |
#board_change(cell) ⇒ String?
Read a single board change for a given CELL.
115 116 117 |
# File 'lib/sashite/stn/transition.rb', line 115 def board_change(cell) @board_changes[cell] end |
#combine(other) ⇒ Transition
Combine this transition with another one, left-to-right. STN composition semantics:
-
board: last write wins per CELL
-
hands: deltas are summed; entries summing to zero are removed
-
toggle: XOR
231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 |
# File 'lib/sashite/stn/transition.rb', line 231 def combine(other) raise Error::Validation, "Expected Transition, got: #{other.class}" unless other.is_a?(Transition) combined_board = @board_changes.merge(other.board_changes) combined_hands = ::Hash.new(0) (@hand_changes.keys | other.hand_changes.keys).each do |k| sum = (@hand_changes[k] || 0) + (other.hand_changes[k] || 0) combined_hands[k] = sum unless sum.zero? end self.class.new( board: combined_board, hands: combined_hands, toggle: (@toggle ^ other.toggle?) ) end |
#empty? ⇒ Boolean
Returns true when no changes at all and no toggle.
102 103 104 |
# File 'lib/sashite/stn/transition.rb', line 102 def empty? @board_changes.empty? && @hand_changes.empty? && !@toggle end |
#hand_change(qpi) ⇒ Integer?
Read a single hand delta for a given QPI key.
123 124 125 |
# File 'lib/sashite/stn/transition.rb', line 123 def hand_change(qpi) @hand_changes[qpi] end |
#has_board_change?(cell) ⇒ Boolean
Whether a CELL is present in the board delta.
131 132 133 |
# File 'lib/sashite/stn/transition.rb', line 131 def has_board_change?(cell) @board_changes.key?(cell) end |
#has_hand_change?(qpi) ⇒ Boolean
Whether a QPI key is present in the hand delta.
139 140 141 |
# File 'lib/sashite/stn/transition.rb', line 139 def has_hand_change?(qpi) @hand_changes.key?(qpi) end |
#hash ⇒ Integer
Hash code consistent with #==.
280 281 282 |
# File 'lib/sashite/stn/transition.rb', line 280 def hash [@board_changes, @hand_changes, @toggle].hash end |
#invert ⇒ Transition
Compute an inverse transition for hands and toggle only. Board inversion requires knowledge of the surrounding positions and therefore is not attempted here (board delta left untouched).
If you need a full board inverse, use #invert_board_against.
299 300 301 302 |
# File 'lib/sashite/stn/transition.rb', line 299 def invert inv_hands = @hand_changes.transform_values(&:-@) self.class.new(board: @board_changes, hands: inv_hands, toggle: @toggle) end |
#invert_board_against(previous_board:) ⇒ Transition
Build a board inverse against a known before position snapshot. Given a map of previous CELL states (QPI or nil), construct a transition that would restore those cells. Hands and toggle are inverted like #invert.
317 318 319 320 321 322 323 324 325 326 327 328 329 330 |
# File 'lib/sashite/stn/transition.rb', line 317 def invert_board_against(previous_board:) raise Error::Validation, "previous_board must be a Hash of CELL=>QPI/nil" unless previous_board.is_a?(::Hash) inv_board = {} @board_changes.each_key do |cell| inv_board[cell] = previous_board[cell] end self.class.new( board: inv_board, hands: @hand_changes.transform_values(&:-@), toggle: @toggle ) end |
#pass_move? ⇒ Boolean
Returns true when there is a toggle only (no board/hands).
107 108 109 |
# File 'lib/sashite/stn/transition.rb', line 107 def pass_move? @board_changes.empty? && @hand_changes.empty? && @toggle end |
#to_h ⇒ Hash
Produce a Ruby Hash representation suitable for serialization. Keys at the top level use Ruby symbols (:board, :hands, :toggle). Omitted fields are not present in the result.
257 258 259 260 261 262 263 |
# File 'lib/sashite/stn/transition.rb', line 257 def to_h h = {} h[:board] = @board_changes unless @board_changes.empty? h[:hands] = @hand_changes unless @hand_changes.empty? h[:toggle] = true if @toggle h end |
#toggle? ⇒ Boolean
Returns true if toggle
is set.
97 98 99 |
# File 'lib/sashite/stn/transition.rb', line 97 def toggle? @toggle end |
#with_board_change(cell, value) ⇒ Transition
Replace or add a board entry (CELL => value) and return a new Transition.
151 152 153 154 155 156 157 |
# File 'lib/sashite/stn/transition.rb', line 151 def with_board_change(cell, value) self.class.new( board: @board_changes.merge(cell.to_s => value), hands: @hand_changes, toggle: @toggle ) end |
#with_hand_change(qpi, delta) ⇒ Transition
Replace or add a single hand delta and return a new Transition.
167 168 169 170 171 172 173 |
# File 'lib/sashite/stn/transition.rb', line 167 def with_hand_change(qpi, delta) self.class.new( board: @board_changes, hands: @hand_changes.merge(qpi.to_s => delta), toggle: @toggle ) end |
#with_toggle(value) ⇒ Transition
Return a new Transition with the given toggle flag.
182 183 184 185 186 187 188 |
# File 'lib/sashite/stn/transition.rb', line 182 def with_toggle(value) self.class.new( board: @board_changes, hands: @hand_changes, toggle: value ) end |
#without_board_change(cell) ⇒ Transition
Remove a board entry (if present) and return a new Transition.
194 195 196 197 198 199 200 201 202 203 |
# File 'lib/sashite/stn/transition.rb', line 194 def without_board_change(cell) key = cell.to_s return self unless @board_changes.key?(key) self.class.new( board: @board_changes.reject { |k, _| k == key }, hands: @hand_changes, toggle: @toggle ) end |
#without_hand_change(qpi) ⇒ Transition
Remove a hand entry (if present) and return a new Transition.
209 210 211 212 213 214 215 216 217 218 |
# File 'lib/sashite/stn/transition.rb', line 209 def without_hand_change(qpi) key = qpi.to_s return self unless @hand_changes.key?(key) self.class.new( board: @board_changes, hands: @hand_changes.reject { |k, _| k == key }, toggle: @toggle ) end |