gamefic 1.5.1 → 1.6.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.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/lib/gamefic.rb +1 -3
  3. data/lib/gamefic/action.rb +140 -79
  4. data/lib/gamefic/character.rb +120 -53
  5. data/lib/gamefic/character/state.rb +12 -0
  6. data/lib/gamefic/core_ext/array.rb +53 -11
  7. data/lib/gamefic/core_ext/string.rb +1 -0
  8. data/lib/gamefic/describable.rb +37 -11
  9. data/lib/gamefic/engine/base.rb +17 -4
  10. data/lib/gamefic/engine/tty.rb +4 -0
  11. data/lib/gamefic/entity.rb +4 -15
  12. data/lib/gamefic/matchable.rb +50 -0
  13. data/lib/gamefic/messaging.rb +45 -0
  14. data/lib/gamefic/node.rb +16 -0
  15. data/lib/gamefic/plot.rb +27 -33
  16. data/lib/gamefic/plot/{article_mount.rb → articles.rb} +22 -22
  17. data/lib/gamefic/plot/callbacks.rb +30 -4
  18. data/lib/gamefic/plot/{command_mount.rb → commands.rb} +121 -121
  19. data/lib/gamefic/plot/entities.rb +3 -3
  20. data/lib/gamefic/plot/host.rb +3 -3
  21. data/lib/gamefic/plot/playbook.rb +74 -30
  22. data/lib/gamefic/plot/scenes.rb +149 -0
  23. data/lib/gamefic/plot/snapshot.rb +14 -39
  24. data/lib/gamefic/plot/theater.rb +73 -0
  25. data/lib/gamefic/query.rb +6 -19
  26. data/lib/gamefic/query/base.rb +127 -246
  27. data/lib/gamefic/query/children.rb +6 -7
  28. data/lib/gamefic/query/descendants.rb +15 -0
  29. data/lib/gamefic/query/family.rb +19 -7
  30. data/lib/gamefic/query/itself.rb +13 -0
  31. data/lib/gamefic/query/matches.rb +67 -11
  32. data/lib/gamefic/query/parent.rb +6 -7
  33. data/lib/gamefic/query/siblings.rb +10 -7
  34. data/lib/gamefic/query/text.rb +39 -35
  35. data/lib/gamefic/scene.rb +1 -1
  36. data/lib/gamefic/scene/active.rb +12 -6
  37. data/lib/gamefic/scene/base.rb +56 -5
  38. data/lib/gamefic/scene/conclusion.rb +3 -0
  39. data/lib/gamefic/scene/custom.rb +0 -83
  40. data/lib/gamefic/scene/multiple_choice.rb +54 -32
  41. data/lib/gamefic/scene/multiple_scene.rb +11 -6
  42. data/lib/gamefic/scene/pause.rb +3 -4
  43. data/lib/gamefic/scene/yes_or_no.rb +23 -9
  44. data/lib/gamefic/script/base.rb +4 -0
  45. data/lib/gamefic/subplot.rb +22 -19
  46. data/lib/gamefic/syntax.rb +7 -15
  47. data/lib/gamefic/user/base.rb +7 -13
  48. data/lib/gamefic/user/buffer.rb +7 -0
  49. data/lib/gamefic/user/tty.rb +13 -12
  50. data/lib/gamefic/version.rb +1 -1
  51. metadata +11 -37
  52. data/lib/gamefic/director.rb +0 -23
  53. data/lib/gamefic/director/delegate.rb +0 -126
  54. data/lib/gamefic/director/order.rb +0 -17
  55. data/lib/gamefic/director/parser.rb +0 -137
  56. data/lib/gamefic/keywords.rb +0 -67
  57. data/lib/gamefic/plot/query_mount.rb +0 -9
  58. data/lib/gamefic/plot/scene_mount.rb +0 -182
  59. data/lib/gamefic/query/ambiguous_children.rb +0 -5
  60. data/lib/gamefic/query/expression.rb +0 -47
  61. data/lib/gamefic/query/many_children.rb +0 -7
  62. data/lib/gamefic/query/plural_children.rb +0 -14
  63. data/lib/gamefic/query/self.rb +0 -10
  64. data/lib/gamefic/scene_data.rb +0 -10
  65. data/lib/gamefic/scene_data/base.rb +0 -12
  66. data/lib/gamefic/scene_data/multiple_choice.rb +0 -19
  67. data/lib/gamefic/scene_data/multiple_scene.rb +0 -21
  68. data/lib/gamefic/scene_data/yes_or_no.rb +0 -18
  69. data/lib/gamefic/serialized.rb +0 -24
  70. data/lib/gamefic/stage.rb +0 -106
