gamefic 1.6.0 → 2.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (105) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +12 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +16 -0
  5. data/.solargraph.yml +5 -0
  6. data/CHANGELOG.md +6 -0
  7. data/Gemfile +7 -0
  8. data/LICENSE +20 -0
  9. data/README.md +28 -0
  10. data/Rakefile +10 -0
  11. data/gamefic.gemspec +27 -0
  12. data/lib/gamefic.rb +11 -8
  13. data/lib/gamefic/action.rb +68 -58
  14. data/lib/gamefic/active.rb +331 -0
  15. data/lib/gamefic/actor.rb +8 -0
  16. data/lib/gamefic/command.rb +9 -7
  17. data/lib/gamefic/core_ext/array.rb +27 -49
  18. data/lib/gamefic/core_ext/string.rb +25 -16
  19. data/lib/gamefic/describable.rb +37 -22
  20. data/lib/gamefic/element.rb +47 -0
  21. data/lib/gamefic/entity.rb +24 -48
  22. data/lib/gamefic/{matchable.rb → keywords.rb} +52 -50
  23. data/lib/gamefic/messaging.rb +43 -45
  24. data/lib/gamefic/node.rb +14 -5
  25. data/lib/gamefic/plot.rb +73 -85
  26. data/lib/gamefic/plot/darkroom.rb +80 -0
  27. data/lib/gamefic/plot/host.rb +42 -46
  28. data/lib/gamefic/plot/snapshot.rb +14 -214
  29. data/lib/gamefic/query.rb +15 -17
  30. data/lib/gamefic/query/base.rb +51 -42
  31. data/lib/gamefic/query/children.rb +0 -0
  32. data/lib/gamefic/query/descendants.rb +2 -2
  33. data/lib/gamefic/query/external.rb +18 -0
  34. data/lib/gamefic/query/family.rb +3 -7
  35. data/lib/gamefic/query/matches.rb +75 -67
  36. data/lib/gamefic/query/parent.rb +0 -0
  37. data/lib/gamefic/query/siblings.rb +0 -0
  38. data/lib/gamefic/query/text.rb +12 -12
  39. data/lib/gamefic/query/tree.rb +17 -0
  40. data/lib/gamefic/scene.rb +1 -5
  41. data/lib/gamefic/scene/{active.rb → activity.rb} +4 -6
  42. data/lib/gamefic/scene/base.rb +77 -13
  43. data/lib/gamefic/scene/conclusion.rb +0 -2
  44. data/lib/gamefic/scene/custom.rb +0 -2
  45. data/lib/gamefic/scene/multiple_choice.rb +18 -16
  46. data/lib/gamefic/scene/multiple_scene.rb +29 -20
  47. data/lib/gamefic/scene/pause.rb +7 -2
  48. data/lib/gamefic/scene/yes_or_no.rb +21 -9
  49. data/lib/gamefic/scriptable.rb +88 -0
  50. data/lib/gamefic/serialize.rb +223 -0
  51. data/lib/gamefic/subplot.rb +47 -51
  52. data/lib/gamefic/syntax.rb +15 -13
  53. data/lib/gamefic/version.rb +3 -3
  54. data/lib/gamefic/world.rb +18 -0
  55. data/lib/gamefic/world/callbacks.rb +135 -0
  56. data/lib/gamefic/world/commands.rb +184 -0
  57. data/lib/gamefic/world/entities.rb +98 -0
  58. data/lib/gamefic/{plot → world}/playbook.rb +245 -236
  59. data/lib/gamefic/world/players.rb +37 -0
  60. data/lib/gamefic/world/scenes.rb +226 -0
  61. metadata +40 -108
  62. data/bin/gamefic +0 -9
  63. data/lib/gamefic/character.rb +0 -232
  64. data/lib/gamefic/character/state.rb +0 -12
  65. data/lib/gamefic/engine.rb +0 -7
  66. data/lib/gamefic/engine/base.rb +0 -66
  67. data/lib/gamefic/engine/tty.rb +0 -24
  68. data/lib/gamefic/grammar.rb +0 -13
  69. data/lib/gamefic/grammar/conjugator.rb +0 -20
  70. data/lib/gamefic/grammar/gender.rb +0 -11
  71. data/lib/gamefic/grammar/person.rb +0 -10
  72. data/lib/gamefic/grammar/plural.rb +0 -13
  73. data/lib/gamefic/grammar/pronouns.rb +0 -105
  74. data/lib/gamefic/grammar/tense.rb +0 -6
  75. data/lib/gamefic/grammar/verb_set.rb +0 -43
  76. data/lib/gamefic/grammar/verbs.rb +0 -26
  77. data/lib/gamefic/grammar/word_adapter.rb +0 -49
  78. data/lib/gamefic/plot/articles.rb +0 -22
  79. data/lib/gamefic/plot/callbacks.rb +0 -127
  80. data/lib/gamefic/plot/commands.rb +0 -121
  81. data/lib/gamefic/plot/entities.rb +0 -88
  82. data/lib/gamefic/plot/players.rb +0 -15
  83. data/lib/gamefic/plot/scenes.rb +0 -149
  84. data/lib/gamefic/plot/theater.rb +0 -73
  85. data/lib/gamefic/plot/you_mount.rb +0 -22
  86. data/lib/gamefic/script.rb +0 -13
  87. data/lib/gamefic/script/base.rb +0 -42
  88. data/lib/gamefic/script/file.rb +0 -14
  89. data/lib/gamefic/script/text.rb +0 -14
  90. data/lib/gamefic/shell.rb +0 -76
  91. data/lib/gamefic/source.rb +0 -14
  92. data/lib/gamefic/source/base.rb +0 -12
  93. data/lib/gamefic/source/file.rb +0 -23
  94. data/lib/gamefic/source/text.rb +0 -16
  95. data/lib/gamefic/tester.rb +0 -19
  96. data/lib/gamefic/text.rb +0 -8
  97. data/lib/gamefic/text/ansi.rb +0 -53
  98. data/lib/gamefic/text/html.rb +0 -68
  99. data/lib/gamefic/text/html/conversions.rb +0 -250
  100. data/lib/gamefic/text/html/entities.rb +0 -9
  101. data/lib/gamefic/tty.rb +0 -10
  102. data/lib/gamefic/user.rb +0 -8
  103. data/lib/gamefic/user/base.rb +0 -15
  104. data/lib/gamefic/user/buffer.rb +0 -32
  105. data/lib/gamefic/user/tty.rb +0 -54
