gamefic 1.5.1 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
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