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