@@ -1,5 +1,4 @@
1
1
  module Gamefic
2
-
3
2
  # A Conclusion ends the Plot (or the character's participation in it).
4
3
  #
5
4
  class Scene::Conclusion < Scene::Custom
@@ -7,5 +6,4 @@ module Gamefic
7
6
  @type ||= 'Conclusion'
8
7
  end
9
8
  end
10
-
11
9
  end
@@ -1,9 +1,7 @@
1
1
  module Gamefic
2
-
3
2
  # A Custom Scene allows for complete configuration of its behavior upon
4
3
  # instantiation. It is suitable for direct instantiation or subclassing.
5
4
  #
6
5
  class Scene::Custom < Scene::Base
7
6
  end
8
-
9
7
  end
@@ -1,5 +1,4 @@
1
1
  module Gamefic
2
-
3
2
  # Provide a list of options and process the selection in the scene's finish
4
3
  # block. After the scene is finished, the :active scene will be cued unless
5
4
  # some other scene has already been prepared or cued.
@@ -8,9 +7,21 @@ module Gamefic
8
7
  # instead of a String.
9
8
  #
10
9
  class Scene::MultipleChoice < Scene::Custom
10
+ # The zero-based index of the selected option.
11
+ #
12
+ # @return [Integer]
11
13
  attr_reader :index
14
+
15
+ # The one-based index of the selected option.
16
+ #
17
+ # @return [Integer]
12
18
  attr_reader :number
19
+
20
+ # The full text of the selected option.
21
+ #
22
+ # @return [String]
13
23
  attr_reader :selection
24
+
14
25
  attr_writer :invalid_message
15
26
 
16
27
  def post_initialize
@@ -18,20 +29,7 @@ module Gamefic
18
29
  self.prompt = 'Enter a choice:'
19
30
  end
20
31
 
21
- #def start actor
22
- # data = start_data_for(actor)
23
- # data.clear
24
- # do_start_block actor, data
25
- # tell_options
26
- #end
27
-
28
- def start
29
- super
30
- raise "MultipleChoice scene has zero options" if options.empty?
31
- end
32
-
33
32
  def finish
34
- #data = finish_data_for(actor, input)
35
33
  get_choice
36
34
  if selection.nil?
37
35
  actor.tell invalid_message
@@ -41,10 +39,16 @@ module Gamefic
41
39
  end
42
40
  end
43
41
 
42
+ # The array of available options.
43
+ #
44
+ # @return [Array<String>]
44
45
  def options
45
46
  @options ||= []
46
47
  end
47
48
 
