gamefic 2.0.2 → 2.2.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.
@@ -11,8 +11,9 @@ module Gamefic
11
11
  attr_reader :plot
12
12
 
13
13
  # @param plot [Gamefic::Plot]
14
- # @param introduce [Gamefic::Actor]
15
- # @param next_cue [Class<Gamefic::Scene::Base>]
14
+ # @param introduce [Gamefic::Actor, nil]
15
+ # @param next_cue [Class<Gamefic::Scene::Base>, nil]
16
+ # @param more [Hash]
16
17
  def initialize plot, introduce: nil, next_cue: nil, **more
17
18
  @plot = plot
18
19
  @next_cue = next_cue
@@ -98,15 +99,5 @@ module Gamefic
98
99
  #
99
100
  def configure more
100
101
  end
101
-
102
- # def to_serial(index)
103
- # puts "Serializing #{self}"
104
- # super
105
- # end
106
-
107
- # def from_serial index = []
108
- # # @todo Customize subplot unserialization
109
- # super
110
- # end
111
102
  end
112
103
  end
@@ -1,5 +1,3 @@
1
- require 'gamefic/command'
2
-
3
1
  module Gamefic
4
2
  class Syntax
5
3
  attr_reader :token_count, :first_word, :verb, :template, :command
@@ -57,17 +55,18 @@ module Gamefic
57
55
  m = text.match(@regexp)
58
56
  return nil if m.nil?
59
57
  arguments = []
60
- # HACK: Skip the first word if the verb is not nil.
61
- # This is ugly.
62
58
  b = @verb.nil? ? 0 : 1
59
+ xverb = @verb
63
60
  @replace.to_s.split_words[b..-1].each { |r|
64
61
  if r.match(/^\{\$[0-9]+\}$/)
65
62
  arguments.push m[r[2..-2].to_i]
63
+ elsif arguments.empty? && xverb.nil?
64
+ xverb = r.to_sym
66
65
  else
67
66
  arguments.push r
68
67
  end
69
68
  }
70
- Command.new @verb, arguments
69
+ Command.new xverb, arguments
71
70
  end
72
71
 
73
72
  # Determine if the specified text matches the syntax's expected pattern.
@@ -86,28 +85,36 @@ module Gamefic
86
85
  end
87
86
 
88
87
  def ==(other)
88
+ return false unless other.is_a?(Syntax)
89
89
  signature == other.signature
90
90
  end
91
91
 
92
- # Tokenize an Array of Commands from the specified text.
92
+ def eql?(other)
93
+ self == other
94
+ end
95
+
96
+ def hash
97
+ signature.hash
98
+ end
99
+
100
+ # Tokenize an Array of Commands from the specified text. The resulting
101
+ # array is in descending order of specificity, i.e., most to least matched
102
+ # tokens.
93
103
  #
94
104
  # @param text [String] The text to tokenize.
95
105
  # @param syntaxes [Array<Gamefic::Syntax>] The Syntaxes to use.
96
106
  # @return [Array<Gamefic::Command>] The tokenized commands.
97
107
  def self.tokenize text, syntaxes
98
- matches = []
99
- syntaxes.each { |syntax|
100
- result = syntax.tokenize text
101
- matches.push(result) if !result.nil?
102
- }
103
- matches.sort! { |a,b|
104
- if a.arguments.length == b.arguments.length
105
- b.verb.to_s <=> a.verb.to_s
106
- else
107
- b.arguments.length <=> a.arguments.length
108
+ syntaxes
109
+ .map { |syn| syn.tokenize(text) }
110
+ .reject(&:nil?)
111
+ .sort do |a, b|
112
+ if a.arguments.length == b.arguments.length
113
+ b.verb.to_s <=> a.verb.to_s
114
+ else
115
+ b.arguments.length <=> a.arguments.length
116
+ end
108
117
  end
109
- }
110
- matches
111
118
  end
112
119
  end
113
120
  end
@@ -1,3 +1,3 @@
1
1
  module Gamefic
2
- VERSION = '2.0.2'
2
+ VERSION = '2.2.0'
3
3
  end
@@ -95,25 +95,6 @@ module Gamefic
95
95
  playbook.meta command, *queries, &proc
96
96
  end
97
97
 
