gamefic 3.0.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -1
- data/lib/gamefic/action.rb +9 -1
- data/lib/gamefic/active/epic.rb +8 -3
- data/lib/gamefic/active/take.rb +5 -5
- data/lib/gamefic/active.rb +30 -13
- data/lib/gamefic/command.rb +4 -15
- data/lib/gamefic/composer.rb +68 -0
- data/lib/gamefic/dispatcher.rb +47 -40
- data/lib/gamefic/entity.rb +3 -5
- data/lib/gamefic/expression.rb +31 -0
- data/lib/gamefic/plot.rb +1 -1
- data/lib/gamefic/props/default.rb +12 -4
- data/lib/gamefic/props/output.rb +82 -0
- data/lib/gamefic/props.rb +1 -0
- data/lib/gamefic/query/base.rb +24 -0
- data/lib/gamefic/query/general.rb +4 -0
- data/lib/gamefic/query/scoped.rb +5 -0
- data/lib/gamefic/query/text.rb +16 -5
- data/lib/gamefic/response.rb +19 -25
- data/lib/gamefic/rulebook/events.rb +7 -7
- data/lib/gamefic/rulebook.rb +2 -2
- data/lib/gamefic/scanner.rb +52 -25
- data/lib/gamefic/scene/default.rb +3 -3
- data/lib/gamefic/scene/multiple_choice.rb +1 -1
- data/lib/gamefic/scriptable/entities.rb +8 -5
- data/lib/gamefic/scriptable/proxy.rb +11 -0
- data/lib/gamefic/scriptable/queries.rb +2 -2
- data/lib/gamefic/scriptable/scenes.rb +6 -6
- data/lib/gamefic/snapshot.rb +9 -1
- data/lib/gamefic/syntax.rb +4 -4
- data/lib/gamefic/version.rb +1 -1
- data/lib/gamefic.rb +2 -0
- metadata +5 -2
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 3900923aaef12a43321ce6f9cc5957953b12bba7d939e0a1666d8c6c75703855
         | 
| 4 | 
            +
              data.tar.gz: 6023310c5e26e9632ed4c053a9bea619e1455dbb66966d82fc9e772044d19d3a
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: aea9a57211541f056c730707227020f2c35376d59cc4e69c3c7463e8a8bafa7b6fcd4585a99bf2ce235fdc4c583393f53e060554e06f6e5912358959305627eb
         | 
| 7 | 
            +
              data.tar.gz: 5351ea7cc8b3ff4e01e064eb12a594dc2d928641b19e8593243da00b672780acff0d808d73eca1a80cf2e2139fd657b870fa7a5a5315e0a2a3998bc306db0c05
         | 
    
        data/CHANGELOG.md
    CHANGED
    
    | @@ -1,4 +1,13 @@ | |
| 1 | 
            -
            ## 3. | 
| 1 | 
            +
            ## 3.1.0 - April 8, 2024
         | 
| 2 | 
            +
            - Dispatcher prioritizes strict token matches
         | 
| 3 | 
            +
            - Scanner builds commands
         | 
| 4 | 
            +
            - Tokenize expressions and execute commands
         | 
| 5 | 
            +
            - Delete concluded subplots last in Plot#ready
         | 
| 6 | 
            +
            - Fix plot conclusion check after subplots conclude
         | 
| 7 | 
            +
            - Correct contexts for conclude and output blocks
         | 
| 8 | 
            +
            - Reinstate Active#last_input
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            ## 3.0.0 - January 27, 2024
         | 
| 2 11 | 
             
            - Instantiate subplots from snapshots
         | 
| 3 12 | 
             
            - Split Action into Response and Action
         | 
| 4 13 | 
             
            - Logging
         | 
    
        data/lib/gamefic/action.rb
    CHANGED
    
    | @@ -13,8 +13,10 @@ module Gamefic | |
| 13 13 | 
             
                # action's performance.
         | 
| 14 14 | 
             
                #
         | 
| 15 15 | 
             
                class Hook
         | 
| 16 | 
            +
                  # @param [Array<Symbol>]
         | 
| 16 17 | 
             
                  attr_reader :verbs
         | 
| 17 18 |  | 
| 19 | 
            +
                  # @param [Proc]
         | 
| 18 20 | 
             
                  attr_reader :block
         | 
| 19 21 |  | 
| 20 22 | 
             
                  def initialize *verbs, &block
         | 
| @@ -45,11 +47,13 @@ module Gamefic | |
| 45 47 | 
             
                  @response = response
         | 
| 46 48 | 
             
                end
         | 
| 47 49 |  | 
| 50 | 
            +
                # @return [self]
         | 
| 48 51 | 
             
                def execute
         | 
| 49 | 
            -
                  return if cancelled?
         | 
| 52 | 
            +
                  return self if cancelled? || executed?
         | 
| 50 53 |  | 
| 51 54 | 
             
                  @executed = true
         | 
| 52 55 | 
             
                  response.execute actor, *arguments
         | 
| 56 | 
            +
                  self
         | 
| 53 57 | 
             
                end
         | 
| 54 58 |  | 
| 55 59 | 
             
                # True if the response has been executed. False typically means that the
         | 
| @@ -75,6 +79,10 @@ module Gamefic | |
| 75 79 | 
             
                  response.verb
         | 
| 76 80 | 
             
                end
         | 
| 77 81 |  | 
| 82 | 
            +
                def narrative
         | 
| 83 | 
            +
                  response.narrative
         | 
| 84 | 
            +
                end
         | 
| 85 | 
            +
             | 
| 78 86 | 
             
                def meta?
         | 
| 79 87 | 
             
                  response.meta?
         | 
| 80 88 | 
             
                end
         | 
    
        data/lib/gamefic/active/epic.rb
    CHANGED
    
    | @@ -47,9 +47,14 @@ module Gamefic | |
| 47 47 | 
             
                    narratives.one?
         | 
| 48 48 | 
             
                  end
         | 
| 49 49 |  | 
| 50 | 
            -
                   | 
| 51 | 
            -
             | 
| 52 | 
            -
             | 
| 50 | 
            +
                  def syntaxes
         | 
| 51 | 
            +
                    rulebooks.flat_map(&:syntaxes)
         | 
| 52 | 
            +
                  end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                  def responses_for(*verbs)
         | 
| 55 | 
            +
                    rulebooks.to_a
         | 
| 56 | 
            +
                             .reverse
         | 
| 57 | 
            +
                             .flat_map { |rb| rb.responses_for(*verbs) }
         | 
| 53 58 | 
             
                  end
         | 
| 54 59 |  | 
| 55 60 | 
             
                  # @param name [Symbol]
         | 
    
        data/lib/gamefic/active/take.rb
    CHANGED
    
    | @@ -16,7 +16,7 @@ module Gamefic | |
| 16 16 |  | 
| 17 17 | 
             
                  # @param actor [Active]
         | 
| 18 18 | 
             
                  # @param cue [Active::Cue]
         | 
| 19 | 
            -
                  # @param props [Props::Default]
         | 
| 19 | 
            +
                  # @param props [Props::Default, nil]
         | 
| 20 20 | 
             
                  def initialize actor, cue, props = nil
         | 
| 21 21 | 
             
                    @actor = actor
         | 
| 22 22 | 
             
                    @cue = cue
         | 
| @@ -31,12 +31,12 @@ module Gamefic | |
| 31 31 |  | 
| 32 32 | 
             
                  # @return [Props::Default]
         | 
| 33 33 | 
             
                  def start
         | 
| 34 | 
            -
                     | 
| 34 | 
            +
                    props.output[:scene] = scene.to_hash
         | 
| 35 35 | 
             
                    scene.run_start_blocks actor, props
         | 
| 36 36 | 
             
                    scene.start actor, props
         | 
| 37 37 | 
             
                    # @todo See if this can be handled better
         | 