49
+ # The text to display when an invalid selection is received.
50
+ #
51
+ # @return [String]
48
52
  def invalid_message
49
53
  @invalid_message ||= 'That is not a valid choice.'
50
54
  end
@@ -82,7 +86,5 @@ module Gamefic
82
86
  list += "</ol>"
83
87
  actor.tell list
84
88
  end
85
-
86
89
  end
87
-
88
90
  end
@@ -1,20 +1,29 @@
1
- module Gamefic
2
-
3
- class Scene::MultipleScene < Scene::MultipleChoice
4
- def option_map
5
- @option_map ||= {}
6
- end
7
-
8
- def map option, scene
9
- options.push option
10
- option_map[option] = scene
11
- end
12
-
13
- def finish
14
- get_choice
15
- unless selection.nil?
16
- actor.cue option_map[selection]
17
- end
18
- end
19
- end
20
- end
1
+ module Gamefic
2
+ class Scene::MultipleScene < Scene::MultipleChoice
3
+ def option_map
4
+ @option_map ||= {}
5
+ end
6
+
7
+ # @param option [String]
8
+ # @param scene [Class<Gamefic::Scene::Base>]
9
+ def map option, scene
10
+ options.push option
11
+ option_map[option] = scene
12
+ end
13
+
14
+ def finish
15
+ get_choice
16
+ unless selection.nil?
17
+ actor.prepare option_map[selection]
18
+ end
19
+ end
20
+
21
+ def state
22
+ entered = {}
23
+ option_map.each_pair do |k, v|
24
+ entered[k] = actor.entered?(v)
25
+ end
26
+ super.merge entered: entered
27
+ end
28
+ end
29
+ end
@@ -1,5 +1,4 @@
1
1
  module Gamefic
2
-
3
2
  # Pause for user input.
4
3
  #
5
4
  class Scene::Pause < Scene::Custom
@@ -7,6 +6,12 @@ module Gamefic
7
6
  self.type = 'Pause'
8
7
  self.prompt = 'Press enter to continue...'
9
8
  end
9
+
10
+ class << self
11
+ def tracked?
12
+ @tracked = true if @tracked.nil?
13
+ @tracked
14
+ end
15
+ end
10
16
  end
11
-
12
17
  end
@@ -1,32 +1,41 @@
1
1
  module Gamefic
2
-
3
2
  # Prompt the user to answer "yes" or "no". The scene will accept variations
4
3
  # like "YES" or "n" and normalize the answer to "yes" or "no" in the finish
5
4
  # block. After the scene is finished, the :active scene will be cued if no
6
5
  # other scene has been prepared or cued.
7
6
  #
8
7
  class Scene::YesOrNo < Scene::Custom
8
+ attr_writer :invalid_message
9
+
9
10
  def post_initialize
10
11
  self.type = 'YesOrNo'
11
- self.prompt = 'Yes or No?'
12
+ self.prompt = 'Yes or No:'
12
13
  end
13
14
 
15
+ # True if the actor's answer is Yes.
16
+ # Any answer beginning with letter Y is considered Yes.
17
+ #
18
+ # @return [Boolean]
14
19
  def yes?
15
- input.to_s[0,1].downcase == 'y'
20
+ input.to_s[0,1].downcase == 'y' or input.to_i == 1
16
21
  end
17
22
 
23
+ # True if the actor's answer is No.
24
+ # Any answer beginning with letter N is considered No.
25
+ #
26
+ # @return [Boolean]
18
27
  def no?
19
- input.to_s[0,1].downcase == 'n'
28
+ input.to_s[0,1].downcase == 'n' or input.to_i == 2
20
29
  end
21
30
 
31
+ # The message sent to the user for an invalid answer, i.e., the input
32
+ # could not be resolved to either Yes or No.
33
+ #
34
+ # @return [String]
22
35
  def invalid_message
23
36
  @invalid_message ||= 'Please enter Yes or No.'
24
37
  end
25
38
 
26
- def prompt
27
- @prompt ||= 'Yes or No?'
28
- end
29
-
30
39
  def finish
31
40
  if yes? or no?
32
41
  super
@@ -34,6 +43,9 @@ module Gamefic
34
43
  actor.tell invalid_message
35
44
  end
36
45
  end
37
- end
38
46
 
47
+ def state
48
+ super.merge options: ['Yes', 'No']
49
+ end
50
+ end
39
51
  end