98
- # Declare a dismabiguation response for actions.
99
- # The disambigurator is executed when an action expects an argument to
100
- # reference a specific entity but its query matched more than one. For
101
- # example, "red" might refer to either a red key or a red book.
102
- #
103
- # If a disambiguator is not defined, the playbook will use its default
104
- # implementation.
105
- #
106
- # @example Tell the player the list of ambiguous entities.
107
- # disambiguate do |actor, entities|
108
- # actor.tell "I don't know which you mean: #{entities.join_or}."
109
- # end
110
- #
111
- # @yieldparam [Gamefic::Actor]
112
- # @yieldparam [Array<Gamefic::Entity>]
113
- def disambiguate &block
114
- playbook.disambiguate &block
115
- end
116
-
117
98
  # Validate an order before a character can execute its command.
118
99
  #
119
100
  # @yieldparam [Gamefic::Director::Order]
@@ -144,7 +125,7 @@ module Gamefic
144
125
  #
145
126
  # @return [Array<String>]
146
127
  def verbs
147
- playbook.verbs.map { |v| v.to_s }.reject{ |v| v.start_with?('_') }
128
+ playbook.verbs.map(&:to_s).reject { |v| v.start_with?('_') }
148
129
  end
149
130
 
150
131
  # Get an Array of all Actions defined in the Plot.
@@ -164,20 +145,20 @@ module Gamefic
164
145
 
165
146
  private
166
147
 
148
+ # @param queries [Array]
149
+ # @return [Array<Query::Base>]
167
150
  def map_response_args queries
168
- result = []
169
- queries.each do |q|
151
+ queries.map do |q|
170
152
  if q.is_a?(Regexp)
171
- result.push Gamefic::Query::Text.new(q)
153
+ Gamefic::Query::Text.new(q)
172
154
  elsif q.is_a?(Gamefic::Query::Base)
173
- result.push q
155
+ q
174
156
  elsif q.is_a?(Gamefic::Element) || (q.is_a?(Class) && q <= Gamefic::Element)
175
- result.push get_default_query.new(q)
157
+ get_default_query.new(q)
176
158
  else
177
159
  raise ArgumentError.new("Invalid argument for response: #{q}")
178
160
  end
179
161
  end
180
- result
181
162
  end
182
163
  end
183
164
  end
@@ -93,16 +93,6 @@ module Gamefic
93
93
  def players
94
94
  @players ||= []
95
95
  end
96
-
97
- # private
98
-
99
- # def mark_static_entities
100
- # @static_entity_length ||= entities.length
101
- # end
102
-
103
- # def static_entity_length
104
- # @static_entity_length || 0
105
- # end
106
96
  end
107
97
  end
108
98
  end
@@ -1,20 +1,28 @@
1
+ require 'set'
2
+
1
3
  module Gamefic
2
4
  module World
3
5
  # A collection of rules for performing commands.
4
6
  #
5
7
  class Playbook
6
- def initialize commands: {}, syntaxes: [], validators: [], disambiguator: nil
7
- @commands = commands
8
- @syntaxes = syntaxes
9
- @validators = validators
10
- @disambiguator = disambiguator
11
- end
12
-
13
8
  # An array of available syntaxes.
14
9
  #
15
10
  # @return [Array<Gamefic::Syntax>]
16
- def syntaxes
17
- @syntaxes
11
+ attr_reader :syntaxes
12
+
13
+ # An array of defined validators.
14
+ #
15
+ # @return [Array<Proc>]
16
+ attr_reader :validators
17
+
18
+ # @param commands [Hash]
19
+ # @param syntaxes [Array<Syntax>, Set<Syntax>]
20
+ # @param validators [Array]
21
+ def initialize commands: {}, syntaxes: [], validators: []
22
+ @commands = commands
23
+ @syntax_set = syntaxes.to_set
24
+ sort_syntaxes
25
+ @validators = validators
18
26
  end
19
27
 
20
28
  # An array of available actions.
@@ -31,32 +39,6 @@ module Gamefic
31
39
  @commands.keys
32
40
  end
33
41
 
34
- # An array of defined validators.
35
- #
36
- # @return [Array<Proc>]
37
- def validators
38
- @validators
39
- end
40
-
41
- # Get the action for handling ambiguous entity references.
42
- #
43
- def disambiguator
44
- @disambiguator ||= Action.subclass(nil, Query::Base.new) do |actor, entities|
45
- definites = []
46
- entities.each { |entity|
47
- definites.push entity.definitely
48
- }
49
- actor.tell "I don't know which you mean: #{definites.join_or}."
50
- end
51
- end
52
-
53
- # Set the action for handling ambiguous entity references.
54
- #
55
- def disambiguate &block
56
- @disambiguator = Action.subclass(nil, Query::Base.new, meta: true, &block)
57
- @disambiguator
58
- end
59
-
60
42
  # Add a block that determines whether an action can be executed.
