gamefic 1.7.0 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (101) 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 +10 -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 +7 -7
  13. data/lib/gamefic/action.rb +66 -60
  14. data/lib/gamefic/active.rb +331 -280
  15. data/lib/gamefic/actor.rb +8 -5
  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 +21 -23
  20. data/lib/gamefic/element.rb +47 -31
  21. data/lib/gamefic/entity.rb +6 -12
  22. data/lib/gamefic/{matchable.rb → keywords.rb} +52 -50
  23. data/lib/gamefic/messaging.rb +43 -44
  24. data/lib/gamefic/node.rb +14 -5
  25. data/lib/gamefic/plot.rb +69 -91
  26. data/lib/gamefic/plot/darkroom.rb +80 -264
  27. data/lib/gamefic/plot/host.rb +42 -48
  28. data/lib/gamefic/plot/snapshot.rb +14 -19
  29. data/lib/gamefic/query.rb +15 -18
  30. data/lib/gamefic/query/base.rb +50 -37
  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 -14
  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 +0 -2
  41. data/lib/gamefic/scene/activity.rb +24 -26
  42. data/lib/gamefic/scene/base.rb +71 -10
  43. data/lib/gamefic/scene/conclusion.rb +1 -3
  44. data/lib/gamefic/scene/multiple_choice.rb +19 -14
  45. data/lib/gamefic/scene/multiple_scene.rb +29 -20
  46. data/lib/gamefic/scene/pause.rb +8 -3
  47. data/lib/gamefic/scene/yes_or_no.rb +22 -10
  48. data/lib/gamefic/scriptable.rb +88 -0
  49. data/lib/gamefic/serialize.rb +223 -0
  50. data/lib/gamefic/subplot.rb +38 -35
  51. data/lib/gamefic/syntax.rb +15 -13
  52. data/lib/gamefic/version.rb +3 -3
  53. data/lib/gamefic/world.rb +18 -0
  54. data/lib/gamefic/world/callbacks.rb +135 -0
  55. data/lib/gamefic/world/commands.rb +184 -0
  56. data/lib/gamefic/{plot → world}/entities.rb +33 -35
  57. data/lib/gamefic/{plot → world}/playbook.rb +245 -240
  58. data/lib/gamefic/world/players.rb +37 -0
  59. data/lib/gamefic/world/scenes.rb +226 -0
  60. metadata +37 -88
  61. data/bin/gamefic +0 -9
  62. data/lib/gamefic/engine.rb +0 -7
  63. data/lib/gamefic/engine/base.rb +0 -59
  64. data/lib/gamefic/engine/tty.rb +0 -24
  65. data/lib/gamefic/grammar.rb +0 -13
  66. data/lib/gamefic/grammar/conjugator.rb +0 -20
  67. data/lib/gamefic/grammar/gender.rb +0 -11
  68. data/lib/gamefic/grammar/person.rb +0 -10
  69. data/lib/gamefic/grammar/plural.rb +0 -13
  70. data/lib/gamefic/grammar/pronouns.rb +0 -106
  71. data/lib/gamefic/grammar/tense.rb +0 -6
  72. data/lib/gamefic/grammar/verb_set.rb +0 -43
  73. data/lib/gamefic/grammar/verbs.rb +0 -26
  74. data/lib/gamefic/grammar/word_adapter.rb +0 -49
  75. data/lib/gamefic/plot/articles.rb +0 -22
  76. data/lib/gamefic/plot/callbacks.rb +0 -126
  77. data/lib/gamefic/plot/commands.rb +0 -120
  78. data/lib/gamefic/plot/players.rb +0 -15
  79. data/lib/gamefic/plot/scenes.rb +0 -187
  80. data/lib/gamefic/plot/theater.rb +0 -73
  81. data/lib/gamefic/plot/you_mount.rb +0 -22
  82. data/lib/gamefic/scene/custom.rb +0 -9
  83. data/lib/gamefic/script.rb +0 -13
  84. data/lib/gamefic/script/base.rb +0 -42
  85. data/lib/gamefic/script/file.rb +0 -14
  86. data/lib/gamefic/script/text.rb +0 -14
  87. data/lib/gamefic/shell.rb +0 -76
  88. data/lib/gamefic/source.rb +0 -14
  89. data/lib/gamefic/source/base.rb +0 -12
  90. data/lib/gamefic/source/file.rb +0 -23
  91. data/lib/gamefic/source/text.rb +0 -16
  92. data/lib/gamefic/tester.rb +0 -19
  93. data/lib/gamefic/text.rb +0 -8
  94. data/lib/gamefic/text/ansi.rb +0 -53
  95. data/lib/gamefic/text/html.rb +0 -68
  96. data/lib/gamefic/text/html/conversions.rb +0 -250
  97. data/lib/gamefic/text/html/entities.rb +0 -9
  98. data/lib/gamefic/tty.rb +0 -10
  99. data/lib/gamefic/user.rb +0 -7
  100. data/lib/gamefic/user/base.rb +0 -29
  101. data/lib/gamefic/user/tty.rb +0 -38