@@ -0,0 +1,88 @@
1
+ module Gamefic
2
+ # The Scriptable module provides a clean room (aka "theater") for scripts.
3
+ #
4
+ # @!method stage(*args, &block)
5
+ # Execute a block of code in a subset of the owner's scope.
6
+ #
7
+ # The provided code is evaluated inside a clean room object that has its
8
+ # own instance variables and access to the owner's public methods. The proc
9
+ # can accept the method call's arguments.
10
+ #
11
+ # @example Execute a block of code
12
+ # stage {
13
+ # puts 'Hello'
14
+ # }
15
+ #
16
+ # @example Execute a block of code with arguments
17
+ # stage 'hello' { |message|
18
+ # puts message # <- prints 'hello'
19
+ # }
20
+ #
21
+ # @example Use an instance variable
22
+ # stage { @message = 'hello'" }
23
+ # stage { puts @message } # <- prints 'hello'
24
+ #
25
+ # @yieldpublic [Gamefic::Plot]
26
+ # @return [Object] The value returned by the executed code
27
+ #
28
+ # @!method theater
29
+ # The object that acts as an isolated namespace for staged code.
30
+ # @return [Object]
31
+ #
32
+ # @!parse alias cleanroom theater
33
+ module Scriptable
34
+ module ClassMethods
35
+ # An array of blocks that were added by the `script` class method.
36
+ #
37
+ # @return [Array<Proc>]
38
+ def blocks
39
+ @blocks ||= []
40
+ end
41
+
42
+ # Add a block to be executed by the instance's `stage` method.
43
+ #
44
+ # Note that `script` does not execute the block instantly, but stores
45
+ # it in the `blocks` array to be executed later.
46
+ #
47
+ # @yieldpublic [Gamefic::Plot]
48
+ def script &block
49
+ blocks.push block
50
+ end
51
+ end
52
+
53
+ def self.included klass
54
+ klass.extend ClassMethods
55
+ end
56
+
57
+ private
58
+
59
+ # Execute all the scripts that were added by the `script` class method.
60
+ #
61
+ def run_scripts
62
+ self.class.blocks.each { |blk| stage &blk }
63
+ end
64
+ end
65
+ end
66
+
67
+ # @note #stage and #theater are implemented this way so the clean room object
68
+ # defines its classes and modules in the root namespace.
69
+ Gamefic::Scriptable.module_exec do
70
+ define_method :stage do |*args, &block|
71
+ theater.instance_exec *args, &block
72
+ end
73
+
74
+ define_method :theater do
75
+ @theater ||= begin
76
+ instance = self
77
+ theater ||= Object.new
78
+ theater.instance_exec do
79
+ define_singleton_method :method_missing do |symbol, *args, &block|
80
+ instance.public_send :public_send, symbol, *args, &block
81
+ end
82
+ end
83
+ theater.extend Gamefic::Serialize
84
+ theater
85
+ end
86
+ end
87
+ alias cleanroom theater
88
+ end
@@ -0,0 +1,223 @@
1
+ require 'set'
2
+
3
+ module Gamefic
4
+ module Serialize
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
50
+ end
51
+ end
52
+ end
53
+
54
+ class Object
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)
66
+ return self if [true, false, nil].include?(self)
67
+ # @todo This warning is a little too spammy. Set up a logger so it can be
68
+ # limited to an info or debug level.
69
+ # STDERR.puts "Unable to convert #{self} to element"
70
+ "#<UNKNOWN>"
71
+ end
72
+
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)
139
+ result = {}
140
+ instance_variables.each do |k|
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
151
+ end
152
+ result
153
+ end
154
+ end
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
+
169
+ class Symbol
170
+ def to_serial(_index = [])
171
+ "#<SYM:#{self}>"
172
+ end
173
+ end
174
+
175
+ class String
176
+ def to_serial(_index = [])
177
+ self
178
+ end
179
+ end
180
+
181
+ class Numeric
182
+ def to_serial(_index = [])
183
+ self
184
+ end
185
+ end
186
+
187
+ class Array
188
+ def to_serial(index = [])
189
+ map do |e|
190
+ s = e.to_serial(index)
191
+ return "#<UNKNOWN>" if s == "#<UNKNOWN>"
192
+ s
193
+ end
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
201
+ end
202
+
203
+ class Hash
204
+ def to_serial(index = [])
205
+ result = {'class' => 'Hash', 'data' => []}
206
+ each_pair do |key, value|
207
+ k2 = key.to_serial(index)
208
+ v2 = value.to_serial(index)
209
+ return "#<UNKNOWN>" if k2 == "#<UNKNOWN>" || v2 == "#<UNKNOWN>"
210
+ result['data'].push [k2, v2]
211
+ end
212
+ result
213
+ end
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