Class: PGN::Node

Inherits:
Object
  • Object
show all
Defined in:
lib/pgn/node.rb

Overview

Node is a live, lazily-built view over the MoveText tree.

A node represents the position reached by playing move from its +parent+'s position; the root has no move and is the game's starting position. The underlying MoveText structure is the source of truth (the parser builds it and the serializer reads it), so a node tree never disagrees with the serialized output.

Mutations edit the underlying MoveText arrays in place and, for sibling reorders, normalize the affected branching point to flat sibling storage. After any structural mutation, Game#root returns a fresh tree — outstanding node references are stale.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(move:, parent:, line:, index:, starting_position: nil, game: nil) ⇒ Node

Returns a new instance of Node.

Parameters:

  • move (PGN::MoveText, nil)

    the move played to reach this node

  • parent (PGN::Node, nil)

    the parent node (nil for the root)

  • line (Array<PGN::MoveText>, nil)

    the line this node's move lives in (the mainline for mainline nodes, the variation Array for variation nodes; nil conceptually for the root, which passes the mainline)

  • index (Integer)

    index of this node's move within line (-1 for the root)

  • starting_position (PGN::Position) (defaults to: nil)

    the root's position

  • game (PGN::Game) (defaults to: nil)

    back-reference so mutations can return a fresh root



30
31
32
33
34
35
36
37
# File 'lib/pgn/node.rb', line 30

def initialize(move:, parent:, line:, index:, starting_position: nil, game: nil)
  @move = move
  @parent = parent
  @line = line
  @index = index
  @starting_position = starting_position
  @game = game
end

Instance Attribute Details

#indexObject (readonly)

Returns the value of attribute index.



17
18
19
# File 'lib/pgn/node.rb', line 17

def index
  @index
end

#lineObject (readonly)

Returns the value of attribute line.



17
18
19
# File 'lib/pgn/node.rb', line 17

def line
  @line
end

#moveObject (readonly)

Returns the value of attribute move.



17
18
19
# File 'lib/pgn/node.rb', line 17

def move
  @move
end

#parentObject (readonly)

Returns the value of attribute parent.



17
18
19
# File 'lib/pgn/node.rb', line 17

def parent
  @parent
end

Instance Method Details

#[](idx) ⇒ Object



86
87
88
# File 'lib/pgn/node.rb', line 86

def [](idx)
  children[idx]
end

#add_main_variation(move_or_moves) ⇒ Object

Make the given moves the new mainline continuation from this node's position. At a terminal node this extends the line; otherwise the old continuation becomes a variation of the new first move. Returns a fresh game.root.



136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/pgn/node.rb', line 136

def add_main_variation(move_or_moves)
  new_line = build_movetexts(move_or_moves)
  cont = continuation_movetext

  if cont.nil?
    @line.push(*new_line)
  else
    normalize_branch_point(cont)
    vars = cont.variations
    pos = @index + 1
    old_tail = @line[(pos + 1)..] || []
    n0 = new_line[0]
    @line[pos] = n0
    @line[(pos + 1)..] = (new_line[1..] || [])
    cont.variations = []
    n0.variations = [[cont, *old_tail], *vars]
  end
  @game.root
end

#add_variation(move_or_moves) ⇒ Object

Append a new variation line (a single SAN String or an Array) branching before this node's next mainline move. Returns a fresh game.root. Raises ArgumentError at a terminal node (a variation must branch before an existing move; use add_main_variation to extend the line).

Raises:

  • (ArgumentError)


123
124
125
126
127
128
129
130
# File 'lib/pgn/node.rb', line 123

def add_variation(move_or_moves)
  cont = continuation_movetext
  raise ArgumentError, 'cannot add a variation at a terminal node' if cont.nil?

  normalize_branch_point(cont)
  cont.variations << build_movetexts(move_or_moves)
  @game.root
end

#annotationObject



47
48
49
# File 'lib/pgn/node.rb', line 47

def annotation
  @move&.annotation
end

#childrenObject

All moves playable from this node's position: the continuation move (the next MoveText in this node's line) plus every variation first-move branching at that same position, recursively through nested brackets. The first child is the mainline continuation; the rest are variations in source order.



60
61
62
63
64
65
66
67
68
69
# File 'lib/pgn/node.rb', line 60

def children
  @children ||= begin
    cont = continuation_movetext
    list = []
    collect_first_moves(cont, @line, @index + 1) do |mt, l, idx|
      list << Node.new(move: mt, parent: self, line: l, index: idx, game: @game)
    end
    list
  end
end

#commentObject



51
52
53
# File 'lib/pgn/node.rb', line 51