| 38 | 
            -
                    actor.epic.rulebooks.each { |rlbk| rlbk.run_player_output_blocks actor,  | 
| 39 | 
            -
                     | 
| 38 | 
            +
                    actor.epic.rulebooks.each { |rlbk| rlbk.run_player_output_blocks actor, props.output }
         | 
| 39 | 
            +
                    props.output.merge!({
         | 
| 40 40 | 
             
                                          messages: actor.messages,
         | 
| 41 41 | 
             
                                          queue: actor.queue
         | 
| 42 42 | 
             
                                        })
         | 
| @@ -47,7 +47,7 @@ module Gamefic | |
| 47 47 | 
             
                  def finish
         | 
| 48 48 | 
             
                    actor.flush
         | 
| 49 49 | 
             
                    scene.finish(actor, props)
         | 
| 50 | 
            -
                     | 
| 50 | 
            +
                    props.output.replace(last_prompt: props.prompt, last_input: props.input)
         | 
| 51 51 | 
             
                    scene.run_finish_blocks actor, props
         | 
| 52 52 | 
             
                  end
         | 
| 53 53 |  | 
    
        data/lib/gamefic/active.rb
    CHANGED
    
    | @@ -21,6 +21,9 @@ module Gamefic | |
| 21 21 | 
             
                # @return [Active::Cue, nil]
         | 
| 22 22 | 
             
                attr_reader :next_cue
         | 
| 23 23 |  | 
| 24 | 
            +
                # @return [String, nil]
         | 
| 25 | 
            +
                attr_reader :last_input
         | 
| 26 | 
            +
             | 
| 24 27 | 
             
                # @return [Symbol, nil]
         | 
| 25 28 | 
             
                def next_scene
         | 
| 26 29 | 
             
                  next_cue&.scene
         | 
| @@ -48,12 +51,22 @@ module Gamefic | |
| 48 51 | 
             
                  @queue ||= []
         | 
| 49 52 | 
             
                end
         | 
| 50 53 |  | 
| 51 | 
            -
                #  | 
| 52 | 
            -
                #  | 
| 54 | 
            +
                # Data that will be sent to the user. The output is typically sent after a
         | 
| 55 | 
            +
                # scene has started and before the user is prompted for input.
         | 
| 56 | 
            +
                #
         | 
| 57 | 
            +
                # The output object attached to the actor is always frozen. Authors should
         | 
| 58 | 
            +
                # use on_player_output blocks to modify output to be sent to the user.
         | 
| 53 59 | 
             
                #
         | 
| 54 | 
            -
                # @return [ | 
| 60 | 
            +
                # @return [Props::Output]
         | 
| 55 61 | 
             
                def output
         | 
| 56 | 
            -
                  @output ||=  | 
| 62 | 
            +
                  @output ||= Props::Output.new.freeze
         | 
| 63 | 
            +
                end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                # The output from the previous turn.
         | 
| 66 | 
            +
                #
         | 
| 67 | 
            +
                # @return [Props::Output]
         | 
| 68 | 
            +
                def last_output
         | 
| 69 | 
            +
                  @last_output ||= output
         | 
| 57 70 | 
             
                end
         | 
| 58 71 |  | 
| 59 72 | 
             
                # Perform a command.
         | 
| @@ -65,11 +78,10 @@ module Gamefic | |
| 65 78 | 
             
                #   character.perform "take the key"
         | 
| 66 79 | 
             
                #
         | 
| 67 80 | 
             
                # @param command [String]
         | 
| 68 | 
            -
                # @return [ | 
| 81 | 
            +
                # @return [Action, nil]
         | 
| 69 82 | 
             
                def perform(command)
         | 
| 70 83 | 
             
                  dispatchers.push Dispatcher.dispatch(self, command)
         | 
| 71 | 
            -
                  dispatchers.last.execute
         | 
| 72 | 
            -
                  dispatchers.pop
         | 
| 84 | 
            +
                  dispatchers.last.execute.tap { dispatchers.pop }
         | 
| 73 85 | 
             
                end
         | 
| 74 86 |  | 
| 75 87 | 
             
                # Quietly perform a command.
         | 
| @@ -95,11 +107,10 @@ module Gamefic | |
| 95 107 | 
             
                #
         | 
| 96 108 | 
             
                # @param verb [Symbol]
         | 
| 97 109 | 
             
                # @param params [Array]
         | 
| 98 | 
            -
                # @return [ | 
| 110 | 
            +
                # @return [Action, nil]
         | 
| 99 111 | 
             
                def execute(verb, *params)
         | 
| 100 112 | 
             
                  dispatchers.push Dispatcher.dispatch_from_params(self, verb, params)
         | 
| 101 | 
            -
                  dispatchers.last.execute
         | 
| 102 | 
            -
                  dispatchers.pop
         | 
| 113 | 
            +
                  dispatchers.last.execute.tap { dispatchers.pop }
         | 
| 103 114 | 
             
                end
         | 
| 104 115 |  | 
| 105 116 | 
             
                # Proceed to the next Action in the current stack.
         | 
| @@ -125,9 +136,9 @@ module Gamefic | |
| 125 136 | 
             
                #     end
         | 
| 126 137 | 
             
                #   end
         | 
| 127 138 | 
             
                #
         | 
| 128 | 
            -
                # @return [ | 
| 139 | 
            +
                # @return [Action, nil]
         | 
| 129 140 | 
             
                def proceed
         | 
| 130 | 
            -
                  dispatchers.last&.proceed | 
| 141 | 
            +
                  dispatchers.last&.proceed
         | 
| 131 142 | 
             
                end
         | 
| 132 143 |  | 
| 133 144 | 
             
                # Cue a scene to start in the next turn.
         | 
| @@ -146,17 +157,22 @@ module Gamefic | |
| 146 157 | 
             
                end
         | 
| 147 158 | 
             
                alias prepare cue
         | 
| 148 159 |  | 
| 160 | 
            +
                # @return [void]
         | 
| 149 161 | 
             
                def start_take
         | 
| 150 162 | 
             
                  ensure_cue
         | 
| 151 163 | 
             
                  @last_cue = @next_cue
         | 
| 152 164 | 
             
                  cue :default_scene
         | 
| 153 165 | 
             
                  @props = Take.start(self, @last_cue)
         | 
| 166 | 
            +
                  @last_output = self.output
         | 
| 167 | 
            +
                  @output = @props.output.dup.freeze
         | 
| 154 168 | 
             
                end
         | 
| 155 169 |  | 
| 170 | 
            +
                # @return [void]
         | 
| 156 171 | 
             
                def finish_take
         | 
| 157 172 | 
             
                  return unless @last_cue
         | 
| 158 173 |  | 
| 159 174 | 
             
                  Take.finish(self, @last_cue, @props)
         | 
| 175 | 
            +
                  @last_input = @props.input
         | 
| 160 176 | 
             
                end
         | 
| 161 177 |  | 
| 162 178 | 
             
                # Restart the scene from the most recent cue.
         | 
| @@ -175,6 +191,7 @@ module Gamefic | |
| 175 191 | 
             
                #
         | 
| 176 192 | 
             
                # @param new_scene [Symbol]
         | 
| 177 193 | 
             
                # @oaram context [Hash] Additional scene data
         | 
| 194 | 
            +
                # @return [Cue]
         | 
| 178 195 | 
             
                def conclude scene, **context
         | 
| 179 196 | 
             
                  cue scene, **context
         | 
| 180 197 | 
             
                  available = epic.select_scene(scene)
         | 
| @@ -186,7 +203,7 @@ module Gamefic | |
| 186 203 | 
             
                # True if the actor is ready to leave the game.
         | 
| 187 204 | 
             
                #
         | 
| 188 205 | 
             
                def concluding?
         | 
| 189 | 
            -
                  epic.empty? ||  | 
| 206 | 
            +
                  epic.empty? || @props&.scene&.type == 'Conclusion'
         | 
| 190 207 | 
             
                end
         | 
| 191 208 |  | 
| 192 209 | 
             
                def accessible?
         | 
    
        data/lib/gamefic/command.rb
    CHANGED
    
    | @@ -1,31 +1,20 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 3 | 
             
            module Gamefic
         | 
| 4 | 
            -
              # A  | 
| 5 | 
            -
              #
         | 
| 6 | 
            -
              # Commands are typically derived from tokenization against syntaxes.
         | 
| 4 | 
            +
              # A concrete representation of an input as a verb and an array of arguments.
         | 
| 7 5 | 
             
              #
         | 
| 8 6 | 
             
              class Command
         | 
| 9 7 | 
             
                # @return [Symbol]
         | 
| 10 8 | 
             
                attr_reader :verb
         | 
| 11 9 |  | 
| 12 | 
            -
                # @return [Array<String>]
         | 
| 10 | 
            +
                # @return [Array<Array<Entity>, Entity, String>]
         | 
| 13 11 | 
             
                attr_reader :arguments
         | 
| 14 12 |  | 
| 13 | 
            +
                # @param verb [Symbol]
         | 
| 14 | 
            +
                # @param arguments [Array<Array<Entity>, Entity, String>]
         | 
| 15 15 | 
             
                def initialize verb, arguments
         | 
| 16 16 | 
             
                  @verb = verb
         | 
| 17 17 | 
             
                  @arguments = arguments
         | 
| 18 18 | 
             
                end
         | 
| 19 | 
            -
             | 
| 20 | 
            -
                # Compare two syntaxes for the purpose of ordering them by relevance while
         | 
| 21 | 
            -
                # dispatching.
         | 
| 22 | 
            -
                #
         | 
| 23 | 
            -
                def compare other
         | 
| 24 | 
            -
                  if verb == other.verb
         | 
| 25 | 
            -
                    other.arguments.compact.length <=> arguments.compact.length
         | 
| 26 | 
            -
                  else
         | 
| 27 | 
            -
                    (other.verb ? 1 : 0) <=> (verb ? 1 : 0)
         | 
| 28 | 
            -
                  end
         | 
| 29 | 
            -
                end
         | 
| 30 19 | 
             
              end
         | 
| 31 20 | 
             
            end
         | 
| @@ -0,0 +1,68 @@ | |
| 1 | 
            +
            module Gamefic
         | 
| 2 | 
            +
              # A function module for creating commands from expressions.
         | 
| 3 | 
            +
              #
         | 
| 4 | 
            +
              module Composer
         | 
| 5 | 
            +
                # Create a command from the first expression that matches a response.
         | 
| 6 | 
            +
                #
         | 
| 7 | 
            +
                # @param actor [Actor]
         | 
| 8 | 
            +
                # @param expressions [Array<Expression>]
         | 
| 9 | 
            +
                # @return [Command]
         | 
| 10 | 
            +
                def self.compose actor, expressions
         | 
| 11 | 
            +
                  %i[strict fuzzy].each do |method|
         | 
| 12 | 
            +
                    result = match_expressions_to_response actor, expressions, method
         | 
| 13 | 
            +
                    return result if result
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
                  Command.new(nil, [])
         | 
| 16 | 
            +
                end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                class << self
         | 
| 19 | 
            +
                  private
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                  def match_expressions_to_response actor, expressions, method
         | 
| 22 | 
            +
                    expressions.each do |expression|
         | 
| 23 | 
            +
                      result = match_response_arguments actor, expression, method
         | 
| 24 | 
            +
                      return result if result
         | 
| 25 | 
            +
                    end
         | 
| 26 | 
            +
                    nil
         | 
| 27 | 
            +
                  end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                  def match_response_arguments actor, expression, method
         | 
| 30 | 
            +
                    actor.epic.responses_for(expression.verb).each do |response|
         | 
| 31 | 
            +
                      next unless response.queries.length >= expression.tokens.length
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                      result = match_query_arguments(actor, expression, response, method)
         | 
| 34 | 
            +
                      return result if result
         | 
| 35 | 
            +
                    end
         | 
| 36 | 
            +
                    nil
         | 
| 37 | 
            +
                  end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                  def match_query_arguments actor, expression, response, method
         | 
| 40 | 
            +
                    remainder = response.verb ? '' : expression.verb.to_s
         | 
| 41 | 
            +
                    arguments = []
         | 
| 42 | 
            +
                    response.queries.each_with_index do |query, idx|
         | 
| 43 | 
            +
                      result = Scanner.send(method, query.select(actor), "#{remainder} #{expression.tokens[idx]}".strip)
         | 
| 44 | 
            +
                      break unless validate_result_from_query(result, query)
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                      if query.ambiguous?
         | 
| 47 | 
            +
                        arguments.push result.matched
         | 
| 48 | 
            +
                      else
         | 
| 49 | 
            +
                        arguments.push result.matched.first
         | 
| 50 | 
            +
                      end
         | 
| 51 | 
            +
                      remainder = result.remainder
         | 
| 52 | 
            +
                    end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                    return nil if arguments.length != response.queries.length || remainder != ''
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                    Command.new(response.verb, arguments)
         | 
| 57 | 
            +
                  end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                  # @param result [Scanner::Result]
         | 
| 60 | 
            +
                  # @param query [Query::Base]
         | 
| 61 | 
            +
                  def validate_result_from_query result, query
         | 
| 62 | 
            +
                    return false if result.matched.empty?
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                    result.matched.length == 1 || query.ambiguous?
         | 
| 65 | 
            +
                  end
         | 
| 66 | 
            +
                end
         | 
| 67 | 
            +
              end
         | 
| 68 | 
            +
            end
         | 
    
        data/lib/gamefic/dispatcher.rb
    CHANGED
    
    | @@ -1,63 +1,54 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 3 | 
             
            module Gamefic
         | 
| 4 | 
            -
              # The action  | 
| 4 | 
            +
              # The action executor for character commands.
         | 
| 5 5 | 
             
              #
         | 
| 6 6 | 
             
              class Dispatcher
         | 
| 7 7 | 
             
                # @param actor [Actor]
         | 
| 8 | 
            -
                # @param  | 
| 9 | 
            -
                 | 
| 10 | 
            -
                def initialize actor, commands = [], responses = []
         | 
| 8 | 
            +
                # @param command [Command]
         | 
| 9 | 
            +
                def initialize actor, command
         | 
| 11 10 | 
             
                  @actor = actor
         | 
| 12 | 
            -
                  @ | 
| 13 | 
            -
                  @responses = responses
         | 
| 11 | 
            +
                  @command = command
         | 
| 14 12 | 
             
                  @executed = false
         | 
| 13 | 
            +
                  @finalized = false
         | 
| 15 14 | 
             
                end
         | 
| 16 15 |  | 
| 17 16 | 
             
                # Run the dispatcher.
         | 
| 18 17 | 
             
                #
         | 
| 18 | 
            +
                # @return [Action, nil]
         | 
| 19 19 | 
             
                def execute
         | 
| 20 20 | 
             
                  return if @executed
         | 
| 21 21 |  | 
| 22 | 
            -
                   | 
| 22 | 
            +
                  @executed = true
         | 
| 23 | 
            +
                  action = next_action
         | 
| 23 24 | 
             
                  return unless action
         | 
| 24 25 |  | 
| 25 | 
            -
                  @executed = action.arguments
         | 
| 26 26 | 
             
                  run_before_action_hooks action
         | 
| 27 27 | 
             
                  return if action.cancelled?
         | 
| 28 28 |  | 
| 29 29 | 
             
                  action.execute
         | 
| 30 30 | 
             
                  run_after_action_hooks action
         | 
| 31 | 
            +
                  action
         | 
| 31 32 | 
             
                end
         | 
| 32 33 |  | 
| 33 | 
            -
                #  | 
| 34 | 
            +
                # Execute the next available action.
         | 
| 35 | 
            +
                #
         | 
| 36 | 
            +
                # Actors should run #execute first.
         | 
| 34 37 | 
             
                #
         | 
| 35 38 | 
             
                # @return [Action, nil]
         | 
| 36 39 | 
             
                def proceed
         | 
| 37 | 
            -
                   | 
| 38 | 
            -
                    commands.each do |cmd|
         | 
| 39 | 
            -
                      action = response.attempt(actor, cmd)
         | 
| 40 | 
            -
                      next unless action && arguments_match?(action.arguments)
         | 
| 40 | 
            +
                  return unless @executed
         | 
| 41 41 |  | 
| 42 | 
            -
             | 
| 43 | 
            -
                    end
         | 
| 44 | 
            -
                  end
         | 
| 45 | 
            -
                  nil # Without this, return value in Opal is undefined
         | 
| 42 | 
            +
                  next_action&.execute
         | 
| 46 43 | 
             
                end
         | 
| 47 44 |  | 
| 48 45 | 
             
                # @param actor [Active]
         | 
| 49 46 | 
             
                # @param input [String]
         | 
| 50 47 | 
             
                # @return [Dispatcher]
         | 
| 51 48 | 
             
                def self.dispatch actor, input
         | 
| 52 | 
            -
                   | 
| 53 | 
            -
                   | 
| 54 | 
            -
                   | 
| 55 | 
            -
                                   .rulebooks
         | 
| 56 | 
            -
                                   .to_a
         | 
| 57 | 
            -
                                   .reverse
         | 
| 58 | 
            -
                                   .flat_map { |pb| pb.responses_for(*verbs) }
         | 
| 59 | 
            -
                                   .reject(&:hidden?)
         | 
| 60 | 
            -
                  new(actor, commands, responses)
         | 
| 49 | 
            +
                  expressions = Syntax.tokenize(input, actor.epic.syntaxes)
         | 
| 50 | 
            +
                  command = Composer.compose(actor, expressions)
         | 
| 51 | 
            +
                  new(actor, command)
         | 
| 61 52 | 
             
                end
         | 
| 62 53 |  | 
| 63 54 | 
             
                # @param actor [Active]
         | 
| @@ -66,12 +57,7 @@ module Gamefic | |
| 66 57 | 
             
                # @return [Dispatcher]
         | 
| 67 58 | 
             
                def self.dispatch_from_params actor, verb, params
         | 
| 68 59 | 
             
                  command = Command.new(verb, params)
         | 
| 69 | 
            -
                   | 
| 70 | 
            -
                                   .rulebooks
         | 
| 71 | 
            -
                                   .to_a
         | 
| 72 | 
            -
                                   .reverse
         | 
| 73 | 
            -
                                   .flat_map { |pb| pb.responses_for(verb) }
         | 
| 74 | 
            -
                  new(actor, [command], responses)
         | 
| 60 | 
            +
                  new(actor, command)
         | 
| 75 61 | 
             
                end
         | 
| 76 62 |  | 
| 77 63 | 
             
                protected
         | 
| @@ -79,27 +65,48 @@ module Gamefic | |
| 79 65 | 
             
                # @return [Actor]
         | 
| 80 66 | 
             
                attr_reader :actor
         | 
| 81 67 |  | 
| 82 | 
            -
                # @return [ | 
| 83 | 
            -
                attr_reader : | 
| 68 | 
            +
                # @return [Command]
         | 
| 69 | 
            +
                attr_reader :command
         | 
| 84 70 |  | 
| 85 71 | 
             
                # @return [Array<Response>]
         | 
| 86 | 
            -
                 | 
| 72 | 
            +
                def responses
         | 
| 73 | 
            +
                  @responses ||= actor.epic.responses_for(command.verb)
         | 
| 74 | 
            +
                end
         | 
| 87 75 |  | 
| 88 76 | 
             
                private
         | 
| 89 77 |  | 
| 90 | 
            -
                #  | 
| 91 | 
            -
                 | 
| 92 | 
            -
             | 
| 93 | 
            -
             | 
| 94 | 
            -
             | 
| 78 | 
            +
                # @return [Action, nil]
         | 
| 79 | 
            +
                def next_action
         | 
| 80 | 
            +
                  while (response = responses.shift)
         | 
| 81 | 
            +
                    next if response.queries.length < @command.arguments.length
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                    return Action.new(actor, @command.arguments, response) if response.accept?(actor, @command)
         | 
| 84 | 
            +
                  end
         | 
| 85 | 
            +
                  finalize
         | 
| 95 86 | 
             
                end
         | 
| 96 87 |  | 
| 88 | 
            +
                # @return [void]
         | 
| 97 89 | 
             
                def run_before_action_hooks action
         | 
| 98 90 | 
             
                  actor.epic.rulebooks.flat_map { |rlbk| rlbk.run_before_actions action }
         | 
| 99 91 | 
             
                end
         | 
| 100 92 |  | 
| 93 | 
            +
                # @return [void]
         | 
| 101 94 | 
             
                def run_after_action_hooks action
         | 
| 102 95 | 
             
                  actor.epic.rulebooks.flat_map { |rlbk| rlbk.run_after_actions action }
         | 
| 103 96 | 
             
                end
         | 
| 97 | 
            +
             | 
| 98 | 
            +
                # If the dispatcher proceeds through all possible responses, it can fall
         | 
| 99 | 
            +
                # back to a nil response as a catchall for commands that could not be
         | 
| 100 | 
            +
                # completed.
         | 
| 101 | 
            +
                #
         | 
| 102 | 
            +
                # @return [Action, nil]
         | 
| 103 | 
            +
                def finalize
         | 
| 104 | 
            +
                  return nil if @finalized
         | 
| 105 | 
            +
             | 
| 106 | 
            +
                  @finalized = true
         | 
| 107 | 
            +
                  @command = Command.new(nil, ["#{command.verb} #{command.arguments.join(' ').strip}"])
         | 
| 108 | 
            +
                  @responses = actor.epic.responses_for(nil)
         | 
| 109 | 
            +
                  next_action
         | 
| 110 | 
            +
                end
         | 
| 104 111 | 
             
              end
         | 
| 105 112 | 
             
            end
         | 
    
        data/lib/gamefic/entity.rb
    CHANGED
    
    | @@ -9,7 +9,6 @@ module Gamefic | |
| 9 9 | 
             
              class Entity
         | 
| 10 10 | 
             
                include Describable
         | 
| 11 11 | 
             
                include Node
         | 
| 12 | 
            -
                # include Messaging
         | 
| 13 12 |  | 
| 14 13 | 
             
                def initialize **args
         | 
| 15 14 | 
             
                  klass = self.class
         | 
| @@ -57,14 +56,13 @@ module Gamefic | |
| 57 56 | 
             
                end
         | 
| 58 57 |  | 
| 59 58 | 
             
                class << self
         | 
| 60 | 
            -
                  # Set or update the default  | 
| 59 | 
            +
                  # Set or update the default attributes for new instances.
         | 
| 61 60 | 
             
                  #
         | 
| 62 | 
            -
                   | 
| 63 | 
            -
                  def set_default attrs = {}
         | 
| 61 | 
            +
                  def set_default **attrs
         | 
| 64 62 | 
             
                    default_attributes.merge! attrs
         | 
| 65 63 | 
             
                  end
         | 
| 66 64 |  | 
| 67 | 
            -
                  # A hash of default  | 
| 65 | 
            +
                  # A hash of default attributes when creating an instance.
         | 
| 68 66 | 
             
                  #
         | 
| 69 67 | 
             
                  # @return [Hash]
         | 
| 70 68 | 
             
                  def default_attributes
         | 
| @@ -0,0 +1,31 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Gamefic
         | 
| 4 | 
            +
              # A tokenization of an input from available syntaxes.
         | 
| 5 | 
            +
              #
         | 
| 6 | 
            +
              class Expression
         | 
| 7 | 
            +
                # @return [Symbol]
         | 
| 8 | 
            +
                attr_reader :verb
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                # @return [Array<String>]
         | 
| 11 | 
            +
                attr_reader :tokens
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                # @param verb [Symbol, nil]
         | 
| 14 | 
            +
                # @param tokens [Array<String>]
         | 
| 15 | 
            +
                def initialize verb, tokens
         | 
| 16 | 
            +
                  @verb = verb
         | 
| 17 | 
            +
                  @tokens = tokens
         | 
| 18 | 
            +
                end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                # Compare two syntaxes for the purpose of ordering them by relevance while
         | 
| 21 | 
            +
                # dispatching.
         | 
| 22 | 
            +
                #
         | 
| 23 | 
            +
                def compare other
         | 
| 24 | 
            +
                  if verb == other.verb
         | 
| 25 | 
            +
                    other.tokens.compact.length <=> tokens.compact.length
         | 
| 26 | 
            +
                  else
         | 
| 27 | 
            +
                    (other.verb ? 1 : 0) <=> (verb ? 1 : 0)
         | 
| 28 | 
            +
                  end
         | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
              end
         | 
| 31 | 
            +
            end
         | 
    
        data/lib/gamefic/plot.rb
    CHANGED
    
    | @@ -9,8 +9,8 @@ module Gamefic | |
| 9 9 | 
             
                  super
         | 
| 10 10 | 
             
                  subplots.each(&:ready)
         | 
| 11 11 | 
             
                  players.each(&:start_take)
         | 
| 12 | 
            -
                  subplots.delete_if(&:concluding?)
         | 
| 13 12 | 
             
                  players.select(&:concluding?).each { |plyr| rulebook.run_player_conclude_blocks plyr }
         | 
| 13 | 
            +
                  subplots.delete_if(&:concluding?)
         | 
| 14 14 | 
             
                end
         | 
| 15 15 |  | 
| 16 16 | 
             
                def update
         | 
| @@ -2,6 +2,8 @@ | |
| 2 2 |  | 
| 3 3 | 
             
            module Gamefic
         | 
| 4 4 | 
             
              module Props
         | 
| 5 | 
            +
                SceneData = Struct.new(:name, :type)
         | 
| 6 | 
            +
             | 
| 5 7 | 
             
                # A collection of data related to a scene. Scenes define which Props class
         | 
| 6 8 | 
             
                # they use. Props can be accessed in a scene's on_start and on_finish
         | 
| 7 9 | 
             
                # callbacks.
         | 
| @@ -25,17 +27,23 @@ module Gamefic | |
| 25 27 | 
             
                  attr_reader :context
         | 
| 26 28 | 
             
                  alias data context
         | 
| 27 29 |  | 
| 28 | 
            -
                  # @ | 
| 30 | 
            +
                  # @return [SceneData]
         | 
| 31 | 
            +
                  attr_reader :scene
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                  # @param scene [Scene]
         | 
| 29 34 | 
             
                  # @param context [Hash]
         | 
| 30 | 
            -
                  def initialize  | 
| 31 | 
            -
                    @ | 
| 32 | 
            -
                    @scene_type = type
         | 
| 35 | 
            +
                  def initialize scene, **context
         | 
| 36 | 
            +
                    @scene = SceneData.new(scene.name, scene.type)
         | 
| 33 37 | 
             
                    @context = context
         | 
| 34 38 | 
             
                  end
         | 
| 35 39 |  | 
| 36 40 | 
             
                  def prompt
         | 
| 37 41 | 
             
                    @prompt ||= '>'
         | 
| 38 42 | 
             
                  end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                  def output
         | 
| 45 | 
            +
                    @output ||= Props::Output.new
         | 
| 46 | 
            +
                  end
         | 
| 39 47 | 
             
                end
         | 
| 40 48 | 
             
              end
         | 
| 41 49 | 
             
            end
         | 
| @@ -0,0 +1,82 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Gamefic
         | 
| 4 | 
            +
              module Props
         | 
| 5 | 
            +
                # A container for output sent to players with a hash interface for custom
         | 
| 6 | 
            +
                # data.
         | 
| 7 | 
            +
                #
         | 
| 8 | 
            +
                class Output
         | 
| 9 | 
            +
                  # @return [String, nil]
         | 
| 10 | 
            +
                  attr_reader :last_input
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  # @return [String, nil]
         | 
| 13 | 
            +
                  attr_reader :last_prompt
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                  def initialize **data
         | 
| 16 | 
            +
                    @raw_data = {
         | 
| 17 | 
            +
                      messages: '',
         | 
| 18 | 
            +
                      options: [],
         | 
| 19 | 
            +
                      queue: [],
         | 
| 20 | 
            +
                      scene: {},
         | 
| 21 | 
            +
                      prompt: ''
         | 
| 22 | 
            +
                    }
         | 
| 23 | 
            +
                    merge! data
         | 
| 24 | 
            +
                  end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                  # @return [String]
         | 
| 27 | 
            +
                  def messages
         | 
| 28 | 
            +
                    raw_data[:messages]
         | 
| 29 | 
            +
                  end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                  # @return [Array<String>]
         | 
| 32 | 
            +
                  def options
         | 
| 33 | 
            +
                    raw_data[:options]
         | 
| 34 | 
            +
                  end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                  # @return [Array<String>]
         | 
| 37 | 
            +
                  def queue
         | 
| 38 | 
            +
                    raw_data[:queue]
         | 
| 39 | 
            +
                  end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                  # @todo Should this be a concrete class?
         | 
| 42 | 
            +
                  # @return [Hash]
         | 
| 43 | 
            +
                  def scene
         | 
| 44 | 
            +
                    raw_data[:scene]
         | 
| 45 | 
            +
                  end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                  # @return [String]
         | 
| 48 | 
            +
                  def prompt
         | 
| 49 | 
            +
                    raw_data[:prompt]
         | 
| 50 | 
            +
                  end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                  def [] key
         | 
| 53 | 
            +
                    raw_data[key]
         | 
| 54 | 
            +
                  end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                  def []= key, value
         | 
| 57 | 
            +
                    raw_data[key] = value
         | 
| 58 | 
            +
                  end
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                  # @return [Hash]
         | 
| 61 | 
            +
                  def to_hash
         | 
| 62 | 
            +
                    raw_data.dup
         | 
| 63 | 
            +
                  end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                  def to_json _ = nil
         | 
| 66 | 
            +
                    raw_data.to_json
         | 
| 67 | 
            +
                  end
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                  def merge! data
         | 
| 70 | 
            +
                    data.each { |key, val| self[key] = val }
         | 
| 71 | 
            +
                  end
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                  def replace data
         | 
| 74 | 
            +
                    raw_data.replace data
         | 
| 75 | 
            +
                  end
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                  private
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                  attr_reader :raw_data
         | 
| 80 | 
            +
                end
         | 
| 81 | 
            +
              end
         | 
| 82 | 
            +
            end
         | 
    
        data/lib/gamefic/props.rb
    CHANGED
    
    
    
        data/lib/gamefic/query/base.rb
    CHANGED
    
    | @@ -24,6 +24,12 @@ module Gamefic | |
| 24 24 | 
             
                    @ambiguous = ambiguous
         | 
| 25 25 | 
             
                  end
         | 
| 26 26 |  | 
| 27 | 
            +
                  # @deprecated Queries should only be used to select entities that are
         | 
| 28 | 
            +
                  #   eligible to be response arguments. After a text command is tokenized
         | 
| 29 | 
            +
                  #   into an array of expressions, the composer builds the command that
         | 
| 30 | 
            +
                  #   the dispatcher uses to execute actions. The #accept? method verifies
         | 
| 31 | 
            +
                  #   that the command's arguments match the response's queries.
         | 
| 32 | 
            +
                  #
         | 
| 27 33 | 
             
                  # @param subject [Gamefic::Entity]
         | 
| 28 34 | 
             
                  # @param token [String]
         | 
| 29 35 | 
             
                  # @return [Result]
         | 
| @@ -31,6 +37,24 @@ module Gamefic | |
| 31 37 | 
             
                    raise "#query not implemented for #{self.class}"
         | 
| 32 38 | 
             
                  end
         | 
| 33 39 |  | 
| 40 | 
            +
                  # Get an array of entities that match the query from the context of the
         | 
| 41 | 
            +
                  # subject.
         | 
| 42 | 
            +
                  #
         | 
| 43 | 
            +
                  # @param subject [Entity]
         | 
| 44 | 
            +
                  # @return [Array<Entity>]
         | 
| 45 | 
            +
                  def select subject
         | 
| 46 | 
            +
                    raise "#select not implemented for #{self.class}"
         | 
| 47 | 
            +
                  end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                  def accept?(subject, object)
         | 
| 50 | 
            +
                    available = select(subject)
         | 
| 51 | 
            +
                    if ambiguous?
         | 
| 52 | 
            +
                      object & available == object
         | 
| 53 | 
            +
                    else
         | 
| 54 | 
            +
                      available.include?(object)
         | 
| 55 | 
            +
                    end
         | 
| 56 | 
            +
                  end
         | 
| 57 | 
            +
             | 
| 34 58 | 
             
                  # @return [Integer]
         | 
| 35 59 | 
             
                  def precision
         | 
| 36 60 | 
             
                    @precision ||= calculate_precision
         | 
| @@ -19,6 +19,10 @@ module Gamefic | |
| 19 19 | 
             
                    @entities = entities
         | 
| 20 20 | 
             
                  end
         | 
| 21 21 |  | 
| 22 | 
            +
                  def select subject
         | 
| 23 | 
            +
                    available_entities(subject).that_are(*@arguments)
         | 
| 24 | 
            +
                  end
         | 
| 25 | 
            +
             | 
| 22 26 | 
             
                  def query subject, token
         | 
| 23 27 | 
             
                    filtered = available_entities(subject).that_are(*@arguments)
         | 
| 24 28 | 
             
                    return Result.new(token, nil) if filtered.include?(token)
         | 
    
        data/lib/gamefic/query/scoped.rb
    CHANGED
    
    
    
        data/lib/gamefic/query/text.rb
    CHANGED
    
    | @@ -5,12 +5,17 @@ module Gamefic | |
| 5 5 | 
             
                # A special query that handles text instead of entities.
         | 
| 6 6 | 
             
                #
         | 
| 7 7 | 
             
                class Text
         | 
| 8 | 
            -
                  # @param argument [String, Regexp | 
| 9 | 
            -
                  def initialize argument =  | 
| 8 | 
            +
                  # @param argument [String, Regexp]
         | 
| 9 | 
            +
                  def initialize argument = /.*/
         | 
| 10 10 | 
             
                    @argument = argument
         | 
| 11 11 | 
             
                    validate
         | 
| 12 12 | 
             
                  end
         | 
| 13 13 |  | 
| 14 | 
            +
                  # @return [String, Regexp]
         | 
| 15 | 
            +
                  def select(_subject)
         | 
| 16 | 
            +
                    @argument
         | 
| 17 | 
            +
                  end
         | 
| 18 | 
            +
             | 
| 14 19 | 
             
                  def query _subject, token
         | 
| 15 20 | 
             
                    if match? token
         | 
| 16 21 | 
             
                      Result.new(token, '')
         | 
| @@ -23,11 +28,17 @@ module Gamefic | |
| 23 28 | 
             
                    0
         | 
| 24 29 | 
             
                  end
         | 
| 25 30 |  | 
| 31 | 
            +
                  def accept? _subject, argument
         | 
| 32 | 
            +
                    match? argument
         | 
| 33 | 
            +
                  end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                  def ambiguous?
         | 
| 36 | 
            +
                    true
         | 
| 37 | 
            +
                  end
         | 
| 38 | 
            +
             | 
| 26 39 | 
             
                  private
         | 
| 27 40 |  | 
| 28 41 | 
             
                  def match? token
         | 
| 29 | 
            -
                    return true if @argument.nil?
         | 
| 30 | 
            -
             | 
| 31 42 | 
             
                    case @argument
         | 
| 32 43 | 
             
                    when Regexp
         | 
| 33 44 | 
             
                      token =~ @argument
         | 
| @@ -37,7 +48,7 @@ module Gamefic | |
| 37 48 | 
             
                  end
         | 
| 38 49 |  | 
| 39 50 | 
             
                  def validate
         | 
| 40 | 
            -
                    return if @argument. | 
| 51 | 
            +
                    return if @argument.is_a?(String) || @argument.is_a?(Regexp)
         | 
| 41 52 |  | 
| 42 53 | 
             
                    raise ArgumentError, 'Invalid text query argument'
         | 
| 43 54 | 
             
                  end
         | 
    
        data/lib/gamefic/response.rb
    CHANGED
    
    | @@ -1,4 +1,4 @@ | |
| 1 | 
            -
            #  | 
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 3 | 
             
            module Gamefic
         | 
| 4 4 | 
             
              # A proc to be executed in response to a command that matches its verb and
         | 
| @@ -11,17 +11,17 @@ module Gamefic | |
| 11 11 | 
             
                # @return [Array<Query::Base>]
         | 
| 12 12 | 
             
                attr_reader :queries
         | 
| 13 13 |  | 
| 14 | 
            -
                # @return [ | 
| 15 | 
            -
                 | 
| 14 | 
            +
                # @return [Narrative]
         | 
| 15 | 
            +
                attr_reader :narrative
         | 
| 16 16 |  | 
| 17 17 | 
             
                # @param verb [Symbol]
         | 
| 18 | 
            -
                # @param  | 
| 18 | 
            +
                # @param narrative [Narrative]
         | 
| 19 19 | 
             
                # @param queries [Array<Query::Base>]
         | 
| 20 20 | 
             
                # @param meta [Boolean]
         | 
| 21 21 | 
             
                # @param block [Proc]
         | 
| 22 | 
            -
                def initialize verb,  | 
| 22 | 
            +
                def initialize verb, narrative, *queries, meta: false, &block
         | 
| 23 23 | 
             
                  @verb = verb
         | 
| 24 | 
            -
                  @ | 
| 24 | 
            +
                  @narrative = narrative
         | 
| 25 25 | 
             
                  @queries = map_queryable_objects(queries)
         | 
| 26 26 | 
             
                  @meta = meta
         | 
| 27 27 | 
             
                  @block = block
         | 
| @@ -48,35 +48,29 @@ module Gamefic | |
| 48 48 | 
             
                #
         | 
| 49 49 | 
             
                # @param actor [Entity]
         | 
| 50 50 | 
             
                # @param command [Command]
         | 
| 51 | 
            -
                # @param with_hooks [Boolean]
         | 
| 52 51 | 
             
                # @return [Action, nil]
         | 
| 53 52 | 
             
                def attempt actor, command
         | 
| 54 | 
            -
                  return nil  | 
| 53 | 
            +
                  return nil unless accept?(actor, command)
         | 
| 55 54 |  | 
| 56 | 
            -
                   | 
| 57 | 
            -
             | 
| 58 | 
            -
                  remainder = ''
         | 
| 59 | 
            -
             | 
| 60 | 
            -
                  queries.each do |qd|
         | 
| 61 | 
            -
                    token = tokens.shift
         | 
| 62 | 
            -
                    txt = "#{remainder} #{token}".strip
         | 
| 63 | 
            -
                    return nil if txt.empty?
         | 
| 64 | 
            -
             | 
| 65 | 
            -
                    response = qd.query(actor, txt)
         | 
| 66 | 
            -
                    return nil if response.match.nil?
         | 
| 55 | 
            +
                  Action.new(actor, command.arguments, self)
         | 
| 56 | 
            +
                end
         | 
| 67 57 |  | 
| 68 | 
            -
             | 
| 58 | 
            +
                # True if the Response can be executed for the given actor and command.
         | 
| 59 | 
            +
                #
         | 
| 60 | 
            +
                # @param actor [Active]
         | 
| 61 | 
            +
                # @param command [Command]
         | 
| 62 | 
            +
                def accept? actor, command
         | 
| 63 | 
            +
                  return false if command.verb != verb || command.arguments.length != queries.length
         | 
| 69 64 |  | 
| 70 | 
            -
             | 
| 65 | 
            +
                  queries.each_with_index do |query, idx|
         | 
| 66 | 
            +
                    return false unless query.accept?(actor, command.arguments[idx])
         | 
| 71 67 | 
             
                  end
         | 
| 72 68 |  | 
| 73 | 
            -
                   | 
| 74 | 
            -
             | 
| 75 | 
            -
                  Action.new(actor, result, self)
         | 
| 69 | 
            +
                  true
         | 
| 76 70 | 
             
                end
         | 
| 77 71 |  | 
| 78 72 | 
             
                def execute *args
         | 
| 79 | 
            -
                  Stage.run( | 
| 73 | 
            +
                  Stage.run(narrative, *args, &@block)
         | 
| 80 74 | 
             
                end
         | 
| 81 75 |  | 
| 82 76 | 
             
                def precision
         | 
| @@ -34,16 +34,16 @@ module Gamefic | |
| 34 34 | 
             
                    self
         | 
| 35 35 | 
             
                  end
         | 
| 36 36 |  | 
| 37 | 
            -
                  # @return [ | 
| 37 | 
            +
                  # @return [void]
         | 
| 38 38 | 
             
                  def on_ready &block
         | 
| 39 39 | 
             
                    @ready_blocks.push block
         | 
| 40 40 | 
             
                  end
         | 
| 41 41 |  | 
| 42 42 | 
             
                  # @yieldparam [Actor]
         | 
| 43 | 
            -
                  # @return [ | 
| 43 | 
            +
                  # @return [void]
         | 
| 44 44 | 
             
                  def on_player_ready &block
         | 
| 45 45 | 
             
                    @ready_blocks.push(proc do
         | 
| 46 | 
            -
                      players.each { |plyr|  | 
| 46 | 
            +
                      players.each { |plyr| instance_exec plyr, &block }
         | 
| 47 47 | 
             
                    end)
         | 
| 48 48 | 
             
                  end
         | 
| 49 49 |  | 
| @@ -53,24 +53,24 @@ module Gamefic | |
| 53 53 |  | 
| 54 54 | 
             
                  def on_player_update &block
         | 
| 55 55 | 
             
                    @update_blocks.push(proc do
         | 
| 56 | 
            -
                      players.each { |plyr|  | 
| 56 | 
            +
                      players.each { |plyr| instance_exec plyr, &block }
         | 
| 57 57 | 
             
                    end)
         | 
| 58 58 | 
             
                  end
         | 
| 59 59 |  | 
| 60 | 
            -
                  # @return [ | 
| 60 | 
            +
                  # @return [void]
         | 
| 61 61 | 
             
                  def on_conclude &block
         | 
| 62 62 | 
             
                    @conclude_blocks.push block
         | 
| 63 63 | 
             
                  end
         | 
| 64 64 |  | 
| 65 65 | 
             
                  # @yieldparam [Actor]
         | 
| 66 | 
            -
                  # @return [ | 
| 66 | 
            +
                  # @return [void]
         | 
| 67 67 | 
             
                  def on_player_conclude &block
         | 
| 68 68 | 
             
                    @player_conclude_blocks.push block
         | 
| 69 69 | 
             
                  end
         | 
| 70 70 |  | 
| 71 71 | 
             
                  # @yieldparam [Actor]
         | 
| 72 72 | 
             
                  # @yieldparam [Hash]
         | 
| 73 | 
            -
                  # @return [ | 
| 73 | 
            +
                  # @return [void]
         | 
| 74 74 | 
             
                  def on_player_output &block
         | 
| 75 75 | 
             
                    @player_output_blocks.push block
         | 
| 76 76 | 
             
                  end
         | 
    
        data/lib/gamefic/rulebook.rb
    CHANGED
    
    | @@ -116,11 +116,11 @@ module Gamefic | |
| 116 116 | 
             
                end
         | 
| 117 117 |  | 
| 118 118 | 
             
                def run_player_conclude_blocks player
         | 
| 119 | 
            -
                  events.player_conclude_blocks.each { |blk| Stage.run(narrative | 
| 119 | 
            +
                  events.player_conclude_blocks.each { |blk| Stage.run(narrative, player, &blk) }
         | 
| 120 120 | 
             
                end
         | 
| 121 121 |  | 
| 122 122 | 
             
                def run_player_output_blocks player, output
         | 
| 123 | 
            -
                  events.player_output_blocks.each { |blk| Stage.run(narrative | 
| 123 | 
            +
                  events.player_output_blocks.each { |blk| Stage.run(narrative, player, output, &blk) }
         | 
| 124 124 | 
             
                end
         | 
| 125 125 |  | 
| 126 126 | 
             
                def empty?
         | 
    
        data/lib/gamefic/scanner.rb
    CHANGED
    
    | @@ -11,7 +11,7 @@ module Gamefic | |
| 11 11 | 
             
                class Result
         | 
| 12 12 | 
             
                  # The scanned objects
         | 
| 13 13 | 
             
                  #
         | 
| 14 | 
            -
                  # @return [Array< | 
| 14 | 
            +
                  # @return [Array<Entity>, String, Regexp]
         | 
| 15 15 | 
             
                  attr_reader :scanned
         | 
| 16 16 |  | 
| 17 17 | 
             
                  # The scanned token
         | 
| @@ -21,7 +21,7 @@ module Gamefic | |
| 21 21 |  | 
| 22 22 | 
             
                  # The matched objects
         | 
| 23 23 | 
             
                  #
         | 
| 24 | 
            -
                  # @return [Array< | 
| 24 | 
            +
                  # @return [Array<Entity>, String]
         | 
| 25 25 | 
             
                  attr_reader :matched
         | 
| 26 26 |  | 
| 27 27 | 
             
                  # The remaining (unmatched) portion of the token
         | 
| @@ -39,36 +39,53 @@ module Gamefic | |
| 39 39 |  | 
| 40 40 | 
             
                # Scan entities against a token.
         | 
| 41 41 | 
             
                #
         | 
| 42 | 
            -
                # @param  | 
| 42 | 
            +
                # @param selection [Array<Entity>, String, Regexp]
         | 
| 43 43 | 
             
                # @param token [String]
         | 
| 44 44 | 
             
                # @return [Result]
         | 
| 45 | 
            -
                def self.scan  | 
| 46 | 
            -
                   | 
| 47 | 
            -
                   | 
| 48 | 
            -
             | 
| 49 | 
            -
             | 
| 50 | 
            -
             | 
| 51 | 
            -
             | 
| 52 | 
            -
             | 
| 53 | 
            -
             | 
| 54 | 
            -
                   | 
| 55 | 
            -
             | 
| 56 | 
            -
                   | 
| 57 | 
            -
             | 
| 58 | 
            -
             | 
| 59 | 
            -
             | 
| 60 | 
            -
             | 
| 61 | 
            -
             | 
| 62 | 
            -
             | 
| 63 | 
            -
             | 
| 64 | 
            -
             | 
| 65 | 
            -
             | 
| 66 | 
            -
                  end
         | 
| 45 | 
            +
                def self.scan selection, token
         | 
| 46 | 
            +
                  strict_result = strict(selection, token)
         | 
| 47 | 
            +
                  strict_result.matched.empty? ? fuzzy(selection, token) : strict_result
         | 
| 48 | 
            +
                end
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                # @param selection [Array<Entity>, String, Regexp]
         | 
| 51 | 
            +
                # @param token [String]
         | 
| 52 | 
            +
                # @return [Result]
         | 
| 53 | 
            +
                def self.strict selection, token
         | 
| 54 | 
            +
                  return Result.new(selection, token, '', token) unless selection.is_a?(Array)
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                  scan_strict_or_fuzzy(selection, token, :select_strict)
         | 
| 57 | 
            +
                end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                # @param selection [Array<Entity>, String, Regexp]
         | 
| 60 | 
            +
                # @param token [String]
         | 
| 61 | 
            +
                # @return [Result]
         | 
| 62 | 
            +
                def self.fuzzy selection, token
         | 
| 63 | 
            +
                  return scan_text(selection, token) unless selection.is_a?(Array)
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                  scan_strict_or_fuzzy(selection, token, :select_fuzzy)
         | 
| 67 66 | 
             
                end
         | 
| 68 67 |  | 
| 69 68 | 
             
                class << self
         | 
| 70 69 | 
             
                  private
         | 
| 71 70 |  | 
| 71 | 
            +
                  def scan_strict_or_fuzzy objects, token, method
         | 
| 72 | 
            +
                    if nested?(token) && objects.all?(&:children)
         | 
| 73 | 
            +
                      denest(objects, token)
         | 
| 74 | 
            +
                    else
         | 
| 75 | 
            +
                      words = token.keywords
         | 
| 76 | 
            +
                      available = objects.clone
         | 
| 77 | 
            +
                      filtered = []
         | 
| 78 | 
            +
                      words.each_with_index do |word, idx|
         | 
| 79 | 
            +
                        tested = send(method, available, word)
         | 
| 80 | 
            +
                        return Result.new(objects, token, filtered, words[idx..].join(' ')) if tested.empty?
         | 
| 81 | 
            +
             | 
| 82 | 
            +
                        filtered = tested
         | 
| 83 | 
            +
                        available = filtered
         | 
| 84 | 
            +
                      end
         | 
| 85 | 
            +
                      Result.new(objects, token, filtered, '')
         | 
| 86 | 
            +
                    end
         | 
| 87 | 
            +
                  end
         | 
| 88 | 
            +
             | 
| 72 89 | 
             
                  def select_strict available, word
         | 
| 73 90 | 
             
                    available.select { |obj| obj.keywords.include?(word) }
         | 
| 74 91 | 
             
                  end
         | 
| @@ -81,6 +98,16 @@ module Gamefic | |
| 81 98 | 
             
                    token.match(NEST_REGEXP)
         | 
| 82 99 | 
             
                  end
         | 
| 83 100 |  | 
| 101 | 
            +
                  def scan_text selection, token
         | 
| 102 | 
            +
                    case selection
         | 
| 103 | 
            +
                    when Regexp
         | 
| 104 | 
            +
                      return Result.new(selection, token, token, '') if token =~ selection
         | 
| 105 | 
            +
                    else
         | 
| 106 | 
            +
                      return Result.new(selection, token, selection, token[selection.length..]) if token.start_with?(selection)
         | 
| 107 | 
            +
                    end
         | 
| 108 | 
            +
                    Result.new(selection, token, '', token)
         | 
| 109 | 
            +
                  end
         | 
| 110 | 
            +
             | 
| 84 111 | 
             
                  def denest(objects, token)
         | 
| 85 112 | 
             
                    parts = token.split(NEST_REGEXP)
         | 
| 86 113 | 
             
                    current = parts.pop
         | 
| @@ -30,7 +30,7 @@ module Gamefic | |
| 30 30 | 
             
                  end
         | 
| 31 31 |  | 
| 32 32 | 
             
                  def new_props(**context)
         | 
| 33 | 
            -
                    self.class.props_class.new( | 
| 33 | 
            +
                    self.class.props_class.new(self, **context)
         | 
| 34 34 | 
             
                  end
         | 
| 35 35 |  | 
| 36 36 | 
             
                  def on_start &block
         | 
| @@ -45,8 +45,8 @@ module Gamefic | |
| 45 45 | 
             
                  # @param props [Props::Default]
         | 
| 46 46 | 
             
                  # @return [void]
         | 
| 47 47 | 
             
                  def start actor, props
         | 
| 48 | 
            -
                     | 
| 49 | 
            -
                     | 
| 48 | 
            +
                    props.output[:scene] = to_hash
         | 
| 49 | 
            +
                    props.output[:prompt] = props.prompt
         | 
| 50 50 | 
             
                  end
         | 
| 51 51 |  | 
| 52 52 | 
             
                  # @param actor [Gamefic::Actor]
         | 
| @@ -55,7 +55,10 @@ module Gamefic | |
| 55 55 | 
             
                  # @param description [String]
         | 
| 56 56 | 
             
                  # @return [Gamefic::Entity, nil]
         | 
| 57 57 | 
             
                  def pick description
         | 
| 58 | 
            -
                     | 
| 58 | 
            +
                    result = Scanner.scan(entities, description)
         | 
| 59 | 
            +
                    return nil unless result.matched.one?
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                    result.matched.first
         | 
| 59 62 | 
             
                  end
         | 
| 60 63 |  | 
| 61 64 | 
             
                  # Same as #pick, but raise an error if a unique match could not be found.
         | 
| @@ -63,13 +66,13 @@ module Gamefic | |
| 63 66 | 
             
                  # @param description [String]
         | 
| 64 67 | 
             
                  # @return [Gamefic::Entity, nil]
         | 
| 65 68 | 
             
                  def pick! description
         | 
| 66 | 
            -
                     | 
| 69 | 
            +
                    result = Scanner.scan(entities, description)
         | 
| 67 70 |  | 
| 68 | 
            -
                    raise "no entity matching '#{description}'" if  | 
| 71 | 
            +
                    raise "no entity matching '#{description}'" if result.matched.empty?
         | 
| 69 72 |  | 
| 70 | 
            -
                    raise "multiple entities matching '#{description}': #{ | 
| 73 | 
            +
                    raise "multiple entities matching '#{description}': #{result.matched.join_and}" unless result.matched.one?
         | 
| 71 74 |  | 
| 72 | 
            -
                     | 
| 75 | 
            +
                    result.matched.first
         | 
| 73 76 | 
             
                  end
         | 
| 74 77 | 
             
                end
         | 
| 75 78 | 
             
              end
         | 
| @@ -15,6 +15,15 @@ module Gamefic | |
| 15 15 | 
             
                    end
         | 
| 16 16 |  | 
| 17 17 | 
             
                    def fetch container
         | 
| 18 | 
            +
                      result = safe_fetch(container)
         | 
| 19 | 
            +
                      raise ArgumentError, "Unable to fetch entity from proxy agent symbol `#{symbol}`" unless result
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                      result
         | 
| 22 | 
            +
                    end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                    private
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                    def safe_fetch container
         | 
| 18 27 | 
             
                      if symbol.to_s =~ /^\d+$/
         | 
| 19 28 | 
             
                        Stage.run(container, symbol) { |sym| entities[sym] }
         | 
| 20 29 | 
             
                      elsif symbol.to_s.start_with?('@')
         | 
| @@ -22,6 +31,8 @@ module Gamefic | |
| 22 31 | 
             
                      else
         | 
| 23 32 | 
             
                        Stage.run(container, symbol) { |sym| send(sym) }
         | 
| 24 33 | 
             
                      end
         | 
| 34 | 
            +
                    rescue NoMethodError
         | 
| 35 | 
            +
                      nil
         | 
| 25 36 | 
             
                    end
         | 
| 26 37 | 
             
                  end
         | 
| 27 38 |  | 
| @@ -63,9 +63,9 @@ module Gamefic | |
| 63 63 | 
             
                  # any text it finds in the command. A successful query returns the
         | 
| 64 64 | 
             
                  # corresponding text instead of an entity.
         | 
| 65 65 | 
             
                  #
         | 
| 66 | 
            -
                  # @param arg [String,  | 
| 66 | 
            +
                  # @param arg [String, Regexp] The string or regular expression to match
         | 
| 67 67 | 
             
                  # @return [Query::Text]
         | 
| 68 | 
            -
                  def plaintext arg =  | 
| 68 | 
            +
                  def plaintext arg = /.*/
         | 
| 69 69 | 
             
                    Query::Text.new arg
         | 
| 70 70 | 
             
                  end
         | 
| 71 71 | 
             
                end
         | 
| @@ -32,8 +32,8 @@ module Gamefic | |
| 32 32 | 
             
                  # @param block [Proc]
         | 
| 33 33 | 
             
                  # @yieldparam [Scene]
         | 
| 34 34 | 
             
                  # @return [Symbol]
         | 
| 35 | 
            -
                  def block name, klass = Scene::Default, on_start: nil, on_finish: nil, & | 
| 36 | 
            -
                    rulebook.scenes.add klass.new(name, rulebook.narrative, on_start: on_start, on_finish: on_finish, & | 
| 35 | 
            +
                  def block name, klass = Scene::Default, on_start: nil, on_finish: nil, &blk
         | 
| 36 | 
            +
                    rulebook.scenes.add klass.new(name, rulebook.narrative, on_start: on_start, on_finish: on_finish, &blk)
         | 
| 37 37 | 
             
                    name
         | 
| 38 38 | 
             
                  end
         | 
| 39 39 | 
             
                  alias scene block
         | 
| @@ -77,14 +77,14 @@ module Gamefic | |
| 77 77 | 
             
                  # @yieldparam [Actor]
         | 
| 78 78 | 
             
                  # @yieldparam [Props::MultipleChoice]
         | 
| 79 79 | 
             
                  # @return [Symbol]
         | 
| 80 | 
            -
                  def multiple_choice name, choices = [], prompt = 'What is your choice?', & | 
| 80 | 
            +
                  def multiple_choice name, choices = [], prompt = 'What is your choice?', &blk
         | 
| 81 81 | 
             
                    block name,
         | 
| 82 82 | 
             
                          Scene::MultipleChoice,
         | 
| 83 83 | 
             
                          on_start: proc { |_actor, props|
         | 
| 84 84 | 
             
                            props.prompt = prompt
         | 
| 85 85 | 
             
                            props.options.concat choices
         | 
| 86 86 | 
             
                          },
         | 
| 87 | 
            -
                          on_finish:  | 
| 87 | 
            +
                          on_finish: blk
         | 
| 88 88 | 
             
                  end
         | 
| 89 89 |  | 
| 90 90 | 
             
                  # Create a yes-or-no scene.
         | 
| @@ -105,13 +105,13 @@ module Gamefic | |
| 105 105 | 
             
                  # @yieldparam [Actor]
         | 
| 106 106 | 
             
                  # @yieldparam [Props::YesOrNo]
         | 
| 107 107 | 
             
                  # @return [Symbol]
         | 
| 108 | 
            -
                  def yes_or_no name, prompt = 'Answer:', & | 
| 108 | 
            +
                  def yes_or_no name, prompt = 'Answer:', &blk
         | 
| 109 109 | 
             
                    block name,
         | 
| 110 110 | 
             
                          Scene::YesOrNo,
         | 
| 111 111 | 
             
                          on_start: proc { |_actor, props|
         | 
| 112 112 | 
             
                            props.prompt = prompt
         | 
| 113 113 | 
             
                          },
         | 
| 114 | 
            -
                          on_finish:  | 
| 114 | 
            +
                          on_finish: blk
         | 
| 115 115 | 
             
                  end
         | 
| 116 116 |  | 
| 117 117 | 
             
                  # Create a scene that pauses the game.
         | 
    
        data/lib/gamefic/snapshot.rb
    CHANGED
    
    | @@ -27,10 +27,18 @@ module Gamefic | |
| 27 27 | 
             
                  Marshal.load(binary).tap do |plot|
         | 
| 28 28 | 
             
                    plot.hydrate
         | 
| 29 29 | 
             
                    # @todo Opal marshal dumps are not idempotent
         | 
| 30 | 
            -
                    next if RUBY_ENGINE == 'opal' ||  | 
| 30 | 
            +
                    next if RUBY_ENGINE == 'opal' || match?(plot, snapshot)
         | 
| 31 31 |  | 
| 32 32 | 
             
                    Logging.logger.warn "Scripts modified #{plot.class} data. Snapshot may not have restored properly"
         | 
| 33 33 | 
             
                  end
         | 
| 34 34 | 
             
                end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                # True if the plot's state matches the snapshot.
         | 
| 37 | 
            +
                #
         | 
| 38 | 
            +
                # @param plot [Plot]
         | 
| 39 | 
            +
                # @param snapshot [String]
         | 
| 40 | 
            +
                def self.match?(plot, snapshot)
         | 
| 41 | 
            +
                  save(plot) == snapshot
         | 
| 42 | 
            +
                end
         | 
| 35 43 | 
             
              end
         | 
| 36 44 | 
             
            end
         | 
    
        data/lib/gamefic/syntax.rb
    CHANGED
    
    | @@ -61,12 +61,12 @@ module Gamefic | |
| 61 61 | 
             
                # Convert a String into a Command.
         | 
| 62 62 | 
             
                #
         | 
| 63 63 | 
             
                # @param text [String]
         | 
| 64 | 
            -
                # @return [ | 
| 64 | 
            +
                # @return [Expression, nil]
         | 
| 65 65 | 
             
                def tokenize text
         | 
| 66 66 | 
             
                  match = text&.match(template.regexp)
         | 
| 67 67 | 
             
                  return nil unless match
         | 
| 68 68 |  | 
| 69 | 
            -
                   | 
| 69 | 
            +
                  Expression.new(verb, match_to_args(match))
         | 
| 70 70 | 
             
                end
         | 
| 71 71 |  | 
| 72 72 | 
             
                # Determine if the specified text matches the syntax's expected pattern.
         | 
| @@ -94,7 +94,7 @@ module Gamefic | |
| 94 94 | 
             
                #
         | 
| 95 95 | 
             
                # @param text [String] The text to tokenize.
         | 
| 96 96 | 
             
                # @param syntaxes [Array<Syntax>] The syntaxes to use.
         | 
| 97 | 
            -
                # @return [Array< | 
| 97 | 
            +
                # @return [Array<Expression>] The tokenized expressions.
         | 
| 98 98 | 
             
                def self.tokenize text, syntaxes
         | 
| 99 99 | 
             
                  syntaxes
         | 
| 100 100 | 
             
                    .map { |syn| syn.tokenize(text) }
         | 
| @@ -109,7 +109,7 @@ module Gamefic | |
| 109 109 | 
             
                end
         | 
| 110 110 |  | 
| 111 111 | 
             
                # @param string [String]
         | 
| 112 | 
            -
                # @return [ | 
| 112 | 
            +
                # @return [Symbol, nil]
         | 
| 113 113 | 
             
                def self.literal_or_nil string
         | 
| 114 114 | 
             
                  string.start_with?(':') ? nil : string.to_sym
         | 
| 115 115 | 
             
                end
         | 
    
        data/lib/gamefic/version.rb
    CHANGED
    
    
    
        data/lib/gamefic.rb
    CHANGED
    
    | @@ -11,6 +11,7 @@ require 'gamefic/rulebook' | |
| 11 11 | 
             
            require 'gamefic/query'
         | 
| 12 12 | 
             
            require 'gamefic/scanner'
         | 
| 13 13 | 
             
            require 'gamefic/scope'
         | 
| 14 | 
            +
            require 'gamefic/expression'
         | 
| 14 15 | 
             
            require 'gamefic/command'
         | 
| 15 16 | 
             
            require 'gamefic/action'
         | 
| 16 17 | 
             
            require 'gamefic/props'
         | 
| @@ -27,6 +28,7 @@ require 'gamefic/node' | |
| 27 28 | 
             
            require 'gamefic/describable'
         | 
| 28 29 | 
             
            require 'gamefic/messenger'
         | 
| 29 30 | 
             
            require 'gamefic/entity'
         | 
| 31 | 
            +
            require 'gamefic/composer'
         | 
| 30 32 | 
             
            require 'gamefic/dispatcher'
         | 
| 31 33 | 
             
            require 'gamefic/active'
         | 
| 32 34 | 
             
            require 'gamefic/active/cue'
         | 
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,14 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: gamefic
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 3. | 
| 4 | 
            +
              version: 3.1.0
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Fred Snyder
         | 
| 8 8 | 
             
            autorequire:
         | 
| 9 9 | 
             
            bindir: bin
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date: 2024- | 
| 11 | 
            +
            date: 2024-04-08 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: opal
         | 
| @@ -136,11 +136,13 @@ files: | |
| 136 136 | 
             
            - lib/gamefic/actor.rb
         | 
| 137 137 | 
             
            - lib/gamefic/block.rb
         | 
| 138 138 | 
             
            - lib/gamefic/command.rb
         | 
| 139 | 
            +
            - lib/gamefic/composer.rb
         | 
| 139 140 | 
             
            - lib/gamefic/core_ext/array.rb
         | 
| 140 141 | 
             
            - lib/gamefic/core_ext/string.rb
         | 
| 141 142 | 
             
            - lib/gamefic/describable.rb
         | 
| 142 143 | 
             
            - lib/gamefic/dispatcher.rb
         | 
| 143 144 | 
             
            - lib/gamefic/entity.rb
         | 
| 145 | 
            +
            - lib/gamefic/expression.rb
         | 
| 144 146 | 
             
            - lib/gamefic/logging.rb
         | 
| 145 147 | 
             
            - lib/gamefic/messenger.rb
         | 
| 146 148 | 
             
            - lib/gamefic/narrative.rb
         | 
| @@ -149,6 +151,7 @@ files: | |
| 149 151 | 
             
            - lib/gamefic/props.rb
         | 
| 150 152 | 
             
            - lib/gamefic/props/default.rb
         | 
| 151 153 | 
             
            - lib/gamefic/props/multiple_choice.rb
         | 
| 154 | 
            +
            - lib/gamefic/props/output.rb
         | 
| 152 155 | 
             
            - lib/gamefic/props/pause.rb
         | 
| 153 156 | 
             
            - lib/gamefic/props/yes_or_no.rb
         | 
| 154 157 | 
             
            - lib/gamefic/query.rb
         |