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.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +30 -0
  3. data/.gitignore +57 -0
  4. data/.ruby-version +1 -0
  5. data/CODE_OF_CONDUCT.md +74 -0
  6. data/Gemfile +7 -0
  7. data/Gemfile.lock +30 -0
  8. data/LICENSE +21 -0
  9. data/README.md +2 -0
  10. data/Rakefile +10 -0
  11. data/bin/console +14 -0
  12. data/bin/setup +8 -0
  13. data/bin/test +8 -0
  14. data/fable.gemspec +34 -0
  15. data/fable.sublime-project +8 -0
  16. data/lib/fable.rb +49 -0
  17. data/lib/fable/call_stack.rb +351 -0
  18. data/lib/fable/choice.rb +31 -0
  19. data/lib/fable/choice_point.rb +65 -0
  20. data/lib/fable/container.rb +218 -0
  21. data/lib/fable/control_command.rb +156 -0
  22. data/lib/fable/debug_metadata.rb +13 -0
  23. data/lib/fable/divert.rb +100 -0
  24. data/lib/fable/glue.rb +7 -0
  25. data/lib/fable/ink_list.rb +425 -0
  26. data/lib/fable/list_definition.rb +44 -0
  27. data/lib/fable/list_definitions_origin.rb +35 -0
  28. data/lib/fable/native_function_call.rb +324 -0
  29. data/lib/fable/native_function_operations.rb +149 -0
  30. data/lib/fable/observer.rb +205 -0
  31. data/lib/fable/path.rb +186 -0
  32. data/lib/fable/pointer.rb +42 -0
  33. data/lib/fable/profiler.rb +287 -0
  34. data/lib/fable/push_pop_type.rb +11 -0
  35. data/lib/fable/runtime_object.rb +159 -0
  36. data/lib/fable/search_result.rb +20 -0
  37. data/lib/fable/serializer.rb +560 -0
  38. data/lib/fable/state_patch.rb +47 -0
  39. data/lib/fable/story.rb +1447 -0
  40. data/lib/fable/story_state.rb +915 -0
  41. data/lib/fable/tag.rb +14 -0
  42. data/lib/fable/value.rb +334 -0
  43. data/lib/fable/variable_assignment.rb +20 -0
  44. data/lib/fable/variable_reference.rb +38 -0
  45. data/lib/fable/variables_state.rb +327 -0
  46. data/lib/fable/version.rb +3 -0
  47. data/lib/fable/void.rb +4 -0
  48. data/zork_mode.rb +23 -0
  49. metadata +149 -0