61
43
  #
62
44
  def validate &block
@@ -66,7 +48,7 @@ module Gamefic
66
48
  # Get an Array of all Actions associated with the specified verb.
67
49
  #
68
50
  # @param verb [Symbol] The Symbol for the verb (e.g., :go or :look)
69
- # @return [Array<Action>] The verb's associated Actions
51
+ # @return [Array<Class<Action>>] The verb's associated Actions
70
52
  def actions_for verb
71
53
  @commands[verb] || []
72
54
  end
@@ -142,40 +124,16 @@ module Gamefic
142
124
  syn
143
125
  end
144
126
 
145
- # Get an array of actions, derived from the specified command, that the
146
- # actor can potentially execute.
147
- # The command can either be a single string (e.g., "examine book") or a
148
- # list of tokens (e.g., :examine, @book).
127
+ # Get a Dispatcher to select actions that can potentially be executed
128
+ # from the specified command string.
149
129
  #
150
- # @return [Array<Gamefic::Action>]
151
- def dispatch(actor, *command)
152
- result = []
153
- if command.length > 1
154
- result.concat dispatch_from_params(actor, command[0], command[1..-1])
155
- end
156
- if result.empty?
157
- result.concat dispatch_from_string(actor, command.join(' '))
158
- end
159
- result
160
- end
161
-
162
- # Get an array of actions, derived from the specified command, that the
163
- # actor can potentially execute.
164
- # The command should be a plain-text string, e.g., "examine the book."
165
- #
166
- # @return [Array<Gamefic::Action>]
167
- def dispatch_from_string actor, text
168
- result = []
130
+ # @param actor [Actor]
131
+ # @param text [String]
132
+ # @return [Dispatcher]
133
+ def dispatch(actor, text)
169
134
  commands = Syntax.tokenize(text, actor.syntaxes)
170
- commands.each { |c|
171
- available = actions_for(c.verb)
172
- available.each { |a|
173
- next if a.hidden?
174
- o = a.attempt(actor, c.arguments)
175
- result.unshift o unless o.nil?
176
- }
177
- }
178
- sort_and_reduce_actions result
135
+ actions = commands.flat_map { |cmd| actions_for(cmd.verb).reject(&:hidden?) }
136
+ Dispatcher.new(actor, commands, sort_and_reduce_actions(actions))
179
137
  end
180
138
 
181
139
  # Get an array of actions, derived from the specified verb and params,
@@ -183,12 +141,8 @@ module Gamefic
183
141
  #
184
142
  # @return [Array<Gamefic::Action>]
185
143
  def dispatch_from_params actor, verb, params
186
- result = []
187
144
  available = actions_for(verb)
188
- available.each { |a|
189
- result.unshift a.new(actor, params) if a.valid?(actor, params)
190
- }
191
- sort_and_reduce_actions result
145
+ Dispatcher.new(actor, [Command.new(verb, params)], sort_and_reduce_actions(available))
192
146
  end
193
147
 
194
148
  # Duplicate the playbook.
@@ -217,38 +171,38 @@ module Gamefic
217
171
  user_friendly = action.verb.to_s.gsub(/_/, ' ')
218
172
  args = []
219
173
  used_names = []
220
- action.queries.each { |c|
174
+ action.queries.each do |_c|
221
175
  num = 1
222
176
  new_name = ":var"
223
177
  while used_names.include? new_name
224
- num = num + 1
178
+ num += 1
225
179
  new_name = ":var#{num}"
226
180
  end
227
181
  used_names.push new_name
228
182
  user_friendly += " #{new_name}"
229
183
  args.push new_name
230
- }
184
+ end
231
185
  add_syntax Syntax.new(user_friendly.strip, "#{action.verb} #{args.join(' ')}") unless action.verb.to_s.start_with?('_')
232
186
  end
233
187
 
234
188
  def add_syntax syntax
