gamefic 2.0.0 → 2.1.1

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.
@@ -2,18 +2,26 @@ require 'json'
2
2
 
3
3
  module Gamefic
4
4
  module Plot::Snapshot
5
+ # Save the current game state as a data hash.
6
+ # See Gamefic::Plot::Darkroom for more information about
7
+ # the data format.
8
+ #
5
9
  # @return [Hash]
6
10
  def save
7
11
  Gamefic::Plot::Darkroom.new(self).save
8
12
  end
9
13
 
14
+ # Restore the game state from a snapshot.
15
+ #
16
+ # If `snapshot` is a string, parse it as a JSON object.
17
+ #
18
+ # @note The string conversion is performed as a convenience for web apps.
19
+ #
20
+ # @param snapshot [Hash, String]
21
+ # @return [void]
10
22
  def restore snapshot
11
- snapshot = JSON.parse(snapshot, symbolize_names: false) if snapshot.is_a?(String)
12
- # HACK: Force conclusion of current subplots
13
- subplots.each { |s| s.conclude }
14
- subplots.clear
23
+ snapshot = JSON.parse(snapshot) if snapshot.is_a?(String)
15
24
  Gamefic::Plot::Darkroom.new(self).restore(snapshot)
16
- entities.each { |e| e.flush }
17
25
  end
18
26
  end
19
27
  end
data/lib/gamefic/query.rb CHANGED
@@ -5,6 +5,7 @@ module Gamefic
5
5
  autoload :Descendants, 'gamefic/query/descendants'
6
6
  autoload :External, 'gamefic/query/external'
7
7
  autoload :Family, 'gamefic/query/family'
8
+ autoload :Tree, 'gamefic/query/tree'
8
9
  autoload :Itself, 'gamefic/query/itself'
9
10
  autoload :Matches, 'gamefic/query/matches'
10
11
  autoload :Parent, 'gamefic/query/parent'
@@ -55,13 +55,18 @@ module Gamefic
55
55
  result.include?(object)
56
56
  end
57
57
 
58
+ # A ranking of how precise the query's arguments are.
59
+ #
60
+ # Query precision is a factor in calculating Action#rank.
61
+ #
62
+ # @return [Integer]
58
63
  def precision
59
64
  if @precision.nil?
60
65
  @precision = 1
61
66
  arguments.each { |a|
62
- if a.kind_of?(Class)
67
+ if a.is_a?(Class)
63
68
  @precision += 100
64
- elsif a.kind_of?(Gamefic::Entity)
69
+ elsif a.is_a?(Gamefic::Entity)
65
70
  @precision += 1000
66
71
  end
67
72
  }
@@ -69,10 +74,7 @@ module Gamefic
69
74
  end
70
75
  @precision
71
76
  end
72
-
73
- def rank
74
- precision
75
- end
77
+ alias rank precision
76
78
 
77
79
  def signature
78
80
  "#{self.class.to_s.split('::').last.downcase}(#{simplify_arguments.join(', ')})"
@@ -83,18 +85,18 @@ module Gamefic
83
85
  # @return [Boolean]
84
86
  def accept?(entity)
85
87
  result = true
86
- arguments.each { |a|
87
- if a.kind_of?(Symbol)
88
- result = (entity.send(a) != false)
89
- elsif a.kind_of?(Regexp)
90
- result = (!entity.to_s.match(a).nil?)
91
- elsif a.is_a?(Module) or a.is_a?(Class)
92
- result = (entity.is_a?(a))
88
+ arguments.each do |a|
89
+ result = if a.is_a?(Symbol)
90
+ (entity.send(a) != false)
91
+ elsif a.is_a?(Regexp)
92
+ !entity.to_s.match(a).nil?
93
+ elsif a.is_a?(Module) || a.is_a?(Class)
94
+ entity.is_a?(a)
93
95
  else
94
- result = (entity == a)
96
+ (entity == a)
95
97
  end
96
98
  break if result == false
97
- }
99
+ end
98
100
  result
99
101
  end
100
102
 
@@ -109,10 +111,10 @@ module Gamefic
109
111
  return [] if entity.nil?
