lib/gamefic/node.rb



# frozen_string_literal: true


require 'set'

module Gamefic
  # Exception raised when setting a node's parent to an invalid object.

  #

  class NodeError < RuntimeError; end

  # Parent/child relationships for objects.

  #

  module Node
    # The object's parent.

    #

    # @return [Node, nil]

    attr_reader :parent

    # An array of the object's children.

    #

    # @return [Array<Node>]

    def children
      child_set.to_a.freeze
    end

    # Get a flat array of all descendants.

    #

    # @return [Array<Node>]

    def flatten
      children.flat_map { |child| [child] + child.flatten }
    end

    # Set the object's parent.

    #

    # @param node [Node, nil]

    def parent=(node)
      return if node == parent

      validate_parent node

      parent&.rem_child self
      @parent = node
      @relation = nil
      parent&.add_child self
    end

    # The node's relation to its parent.

    #

    # The inherently supported relations are `:in` and `:on`, but authors are

    # free to define their own.

    #

    # @return [Symbol, nil]

    def relation
      @relation ||= (parent ? :in : nil)
    end

    # @param symbol [Symbol, nil]

    def relation=(symbol)
      raise NodeError, "Invalid relation #{symbol.inspect} on #{inspect} without parent" unless parent || !symbol

      @relation = symbol
    end

    # Add children to the node. Return all the node's children.

    #

    # @param children [Array<Node, Array<Node>>]

    # @param relation [Symbol, nil]

    # @return [Array<Node>]

    def take *children, relation: nil
      children.flatten.each { |child| child.put self, relation }
      children
    end

    def put(parent, relation = nil)
      self.parent = parent
      @relation = relation
    end
    alias place put

    # Get an array of children that are accessible to external entities.

    #

    # A child is considered accessible if external entities can interact with

    # it. For Example, an author can designate that the contents of a bowl are

    # accessible, while the contents of a locked safe are not. All of an

    # entity's children are accessible by default. Authors should override this

    # method if they need custom behavior.

    #

    # @return [Array<Entity>]

    def accessible
      children
    end

    # True if this node is the other's parent.

    #

    # @param other [Node]

    def include?(other)
      other.parent == self
    end

    # True if this node and the other node have the same parent.

    #

    # @param other [Node]

    def adjacent?(other)
      other.parent == parent
    end

    protected

    def add_child(node)
      child_set.add node
    end

    def rem_child(node)
      child_set.delete node
    end

    private

    def child_set
      @child_set ||= Set.new
    end

    def validate_parent(node)
      raise NodeError, "Parent of #{inspect} must be a Node, received #{node.inspect}" unless node.is_a?(Node) || node.nil?
      raise NodeError, "#{inspect} cannot be its own parent" if node == self
      raise NodeError, "#{inspect} cannot be a child of descendant #{node.inspect}" if flatten.include?(node)
    end
  end
end