@@ -0,0 +1,73 @@
1
+ module Gamefic
2
+
3
+ module Plot::Theater
4
+ # Execute a block of code in a subset of the object's scope. An object's
5
+ # stage is an isolated namespace that has its own instance variables and
6
+ # access to its owner's public methods.
7
+ #
8
+ # There are two ways to execute code on the stage. It will accept either a
9
+ # string of code with an optional file name and line number, or a proc
10
+ # with optional arguments. See module_eval and module_exec for more
11
+ # information.
12
+ #
13
+ # @example Evaluate a string of code
14
+ # stage "puts 'Hello'"
15
+ #
16
+ # @example Evaluate a string of code with a file name and line number
17
+ # stage "puts 'Hello'", "file.rb", 1
18
+ #
19
+ # @example Execute a block of code
20
+ # stage {
21
+ # puts 'Hello'
22
+ # }
23
+ #
24
+ # @example Execute a block of code with arguments
25
+ # stage 'hello' { |message|
26
+ # puts message # <- prints 'hello'
27
+ # }
28
+ #
29
+ # @example Use an instance variable
30
+ # stage "@message = 'hello'"
31
+ # stage "puts @message" # <- prints 'hello'
32
+ #
33
+ # @return [Object] The value returned by the executed code
34
+ def stage *args, &block
35
+ if block.nil?
36
+ theater.module_eval *args
37
+ else
38
+ theater.module_exec *args, &block
39
+ end
40
+ end
41
+
42
+ # The module that acts as an isolated namespace for staged code.
43
+ #
44
+ # @return [Module]
45
+ def theater
46
+ return @theater unless @theater.nil?
47
+ instance = self
48
+
49
+ @theater = Module.new do
50
+ define_singleton_method :method_missing do |symbol, *args, &block|
51
+ instance.public_send :public_send, symbol, *args, &block
52
+ end
53
+
54
+ define_singleton_method :stage do |*args|
55
+ raise NoMethodError.new("The stage method is not available from inside staged scripts")
56
+ end
57
+
58
+ define_singleton_method :to_s do
59
+ "[Theater]"
60
+ end
61
+ end
62
+
63
+ # HACK: Include the theater module in Object so that classes and modules
64
+ # defined in scripts are accessible from procs passed to the stage.
65
+ Object.class_exec(@theater) do |t|
66
+ include t
67
+ end
68
+
69
+ @theater
70
+ end
71
+ end
72
+
73
+ end
data/lib/gamefic/query.rb CHANGED
@@ -1,30 +1,17 @@
1
- require 'gamefic/keywords'
1
+ #require 'gamefic/keywords'
2
2
 
3
3
  module Gamefic
4
4
 
5
5
  module Query
6
6
  autoload :Base, 'gamefic/query/base'
7
- autoload :Text, 'gamefic/query/text'
8
- autoload :Expression, 'gamefic/query/expression'
9
- autoload :Self, 'gamefic/query/self'
10
- autoload :Parent, 'gamefic/query/parent'
11
7
  autoload :Children, 'gamefic/query/children'
12
- autoload :ManyChildren, 'gamefic/query/many_children'
13
- autoload :AmbiguousChildren, 'gamefic/query/ambiguous_children'
14
- autoload :PluralChildren, 'gamefic/query/plural_children'
15
- autoload :Siblings, 'gamefic/query/siblings'
8
+ autoload :Descendants, 'gamefic/query/descendants'
16
9
  autoload :Family, 'gamefic/query/family'
10
+ autoload :Itself, 'gamefic/query/itself'
17
11
  autoload :Matches, 'gamefic/query/matches'
18
-
19
- def self.allow_plurals?
20
- if @allow_plurals.nil?
21
- @allow_plurals = true
22
- end
23
- @allow_plurals
24
- end
25
- def self.allow_plurals= boolean
26
- @allow_plurals = boolean
27
- end
12
+ autoload :Parent, 'gamefic/query/parent'
13
+ autoload :Siblings, 'gamefic/query/siblings'
14
+ autoload :Text, 'gamefic/query/text'
28
15
  end
29
16
 
30
17
  end