@@ -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
@@ -1,39 +1,37 @@
1
1
  require 'gamefic/plot'
2
2
 
3
3
  module Gamefic
4
-
5
- class Subplot
6
- include Plot::Theater
7
- include Plot::Entities
8
- include Plot::Commands
9
- include Plot::Callbacks
10
- include Plot::Scenes
11
- include Plot::Articles
4
+ class Subplot #< Container
5
+ include World
6
+ include Scriptable
7
+ include Gamefic::Serialize
8
+ # @!parse extend Scriptable::ClassMethods
12
9
 
13
10
  # @return [Gamefic::Plot]
14
11
  attr_reader :plot
15
12
 
16
- class << self
17
- attr_reader :start_proc
18
-
19
- protected
20
-
21
- def on_start &block
22
- @start_proc = block
23
- end
24
- end
25
-
26
- def initialize plot, introduce: nil, next_cue: nil
13
+ # @param plot [Gamefic::Plot]
14
+ # @param introduce [Gamefic::Actor, nil]
15
+ # @param next_cue [Class<Gamefic::Scene::Base>, nil]
16
+ # @param more [Hash]
17
+ def initialize plot, introduce: nil, next_cue: nil, **more
27
18
  @plot = plot
28
19
  @next_cue = next_cue
29
20
  @concluded = false
30
- stage &self.class.start_proc unless self.class.start_proc.nil?
21
+ @more = more
22
+ configure more
23
+ run_scripts
31
24
  playbook.freeze
32
25
  self.introduce introduce unless introduce.nil?
26
+ @static = [self] + scene_classes + entities
27
+ end
28
+
29
+ def static
30
+ plot.static
33
31
  end
34
32
 
35
- def add_entity e
36
- @p_entities.push e
33
+ def players
34
+ @players ||= []
37
35
  end
38
36
 
39
37
  def subplot
@@ -58,25 +56,22 @@ module Gamefic
58
56
  ent
59
57
  end
60
58
 
61
- # HACK: Always assume subplots are running for the sake of entity destruction
62
- def running?
63
- true
64
- end
65
-
66
59
  def exeunt player
60
+ player_conclude_procs.each { |block| block.call player }
67
61
  player.playbooks.delete playbook
68
62
  player.cue (@next_cue || default_scene)
69
- p_players.delete player
63
+ players.delete player
70
64
  end
71
65
 
72
66
  def conclude
73
67
  @concluded = true
74
- entities.each { |e|
75
- destroy e
76
- }
77
- players.each { |p|
78
- exeunt p
79
- }
68
+ # Players needed to exit first in case any player_conclude procs need to
69
+ # interact with the subplot's entities.
70
+ players.each { |p| exeunt p }
71
+ # @todo I'm not sure why rejecting nils is necessary here. It's only an
72
+ # issue in Opal.
73
+ entities.reject(&:nil?).each { |e| destroy e }
74
+ # plot.static.remove(scene_classes + entities)
80
75
  end