235
- if @commands[syntax.verb] == nil
236
- raise "No actions exist for \"#{syntax.verb}\""
237
- end
238
- @syntaxes.unshift syntax
239
- @syntaxes.uniq! &:signature
240
- @syntaxes.sort! { |a, b|
189
+ raise "No actions exist for \"#{syntax.verb}\"" if @commands[syntax.verb].nil?
190
+ sort_syntaxes if @syntax_set.add?(syntax)
191
+ end
192
+
193
+ def sort_and_reduce_actions arr
194
+ arr.sort_by.with_index { |a, i| [a.rank, i] }.reverse.uniq
195
+ end
196
+
197
+ def sort_syntaxes
198
+ @syntaxes = @syntax_set.sort do |a, b|
241
199
  if a.token_count == b.token_count
242
- # For syntaxes of the same length, length of action takes precedence
200
+ # For syntaxes of the same length, sort first word
243
201
  b.first_word <=> a.first_word
244
202
  else
245
203
  b.token_count <=> a.token_count
246
204
  end
247
- }
248
- end
249
-
250
- def sort_and_reduce_actions arr
251
- arr.sort_by.with_index{|a, i| [a.rank, -i]}.reverse.uniq(&:class)
205
+ end
252
206
  end
253
207
  end
254
208
  end
@@ -2,6 +2,7 @@ module Gamefic
2
2
  module World
3
3
  module Players
4
4
  include Gamefic::World::Entities
5
+ include Gamefic::World::Commands
5
6
 
6
7
  # An array of entities that are currently connected to users.
7
8
  #
@@ -10,12 +11,27 @@ module Gamefic
10
11
  @players ||= []
11
12
  end
12
13
 
13
- # Get the character that the player will control on introduction.
14
+ def player_class cls = nil
15
+ STDERR.puts "Modifying player_class this way is deprecated. Use set_player_class instead" unless cls.nil?
16
+ @player_class = cls unless cls.nil?
17
+ @player_class ||= Gamefic::Actor
18
+ end
19
+
20
+ # @param cls [Class]
21
+ def set_player_class cls
22
+ unless cls < Gamefic::Active && cls <= Gamefic::Entity
23
+ raise ArgumentError, "Player class must be an active entity"
24
+ end
25
+ @player_class = cls
26
+ end
27
+
28
+ # Make a character that a player will control on introduction.
14
29
  #
15
30
  # @return [Gamefic::Actor]
16
- def get_player_character
31
+ def make_player_character
17
32
  cast player_class, name: 'yourself', synonyms: 'self myself you me', proper_named: true
18
33
  end
34
+ alias get_player_character make_player_character
19
35
  end
20
36
  end
21
37
  end
@@ -95,10 +95,10 @@ module Gamefic
95
95
  #
96
96
  # @param prompt [String]
97
97
  # @yieldparam [Gamefic::Actor]
98
- # @yieldparam [Gamefic::Scene::Custom]
99
- # @return [Class<Gamefic::Scene::Custom>]
98
+ # @yieldparam [Gamefic::Scene::Base]
99
+ # @return [Class<Gamefic::Scene::Base>]
100
100
  def question prompt = 'What is your answer?', &block
101
- s = Scene::Custom.subclass do |actor, scene|
101
+ s = Scene::Base.subclass do |actor, scene|
102
102
  scene.prompt = prompt
103
103
  scene.on_finish &block
104
104
  end
@@ -150,23 +150,23 @@ module Gamefic
150
150
  # Custom scenes should always specify the next scene to be cued or
151
151
  # prepared. If not, the scene will get repeated on the next turn.
152
152
  #
153
- # This method creates a Scene::Custom by default. You can customize other
153
+ # This method creates a Scene::Base by default. You can customize other
154
154
  # scene types by specifying the class to create.
155
155
  #
156
156
  # @example Ask the user for a name
157
- # @scene = custom do |scene|
158
- # data.prompt = "What's your name?"
159
- # scene.on_finish do |actor, data|
160
- # actor.name = data.input
157
+ # @scene = custom do |actor, scene|
158
+ # scene.prompt = "What's your name?"
159
+ # scene.on_finish do
160
+ # actor.name = scene.input
161
161
  # actor.tell "Hello, #{actor.name}!"
162
- # actor.cue :active
162
+ # actor.cue default_scene
163
163
  # end
164
164
  # end
165
165
  #
166
166
  # @param cls [Class] The class of scene to be instantiated.
167
167
  # @yieldparam [Gamefic::Actor]
168
- # @return [Class<Gamefic::Scene::Custom>]
169
- def custom cls = Scene::Custom, &block
168
+ # @return [Class<Gamefic::Scene::Base>]
169
+ def custom cls = Scene::Base, &block
170
170
  s = cls.subclass &block
171
171
  scene_classes.push s
172
172
  s