@@ -1,266 +1,147 @@
1
- module Gamefic::Query
2
- class Base
3
- @@ignored_words = ['a', 'an', 'the', 'and', ',']
4
- @@subquery_prepositions = ['in', 'on', 'of', 'inside', 'from']
5
- # Include is necessary here due to a strange namespace
6
- # resolution bug when interpreting gfic files
7
- include Gamefic
8
- attr_accessor :arguments
9
- def initialize *arguments
10
- test_arguments arguments
11
- @optional = false
12
- if arguments.include?(:optional)
13
- @optional = true
14
- arguments.delete :optional
1
+ module Gamefic
2
+ module Query
3
+ class Base
4
+ NEST_REGEXP = / in | on | of | from | inside /
5
+
6
+ attr_reader :arguments
7
+
8
+ def initialize *args
9
+ @arguments = args
15
10
  end
16
- @arguments = arguments
17
- @match_hash = Hash.new
18
- end
19
- # Check whether the query allows ambiguous matches.
20
- # If allowed, this query's
21
- def allow_ambiguous?
22
- false
23
- end
24
- def allow_many?
25
- false
26
- end
27
- def last_match_for(subject)
28
- @match_hash[subject]
29
- end
30
- def optional?
31
- @optional
32
- end
33
- def context_from(subject)
34
- subject
35
- end
36
- def validate(subject, object)
37
- arr = context_from(subject)
38
- @arguments.each { |arg|
39
- arr = arr.that_are(arg)
40
- }
41
- if (allow_many? or allow_ambiguous?)
42
- if object.kind_of?(Array)
43
- return (object & arr) == object
44
- end
45
- return false
46
- elsif !object.kind_of?(Array)
47
- return arr.include?(object)
11
+
12
+ def ambiguous?
13
+ false
48
14
  end
49
- return false
50
- end
51
- # @return [Array]
52
- def execute(subject, description)
53
- if (allow_many? or allow_ambiguous?) and !Query.allow_plurals?
54
- return Matches.new([], '', description)
15
+
16
+ # Subclasses should override this method with the logic required to collect
17
+ # all entities that exist in the query's context.
18
+ #
19
+ # @return [Array<Object>]
20
+ def context_from(subject)
21
+ []
55
22
  end
56
- if !allow_ambiguous?
57
- if allow_many? and !description.include?(',') and !description.downcase.include?(' and ')
58
- return Matches.new([], '', description)
23
+
24
+ # Get an array of objects that exist in the subject's context and match
25
+ # the provided token.
26
+ #
27
+ def resolve(subject, token, continued: false)
28
+ available = context_from(subject)
29
+ return Matches.new([], '', token) if available.empty?
30
+ if continued
31
+ return Matches.execute(available, token, continued: continued)
32
+ elsif nested?(token)
33
+ drill = denest(available, token)
34
+ drill.keep_if{ |e| accept?(e) }
35
+ return Matches.new(drill, token, '') unless drill.length != 1
36
+ return Matches.new([], '', token)
59
37
  end
38
+ result = available.select{ |e| e.match?(token) }
39
+ result = available.select{ |e| e.match?(token, fuzzy: true) } if result.empty?
40
+ result.keep_if{ |e| accept? e }
41
+ Matches.new(result, (result.empty? ? '' : token), (result.empty? ? token : ''))
60
42
  end
61
- array = context_from(subject)
62
- matches = self.match(description, array)
63
- objects = matches.objects
64
- matches = Matches.new(objects, matches.matching_text, matches.remainder)
65
- if objects.length == 0 and matches.remainder == "it" and subject.respond_to?(:last_object)
66
- if !subject.last_object.nil?
67
- obj = subject.last_object
68
- if validate(subject, obj)
69
- matches = Matches.new([obj], "it", "")
70
- end
71
- end
43
+
44
+ def include?(subject, object)
45
+ return false unless accept?(object)
46
+ result = context_from(subject)
47
+ result.include?(object)
72
48
  end