@@ -0,0 +1,11 @@
1
+ module Fable
2
+ module PushPopType
3
+ TYPES = {
4
+ tunnel: :tunnel,
5
+ function: :function,
6
+ function_evaluation_from_game: :function_evaluation_from_game
7
+ }
8
+
9
+ TYPE_LOOKUP = TYPES.invert
10
+ end
11
+ end
@@ -0,0 +1,159 @@
1
+ module Fable
2
+ class RuntimeObject
3
+ # RuntimeObjects can be included in the main story as a hierarchy
4
+ # usually parents are container objectsd
5
+ attr_accessor :parent, :own_debug_metadata, :path, :original_object
6
+
7
+ def initialize
8
+ @path = nil
9
+ end
10
+
11
+ def debug_metadata
12
+ if @own_debug_metadata.nil?
13
+ if !parent.nil?
14
+ return parent.debug_metadata || DebugMetadata.new
15
+ end
16
+ end
17
+
18
+ return @own_debug_metadata
19
+ end
20
+
21
+ def indentation_string(indentation = 0)
22
+ " " * indentation
23
+ end
24
+
25
+ def debug_line_number_of_path(path)
26
+ return nil if path.nil?
27
+
28
+ # Try to get a line number from debug metadata
29
+ root = self.root_content_container
30
+
31
+ if !root.nil?
32
+ target_content = root.content_at_path(path).object
33
+ if !target_content.nil?
34
+ target_debug_metadata = target_content.debug_metadata
35
+ if !target_debug_metadata.nil?
36
+ target_debug_metadata.start_line_number
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ def path
43
+ if !defined?(@path) || @path.nil?
44
+ if parent.nil?
45
+ @path = Path.new("")
46
+ else
47
+ # Maintain a stack so that the order of the components is reversed
48
+ # when they're added to the Path. We're iterating up from the
49
+ # leaves/children to the root
50
+ components = []
51
+
52
+ child = self
53
+ container = child.parent
54
+
55
+ while !container.nil?
56
+ if child.is_a?(Container) && child.valid_name?
57
+ components << Path::Component.new(name: child.name)
58
+ else
59
+ components << Path::Component.new(index: container.content.index(child))
60
+ end
61
+
62
+ child = container
63
+ container = container.parent
64
+ end
65
+
66
+ @path = Path.new(components.reverse)
67
+ end
68
+ end
69
+
70
+ return @path
71
+ end
72
+
73
+ def resolve_path(path)
74
+ if path.relative?
75
+ nearest_container = self
76
+
77
+ if !nearest_container.is_a?(Container)
78
+ nearest_container = self.parent
79
+ path = path.tail
80
+ end
81
+
82
+ return nearest_container.content_at_path(path)
83
+ else
84
+ return self.root_content_container.content_at_path(path)
85
+ end
86
+ end
87
+
88
+ def convert_path_to_relative(global_path)
89
+ # 1. Find last shared ancestor
90
+ # 2. Drill up using '..' style (actually represented as "^")
91
+ # 3. Re-build downward chain from common ancestor
92
+
93
+ own_path = self.path
94
+
95
+ min_path_length = [global_path.length, own_path.length].min
96
+ last_shared_path_comp_index = -1
97
+ (0..min_path_length).each do |i|
98
+ own_component = own_path.components[i]
99
+ other_component = global_path.components[i]
100
+
101
+ if own_component == other_component
102
+ last_shared_path_comp_index = i
103
+ else
104
+ break
105
+ end
106
+ end
107
+
108
+ # No shared path components, so just use global path
109
+ if last_shared_path_comp_index == -1
110
+ return global_path
111
+ end
112
+
113
+ number_of_upwards_moves = (own_path.length - 1) - last_shared_path_comp_index - 1
114
+ new_path_components = []
115
+
116
+ (0..number_of_upwards_moves).each do |i|
117
+ new_path_components << Path::Component.parent_component
118
+ end
119
+
120
+ (last_shared_path_comp_index + 1..global_path.length).each do |i|
121
+ new_path_components << global_path.components[i]
122
+ end
123
+
124
+ return Path.new(new_path_components, true)
125
+ end
126
+
127
+ # Find the most compact representation for a path,
128
+ # whether relative or global
129
+ def compact_path_string(other_path)
130
+ if other_path.relative?
131
+ relative_path_string = other_path.components_string
132
+ global_path_string = self.path.path_by_appending_path(other_path).components_string
133
+ else
134
+ relative_path = convert_path_to_relative(other_path)
135
+ relative_path_string = relative_path.components_string
136
+ global_path_string = other_path.components_string
137
+ end
138
+
139
+ if relative_path_string.length < global_path_string.length
140
+ return relative_path_string
141
+ else
142
+ return global_path_string
143
+ end
144
+ end
145
+
146
+ def root_content_container
147
+ ancestor = self
148
+ while !ancestor.parent.nil?
149
+ ancestor = ancestor.parent
150
+ end
151
+
152
+ return ancestor
153
+ end
154
+
155
+ def copy
156
+ raise NotImplementedError, "#{self.class} doesn't support copying"
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,20 @@
1
+ module Fable
2
+ class SearchResult
3
+ attr_accessor :object, :approximate
4
+
5
+ alias_method :approximate?, :approximate
6
+
7
+ def correct_object
8
+ if approximate?
9
+ return nil
10
+ else
11
+ return object
12
+ end
13
+ end
14
+
15
+ def container
16
+ return nil if !object.is_a?(Container)
17
+ return object
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,560 @@
1
+ module Fable
2
+ class Serializer
3
+ def self.convert_to_runtime_objects(objects, skip_last = false)
4
+ last_item = objects.last
5
+ runtime_objects = []
6
+
7
+ objects.each do |object|
8
+ next if object == last_item && skip_last
9
+ runtime_objects << convert_to_runtime_object(object)
10
+ end
11
+
12
+ runtime_objects
13
+ end
14
+
15
+ def self.convert_to_runtime_objects_hash(hash)
16
+ runtime_hash = {}
17
+
18
+ hash.each do |key, value|
19
+ runtime_hash[key] = convert_to_runtime_object(value)
20
+ end
21
+
22
+ return runtime_hash
23
+ end
24
+
25
+ def self.array_to_container(array)
26
+ contents = convert_to_runtime_objects(array, true)
27
+ # Final object in the array is always a combination of
28
+ # - named content
29
+ # - an #f key with the count flags
30
+ final_object = array.last
31
+ bit_flags = 0
32
+ container_name = nil
33
+ named_only_content = {}
34
+
35
+ if !final_object.nil?
36
+ final_object.each do |key, value|
37
+ if key == "#f"
38
+ bit_flags = value.to_i
39
+ elsif key == "#n"
40
+ container_name = value
41
+ else
42
+ named_content_item = convert_to_runtime_object(value)
43
+ if named_content_item.is_a?(Container)
44
+ named_content_item.name = key
45
+ end
46
+
47
+ named_only_content[key] = named_content_item
48
+ end
49
+ end
50
+ end
51
+
52
+ container = Container.new(bit_flags)
53
+ container.add_content(contents)
54
+ if !container_name.nil?
55
+ container.name = container_name
56
+ end
57
+
58
+ named_only_content.each do |key, content|
59
+ container.add_to_named_content(content)
60
+ end
61
+
62
+ return container
63
+ end
64
+
65
+ # ----------------------
66
+ # JSON ENCODING SCHEME
67
+ # ----------------------
68
+ #
69
+ # Glue: "<>", "G<", "G>"
70
+ #
71
+ # ControlCommand: "ev", "out", "/ev", "du" "pop", "->->", "~ret", "str", "/str", "nop",
72
+ # "choiceCnt", "turns", "visit", "seq", "thread", "done", "end"
73
+ #
74
+ # NativeFunction: "+", "-", "/", "*", "%" "~", "==", ">", "<", ">=", "<=", "!=", "!"... etc
75
+ #
76
+ # Void: "void"
77
+ #
78
+ # Value: "^string value", "^^string value beginning with ^"
79
+ # 5, 5.2
80
+ # {"^->": "path.target"}
81
+ # {"^var": "varname", "ci": 0}
82
+ #
83
+ # Container: [...]
84
+ # [...,
85
+ # {
86
+ # "subContainerName": ...,
87
+ # "#f": 5, # flags
88
+ # "#n": "containerOwnName" # only if not redundant
89
+ # }
90
+ # ]
91
+ #
92
+ # Divert: {"->": "path.target", "c": true }
93
+ # {"->": "path.target", "var": true}
94
+ # {"f()": "path.func"}
95
+ # {"->t->": "path.tunnel"}
96
+ # {"x()": "externalFuncName", "exArgs": 5}
97
+ #
98
+ # Var Assign: {"VAR=": "varName", "re": true} # reassignment
99
+ # {"temp=": "varName"}
100
+ #
101
+ # Var ref: {"VAR?": "varName"}
102
+ # {"CNT?": "stitch name"}
103
+ #
104
+ # ChoicePoint: {"*": pathString,
105
+ # "flg": 18 }
106
+ #
107
+ # Choice: Nothing too clever, it's only used in the save state,
108
+ # there's not likely to be many of them.
109
+ #
110
+ # Tag: {"#": "the tag text"}
111
+
112
+ def self.convert_to_runtime_object(object)
113
+ case object
114
+ when Numeric
115
+ return Value.create(object)
116
+ when String
117
+ string = object
118
+
119
+ # StringValue
120
+ if string.start_with?("^")
121
+ return StringValue.new(string[1..-1])
122
+ elsif string.start_with?("\n") && string.length == 1
123
+ return StringValue.new("\n")
124
+ end
125
+
126
+ # Glue
127
+ if string == "<>"
128
+ return Glue.new
129
+ end
130
+
131
+ # Control Commands
132
+ if ControlCommand.is_control_command?(string)
133
+ return ControlCommand.get_control_command(string)
134
+ end
135
+
136
+ # Native functions
137
+ # "^" conflicts with the way we identify strings, so now
138
+ # we know it's not a string, we can convert back to the proper symbol
139
+ # for this operator
140
+ if string == "L^"
141
+ string = "^"
142
+ end
143
+
144
+ if NativeFunctionCall.is_native_function?(string)
145
+ return NativeFunctionCall.new(string)
146
+ end
147
+
148
+ # Pop
149
+ if string == ControlCommand::COMMANDS[:POP_TUNNEL]
150
+ return ControlCommand.get_control_command(string)
151
+ elsif string == ControlCommand::COMMANDS[:POP_FUNCTION]
152
+ return ControlCommand.get_control_command(string)
153
+ end
154
+
155
+ if string == "void"
156
+ return Void.new
157
+ end
158
+ when Hash
159
+ given_hash = object
160
+
161
+ # Divert target value to path
162
+ if given_hash["^->"]
163
+ return DivertTargetValue.new(Path.new(given_hash["^->"]))
164
+ end
165
+
166
+ # VariablePointerType
167
+ if given_hash["^var"]
168
+ variable_pointer = VariablePointerValue.new(given_hash["^var"])
169
+ if given_hash["ci"]
170
+ variable_pointer.context_index = given_hash["ci"]
171
+ end
172
+
173
+ return variable_pointer
174
+ end
175
+
176
+ # Divert
177
+ is_divert = false
178
+ pushes_to_stack = false
179
+ divert_push_pop_type = PushPopType::TYPES[:function]
180
+ external = false
181
+ value = nil
182
+
183
+ if given_hash["->"]
184
+ is_divert = true
185
+ value = given_hash["->"]
186
+ elsif given_hash["f()"]
187
+ is_divert = true
188
+ pushes_to_stack = true
189
+ divert_push_pop_type = PushPopType::TYPES[:function]
190
+ value = given_hash["f()"]
191
+ elsif given_hash["->t->"]
192
+ is_divert = true
193
+ pushes_to_stack = true
194
+ divert_push_pop_type = PushPopType::TYPES[:tunnel]
195
+ value = given_hash["->t->"]
196
+ elsif given_hash["x()"]
197
+ is_divert = true
198
+ external = true
199
+ pushes_to_stack = false
200
+ divert_push_pop_type = PushPopType::TYPES[:function]
201
+ value = given_hash["x()"]
202
+ end
203
+
204
+ if is_divert
205
+ divert = Divert.new
206
+ divert.pushes_to_stack = pushes_to_stack
207
+ divert.stack_push_type = divert_push_pop_type
208
+ divert.is_external = external
209
+ target = value.to_s
210
+
211
+ if given_hash["var"]
212
+ divert.variable_divert_name = target
213
+ else
214
+ divert.target_path_string = target
215
+ end
216
+
217
+ divert.is_conditional = given_hash.has_key?("c")
218
+
219
+ if external
220
+ if given_hash["exArgs"]
221
+ divert.external_arguments = given_hash["exArgs"]
222
+ end
223
+ end
224
+
225
+ return divert
226
+ end
227
+
228
+ # Choice
229
+ if given_hash["*"]
230
+ choice = ChoicePoint.new
231
+ choice.path_string_on_choice = given_hash["*"]
232
+
233
+ if given_hash["flg"]
234
+ choice.flags = given_hash["flg"]
235
+ end
236
+
237
+ return choice
238
+ end
239
+
240
+ # Variable Reference
241
+ if given_hash["VAR?"]
242
+ return VariableReference.new(given_hash["VAR?"])
243
+ elsif given_hash["CNT?"]
244
+ read_count_variable_reference = VariableReference.new(nil)
245
+ read_count_variable_reference.path_string_for_count = given_hash["CNT?"]
246
+ return read_count_variable_reference
247
+ end
248
+
249
+ # Variable Assignment
250
+ is_variable_assignment = false
251
+ is_global_variable = false
252
+
253
+ if given_hash["VAR="]
254
+ variable_name = given_hash["VAR="]
255
+ is_variable_assignment = true
256
+ is_global_variable = true
257
+ elsif given_hash["temp="]
258
+ variable_name = given_hash["temp="]
259
+ is_variable_assignment = true
260
+ is_global_variable = false
261
+ end
262
+
263
+ if is_variable_assignment
264
+ is_new_declaration = !given_hash.has_key?("re")
265
+ variable_assignment = VariableAssignment.new(variable_name, is_new_declaration)
266
+ variable_assignment.global = is_global_variable
267
+ return variable_assignment
268
+ end
269
+
270
+ # Tag
271
+ if given_hash["#"]
272
+ return Tag.new(given_hash["#"])
273
+ end
274
+
275
+ # List
276
+ if given_hash["list"]
277
+ list_content = given_hash["list"]
278
+ raw_list = InkList.new
279
+ if given_hash["origins"]
280
+ raw_list.set_initial_origin_names(given_hash["origins"])
281
+ end
282
+
283
+ list_content.each do |key, value|
284
+ item = InkList::InkListItem.new(full_name: key)
285
+ raw_list.list[item] = value.to_i
286
+ end
287
+
288
+ return ListValue.new(raw_list)
289
+ end
290
+
291
+ # Used when serializing save state only
292
+ if given_hash["originalChoicePath"]
293
+ return object_to_choice(object)
294
+ end
295
+ when Array
296
+ return array_to_container(object)
297
+ when NilClass
298
+ return nil
299
+ end
300
+
301
+ raise Error, "Failed to convert to runtime object: #{object}"
302
+ end
303
+
304
+ def self.object_to_choice(object)
305
+ choice = Choice.new
306
+ choice.text = object["text"]
307
+ choice.index = object["index"].to_i
308
+ choice.source_path = object["originalChoicePath"]
309
+ choice.original_thread_index = object["originalThreadIndex"].to_i
310
+ choice.path_string_on_choice = object["targetPath"]
311
+ return choice
312
+ end
313
+
314
+ def self.convert_to_list_definitions(object)
315
+ all_definitions = []
316
+
317
+ object.each do |name, list_definition_hash|
318
+ items = {}
319
+ list_definition_hash.each do |key, value|
320
+ items[key] = value.to_i
321
+ end
322
+
323
+ all_definitions << ListDefinition.new(name, items)
324
+ end
325
+
326
+ return ListDefinitionsOrigin.new(all_definitions)
327
+ end
328
+
329
+ def self.convert_hash_of_runtime_objects(hash_value)
330
+ result = {}
331
+ hash_value.each do |key, value|
332
+ result[key] = convert_from_runtime_object(value)
333
+ end
334
+
335
+ result
336
+ end
337
+
338
+ def self.convert_array_of_runtime_objects(array)
339
+ array.map{|x| convert_from_runtime_object(x) }
340
+ end
341
+
342
+ def self.convert_from_runtime_object(object)
343
+ if object.is_a?(Container)
344
+ return convert_from_container(object)
345
+ end
346
+
347
+ if object.is_a?(Divert)
348
+ return convert_from_divert(object)
349
+ end
350
+
351
+ if object.is_a?(ChoicePoint)
352
+ return convert_from_choice_point(object)
353
+ end
354
+
355
+ if object.is_a?(IntValue) || object.is_a?(FloatValue)
356
+ return object.value
357
+ end
358
+
359
+ if object.is_a?(StringValue)
360
+ return convert_from_string_value(object)
361
+ end
362
+
363
+ if object.is_a?(ListValue) || object.is_a?(InkList)
364
+ return convert_from_list(object)
365
+ end
366
+
367
+ if object.is_a?(DivertTargetValue)
368
+ return convert_divert_target_value(object)
369
+ end
370
+
371
+ if object.is_a?(VariablePointerValue)
372
+ return convert_variable_pointer_value(object)
373
+ end
374
+
375
+ if object.is_a?(Glue)
376
+ return convert_glue(object)
377
+ end
378
+
379
+ if object.is_a?(ControlCommand)
380
+ return convert_control_command(object)
381
+ end
382
+
383
+ if object.is_a?(NativeFunctionCall)
384
+ return convert_native_function_call(object)
385
+ end
386
+
387
+ if object.is_a?(VariableReference)
388
+ return convert_variable_reference(object)
389
+ end
390
+
391
+ if object.is_a?(VariableAssignment)
392
+ return convert_variable_assignment(object)
393
+ end
394
+
395
+ if object.is_a?(Void)
396
+ return convert_void
397
+ end
398
+
399
+ if object.is_a?(Tag)
400
+ return convert_tag(object)
401
+ end
402
+
403
+ if object.is_a?(Choice)
404
+ return convert_choice(object)
405
+ end
406
+
407
+ raise ArgumentError, "Failed to serialize runtime object: #{object}"
408
+ end
409
+
410
+ def self.convert_from_divert(object)
411
+ result = {}
412
+ divert_type_key = "->"
413
+
414
+ if divert.external?
415
+ divert_type_key = "x()"
416
+ elsif divert.pushes_to_stack?
417
+ if divert.stack_push_type == PushPopType::TYPES[:function]
418
+ divert_type_key = "f()"
419
+ else
420
+ divert_type_key = "->t->"
421
+ end
422
+ end
423
+
424
+ if divert.has_variable_target?
425
+ target_string = divert.variable_divert_name
426
+ else
427
+ target_string = divert.target_path_string
428
+ end
429
+
430
+ result[divert_type_key] = target_string
431
+
432
+ if divert.has_variable_target?
433
+ result["var"] = true
434
+ end
435
+
436
+ if divert.is_conditional?
437
+ result["c"] = true
438
+ end
439
+
440
+ if divert.external_arguments > 0
441
+ result["exArgs"] = divert.external_arguments
442
+ end
443
+
444
+ return result
445
+ end
446
+
447
+ def self.convert_from_choice_point(choice_point)
448
+ return {
449
+ "*" => choice_point.path_string_on_choice,
450
+ "flg" => choice_point.flags
451
+ }
452
+ end
453
+
454
+ def self.convert_from_string_value(string_value)
455
+ if string_value.is_newline?
456
+ return "\n"
457
+ else
458
+ return "^#{string_value.value}"
459
+ end
460
+ end
461
+
462
+ def self.convert_from_list(list_value)
463
+ result = {}
464
+ if list_value.is_a?(ListValue)
465
+ raw_list = list_value.value
466
+ else
467
+ raw_list = list_value
468
+ end
469
+
470
+ raw_list.list.each do |list_item, value|
471
+ result[list_item.full_name] = value
472
+ end
473
+
474
+ if raw_list.list.empty? && !raw_list.origin_names.nil? && !raw_list.origin_names.empty?
475
+ result["origins"] = raw_list.origin_names
476
+ end
477
+
478
+ return {"list" => result}
479
+ end
480
+
481
+ def self.convert_divert_target_value(divert_target_value)
482
+ return {"^->" => divert_target_value.value.components_string }
483
+ end
484
+
485
+ def self.convert_variable_pointer_value(variable_pointer_value)
486
+ return {
487
+ "^var" => variable_pointer_value.value,
488
+ "ci" => variable_pointer_value.context_index
489
+ }
490
+ end
491
+
492
+ def self.convert_glue(glue)
493
+ return "<>"
494
+ end
495
+
496
+ def self.convert_control_command(control_command)
497
+ return control_command.command_type
498
+ end
499
+
500
+ def self.convert_native_function_call(native_function_call)
501
+ function_name = native_function_call.name
502
+ if name == "^"
503
+ return "L^"
504
+ else
505
+ return name
506
+ end
507
+ end
508
+
509
+ def self.convert_variable_reference(variable_reference)
510
+ read_count_path = variable_reference.path_string_for_count
511
+ if read_count_path.nil?
512
+ return {"CNT?" => read_count_path}
513
+ else
514
+ return {"VAR?" => variable_reference.name}
515
+ end
516
+ end
517
+
518
+ def self.convert_variable_assignment(variable_assignment)
519
+ result = {}
520
+
521
+ if variable_assignment.global?
522
+ key = "VAR="
523
+ else
524
+ key = "temp="
525
+ end
526
+
527
+ result[key] = variable_assignment.variable_name
528
+
529
+ if !variable_assignment.new_declaration?
530
+ result["re"] = true
531
+ end
532
+
533
+ return result
534
+ end
535
+
536
+ def self.convert_void
537
+ return "void"
538
+ end
539
+
540
+ def self.convert_tag(tag)
541
+ return {
542
+ "#" => tag.text
543
+ }
544
+ end
545
+
546
+ def self.convert_choice(choice)
547
+ return {
548
+ "text" => choice.text,
549
+ "index" => choice.index,
550
+ "originalChoicePath" => choice.source_path,
551
+ "originalThreadIndex" => choice.original_thread_index,
552
+ "targetPath" => choice.path_string_on_choice
553
+ }
554
+ end
555
+
556
+ def self.convert_choices(choices)
557
+ choices.map{|choice| convert_choice(choice) }
558
+ end
559
+ end
560
+ end