110
112
  result = []
111
113
  if entity.accessible?
112
- entity.children.each { |c|
114
+ entity.children.each do |c|
113
115
  result.push c
114
116
  result.concat subquery_accessible(c)
115
- }
117
+ end
116
118
  end
117
119
  result
118
120
  end
@@ -121,7 +123,7 @@ module Gamefic
121
123
 
122
124
  def simplify_arguments
123
125
  arguments.map do |a|
124
- if a.kind_of?(Class) or a.kind_of?(Object)
126
+ if a.is_a?(Class) || a.is_a?(Object)
125
127
  a.to_s.split('::').last.downcase
126
128
  else
127
129
  a.to_s.downcase
@@ -136,13 +138,12 @@ module Gamefic
136
138
  def denest(objects, token)
137
139
  parts = token.split(NEST_REGEXP)
138
140
  current = parts.pop
139
- last_result = objects.select{ |e| e.specified?(current) }
140
- last_result = objects.select{ |e| e.specified?(current, fuzzy: true) } if last_result.empty?
141
- result = last_result
142
- while parts.length > 0
141
+ last_result = objects.select { |e| e.specified?(current) }
142
+ last_result = objects.select { |e| e.specified?(current, fuzzy: true) } if last_result.empty?
143
+ until parts.empty?
143
144
  current = "#{parts.last} #{current}"
144
- result = last_result.select{ |e| e.specified?(current) }
145
- result = last_result.select{ |e| e.specified?(current, fuzzy: true) } if result.empty?
145
+ result = last_result.select { |e| e.specified?(current) }
146
+ result = last_result.select { |e| e.specified?(current, fuzzy: true) } if result.empty?
146
147
  break if result.empty?
147
148
  parts.pop
148
149
  last_result = result
@@ -5,9 +5,9 @@ module Gamefic
5
5
  result = []
6
6
  children = super
7
7
  result.concat children
8
- children.each { |c|
8
+ children.each do |c|
9
9
  result.concat subquery_accessible(c)
10
- }
10
+ end
11
11
  result
12
12
  end
13
13
  end
@@ -1,5 +1,7 @@
1
1
  module Gamefic
2
2
  module Query
3
+ # Query to retrieve the subject's siblings and all accessible descendants.
4
+ #
3
5
  class Family < Base
4
6
  def context_from(subject)
5
7
  result = []
@@ -2,14 +2,15 @@ module Gamefic
2
2
  module Query
3
3
  class Text < Base
4
4
  def initialize *arguments
5
- arguments.each { |a|
6
- if (a.kind_of?(Symbol) or a.kind_of?(String)) and !a.to_s.end_with?('?')
5
+ arguments.each do |a|
6
+ if (a.kind_of?(Symbol) || a.kind_of?(String)) && !a.to_s.end_with?('?')
7
7
  raise ArgumentError.new("Text query arguments can only be boolean method names (:method?) or regular expressions")
8
8
  end
9
- }
9
+ end
10
10
  super
11
11
  end
12
- def resolve(subject, token, continued: false)
12
+
13
+ def resolve _subject, token, continued: false
13
14
  return Matches.new([], '', token) unless accept?(token)
14
15
  parts = token.split(Keywords::SPLIT_REGEXP)
15
16
  cursor = []
@@ -22,20 +23,18 @@ module Gamefic
22
23
  }
23
24
  if continued
24
25
  Matches.new([matches.join(' ')], matches.join(' '), parts[i..-1].join(' '))
26
+ elsif matches.length == parts.length
27
+ Matches.new([matches.join(' ')], matches.join(' '), '')
25
28
  else
26
- if matches.length == parts.length
27
- Matches.new([matches.join(' ')], matches.join(' '), '')
28
- else
29
- Matches.new([], '', parts.join(' '))
30
- end
29
+ Matches.new([], '', parts.join(' '))
31
30
  end
32
31
  end
33
32
 
34
- def include?(subject, token)
33
+ def include? _subject, token
35
34
  accept?(token)
36
35
  end
37
36
 
38
- def accept?(entity)
37
+ def accept? entity
39
38
  return false unless entity.kind_of?(String) and !entity.empty?
