lib/gamefic/query/base.rb



# frozen_string_literal: true


module Gamefic
  module Query
    # A base class for entity-based queries that can be applied to responses.

    # Each query matches a command token to an object that can be passed into

    # a response callback.

    #

    # Most queries return entities, but there are also queries for plain text

    # and integers.

    #

    class Base
      # @return [Array<Object>]

      attr_reader :arguments

      # @raise [ArgumentError] if any of the arguments are nil

      #

      # @param arguments [Array<Object>]

      # @param name [String]

      def initialize *arguments, name: self.class.to_s
        raise ArgumentError, "nil argument in query" if arguments.any?(&:nil?)

        @arguments = arguments
        @name = name
      end

      # Get a query result for a given subject and token.

      #

      # @param subject [Gamefic::Entity]

      # @param token [String]

      # @return [Result]

      def filter(subject, token)
        scan = Scanner.scan(select(subject), token)
        return Result.new(nil, scan.token) unless scan.matched.one?

        Result.new(scan.matched.first, scan.remainder, scan.strictness)
      end

      # Get an array of entities that match the arguments from the context of

      # the subject.

      #

      # @param subject [Entity]

      # @return [Array<Entity>]

      def select(subject)
        span(subject).that_are(*arguments)
      end

      # Get an array of entities that are candidates for selection from the

      # context of the subject. These are the entities that #select will

      # filter through query's arguments.

      #

      # Subclasses should override this method.

      #

      # @param subject [Entity]

      # @return [Array<Entity>]

      def span(_subject)
        []
      end

      # True if the object is selectable by the subject.

      #

      # @param subject [Entity]

      # @param object [Entity]

      # @return [Boolean]

      def accept?(subject, object)
        select(subject).include?(object)
      end

      # The query's precision. The higher the number, the more specific the

      # query is.

      #

      # In general terms, a query's precision is highest if its arguments

      # select for a specific instance of an entity instead of a class of

      # entity.

      #

      # When a command gets parsed, the resulting list of available actions

      # gets sorted in descending order of their responses' overall precision,

      # so the action with the highest precision gets attempted first.

      #

      # @return [::Integer]

      def precision
        @precision ||= calculate_precision
      end

      def name
        @name || self.class.to_s
      end

      def inspect
        "#{name}(#{arguments.map(&:inspect).join(', ')})"
      end

      def bind(narrative)
        clone.tap do |query|
          query.instance_exec do
            @arguments = narrative.unproxy(@arguments)
          end
        end
      end

      def self.plain
        @plain ||= new
      end

      def self.span(subject)
        plain.span(subject)
      end

      private

      def calculate_precision
        arguments.sum(0) do |arg|
          case arg
          when Entity, Proxy::Base
            1000
          when Class, Module
            class_depth(arg) * 100
          else
            1
          end
        end
      end

      def class_depth(klass)
        return 1 unless klass.is_a?(Class)

        depth = 1
        sup = klass
        depth += 1 while (sup = sup.superclass)
        depth
      end
    end
  end
end