def comment
  @move&.comment
end

#deleteObject

Remove this node and its subtree. If it is the mainline continuation, the first remaining variation (if any) takes its place; otherwise the line is truncated at this point. Returns a fresh game.root.



239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
# File 'lib/pgn/node.rb', line 239

def delete
  return @game.root if root?

  i = sibling_index
  return @game.root if i.nil?

  cont = parent_continuation
  normalize_branch_point(cont)
  vars = cont.variations
  if i.zero?
    delete_mainline_continuation(cont, vars)
  else
    vars.delete_at(i - 1)
  end
  @game.root
end

#demoteObject

Move this node one slot toward the end among its siblings. At index 0 (the mainline) this is the inverse swap: variation #1 becomes the new mainline and the old mainline becomes variation #1. No-op at the last index. Returns a fresh game.root.



176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/pgn/node.rb', line 176

def demote
  return @game.root if root?

  i = sibling_index
  sibs = @parent.children
  return @game.root if i.nil? || i == sibs.size - 1

  cont = parent_continuation
  normalize_branch_point(cont)
  vars = cont.variations
  if i.zero?
    v1 = vars.shift
    make_mainline(cont, v1, vars, old_main_position: :first)
  else
    vars[i - 1], vars[i] = vars[i], vars[i - 1]
  end
  @game.root
end

#demote_to_lastObject

Move this node to the last position among its siblings. At index 0 the last variation becomes the new mainline and the old mainline becomes the last variation. Returns a fresh game.root.



215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
# File 'lib/pgn/node.rb', line 215

def demote_to_last
  return @game.root if root?

  i = sibling_index
  return @game.root if i.nil?

  cont = parent_continuation
  normalize_branch_point(cont)
  vars = cont.variations
  if i.zero?
    return @game.root if vars.empty?

    last = vars.pop
    make_mainline(cont, last, vars, old_main_position: :last)
  else
    el = vars.delete_at(i - 1)
    vars.push(el)
  end
  @game.root
end

#main_lineObject

Yields each mainline node from self.next onward (the root is excluded), so main_line.map(&:notation) == game.moves.map(&:notation) and main_line.map(&:position) == game.positions[1..]. Returns an Enumerator when called without a block.



94
95
96
97
98
99
100
101
102
# File 'lib/pgn/node.rb', line 94

def main_line
  return enum_for(:main_line) unless block_given?

  node = self.next
  while node
    yield node
    node = node.next
  end
end

#notationObject



43
44
45
# File 'lib/pgn/node.rb', line 43

def notation
  @move&.notation
end

#positionObject

The position reached at this node: the starting position for the root, otherwise parent.position with move.notation applied. Pure Ruby (no native engine); raises on an illegal SAN exactly like Game#positions. Cached on the node.



108
109
110
111
112
113
114
115
116
# File 'lib/pgn/node.rb', line 108

def position
  return @position if defined?(@position)

  @position = if root?
                @starting_position
              else
                @parent.position.then { |p| p.move(@move.notation) }
              end
end

#previousObject

The parent node, or nil for the root.



82
83
84
# File 'lib/pgn/node.rb', line 82

def previous
  @parent
end

#promoteObject

Move this node one slot toward the mainline among its siblings. No-op for the mainline (index 0) or the first variation (index 1). Returns a fresh game.root.



159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/pgn/node.rb', line 159

def promote
  return @game.root if root?

  i = sibling_index
  return @game.root if i.nil? || i <= 1

  cont = parent_continuation
  normalize_branch_point(cont)
  vars = cont.variations
  vars[i - 1], vars[i - 2] = vars[i - 2], vars[i - 1]
  @game.root
end

#promote_to_mainObject

Make this node the mainline at its branching point (the old mainline becomes variation #1). No-op if already the mainline. Returns a fresh game.root.



198
199
200
201
202
203
204
205
206
207
208
209
210
# File 'lib/pgn/node.rb', line 198

def promote_to_main
  return @game.root if root?

  i = sibling_index
  return @game.root if i.nil? || i.zero?

  cont = parent_continuation
  normalize_branch_point(cont)
  vars = cont.variations
  vk = vars.delete_at(i - 1)
  make_mainline(cont, vk, vars, old_main_position: :first)
  @game.root
end

#root?Boolean

Returns:

  • (Boolean)


39
40
41
# File 'lib/pgn/node.rb', line 39

def root?
  @move.nil?
end

#variationsObject

The non-mainline alternatives at this node's position.



72
73
74
# File 'lib/pgn/node.rb', line 72

def variations
  children[1..] || []
end