40
39
  super
41
40
  end
@@ -0,0 +1,17 @@
1
+ module Gamefic
2
+ module Query
3
+ # Query to retrieve all of the subject's ancestors, siblings, and descendants.
4
+ #
5
+ class Tree < Family
6
+ def context_from(subject)
7
+ result = super
8
+ parent = subject.parent
9
+ until parent.nil?
10
+ result.unshift parent
11
+ parent = parent.parent
12
+ end
13
+ result
14
+ end
15
+ end
16
+ end
17
+ end
data/lib/gamefic/scene.rb CHANGED
@@ -1,7 +1,6 @@
1
1
  module Gamefic
2
2
  module Scene
3
3
  autoload :Base, 'gamefic/scene/base'
4
- autoload :Custom, 'gamefic/scene/custom'
5
4
  autoload :Activity, 'gamefic/scene/activity'
6
5
  autoload :Pause, 'gamefic/scene/pause'
7
6
  autoload :Conclusion, 'gamefic/scene/conclusion'
@@ -1,8 +1,13 @@
1
1
  module Gamefic
2
- # The Base Scene is not intended for instantiation. Other Scene classes
3
- # should inherit from it.
2
+ # An abstract class for building different types of scenes. It can be
3
+ # extended either through concrete subclasses or by creating anonymous
4
+ # subclasses through a scene helper method like
5
+ # `Gamefic::World::Scenes#custom`.
4
6
  #
5
7
  class Scene::Base
8
+ include Gamefic::Serialize
9
+ extend Gamefic::Serialize
10
+
6
11
  # The scene's primary actor.
7
12
  #
8
13
  # @return [Gamefic::Actor]
@@ -1,7 +1,7 @@
1
1
  module Gamefic
2
2
  # A Conclusion ends the Plot (or the character's participation in it).
3
3
  #
4
- class Scene::Conclusion < Scene::Custom
4
+ class Scene::Conclusion < Scene::Base
5
5
  def type
6
6
  @type ||= 'Conclusion'
7
7
  end
@@ -6,7 +6,7 @@ module Gamefic
6
6
  # The finish block's input parameter receives a MultipleChoice::Input object
7
7
  # instead of a String.
8
8
  #
9
- class Scene::MultipleChoice < Scene::Custom
9
+ class Scene::MultipleChoice < Scene::Base
10
10
  # The zero-based index of the selected option.
11
11
  #
12
12
  # @return [Integer]
@@ -14,7 +14,7 @@ module Gamefic
14
14
 
15
15
  # The one-based index of the selected option.
16
16
  #
17
- # @return [Number]
17
+ # @return [Integer]
18
18
  attr_reader :number
19
19
 
20
20
  # The full text of the selected option.
@@ -33,7 +33,6 @@ module Gamefic
33
33
  get_choice
34
34
  if selection.nil?
35
35
  actor.tell invalid_message
36
- tell_options
37
36
  else
38
37
  super
39
38
  end
@@ -77,14 +76,5 @@ module Gamefic
77
76
  }
78
77
  end
79
78
  end
80
-
81
- def tell_options
82
- list = '<ol class="multiple_choice">'
83
- options.each { |o|
84
- list += "<li><a href=\"#\" rel=\"gamefic\" data-command=\"#{o}\">#{o}</a></li>"
85
- }
86
- list += "</ol>"
87
- actor.tell list
88
- end
89
79
  end
90
80
  end
@@ -1,7 +1,7 @@
1
1
  module Gamefic
2
2
  # Pause for user input.
3
3
  #
4
- class Scene::Pause < Scene::Custom
4
+ class Scene::Pause < Scene::Base
5
5
  def post_initialize
6
6
  self.type = 'Pause'
7
7
  self.prompt = 'Press enter to continue...'
@@ -4,7 +4,7 @@ module Gamefic
4
4
  # block. After the scene is finished, the :active scene will be cued if no
5
5
  # other scene has been prepared or cued.
6
6
  #
7
- class Scene::YesOrNo < Scene::Custom
7
+ class Scene::YesOrNo < Scene::Base
8
8
  attr_writer :invalid_message