73
- @match_hash[subject] = matches
74
- matches
75
- end
76
- def base_specificity
77
- 0
78
- end
79
- def specificity
80
- if @specificity == nil
81
- @specificity = base_specificity
82
- magnitude = 1
83
- @arguments.each { |item|
84
- if item.kind_of?(Entity)
85
- @specificity += (magnitude * 10)
86
- item = item.class
87
- end
88
- if item.kind_of?(Class)
89
- s = item
90
- while s != nil
91
- @specificity += magnitude
92
- s = s.superclass
49
+
50
+ def precision
51
+ if @precision.nil?
52
+ @precision = 1
53
+ arguments.each { |a|
54
+ if a.kind_of?(Symbol) or a.kind_of?(Regexp)
55
+ @precision += 1
56
+ elsif a.kind_of?(Class)
57
+ @precision += (count_superclasses(a) * 100)
58
+ elsif a.kind_of?(Module)
59
+ @precision += 10
60
+ elsif a.kind_of?(Object)
61
+ @precision += 1000
93
62
  end
63
+ }
64
+ @precision
65
+ end
66
+ @precision
67
+ end
68
+
69
+ def rank
70
+ precision
71
+ end
72
+
73
+ def signature
74
+ "#{self.class.to_s.downcase}(#{@arguments.join(',')})"
75
+ end
76
+
77
+ def accept?(entity)
78
+ result = true
79
+ arguments.each { |a|
80
+ if a.kind_of?(Symbol)
81
+ result = (entity.send(a) != false)
82
+ elsif a.kind_of?(Regexp)
83
+ result = (!entity.to_s.match(a).nil?)
84
+ elsif a.is_a?(Module) or a.is_a?(Class)
85
+ result = (entity.is_a?(a))
94
86
  else
95
- @specificity += magnitude
87
+ result = (entity == a)
96
88
  end
89
+ break if result == false
97
90
  }
98
- if allow_many?
99
- # HACK Ridiculously high magic number to force queries that return
100
- # arrays to take precedence over everything
101
- @specificity = @specificity * 10
102
- end
91
+ result
103
92
  end
104
- @specificity
105
- end
106
- def signature
107
- "#{self.class}(#{@arguments.join(',')})"
108
- end
109
- def test_arguments arguments
110
- my_classes = [Gamefic::Entity]
111
- my_objects = []
112
- arguments.each { |a|
113
- if a.kind_of?(Class) or a.kind_of?(Module)
114
- my_classes.push a
115
- elsif a.kind_of?(Gamefic::Entity)
116
- my_objects.push a
117
- elsif a.kind_of?(Symbol)
118
- if my_classes.length == 0 and my_objects.length == 0
119
- raise ArgumentError.new("Query signature requires at least one class, module, or object to accept a method symbol")
120
- end
121
- if my_classes.length > 0
122
- responds = false
123
- my_classes.each { |c|
124
- if c.instance_methods.include?(a)
125
- responds = true
126
- break
127
- end
128
- }
129
- if !responds
130
- raise ArgumentError.new("Query signature does not have a target that responds to #{a}")
131
- end
132
- end
133
- my_objects.each { |o|
134
- if !o.respond_to?(a)
135
- raise ArgumentError.new("Query signature contains object '#{o}' that does not respond to '#{a}'")
136
- end
93
+
94
+ protected
95
+
96
+ # Return an array of the entity's children. If the child is neighborly,
97
+ # recursively append its children.
98
+ # The result will NOT include the original entity itself.
99
+ #
100
+ # @return [Array<Object>]
101
+ def subquery_accessible entity
102
+ result = []
103
+ if entity.accessible?
104
+ entity.children.each { |c|
105
+ result.push c
106
+ result.concat subquery_accessible(c)
137
107
  }
138
- else
139
- raise ArgumentError.new("Invalid argument '#{a}' in query signature")
140
- end
141
- }
142
- end
143
- def match(description, array)
144
- keywords = get_keywords(description)
145
- array.each { |e|
146
- if e.uid == keywords[0]
147
- return Matches.new([e], keywords.shift, keywords.join(' '))
148
- end
149
- }
150
- used = []
151
- skipped = []
152
- possibilities = array
153
- at_least_one_match = false
154
- while keywords.length > 0
155
- next_word = keywords.shift
156
- if @@subquery_prepositions.include?(next_word)
157
- if !at_least_one_match
158
- return Matches.new([], '', description)
159
- end
160
- so_far = keywords.join(' ')
161
- in_matched = self.match(so_far, array)
162
- if in_matched.objects.length > 0 and (in_matched.objects.length == 1 or in_matched.objects[0].kind_of?(Array))
163
- # Subset matching should only consider the intersection of the
164
- # original array and the matched object's children. This ensures
165
- # that it won't erroneously match a child that was excluded from
166
- # the original query.
167
- parent = in_matched.objects.shift
168
- subset = self.match(used.join(' '), (array & (parent.kind_of?(Array) ? parent[0].children : parent.children)))
169
- if subset.objects.length == 1
170
- if in_matched.objects.length == 0
171
- return subset
172
- else
173
- return Matches.new([subset.objects] + in_matched.objects, subset.matching_text, subset.remainder)
174
- end
175
- end
176
- end
177
- end
178
- used.push next_word
179
- new_results = []
180
- possibilities.each { |p|
181
- words = Keywords.new(used.last)
182
- if words.length > 0
183
- matches = words.found_in(p.keywords, (allow_many? or allow_ambiguous?))
184
- if matches > 0
185
- new_results.push p
186
- end
187
- end
188
- }
189
- if new_results.length > 0
190
- at_least_one_match = true
191
- intersection = possibilities & new_results
192
- if intersection.length == 0
193
- skipped.push used.pop
194
- else
195
- skipped.clear
196
- possibilities = intersection
197
- end
198
- elsif (next_word.downcase == 'and' or next_word == ',')
199
- while keywords.first == ',' or keywords.first.downcase == 'and'
200
- used.push keywords.shift
201
- end
202
- if allow_ambiguous?
203
- # Ambiguous queries filter based on all keywords instead of
204
- # building an array of specified entities
205
- next
206
- end
207
- so_far = keywords.join(' ')
208
- recursed = self.match(so_far, array)
209
- if possibilities.length == 1 and !allow_ambiguous?
210
- possibilities = [possibilities]
211
- else
212
- # Force lists of things to be uniquely identifying
213
- return Matches.new([], '', description)
214
- end
215
- objects = recursed.objects.clone
216
- while objects.length > 0
217
- obj = objects.shift
218
- if obj.kind_of?(Array)
219
- possibilities.push obj
220
- else
221
- combined = [obj] + objects
222
- possibilities.push combined
223
- break
224
- end
225
- end
226
- used += recursed.matching_text.split_words
227
- skipped = recursed.remainder.split_words
228
- keywords = []
229
- else
230
- # The first unignored word must have at least one match
231
- if at_least_one_match and !@@ignored_words.include?(used.last)
232
- keywords.unshift used.pop
233
- return Matches.new(possibilities, used.join(' '), keywords.join(' '))
234
- else
235
- if !@@ignored_words.include?(used.last)
236
- return Matches.new([], '', description)
237
- end
238
- end
239
108
  end
109
+ result
240
110
  end
241
- if at_least_one_match and (used - @@ignored_words).length > 0
242
- r = Matches.new(possibilities, used.join(' '), skipped.join(' '))
243
- return r
244
- else
245
- return Matches.new([], '', description)
111
+
112
+ private
113
+
114
+ def count_superclasses cls
115
+ s = cls.superclass
116
+ c = 1
117
+ until s.nil? or s == Object or s == BasicObject
118
+ c += 1
119
+ s = s.superclass
120
+ end
121
+ c
246
122
  end
247
- end
248
123
 
249
- private
124
+ def nested?(token)
125
+ !token.match(NEST_REGEXP).nil?
126
+ end
250
127
 
251
- def get_keywords text
252
- if text.include?(',')
253
- tmp = text.split(',', -1)
254
- keywords = []
255
- first = tmp.shift
256
- keywords.push first.strip unless first.strip == ''
257
- tmp.each { |t|
258
- keywords.push ','
259
- keywords += t.strip.split_words unless t.strip == ''
260
- }
261
- keywords.join(' ').split_words
262
- else
263
- text.split_words
128
+ def denest(objects, token)
129
+ parts = token.split(NEST_REGEXP)
130
+ current = parts.pop
131
+ last_result = objects.select{ |e| e.match?(current) }
132
+ last_result = objects.select{ |e| e.match?(current, fuzzy: true) } if last_result.empty?
133
+ result = last_result
134
+ while parts.length > 0
135
+ current = "#{parts.last} #{current}"
136
+ result = last_result.select{ |e| e.match?(current) }
137
+ result = last_result.select{ |e| e.match?(current, fuzzy: true) } if result.empty?
138
+ break if result.empty?
139
+ parts.pop
140
+ last_result = result
141
+ end
142
+ return [] if last_result.empty? or last_result.length > 1
143
+ return last_result if parts.empty?
144
+ denest(last_result[0].children, parts.join(' '))
264
145
  end
265
146
  end
266
147
  end