fable 0.5.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.
- checksums.yaml +7 -0
- data/.circleci/config.yml +30 -0
- data/.gitignore +57 -0
- data/.ruby-version +1 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +30 -0
- data/LICENSE +21 -0
- data/README.md +2 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/bin/test +8 -0
- data/fable.gemspec +34 -0
- data/fable.sublime-project +8 -0
- data/lib/fable.rb +49 -0
- data/lib/fable/call_stack.rb +351 -0
- data/lib/fable/choice.rb +31 -0
- data/lib/fable/choice_point.rb +65 -0
- data/lib/fable/container.rb +218 -0
- data/lib/fable/control_command.rb +156 -0
- data/lib/fable/debug_metadata.rb +13 -0
- data/lib/fable/divert.rb +100 -0
- data/lib/fable/glue.rb +7 -0
- data/lib/fable/ink_list.rb +425 -0
- data/lib/fable/list_definition.rb +44 -0
- data/lib/fable/list_definitions_origin.rb +35 -0
- data/lib/fable/native_function_call.rb +324 -0
- data/lib/fable/native_function_operations.rb +149 -0
- data/lib/fable/observer.rb +205 -0
- data/lib/fable/path.rb +186 -0
- data/lib/fable/pointer.rb +42 -0
- data/lib/fable/profiler.rb +287 -0
- data/lib/fable/push_pop_type.rb +11 -0
- data/lib/fable/runtime_object.rb +159 -0
- data/lib/fable/search_result.rb +20 -0
- data/lib/fable/serializer.rb +560 -0
- data/lib/fable/state_patch.rb +47 -0
- data/lib/fable/story.rb +1447 -0
- data/lib/fable/story_state.rb +915 -0
- data/lib/fable/tag.rb +14 -0
- data/lib/fable/value.rb +334 -0
- data/lib/fable/variable_assignment.rb +20 -0
- data/lib/fable/variable_reference.rb +38 -0
- data/lib/fable/variables_state.rb +327 -0
- data/lib/fable/version.rb +3 -0
- data/lib/fable/void.rb +4 -0
- data/zork_mode.rb +23 -0
- metadata +149 -0
data/lib/fable/choice.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
module Fable
|
2
|
+
class Choice < RuntimeObject
|
3
|
+
# The main text presented to the player for this choice
|
4
|
+
attr_accessor :text
|
5
|
+
|
6
|
+
|
7
|
+
# Get the path to the original choice point - where was this choice
|
8
|
+
# defined in the story?
|
9
|
+
attr_accessor :source_path
|
10
|
+
|
11
|
+
# The original index into current_choices list on the Story when
|
12
|
+
# this choice was generated, for convenience
|
13
|
+
attr_accessor :index
|
14
|
+
|
15
|
+
attr_accessor :target_path, :thread_at_generation, :original_thread_index,
|
16
|
+
:invisible_default
|
17
|
+
|
18
|
+
alias_method :invisible_default?, :invisible_default
|
19
|
+
|
20
|
+
# The target path that the story should be diverted to
|
21
|
+
# if the choice is chosen
|
22
|
+
def path_string_on_choice
|
23
|
+
return self.target_path.to_s
|
24
|
+
end
|
25
|
+
|
26
|
+
def path_string_on_choice=(value)
|
27
|
+
self.target_path = Path.new(value)
|
28
|
+
value
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Fable
|
2
|
+
class ChoicePoint < RuntimeObject
|
3
|
+
attr_accessor :has_condition,
|
4
|
+
:has_start_content, :has_choice_only_content,
|
5
|
+
:invisible_default,
|
6
|
+
:once_only, :path_on_choice
|
7
|
+
|
8
|
+
alias_method :invisible_default?, :invisible_default
|
9
|
+
alias_method :has_start_content?, :has_start_content
|
10
|
+
alias_method :has_condition?, :has_condition
|
11
|
+
alias_method :has_choice_only_content?, :has_choice_only_content
|
12
|
+
alias_method :once_only?, :once_only
|
13
|
+
|
14
|
+
# The ChoicePoint represents the point within the Story
|
15
|
+
# where a Choice instance gets generated. The distinction
|
16
|
+
# is made because the text of the choice can be dynamically
|
17
|
+
# generated
|
18
|
+
def path_on_choice
|
19
|
+
# Resolve any relative pahts to global ones as we come across them
|
20
|
+
if !@path_on_choice.nil? && @path_on_choice.relative?
|
21
|
+
choice_target_object = choice_target
|
22
|
+
if !choice_target_object.nil?
|
23
|
+
@path_on_choice = choice_target_object.path
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
return @path_on_choice
|
28
|
+
end
|
29
|
+
|
30
|
+
def path_on_choice=(value)
|
31
|
+
@path_on_choice = value
|
32
|
+
end
|
33
|
+
|
34
|
+
def choice_target
|
35
|
+
self.resolve_path(@path_on_choice).container
|
36
|
+
end
|
37
|
+
|
38
|
+
def path_string_on_choice
|
39
|
+
compact_path_string(path_on_choice)
|
40
|
+
end
|
41
|
+
|
42
|
+
def path_string_on_choice=(value)
|
43
|
+
self.path_on_choice = Path.new(value)
|
44
|
+
end
|
45
|
+
|
46
|
+
def flags=(flag)
|
47
|
+
self.has_condition = (flag & 1) > 0
|
48
|
+
self.has_start_content = (flag & 2) > 0
|
49
|
+
self.has_choice_only_content = (flag & 4) > 0
|
50
|
+
self.invisible_default = (flag & 8) > 0
|
51
|
+
self.once_only = (flag & 16) > 0
|
52
|
+
end
|
53
|
+
|
54
|
+
def to_s
|
55
|
+
target_line_number = debug_line_number_of_path(path_on_choice)
|
56
|
+
target_string = path_on_choice.to_s
|
57
|
+
|
58
|
+
if !target_line_number.nil?
|
59
|
+
target_string = " line #{target_line_number} (#{target_string})"
|
60
|
+
end
|
61
|
+
|
62
|
+
return "Choice: -> #{target_string}"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,218 @@
|
|
1
|
+
module Fable
|
2
|
+
class Container < RuntimeObject
|
3
|
+
attr_accessor :bit_flags, :name, :content, :named_content,
|
4
|
+
:visits_should_be_counted, :turn_index_should_be_counted,
|
5
|
+
:counting_at_start_only, :path_to_first_leaf_content
|
6
|
+
|
7
|
+
def initialize(flags)
|
8
|
+
super()
|
9
|
+
self.bit_flags = flags
|
10
|
+
self.content = []
|
11
|
+
self.named_content = {}
|
12
|
+
self.process_bit_flags
|
13
|
+
end
|
14
|
+
|
15
|
+
def add_content(content_to_add)
|
16
|
+
if content_to_add.is_a?(Enumerable)
|
17
|
+
content_to_add.each{|individual_items| add_content(individual_items) }
|
18
|
+
return
|
19
|
+
end
|
20
|
+
|
21
|
+
debugger if content_to_add.nil?
|
22
|
+
|
23
|
+
if !content_to_add.parent.nil?
|
24
|
+
raise Error, "content is already in #{content_to_add.parent}"
|
25
|
+
end
|
26
|
+
|
27
|
+
content_to_add.parent = self
|
28
|
+
content << content_to_add
|
29
|
+
|
30
|
+
try_adding_to_named_content(content_to_add)
|
31
|
+
end
|
32
|
+
|
33
|
+
def insert_content_at(content_to_add, index)
|
34
|
+
if !content_to_add.parent.nil?
|
35
|
+
raise Error, "content is already in #{content_to_add.parent}"
|
36
|
+
end
|
37
|
+
|
38
|
+
content_to_add.parent = self
|
39
|
+
content.insert(index, content_to_add)
|
40
|
+
try_adding_to_named_content(content_to_add)
|
41
|
+
end
|
42
|
+
|
43
|
+
def try_adding_to_named_content(content_to_add)
|
44
|
+
if content_to_add.respond_to?(:valid_name?) && content_to_add.valid_name?
|
45
|
+
add_to_named_content(content_to_add)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def add_to_named_content(content_to_add)
|
50
|
+
content_to_add.parent = self
|
51
|
+
named_content[content_to_add.name] = content_to_add
|
52
|
+
end
|
53
|
+
|
54
|
+
def add_contents_of_container(other_container)
|
55
|
+
content += other_container.content
|
56
|
+
other_container.content.each do |content_to_add|
|
57
|
+
content_to_add.parent = self
|
58
|
+
try_adding_to_named_content(content_to_add)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def content_with_path_component(component)
|
63
|
+
if component.is_index?
|
64
|
+
return content[component.index]
|
65
|
+
elsif component.is_parent?
|
66
|
+
return self.parent
|
67
|
+
else
|
68
|
+
return named_content[component.name]
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def content_at_path(path, options= { partial_path_start: 0, partial_path_length: -1 })
|
73
|
+
partial_path_length = options[:partial_path_length]
|
74
|
+
partial_path_start = options[:partial_path_start]
|
75
|
+
|
76
|
+
if partial_path_length == -1
|
77
|
+
partial_path_length = path.length
|
78
|
+
end
|
79
|
+
|
80
|
+
result = SearchResult.new
|
81
|
+
result.approximate = false
|
82
|
+
|
83
|
+
current_container = self
|
84
|
+
current_object = self
|
85
|
+
|
86
|
+
partial_path_end = (partial_path_length - 1)
|
87
|
+
|
88
|
+
(partial_path_start..partial_path_end).each do |i|
|
89
|
+
component = path.components[i]
|
90
|
+
|
91
|
+
# Path component was wrong type
|
92
|
+
if current_container.nil?
|
93
|
+
result.approximate = true
|
94
|
+
break
|
95
|
+
end
|
96
|
+
|
97
|
+
found_object = current_container.content_with_path_component(component)
|
98
|
+
|
99
|
+
# Couldn't resolve entire path?
|
100
|
+
if found_object.nil?
|
101
|
+
result.approximate = true
|
102
|
+
break
|
103
|
+
end
|
104
|
+
|
105
|
+
current_object = found_object
|
106
|
+
if found_object.is_a?(Container)
|
107
|
+
current_container = found_object
|
108
|
+
else
|
109
|
+
current_container = nil
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
result.object = current_object
|
114
|
+
return result
|
115
|
+
end
|
116
|
+
|
117
|
+
def build_string_of_hierarchy(io, indentation, pointed_object)
|
118
|
+
io << indentation_string(indentation)
|
119
|
+
io << "["
|
120
|
+
|
121
|
+
if self.valid_name?
|
122
|
+
io << " (#{self.name})"
|
123
|
+
end
|
124
|
+
|
125
|
+
if self == pointed_object
|
126
|
+
io << " <---"
|
127
|
+
end
|
128
|
+
|
129
|
+
io << "\n"
|
130
|
+
|
131
|
+
indentation += 1
|
132
|
+
|
133
|
+
|
134
|
+
content.each_with_index do |object, index|
|
135
|
+
if object.is_a?(Container)
|
136
|
+
object.build_string_of_hierarchy(io, indentation, pointed_object)
|
137
|
+
else
|
138
|
+
io << indentation_string(indentation)
|
139
|
+
if object.is_a?(StringValue)
|
140
|
+
io << "\"#{object.to_s.gsub("\n", "\\n")}\""
|
141
|
+
else
|
142
|
+
io << object.to_s
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
if index != (content.size - 1)
|
147
|
+
io << ","
|
148
|
+
end
|
149
|
+
|
150
|
+
if !object.is_a?(Container) && object == pointed_object
|
151
|
+
io << " <---"
|
152
|
+
end
|
153
|
+
|
154
|
+
io << "\n"
|
155
|
+
end
|
156
|
+
|
157
|
+
only_named_content = named_content.reject{|name, item| content.include?(item) }
|
158
|
+
|
159
|
+
if only_named_content.any?
|
160
|
+
io << indentation_string(indentation)
|
161
|
+
io << "-- named: --\n"
|
162
|
+
|
163
|
+
only_named_content.each do |key, container|
|
164
|
+
container.build_string_of_hierarchy(io, indentation, pointed_object)
|
165
|
+
io << "\n"
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
indentation -= 1
|
170
|
+
|
171
|
+
io << indentation_string(indentation)
|
172
|
+
io << "]"
|
173
|
+
end
|
174
|
+
|
175
|
+
def path_to_first_leaf_content
|
176
|
+
@path_to_first_leaf_content ||= path.path_by_appending_path(internal_path_to_first_lead_content)
|
177
|
+
end
|
178
|
+
|
179
|
+
def valid_name?
|
180
|
+
!name.to_s.empty?
|
181
|
+
end
|
182
|
+
|
183
|
+
alias_method :visits_should_be_counted?, :visits_should_be_counted
|
184
|
+
alias_method :turn_index_should_be_counted?, :turn_index_should_be_counted
|
185
|
+
alias_method :counting_at_start_only?, :counting_at_start_only
|
186
|
+
|
187
|
+
def process_bit_flags
|
188
|
+
if has_bit_flags?
|
189
|
+
self.visits_should_be_counted = (bit_flags & 1) > 0
|
190
|
+
self.turn_index_should_be_counted = (bit_flags & 2) > 0
|
191
|
+
self.counting_at_start_only = (bit_flags & 4) > 0
|
192
|
+
else
|
193
|
+
self.visits_should_be_counted = false
|
194
|
+
self.turn_index_should_be_counted = false
|
195
|
+
self.counting_at_start_only = false
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def has_bit_flags?
|
200
|
+
!self.bit_flags.nil?
|
201
|
+
end
|
202
|
+
|
203
|
+
protected
|
204
|
+
|
205
|
+
def internal_path_to_first_lead_content
|
206
|
+
components = []
|
207
|
+
container = self
|
208
|
+
while !container.nil?
|
209
|
+
if container.content.size > 0
|
210
|
+
components << Path::Component.new(0)
|
211
|
+
container = container.content.first
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
return Path.new(components)
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
module Fable
|
2
|
+
class ControlCommand < RuntimeObject
|
3
|
+
COMMANDS = {
|
4
|
+
NOT_SET: -1,
|
5
|
+
EVALUATION_START: "ev",
|
6
|
+
EVALUATION_OUTPUT: "out",
|
7
|
+
EVALUATION_END: "/ev",
|
8
|
+
DUPLICATE_TOPMOST: "du",
|
9
|
+
POP_EVALUATED_VALUE: "pop",
|
10
|
+
POP_FUNCTION: "~ret",
|
11
|
+
POP_TUNNEL: "->->",
|
12
|
+
BEGIN_STRING_EVALUATION_MODE: "str",
|
13
|
+
END_STRING_EVALUATION_MODE: "/str",
|
14
|
+
NOOP: "nop",
|
15
|
+
PUSH_CHOICE_COUNT: "choiceCnt",
|
16
|
+
TURNS: "turn",
|
17
|
+
TURNS_SINCE: "turns",
|
18
|
+
READ_COUNT: "readc",
|
19
|
+
RANDOM: "rnd",
|
20
|
+
SEED_RANDOM: "srnd",
|
21
|
+
VISIT_INDEX: "visit",
|
22
|
+
SEQUENCE_SHUFFLE_INDEX: "seq",
|
23
|
+
START_THREAD: "thread",
|
24
|
+
DONE: "done",
|
25
|
+
STORY_END: "end",
|
26
|
+
LIST_FROM_INT: "listInt",
|
27
|
+
LIST_RANGE: "range",
|
28
|
+
LIST_RANDOM: "lrnd",
|
29
|
+
|
30
|
+
GLUE: "<>",
|
31
|
+
}.freeze
|
32
|
+
|
33
|
+
LOOKUP = COMMANDS.invert.freeze
|
34
|
+
|
35
|
+
attr_accessor :command_type
|
36
|
+
|
37
|
+
def self.is_control_command?(value)
|
38
|
+
LOOKUP.has_key?(value)
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.get_control_command(value)
|
42
|
+
raise ArgumentError if !is_control_command?(value)
|
43
|
+
self.new(value)
|
44
|
+
end
|
45
|
+
|
46
|
+
def initialize(command_symbol)
|
47
|
+
super()
|
48
|
+
self.command_type = LOOKUP[command_symbol]
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.is_instance_of?(object, command_type)
|
52
|
+
object.is_a?(self) && object.command_type == command_type
|
53
|
+
end
|
54
|
+
|
55
|
+
def to_s
|
56
|
+
command_type.to_s
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.evaluation_start
|
60
|
+
self.new(COMMANDS[:EVALUATION_START])
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.evaluation_output
|
64
|
+
self.new(COMMANDS[:EVALUATION_OUTPUT])
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.evaluation_end
|
68
|
+
self.new(COMMANDS[:EVALUATION_END])
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.duplicate_topmost
|
72
|
+
self.new(COMMANDS[:DUPLICATE_TOPMOST])
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.pop_evaluated_value
|
76
|
+
self.new(COMMANDS[:POP_EVALUATED_VALUE])
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.pop_function
|
80
|
+
self.new(COMMANDS[:POP_FUNCTION])
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.pop_tunnel
|
84
|
+
self.new(COMMANDS[:POP_TUNNEL])
|
85
|
+
end
|
86
|
+
|
87
|
+
def self.begin_string_evaluation_mode
|
88
|
+
self.new(COMMANDS[:BEGIN_STRING_EVALUATION_MODE])
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.end_string_evaluation_mode
|
92
|
+
self.new(COMMANDS[:END_STRING_EVALUATION_MODE])
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.noop
|
96
|
+
self.new(COMMANDS[:NOOP])
|
97
|
+
end
|
98
|
+
|
99
|
+
def self.push_choice_count
|
100
|
+
self.new(COMMANDS[:PUSH_CHOICE_COUNT])
|
101
|
+
end
|
102
|
+
|
103
|
+
def self.turns
|
104
|
+
self.new(COMMANDS[:TURNS])
|
105
|
+
end
|
106
|
+
|
107
|
+
def self.turns_since
|
108
|
+
self.new(COMMANDS[:TURNS_SINCE])
|
109
|
+
end
|
110
|
+
|
111
|
+
def self.read_count
|
112
|
+
self.new(COMMANDS[:READ_COUNT])
|
113
|
+
end
|
114
|
+
|
115
|
+
def self.random
|
116
|
+
self.new(COMMANDS[:RANDOM])
|
117
|
+
end
|
118
|
+
|
119
|
+
def self.seed_random
|
120
|
+
self.new(COMMANDS[:SEED_RANDOM])
|
121
|
+
end
|
122
|
+
|
123
|
+
def self.visit_index
|
124
|
+
self.new(COMMANDS[:VISIT_INDEX])
|
125
|
+
end
|
126
|
+
|
127
|
+
def self.sequence_shuffle_index
|
128
|
+
self.new(COMMANDS[:SEQUENCE_SHUFFLE_INDEX])
|
129
|
+
end
|
130
|
+
|
131
|
+
def self.start_thread
|
132
|
+
self.new(COMMANDS[:START_THREAD])
|
133
|
+
end
|
134
|
+
|
135
|
+
def self.done
|
136
|
+
self.new(COMMANDS[:DONE])
|
137
|
+
end
|
138
|
+
|
139
|
+
def self.story_end
|
140
|
+
self.new(COMMANDS[:STORY_END])
|
141
|
+
end
|
142
|
+
|
143
|
+
def self.list_from_int
|
144
|
+
self.new(COMMANDS[:LIST_FROM_INT])
|
145
|
+
end
|
146
|
+
|
147
|
+
def self.list_range
|
148
|
+
self.new(COMMANDS[:LIST_RANGE])
|
149
|
+
end
|
150
|
+
|
151
|
+
def self.list_random
|
152
|
+
self.new(COMMANDS[:LIST_RANDOM])
|
153
|
+
end
|
154
|
+
|
155
|
+
end
|
156
|
+
end
|