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
# 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)
    make_mainline(@line, @index + 1, cont, new_line, cont.variations, old_main_position: :first)
  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.



203
204
205
206
207
208
209
210
211
# File 'lib/pgn/node.rb', line 203

def delete
  mutate_sibling do |i, cont, vars|
    if i.zero?
      delete_mainline_continuation(cont, vars)
    else
      vars.delete_at(i - 1)
    end
  end
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.



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

def demote
  mutate_sibling(noop: ->(i) { i == @parent.children.size - 1 }) do |i, cont, vars|
    if i.zero?
      v1 = vars.shift
      make_mainline(@parent.line, @parent.index + 1, cont, v1, vars, old_main_position: :first)
    else
      vars[i - 1], vars[i] = vars[i], vars[i - 1]
    end
  end
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.



186
187
188
189
190
191
192
193
194
195
196
197
198
# File 'lib/pgn/node.rb', line 186

def demote_to_last
  mutate_sibling do |i, cont, vars|
    if i.zero?
      unless vars.empty?
        last = vars.pop
        make_mainline(@parent.line, @parent.index + 1, cont, last, vars, old_main_position: :last)
      end
    else
      el = vars.delete_at(i - 1)
      vars.push(el)
    end
  end
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.



152
153
154
155
156
# File 'lib/pgn/node.rb', line 152

def promote
  mutate_sibling(noop: ->(i) { i <= 1 }) do |i, _cont, vars|
    vars[i - 1], vars[i - 2] = vars[i - 2], vars[i - 1]
  end
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.



176
177
178
179
180
181
# File 'lib/pgn/node.rb', line 176

def promote_to_main
  mutate_sibling(noop: lambda(&:zero?)) do |i, cont, vars|
    vk = vars.delete_at(i - 1)
    make_mainline(@parent.line, @parent.index + 1, cont, vk, vars, old_main_position: :first)
  end
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