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.
- checksums.yaml +4 -4
- data/.rubocop.yml +4 -1
- data/CHANGELOG.md +13 -0
- data/lib/gamefic.rb +1 -2
- data/lib/gamefic/action.rb +34 -38
- data/lib/gamefic/active.rb +18 -12
- data/lib/gamefic/core_ext/array.rb +3 -0
- data/lib/gamefic/element.rb +6 -2
- data/lib/gamefic/plot.rb +10 -12
- data/lib/gamefic/plot/darkroom.rb +49 -61
- data/lib/gamefic/plot/snapshot.rb +13 -5
- data/lib/gamefic/query.rb +1 -0
- data/lib/gamefic/query/base.rb +25 -24
- data/lib/gamefic/query/descendants.rb +2 -2
- data/lib/gamefic/query/family.rb +2 -0
- data/lib/gamefic/query/text.rb +10 -11
- data/lib/gamefic/query/tree.rb +17 -0
- data/lib/gamefic/scene.rb +0 -1
- data/lib/gamefic/scene/base.rb +7 -2
- data/lib/gamefic/scene/conclusion.rb +1 -1
- data/lib/gamefic/scene/multiple_choice.rb +2 -12
- data/lib/gamefic/scene/pause.rb +1 -1
- data/lib/gamefic/scene/yes_or_no.rb +1 -1
- data/lib/gamefic/scriptable.rb +1 -0
- data/lib/gamefic/serialize.rb +172 -17
- data/lib/gamefic/subplot.rb +11 -2
- data/lib/gamefic/syntax.rb +1 -0
- data/lib/gamefic/version.rb +1 -1
- data/lib/gamefic/world.rb +2 -0
- data/lib/gamefic/world/commands.rb +8 -8
- data/lib/gamefic/world/entities.rb +11 -14
- data/lib/gamefic/world/playbook.rb +30 -40
- data/lib/gamefic/world/players.rb +18 -2
- data/lib/gamefic/world/scenes.rb +13 -13
- metadata +18 -18
- data/lib/gamefic/index.rb +0 -121
- data/lib/gamefic/scene/custom.rb +0 -7
@@ -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
|
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'
|
data/lib/gamefic/query/base.rb
CHANGED
@@ -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.
|
67
|
+
if a.is_a?(Class)
|
63
68
|
@precision += 100
|
64
|
-
elsif a.
|
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
|
87
|
-
if a.
|
88
|
-
|
89
|
-
elsif a.
|
90
|
-
|
91
|
-
elsif a.is_a?(Module)
|
92
|
-
|
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
|
-
|
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
|
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.
|
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
|
-
|
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
|
data/lib/gamefic/query/family.rb
CHANGED
data/lib/gamefic/query/text.rb
CHANGED
@@ -2,14 +2,15 @@ module Gamefic
|
|
2
2
|
module Query
|
3
3
|
class Text < Base
|
4
4
|
def initialize *arguments
|
5
|
-
arguments.each
|
6
|
-
if (a.kind_of?(Symbol)
|
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
|
-
|
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
|
-
|
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?
|
33
|
+
def include? _subject, token
|
35
34
|
accept?(token)
|
36
35
|
end
|
37
36
|
|
38
|
-
def accept?
|
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
data/lib/gamefic/scene/base.rb
CHANGED
@@ -1,8 +1,13 @@
|
|
1
1
|
module Gamefic
|
2
|
-
#
|
3
|
-
#
|
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]
|
@@ -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::
|
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 [
|
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
|
data/lib/gamefic/scene/pause.rb
CHANGED
@@ -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::
|
7
|
+
class Scene::YesOrNo < Scene::Base
|
8
8
|
attr_writer :invalid_message
|
9
9
|
|
10
10
|
def post_initialize
|
data/lib/gamefic/scriptable.rb
CHANGED
data/lib/gamefic/serialize.rb
CHANGED
@@ -1,15 +1,68 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
1
3
|
module Gamefic
|
2
4
|
module Serialize
|
3
|
-
def to_serial
|
4
|
-
|
5
|
-
|
6
|
-
|
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
|
-
|
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
|
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
|
-
|
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[
|
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
|