gamefic 2.0.0 → 2.1.1

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