Class: Sashite::Stn::Transition

Inherits:
Object
  • Object
show all
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.

Examples:

Board-only change with toggle

t = Sashite::Stn::Transition.new(
      board: { "e2" => nil, "e4" => "C:P" },
      toggle: true
    )
t.toggle?        # => true
t.board_changes  # => { "e2" => nil, "e4" => "C:P" }

Hand-only delta (add one black pawn to reserve)

t = Sashite::Stn::Transition.new(hands: { "c:p" => 1 })
t.hand_changes   # => { "c:p" => 1 }

Empty transition

Sashite::Stn::Transition.new.empty? # => true

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

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.

Examples:

Sashite::Stn::Transition.new(board: { "e1" => nil, "g1" => "C:K" }, toggle: true)

Parameters:

  • board (Hash{String,Symbol=>String,nil}) (defaults to: {})
  • hands (Hash{String,Symbol=>Integer}) (defaults to: {})
  • toggle (Boolean) (defaults to: false)

Raises:



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_changesHash{String=>String,nil} (readonly)

Returns board final states by CELL.

Returns:

  • (Hash{String=>String,nil})

    board final states by CELL



33
34
35
# File 'lib/sashite/stn/transition.rb', line 33

def board_changes
  @board_changes
end

#hand_changesHash{String=>Integer} (readonly)

Returns hand deltas by QPI (non-zero).

Returns:

  • (Hash{String=>Integer})

    hand deltas by QPI (non-zero)



35
36
37
# File 'lib/sashite/stn/transition.rb', line 35

def hand_changes
  @hand_changes
end

#toggleBoolean (readonly)

Returns true if active player should switch.

Returns:

  • (Boolean)

    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.

Examples:

Sashite::Stn::Transition.parse(
  "board" => { "e2" => nil, "e4" => "C:P" }, "toggle" => true
)

Parameters:

  • data (Hash)

Returns:

Raises:



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

.valid?(data) ⇒ Boolean

Predicate wrapper for parse that traps validation errors.

Examples:

Sashite::Stn::Transition.valid?({ "board" => { "a0" => "C:P" } }) # => false

Parameters:

  • data (Hash)

Returns:

  • (Boolean)


90
91
92
93
94
# File 'lib/sashite/stn/transition.rb', line 90

def self.valid?(data)
  !!parse(data)
rescue ::Sashite::Stn::Error
  false
end

Instance Method Details

#==(other) ⇒ Boolean Also known as: eql?

Structural equality.

Parameters:

  • other (Object)

Returns:

  • (Boolean)


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.

Parameters:

  • cell (String)

Returns:

  • (String, nil)

    QPI or nil for empty; nil if the cell is not changed by this transition



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

Examples:

t = t1.combine(t2)

Parameters:

Returns:

Raises:



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.

Returns:

  • (Boolean)

    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.

Parameters:

  • qpi (String)

Returns:

  • (Integer, nil)


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.

Parameters:

  • cell (String)

Returns:

  • (Boolean)


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.

Parameters:

  • qpi (String)

Returns:

  • (Boolean)


139
140
141
# File 'lib/sashite/stn/transition.rb', line 139

def has_hand_change?(qpi)
  @hand_changes.key?(qpi)
end

#hashInteger

Hash code consistent with #==.

Returns:

  • (Integer)


280
281
282
# File 'lib/sashite/stn/transition.rb', line 280

def hash
  [@board_changes, @hand_changes, @toggle].hash
end

#invertTransition

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.

Examples:

t  = Sashite::Stn::Transition.new(hands: { "c:p" => 2 }, toggle: true)
ti = t.invert
ti.hand_changes # => { "c:p" => -2 }
ti.toggle?      # => true

Returns:



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.

Examples:

# Suppose before: e2 => "C:P", e4 => nil   and t sets e2=>nil, e4=>"C:P"
before = { "e2" => "C:P", "e4" => nil }
t  = Sashite::Stn::Transition.new(board: { "e2" => nil, "e4" => "C:P" }, toggle: true)
ti = t.invert_board_against(previous_board: before)
ti.board_changes # => { "e2" => "C:P", "e4" => nil }

Parameters:

  • previous_board (Hash{String=>String,nil})

    canonical “before” snapshot

Returns:

Raises:



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).

Returns:

  • (Boolean)

    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_hHash

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.

Examples:

Sashite::Stn::Transition.new(toggle: true).to_h # => { :toggle=>true }

Returns:

  • (Hash)


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.

Returns:

  • (Boolean)

    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.

Examples:

t2 = t1.with_board_change("f3", "S:+N")

Parameters:

  • cell (String, Symbol)
  • value (String, nil)

    QPI or nil

Returns:



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.

Examples:

t2 = t1.with_hand_change("c:b", 1)

Parameters:

  • qpi (String, Symbol)
  • delta (Integer)

    non-zero

Returns:



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.

Examples:

t2 = t1.with_toggle(false)

Parameters:

  • value (Boolean)

Returns:



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.

Parameters:

  • cell (String, Symbol)

Returns:



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.

Parameters:

  • qpi (String, Symbol)

Returns:



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