9
9
 
10
10
  def post_initialize
@@ -80,6 +80,7 @@ Gamefic::Scriptable.module_exec do
80
80
  instance.public_send :public_send, symbol, *args, &block
81
81
  end
82
82
  end
83
+ theater.extend Gamefic::Serialize
83
84
  theater
84
85
  end
85
86
  end
@@ -1,15 +1,68 @@
1
+ require 'set'
2
+
1
3
  module Gamefic
2
4
  module Serialize
3
- def to_serial
4
- {
5
- 'class' => self.class.to_s
6
- }.merge serialize_instance_variables
5
+ def to_serial(index = [])
6
+ if index.include?(self)
7
+ {
8
+ 'instance' => "#<ELE_#{index.index(self)}>",
9
+ 'ivars' => {}
10
+ }
11
+ else
12
+ if self.class == Class && self.name
13
+ {
14
+ 'class' => 'Class',
15
+ 'name' => name
16
+ }
17
+ else
18
+ index.push self if self.is_a?(Gamefic::Serialize)
19
+ {
20
+ 'class' => serialized_class(index),
21
+ 'ivars' => serialize_instance_variables(index)
22
+ }
23
+ end
24
+ end
25
+ end
26
+
27
+ def serialized_class index
28
+ if index.include?(self.class)
29
+ "#<ELE_#{index.index(self.class)}>"
30
+ else
31
+ self.class.to_s
32
+ end
33
+ end
34
+
35
+ def self.instances
36
+ GC.start
37
+ result = []
38
+ ObjectSpace.each_object(Gamefic::Serialize) { |obj| result.push obj }
39
+ result
40
+ end
41
+
42
+ # @param string [String]
43
+ # @return [Object]
44
+ def self.string_to_constant string
45
+ space = Object
46
+ string.split('::').each do |part|
47
+ space = space.const_get(part)
48
+ end
49
+ space
7
50
  end
8
51
  end
9
52
  end
10
53
 
11
54
  class Object
12
- def to_serial
55
+ class << self
56
+ def exclude_from_serial ary
57
+ @excluded_from_serial = ary
58
+ end
59
+
60
+ def excluded_from_serial
61
+ @excluded_from_serial ||= []
62
+ end
63
+ end
64
+
65
+ def to_serial(_index)
13
66
  return self if [true, false, nil].include?(self)
14
67
  # @todo This warning is a little too spammy. Set up a logger so it can be
15
68
  # limited to an info or debug level.
@@ -17,52 +70,154 @@ class Object
17
70
  "#<UNKNOWN>"
18
71
  end
19
72
 