81
76
 
82
77
  def concluded?
@@ -84,6 +79,9 @@ module Gamefic
84
79
  end
85
80
 
86
81
  def ready
82
+ # @todo We might not want to conclude subplots without players. There
83
+ # might be cases where a subplot gets created with the intention of
84
+ # introducing players in a later turn.
87
85
  conclude if players.empty?
88
86
  return if concluded?
89
87
  playbook.freeze
@@ -95,6 +93,11 @@ module Gamefic
95
93
  call_player_update
96
94
  call_update
97
95
  end
98
- end
99
96
 
97
+ # Subclasses can override this method to handle additional configuration
98
+ # options.
99
+ #
100
+ def configure more
101
+ end
102
+ end
100
103
  end
@@ -1,11 +1,9 @@
1
1
  require 'gamefic/command'
2
2
 
3
3
  module Gamefic
4
-
5
4
  class Syntax
6
5
  attr_reader :token_count, :first_word, :verb, :template, :command
7
- @@phrase = '([\w\W\s\S]*?)'
8
-
6
+
9
7
  def initialize template, command
10
8
  words = template.split_words
11
9
  @token_count = words.length
@@ -15,8 +13,8 @@ module Gamefic
15
13
  @token_count -= 1
16
14
  @first_word = ''
17
15
  else
18
- @verb = command_words[0].to_sym if !command_words[0].nil?
19
16
  @first_word = words[0].to_s
17
+ @verb = command_words[0].to_sym
20
18
  end
21
19
  @command = command_words.join(' ')
22
20
  @template = words.join(' ')
@@ -29,7 +27,7 @@ module Gamefic
29
27
  if last_token_is_reg
30
28
  next
31
29
  else
32
- tokens.push @@phrase
30
+ tokens.push '([\w\W\s\S]*?)'
33
31
  last_token_is_reg = true
34
32
  end
35
33
  else
@@ -50,11 +48,11 @@ module Gamefic
50
48
  @replace = subs.join(' ')
51
49
  @regexp = Regexp.new("^#{tokens.join(' ')}$", Regexp::IGNORECASE)
52
50
  end
53
-
51
+
54
52
  # Convert a String into a Command.
55
53
  #
56
54
  # @param text [String]
57
- # @return [Command]
55
+ # @return [Gamefic::Command]
58
56
  def tokenize text
59
57
  m = text.match(@regexp)
60
58
  return nil if m.nil?
@@ -71,7 +69,11 @@ module Gamefic
71
69
  }
72
70
  Command.new @verb, arguments
73
71
  end
74
-
72
+
73
+ # Determine if the specified text matches the syntax's expected pattern.
74
+ #
75
+ # @param text [String]
76
+ # @return [Boolean]
75
77
  def accept? text
76
78
  !text.match(@regexp).nil?
77
79
  end
@@ -82,16 +84,17 @@ module Gamefic
82
84
  def signature
83
85
  [@regexp, @replace]
84
86
  end
85
-
87
+
86
88
  def ==(other)
89
+ return false unless other.is_a?(Syntax)
87
90
  signature == other.signature
88
91
  end
89
-
92
+
90
93
  # Tokenize an Array of Commands from the specified text.
91
94
  #
92
95
  # @param text [String] The text to tokenize.
93
- # @param syntaxes [Array<Syntax>] The Syntaxes to use.
94
- # @return [Array<Command>] The tokenized commands.
96
+ # @param syntaxes [Array<Gamefic::Syntax>] The Syntaxes to use.
97
+ # @return [Array<Gamefic::Command>] The tokenized commands.
95
98
  def self.tokenize text, syntaxes
96
99
  matches = []
97
100
  syntaxes.each { |syntax|
@@ -108,5 +111,4 @@ module Gamefic
108
111
  matches
109
112
  end
110
113
  end
111
-
112
114
  end