20
- def serialize_instance_variables
73
+ def from_serial(index = [])
74
+ if self.is_a?(Hash) && (self['class'] || self['instance'])
75
+ if self['instance']
76
+ elematch = self['instance'].match(/^#<ELE_([\d]+)>$/)
77
+ object = index[elematch[1].to_i]
78
+ raise "Unable to load indexed element ##{elematch[1]} #{self}" if object.nil?
79
+ elsif self['class']
80
+ if self['class'] == 'Hash'
81
+ object = {}
82
+ self['data'].each do |arr|
83
+ object[arr[0].from_serial(index)] = arr[1].from_serial(index)
84
+ end
85
+ return object
86
+ elsif self['class'] == 'Class'
87
+ return Gamefic::Serialize.string_to_constant(self['name'])
88
+ elsif self['class'] == 'Set'
89
+ return Set.new(self['data'].map { |el| el.from_serial(index) })
90
+ else
91
+ elematch = self['class'].match(/^#<ELE_([\d]+)>$/)
92
+ if elematch
93
+ klass = index[elematch[1].to_i]
94
+ else
95
+ klass = Gamefic::Serialize.string_to_constant(self['class'])
96
+ end
97
+ raise "Unable to find class #{self['class']} #{self}" if klass.nil?
98
+ object = klass.allocate
99
+ index.push object if object.is_a?(Gamefic::Serialize)
100
+ end
101
+ end
102
+ self['ivars'].each_pair do |k, v|
103
+ object.instance_variable_set(k, v.from_serial(index))
104
+ end
105
+ object
106
+ elsif self.is_a?(Numeric)
107
+ self
108
+ elsif self.is_a?(String)
109
+ match = self.match(/#<ELE_([0-9]+)>/)
110
+ return index.index(match[1].to_i) if match
111
+ match = self.match(/#<SYM:([a-z0-9_\?\!]+)>/i)
112
+ return match[1].to_sym if match
113
+ # return nil if self == '#<UNKNOWN>'
114
+ self
115
+ elsif self.is_a?(Hash)
116
+ result = {}
117
+ unknown = false
118
+ self.each_pair do |k, v|
119
+ k2 = k.from_serial(index)
120
+ v2 = v.from_serial(index)
121
+ if k2 == "#<UNKNOWN>" || v2 == "#<UNKNOWN>"
122
+ unknown = true
123
+ break
124
+ end
125
+ result[k2] = v2
126
+ end
127
+ result = "#<UNKNOWN>" if unknown
128
+ result
129
+ elsif self && self != true
130
+ STDERR.puts "Unable to unserialize #{self.class}"
131
+ nil
132
+ else
133
+ # true, false, or nil
134
+ self
135
+ end
136
+ end
137
+
138
+ def serialize_instance_variables(index)
21
139
  result = {}
22
140
  instance_variables.each do |k|
23
- result[k.to_s] = instance_variable_get(k).to_serial
141
+ next if self.class.excluded_from_serial.include?(k)
142
+ val = instance_variable_get(k)
143
+ if index.include?(val)
144
+ result[k.to_s] = {
145
+ 'instance' => "#<ELE_#{index.index(val)}>",
146
+ 'ivars' => {}
147
+ }
148
+ else
149
+ result[k.to_s] = val.to_serial(index)
150
+ end
24
151
  end
25
152
  result
26
153
  end
27
154
  end
28
155
 
156
+ class Class
157
+ def to_serial(index = [])
158
+ if name.nil?
159
+ super
160
+ else
161
+ {
162
+ 'class' => 'Class',
163
+ 'name' => name
164
+ }
165
+ end
166
+ end
167
+ end
168
+
29
169
  class Symbol
30
- def to_serial
170
+ def to_serial(_index = [])
31
171
  "#<SYM:#{self}>"
32
172
  end
33
173
  end
34
174
 
35
175
  class String
36
- def to_serial
176
+ def to_serial(_index = [])
37
177
  self
38
178
  end
39
179
  end
40
180
 
41
181
  class Numeric
42
- def to_serial
182
+ def to_serial(_index = [])
43
183
  self
44
184
  end
45
185
  end
46
186
 
47
187
  class Array
48
- def to_serial
188
+ def to_serial(index = [])
49
189
  map do |e|
50
- s = e.to_serial
190
+ s = e.to_serial(index)
51
191
  return "#<UNKNOWN>" if s == "#<UNKNOWN>"
52
192
  s
53
193
  end
54
194
  end
195
+
196
+ def from_serial(index = [])
197
+ result = map { |e| e.from_serial(index) }
198
+ result = "#<UNKNOWN>" if result.any? { |e| e == "#<UNKNOWN>" }
199
+ result
200
+ end
55
201
  end
56
202
 
57
203
  class Hash
58
- def to_serial
59
- result = {}
204
+ def to_serial(index = [])
205
+ result = {'class' => 'Hash', 'data' => []}
60
206
  each_pair do |key, value|
61
- k2 = key.to_serial
62
- v2 = value.to_serial
207
+ k2 = key.to_serial(index)
208
+ v2 = value.to_serial(index)
63
209
  return "#<UNKNOWN>" if k2 == "#<UNKNOWN>" || v2 == "#<UNKNOWN>"
64
- result[k2] = v2
210
+ result['data'].push [k2, v2]
65
211
  end
66
212
  result
67
213
  end
68
214
  end
215
+
216
+ class Set
217
+ def to_serial(index = [])
218
+ {
219
+ 'class' => 'Set',
220
+ 'data' => to_a.map { |el| el.to_serial(index) }
221
+ }
222
+ end
223
+ end