gamefic 1.7.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (99) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +12 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +13 -0
  5. data/.solargraph.yml +5 -0
  6. data/Gemfile +7 -0
  7. data/LICENSE +20 -0
  8. data/README.md +28 -0
  9. data/Rakefile +10 -0
  10. data/gamefic.gemspec +27 -0
  11. data/lib/gamefic.rb +7 -6
  12. data/lib/gamefic/action.rb +38 -28
  13. data/lib/gamefic/active.rb +325 -280
  14. data/lib/gamefic/actor.rb +8 -5
  15. data/lib/gamefic/command.rb +9 -7
  16. data/lib/gamefic/core_ext/array.rb +24 -49
  17. data/lib/gamefic/core_ext/string.rb +25 -16
  18. data/lib/gamefic/describable.rb +21 -23
  19. data/lib/gamefic/element.rb +43 -31
  20. data/lib/gamefic/entity.rb +6 -12
  21. data/lib/gamefic/index.rb +121 -0
  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 -89
  26. data/lib/gamefic/plot/darkroom.rb +92 -264
  27. data/lib/gamefic/plot/host.rb +42 -48
  28. data/lib/gamefic/plot/snapshot.rb +5 -18
  29. data/lib/gamefic/query.rb +14 -18
  30. data/lib/gamefic/query/base.rb +30 -18
  31. data/lib/gamefic/query/children.rb +0 -0
  32. data/lib/gamefic/query/external.rb +18 -14
  33. data/lib/gamefic/query/family.rb +1 -7
  34. data/lib/gamefic/query/matches.rb +75 -67
  35. data/lib/gamefic/query/parent.rb +0 -0
  36. data/lib/gamefic/query/siblings.rb +0 -0
  37. data/lib/gamefic/query/text.rb +2 -1
  38. data/lib/gamefic/scene.rb +0 -2
  39. data/lib/gamefic/scene/activity.rb +24 -26
  40. data/lib/gamefic/scene/base.rb +64 -8
  41. data/lib/gamefic/scene/conclusion.rb +0 -2
  42. data/lib/gamefic/scene/custom.rb +0 -2
  43. data/lib/gamefic/scene/multiple_choice.rb +18 -3
  44. data/lib/gamefic/scene/multiple_scene.rb +29 -20
  45. data/lib/gamefic/scene/pause.rb +7 -2
  46. data/lib/gamefic/scene/yes_or_no.rb +21 -9
  47. data/lib/gamefic/scriptable.rb +87 -0
  48. data/lib/gamefic/serialize.rb +68 -0
  49. data/lib/gamefic/subplot.rb +29 -35
  50. data/lib/gamefic/syntax.rb +14 -13
  51. data/lib/gamefic/version.rb +3 -3
  52. data/lib/gamefic/world.rb +16 -0
  53. data/lib/gamefic/world/callbacks.rb +135 -0
  54. data/lib/gamefic/world/commands.rb +184 -0
  55. data/lib/gamefic/{plot → world}/entities.rb +30 -29
  56. data/lib/gamefic/{plot → world}/playbook.rb +255 -240
  57. data/lib/gamefic/world/players.rb +21 -0
  58. data/lib/gamefic/world/scenes.rb +226 -0
  59. metadata +41 -92
  60. data/bin/gamefic +0 -9
  61. data/lib/gamefic/engine.rb +0 -7
  62. data/lib/gamefic/engine/base.rb +0 -59
  63. data/lib/gamefic/engine/tty.rb +0 -24
  64. data/lib/gamefic/grammar.rb +0 -13
  65. data/lib/gamefic/grammar/conjugator.rb +0 -20
  66. data/lib/gamefic/grammar/gender.rb +0 -11
  67. data/lib/gamefic/grammar/person.rb +0 -10
  68. data/lib/gamefic/grammar/plural.rb +0 -13
  69. data/lib/gamefic/grammar/pronouns.rb +0 -106
  70. data/lib/gamefic/grammar/tense.rb +0 -6
  71. data/lib/gamefic/grammar/verb_set.rb +0 -43
  72. data/lib/gamefic/grammar/verbs.rb +0 -26
  73. data/lib/gamefic/grammar/word_adapter.rb +0 -49
  74. data/lib/gamefic/plot/articles.rb +0 -22
  75. data/lib/gamefic/plot/callbacks.rb +0 -126
  76. data/lib/gamefic/plot/commands.rb +0 -120
  77. data/lib/gamefic/plot/players.rb +0 -15
  78. data/lib/gamefic/plot/scenes.rb +0 -187
  79. data/lib/gamefic/plot/theater.rb +0 -73
  80. data/lib/gamefic/plot/you_mount.rb +0 -22
  81. data/lib/gamefic/script.rb +0 -13
  82. data/lib/gamefic/script/base.rb +0 -42
  83. data/lib/gamefic/script/file.rb +0 -14
  84. data/lib/gamefic/script/text.rb +0 -14
  85. data/lib/gamefic/shell.rb +0 -76
  86. data/lib/gamefic/source.rb +0 -14
  87. data/lib/gamefic/source/base.rb +0 -12
  88. data/lib/gamefic/source/file.rb +0 -23
  89. data/lib/gamefic/source/text.rb +0 -16
  90. data/lib/gamefic/tester.rb +0 -19
  91. data/lib/gamefic/text.rb +0 -8
  92. data/lib/gamefic/text/ansi.rb +0 -53
  93. data/lib/gamefic/text/html.rb +0 -68
  94. data/lib/gamefic/text/html/conversions.rb +0 -250
  95. data/lib/gamefic/text/html/entities.rb +0 -9
  96. data/lib/gamefic/tty.rb +0 -10
  97. data/lib/gamefic/user.rb +0 -7
  98. data/lib/gamefic/user/base.rb +0 -29
  99. data/lib/gamefic/user/tty.rb +0 -38
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: f3811a2f448c91712eaa8eec46000577c7d38f7a
4
- data.tar.gz: 6cb83cb93b14fd5c94401367e51828bad90a8960
2
+ SHA256:
3
+ metadata.gz: 758b3f682dd5837841ce39281a53e6d8beada80ca0f0f0308f2b3909d12349b7
4
+ data.tar.gz: c330934f0a69040e96cf7c18936b93e5b5cf7580ba95a0ef46e633a79975f513
5
5
  SHA512:
6
- metadata.gz: 12f2199a4d803e7a179b49e1d50f6e83571f21d601e67828546de918c65b3da0ee97790e66ee5f73f34b1a901d515284e3c8bd030ff0dc5d816678b7c9a0ba48
7
- data.tar.gz: 31118c91be341821662fe8624b85924b196c52f9b2bcd0c758f5af13fc9d1dceaa76f204d6f6f3d2e274ef049153b416dcd2648a207e4ad07ea02ea4b9b440c4
6
+ metadata.gz: f95a19e039ef00076f23f90902077ba78941b2b186bad9963222da21001148741800058c97c7a110662f660af3334ef0a4732046210946ce9a9c54a340d8d566
7
+ data.tar.gz: dfb08b610f13a75c5515bf95d41834db415bee8a3d196238f53385c6d2983a84dcd8906d048440c21601858f13c48a0f2359da21ea0596455ee7fac7ddd66acf
@@ -0,0 +1,12 @@
1
+ Gemfile.lock
2
+ .buildpath
3
+ .project
4
+ .settings
5
+ .DS_Store
6
+ coverage
7
+ examples/*/build
8
+ examples/*/release
9
+ .yardoc
10
+ doc
11
+ .vscode
12
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
@@ -0,0 +1,13 @@
1
+ Metrics/LineLength:
2
+ Description: 'Limit lines to 80 characters.'
3
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#80-character-limits'
4
+ Enabled: false
5
+
6
+ Style/StringLiterals:
7
+ Description: 'Checks if uses of quotes match the configured preference.'
8
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#consistent-string-literals'
9
+ Enabled: false
10
+
11
+ Style/Documentation:
12
+ Description: 'Document classes and non-namespace modules.'
13
+ Enabled: false
@@ -0,0 +1,5 @@
1
+ include:
2
+ - lib/**/*.rb
3
+ exclude:
4
+ - spec/**/*
5
+ - test/**/*
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ group :test do
6
+ gem "simplecov"
7
+ end
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Gamefic
2
+ Copyright (c) 2013 by Fred Snyder for Castwide Technologies
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in
12
+ all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+ THE SOFTWARE.
@@ -0,0 +1,28 @@
1
+ # Gamefic
2
+
3
+ **A Ruby Interactive Fiction Framework**
4
+
5
+ Gamefic is a system for developing and playing adventure games and interactive
6
+ fiction. This gem provides the core library for running game narratives.
7
+
8
+ Developers should refer to the [Gamefic SDK](https://github.com/castwide/gamefic-sdk)
9
+ for information about writing, building, and distributing Gamefic apps.
10
+
11
+ ## Development
12
+
13
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
14
+
15
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
16
+
17
+ ## Contributing
18
+
19
+ Bug reports and pull requests are welcome on GitHub at https://github.com/castwide/gamefic-sdk.
20
+
21
+ ## License
22
+
23
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
24
+
25
+ ## More Information
26
+
27
+ Go to [the official Gamefic website](http://gamefic.com) for games, news, and
28
+ more documentation.
@@ -0,0 +1,10 @@
1
+ require 'rake'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec) do |t|
5
+ t.pattern = Dir.glob('spec/**/*_spec.rb')
6
+ t.rspec_opts = '--format documentation'
7
+ # t.rspec_opts << ' more options'
8
+ #t.rcov = true
9
+ end
10
+ task :default => :spec
@@ -0,0 +1,27 @@
1
+ lib = File.expand_path('../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'gamefic/version'
4
+ require 'date'
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = 'gamefic'
8
+ s.version = Gamefic::VERSION
9
+ s.date = Date.today.strftime("%Y-%m-%d")
10
+ s.summary = "Gamefic"
11
+ s.description = "An adventure game and interactive fiction framework"
12
+ s.authors = ["Fred Snyder"]
13
+ s.email = 'fsnyder@gamefic.com'
14
+ s.homepage = 'http://gamefic.com'
15
+ s.license = 'MIT'
16
+
17
+ s.files = Dir.chdir(File.expand_path('..', __FILE__)) do
18
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
19
+ end
20
+ s.require_paths = ['lib']
21
+
22
+ s.required_ruby_version = '>= 2.1.0'
23
+
24
+ s.add_development_dependency 'rake', '~> 12.3', '>= 12.3'
25
+ s.add_development_dependency 'rspec', '~> 3.5', '>= 3.5.0'
26
+ s.add_development_dependency 'simplecov', '~> 0.14'
27
+ end
@@ -1,9 +1,12 @@
1
- require 'gamefic/matchable'
1
+ require 'gamefic/version'
2
+
3
+ require 'gamefic/keywords'
2
4
  require 'gamefic/core_ext/array'
3
5
  require 'gamefic/core_ext/string'
4
6
 
5
- require 'gamefic/grammar'
6
7
  require 'gamefic/describable'
8
+ require 'gamefic/index'
9
+ require 'gamefic/serialize'
7
10
  require 'gamefic/element'
8
11
  require 'gamefic/entity'
9
12
  require 'gamefic/active'
@@ -12,9 +15,7 @@ require "gamefic/scene"
12
15
  require "gamefic/query"
13
16
  require "gamefic/action"
14
17
  require "gamefic/syntax"
18
+ require 'gamefic/world'
19
+ require 'gamefic/scriptable'
15
20
  require "gamefic/plot"
16
21
  require 'gamefic/subplot'
17
- require "gamefic/engine"
18
- require "gamefic/user"
19
-
20
- require 'gamefic/version'
@@ -5,28 +5,36 @@ module Gamefic
5
5
  end
6
6
 
7
7
  class Action
8
- attr_reader :parameters
9
-
10
- def initialize actor, parameters
8
+ # An array of objects on which the action will operate, e.g., an entity
9
+ # that is a direct object of a command.
10
+ #
11
+ # @return [Array<Object>]
12
+ attr_reader :arguments
13
+ alias parameters arguments
14
+
15
+ def initialize actor, arguments
11
16
  @actor = actor
12
- @parameters = parameters
17
+ @arguments = arguments
13
18
  @executed = false
14
19
  end
15
20
 
16
- # @todo Determine whether to call them parameters, arguments, or both.
17
- def arguments
18
- parameters
19
- end
20
-
21
+ # Perform the action.
22
+ #
21
23
  def execute
22
24
  @executed = true
23
- self.class.executor.call(@actor, *@parameters) unless self.class.executor.nil?
25
+ self.class.executor.call(@actor, *arguments) unless self.class.executor.nil?
24
26
  end
25
27
 
28
+ # True if the #execute method has been called for this action.
29
+ #
30
+ # @return [Boolean]
26
31
  def executed?
27
32
  @executed
28
33
  end
29
34
 
35
+ # The verb associated with this action.
36
+ #
37
+ # @return [Symbol] The symbol representing the verb
30
38
  def verb
31
39
  self.class.verb
32
40
  end
@@ -39,19 +47,17 @@ module Gamefic
39
47
  self.class.rank
40
48
  end
41
49
 
50
+ # True if the action is flagged as meta.
51
+ #
52
+ # @return [Boolean]
42
53
  def meta?
43
54
  self.class.meta?
44
55
  end
45
56
 
46
- def order_key
47
- self.class.order_key
48
- end
49
-
50
- def self.subclass verb, *q, meta: false, order_key: 0, &block
57
+ def self.subclass verb, *q, meta: false, &block
51
58
  act = Class.new(self) do
52
59
  self.verb = verb
53
60
  self.meta = meta
54
- self.order_key = order_key
55
61
  q.each { |q|
56
62
  add_query q
57
63
  }
@@ -87,21 +93,27 @@ module Gamefic
87
93
 
88
94
  def signature
89
95
  # @todo This is clearly unfinished
90
- "#{verb} #{queries.map{|m| m.signature}.join(',')}"
96
+ "#{verb} #{queries.map{|m| m.signature}.join(', ')}"
91
97
  end
92
98
 
99
+ # True if this action is not intended to be performed directly by a
100
+ # character.
101
+ # If the action is hidden, users should not be able to perform it with a
102
+ # direct command. By default, any action whose verb starts with an
103
+ # underscore is hidden.
104
+ #
105
+ # @return [Boolean]
93
106
  def hidden?
94
107
  verb.to_s.start_with?('_')
95
108
  end
96
109
 
110
+ # The proc to call when the action is executed
111
+ #
112
+ # @return [Proc]
97
113
  def executor
98
114
  @executor
99
115
  end
100
116
 
101
- def order_key
102
- @order_key ||= 0
103
- end
104
-
105
117
  def rank
106
118
  if @rank.nil?
107
119
  @rank = 0
@@ -131,11 +143,13 @@ module Gamefic
131
143
  return nil if tokens[i].nil? and matches.remaining == ''
132
144
  matches = p.resolve(actor, "#{matches.remaining} #{tokens[i]}".strip, continued: (i < queries.length - 1))
133
145
  return nil if matches.objects.empty?
146
+ accepted = matches.objects.select{|o| p.accept?(o)}
147
+ return nil if accepted.empty?
134
148
  if p.ambiguous?
135
- result.push matches.objects
149
+ result.push accepted
136
150
  else
137
- return nil if matches.objects.length > 1
138
- result.push matches.objects[0]
151
+ return nil if accepted.length != 1
152
+ result.push accepted.first
139
153
  end
140
154
  i += 1
141
155
  }
@@ -151,10 +165,6 @@ module Gamefic
151
165
  def meta= bool
152
166
  @meta = bool
153
167
  end
154
-
155
- def order_key= num
156
- @order_key = num
157
- end
158
168
  end
159
169
  end
160
170
  end
@@ -1,280 +1,325 @@
1
- module Gamefic
2
- class NotConclusionError < Exception
3
- end
4
-
5
- # The Active module gives entities the ability to perform actions and
6
- # participate in scenes.
7
- #
8
- module Active
9
- # @return [Gamefic::Action]
10
- attr_reader :last_action
11
-
12
- # @return [Gamefic::User::Base]
13
- attr_reader :user
14
-
15
- # @return [Gamefic::Scene::Base]
16
- attr_reader :scene
17
-
18
- attr_reader :next_scene
19
-
20
- # @return [Gamefic::Plot::Playbook]
21
- #attr_accessor :playbook
22
-
23
- # @return [Array<Gamefic::Plot::Playbook>]
24
- def playbooks
25
- @playbooks ||= []
26
- end
27
-
28
- def connect user
29
- @user = user
30
- end
31
-
32
- # An array of actions waiting to be performed.
33
- #
34
- # @return [Array<String>]
35
- def queue
36
- @queue ||= []
37
- end
38
-
39
- # A hash of values representing the state of a performing entity.
40
- #
41
- # @return [Hash]
42
- def state
43
- @state = {}
44
- @state.merge! scene.state unless scene.nil?
45
- @state[:output] = messages
46
- @state
47
- end
48
-
49
- # Send a message to the entity.
50
- # This method will automatically wrap the message in HTML paragraphs.
51
- # To send a message without paragraph formatting, use #stream instead.
52
- #
53
- # @param message [String]
54
- def tell(message)
55
- if buffer_stack > 0
56
- append_buffer message
57
- else
58
- super
59
- end
60
- end
61
-
62
- # Send a message to the Character as raw text.
63
- # Unlike #tell, this method will not wrap the message in HTML paragraphs.
64
- #
65
- # @param message [String]
66
- def stream(message)
67
- if buffer_stack > 0
68
- append_buffer message
69
- else
70
- super
71
- end
72
- end
73
-
74
- # Perform a command.
75
- # The command can be specified as a String or a verb with a list of
76
- # parameters. Either form should yield the same result, but the
77
- # verb/parameter form can yield better performance since it bypasses the
78
- # parser.
79
- #
80
- # The command will be executed immediately regardless of the entity's
81
- # state.
82
- #
83
- # @example Send a command as a string
84
- # character.perform "take the key"
85
- #
86
- # @example Send a command as a verb with parameters
87
- # character.perform :take, @key
88
- #
89
- # @return [Gamefic::Action]
90
- def perform(*command)
91
- actions = []
92
- playbooks.reverse.each { |p| actions.concat p.dispatch(self, *command) }
93
- execute_stack actions
94
- end
95
-
96
- # Quietly perform a command.
97
- # This method executes the command exactly as #perform does, except it
98
- # buffers the resulting output instead of sending it to the user.
99
- #
100
- # @return [String] The output that resulted from performing the command.
101
- def quietly(*command)
102
- if buffer_stack == 0
103
- clear_buffer
104
- end
105
- set_buffer_stack buffer_stack + 1
106
- self.perform *command
107
- set_buffer_stack buffer_stack - 1
108
- buffer
109
- end
110
-
111
- # Perform an action.
112
- # This is functionally identical to the `perform` method, except the
113
- # action must be declared as a verb with a list of parameters. Use
114
- # `perform` if you need to parse a string as a command.
115
- #
116
- # The command will be executed immediately regardless of the entity's
117
- # state.
118
- #
119
- # @example
120
- # character.execute :take, @key
121
- #
122
- # @return [Gamefic::Action]
123
- def execute(verb, *params, quietly: false)
124
- actions = []
125
- playbooks.reverse.each { |p| actions.concat p.dispatch_from_params(self, verb, params) }
126
- execute_stack actions, quietly: quietly
127
- end
128
-
129
- # Proceed to the next Action in the current stack.
130
- # This method is typically used in Action blocks to cascade through
131
- # multiple implementations of the same verb.
132
- #
133
- # @example Proceed through two implementations of a verb
134
- # introduction do |actor|
135
- # actor[:has_eaten] = false # Initial value
136
- # end
137
- # respond :eat do |actor|
138
- # actor.tell "You eat something."
139
- # actor[:has_eaten] = true
140
- # end
141
- # respond :eat do |actor|
142
- # # This version will be executed first because it was implemented last
143
- # if actor[:has_eaten]
144
- # actor.tell "You already ate."
145
- # else
146
- # actor.proceed # Execute the previous implementation
147
- # end
148
- # end
149
- #
150
- def proceed quietly: false
151
- #Director::Delegate.proceed_for self
152
- return if performance_stack.empty?
153
- a = performance_stack.last.shift
154
- unless a.nil?
155
- if quietly
156
- if @buffer_stack == 0
157
- @buffer = ""
158
- end
159
- @buffer_stack += 1
160
- end
161
- a.execute
162
- if quietly
163
- @buffer_stack -= 1
164
- @buffer
165
- end
166
- end
167
- end
168
-
169
- # Immediately start a new scene for the character.
170
- # Use #prepare if you want to declare a scene to be started at the
171
- # beginning of the next turn.
172
- #
173
- def cue new_scene
174
- @next_scene = nil
175
- if new_scene.nil?
176
- @scene = nil
177
- else
178
- @scene = new_scene.new(self)
179
- @scene.start
180
- end
181
- end
182
-
183
- # Prepare a scene to be started for this character at the beginning of the
184
- # next turn.
185
- #
186
- def prepare s
187
- @next_scene = s
188
- end
189
-
190
- # Return true if the character is expected to be in the specified scene on
191
- # the next turn.
192
- #
193
- # @return [Boolean]
194
- def will_cue? scene
195
- (@scene.class == scene and @next_scene.nil?) or @next_scene == scene
196
- end
197
-
198
- # Cue a conclusion. This method works like #cue, except it will raise a
199
- # NotConclusionError if the scene is not a Scene::Conclusion.
200
- #
201
- def conclude scene
202
- raise NotConclusionError unless scene <= Scene::Conclusion
203
- cue scene
204
- end
205
-
206
- # True if the character is in a conclusion.
207
- #
208
- # @return [Boolean]
209
- def concluded?
210
- !scene.nil? and scene.kind_of?(Scene::Conclusion)
211
- end
212
-
213
- def performed order
214
- order.freeze
215
- @last_action = order
216
- end
217
-
218
- def accessible?
219
- false
220
- end
221
-
222
- def inspect
223
- to_s
224
- end
225
-
226
- private
227
-
228
- def execute_stack actions, quietly: false
229
- return nil if actions.empty?
230
- a = actions.first
231
- okay = true
232
- unless a.meta?
233
- playbooks.reverse.each do |playbook|
234
- okay = validate_playbook playbook, a
235
- break unless okay
236
- end
237
- end
238
- if okay
239
- performance_stack.push actions
240
- proceed quietly: quietly
241
- performance_stack.pop
242
- end
243
- a
244
- end
245
-
246
- def validate_playbook playbook, action
247
- okay = true
248
- playbook.validators.each { |v|
249
- result = v.call(self, action.verb, action.parameters)
250
- okay = (result != false)
251
- break unless okay
252
- }
253
- okay
254
- end
255
-
256
- def buffer_stack
257
- @buffer_stack ||= 0
258
- end
259
-
260
- def set_buffer_stack num
261
- @buffer_stack = num
262
- end
263
-
264
- def buffer
265
- @buffer ||= ''
266
- end
267
-
268
- def append_buffer str
269
- @buffer += str
270
- end
271
-
272
- def clear_buffer
273
- @buffer = '' unless @buffer.empty?
274
- end
275
-
276
- def performance_stack
277
- @performance_stack ||= []
278
- end
279
- end
280
- end
1
+ module Gamefic
2
+ class NotConclusionError < RuntimeError; end
3
+
4
+ # The Active module gives entities the ability to perform actions and
5
+ # participate in scenes. The Actor class, for example, is an Entity
6
+ # subclass that includes this module.
7
+ #
8
+ module Active
9
+ # The last action executed by the entity, as reported by the
10
+ # Active#performed method.
11
+ #
12
+ # @return [Gamefic::Action]
13
+ attr_reader :last_action
14
+
15
+ # The scene in which the entity is currently participating.
16
+ #
17
+ # @return [Gamefic::Scene::Base]
18
+ attr_reader :scene
19
+
20
+ # The scene class that will be cued for this entity on the next turn.
21
+ # Usually set with the #prepare method.
22
+ #
23
+ # @return [Class<Gamefic::Scene::Base>]
24
+ attr_reader :next_scene
25
+
26
+ attr_reader :next_options
27
+
28
+ # The prompt for the previous scene.
29
+ #
30
+ # @return [String]
31
+ attr_accessor :last_prompt
32
+
33
+ # The input for the previous scene.
34
+ #
35
+ # @return [String]
36
+ attr_accessor :last_input
37
+
38
+ # The playbooks that will be used to perform commands.
39
+ #
40
+ # @return [Array<Gamefic::World::Playbook>]
41
+ def playbooks
42
+ @playbooks ||= []
43
+ end
44
+
45
+ def syntaxes
46
+ playbooks.map(&:syntaxes).flatten
47
+ end
48
+
49
+ # An array of actions waiting to be performed.
50
+ #
51
+ # @return [Array<String>]
52
+ def queue
53
+ @queue ||= []
54
+ end
55
+
56
+ # A hash of values representing the state of a performing entity.
57
+ #
58
+ # @return [Hash{Symbol => Object}]
59
+ def state
60
+ @state ||= {}
61
+ end
62
+
63
+ def output
64
+ @output ||= {}
65
+ end
66
+
67
+ # Send a message to the entity.
68
+ # This method will automatically wrap the message in HTML paragraphs.
69
+ # To send a message without paragraph formatting, use #stream instead.
70
+ #
71
+ # @param message [String]
72
+ def tell(message)
73
+ if buffer_stack > 0
74
+ append_buffer format(message)
75
+ else
76
+ super
77
+ end
78
+ end
79
+
80
+ # Send a message to the Character as raw text.
81
+ # Unlike #tell, this method will not wrap the message in HTML paragraphs.
82
+ #
83
+ # @param message [String]
84
+ def stream(message)
85
+ if buffer_stack > 0
86
+ append_buffer message
87
+ else
88
+ super
89
+ end
90
+ end
91
+
92
+ # Perform a command.
93
+ # The command can be specified as a String or a verb with a list of
94
+ # parameters. Either form should yield the same result, but the
95
+ # verb/parameter form can yield better performance since it bypasses the
96
+ # parser.
97
+ #
98
+ # The command will be executed immediately regardless of the entity's
99
+ # state.
100
+ #
101
+ # @example Send a command as a string
102
+ # character.perform "take the key"
103
+ #
104
+ # @example Send a command as a verb with parameters
105
+ # character.perform :take, @key
106
+ #
107
+ # @return [Gamefic::Action]
108
+ def perform(*command)
109
+ actions = []
110
+ playbooks.reverse.each { |p| actions.concat p.dispatch(self, *command) }
111
+ execute_stack actions
112
+ end
113
+
114
+ # Quietly perform a command.
115
+ # This method executes the command exactly as #perform does, except it
116
+ # buffers the resulting output instead of sending it to the user.
117
+ #
118
+ # @return [String] The output that resulted from performing the command.
119
+ def quietly(*command)
120
+ if buffer_stack == 0
121
+ clear_buffer
122
+ end
123
+ set_buffer_stack buffer_stack + 1
124
+ self.perform *command
125
+ set_buffer_stack buffer_stack - 1
126
+ buffer
127
+ end
128
+
129
+ # Perform an action.
130
+ # This is functionally identical to the `perform` method, except the
131
+ # action must be declared as a verb with a list of parameters. Use
132
+ # `perform` if you need to parse a string as a command.
133
+ #
134
+ # The command will be executed immediately regardless of the entity's
135
+ # state.
136
+ #
137
+ # @example
138
+ # character.execute :take, @key
139
+ #
140
+ # @return [Gamefic::Action]
141
+ def execute(verb, *params, quietly: false)
142
+ actions = []
143
+ playbooks.reverse.each { |p| actions.concat p.dispatch_from_params(self, verb, params) }
144
+ execute_stack actions, quietly: quietly
145
+ end
146
+
147
+ # Proceed to the next Action in the current stack.
148
+ # This method is typically used in Action blocks to cascade through
149
+ # multiple implementations of the same verb.
150
+ #
151
+ # @example Proceed through two implementations of a verb
152
+ # introduction do |actor|
153
+ # actor[:has_eaten] = false # Initial value
154
+ # end
155
+ # respond :eat do |actor|
156
+ # actor.tell "You eat something."
157
+ # actor[:has_eaten] = true
158
+ # end
159
+ # respond :eat do |actor|
160
+ # # This version will be executed first because it was implemented last
161
+ # if actor[:has_eaten]
162
+ # actor.tell "You already ate."
163
+ # else
164
+ # actor.proceed # Execute the previous implementation
165
+ # end
166
+ # end
167
+ #
168
+ def proceed quietly: false
169
+ return if performance_stack.empty?
170
+ a = performance_stack.last.shift
171
+ unless a.nil?
172
+ if quietly
173
+ if buffer_stack == 0
174
+ @buffer = ""
175
+ end
176
+ set_buffer_stack(buffer_stack + 1)
177
+ end
178
+ a.execute
179
+ if quietly
180
+ set_buffer_stack(buffer_stack - 1)
181
+ @buffer
182
+ end
183
+ end
184
+ end
185
+
186
+ # Immediately start a new scene for the character.
187
+ # Use #prepare if you want to declare a scene to be started at the
188
+ # beginning of the next turn.
189
+ #
190
+ # @param new_scene [Class]
191
+ def cue new_scene, **options
192
+ @next_scene = nil
193
+ if new_scene.nil?
194
+ @scene = nil
195
+ else
196
+ @scene = new_scene.new(self, **options)
197
+ @scene.start
198
+ end
199
+ end
200
+
201
+ # Prepare a scene to be started for this character at the beginning of the
202
+ # next turn. As opposed to #cue, a prepared scene will not start until the
203
+ # current scene finishes.
204
+ #
205
+ # @param new_scene [Class]
206
+ def prepare new_scene, **options
207
+ @next_scene = new_scene
208
+ @next_options = options
209
+ end
210
+
211
+ # Return true if the character is expected to be in the specified scene on
212
+ # the next turn.
213
+ #
214
+ # @return [Boolean]
215
+ def will_cue? scene
216
+ (@scene.class == scene and @next_scene.nil?) || @next_scene == scene
217
+ end
218
+
219
+ # Cue a conclusion. This method works like #cue, except it will raise a
220
+ # NotConclusionError if the scene is not a Scene::Conclusion.
221
+ #
222
+ def conclude scene
223
+ raise NotConclusionError unless scene <= Scene::Conclusion
224
+ cue scene
225
+ end
226
+
227
+ # True if the character is in a conclusion.
228
+ #
229
+ # @return [Boolean]
230
+ def concluded?
231
+ !scene.nil? && scene.kind_of?(Scene::Conclusion)
232
+ end
233
+
234
+ # Record the last action the entity executed. This method is typically
235
+ # called when the entity performs an action in response to user input.
236
+ #
237
+ def performed action
238
+ action.freeze
239
+ @last_action = action
240
+ end
241
+
242
+ def accessible?
243
+ false
244
+ end
245
+
246
+ def inspect
247
+ to_s
248
+ end
249
+
250
+ # Track the entity's performance of a scene.
251
+ #
252
+ def entered scene
253
+ klass = (scene.kind_of?(Gamefic::Scene::Base) ? scene.class : scene)
254
+ entered_scenes.push klass unless entered_scenes.include?(klass)
255
+ end
256
+
257
+ # Determine whether the entity has performed the specified scene.
258
+ #
259
+ # @return [Boolean]
260
+ def entered? scene
261
+ klass = (scene.kind_of?(Gamefic::Scene::Base) ? scene.class : scene)
262
+ entered_scenes.include?(klass)
263
+ end
264
+
265
+ private
266
+
267
+ # @return [Array<Gamefic::Scene::Base>]
268
+ def entered_scenes
269
+ @entered_scenes ||= []
270
+ end
271
+
272
+ def execute_stack actions, quietly: false
273
+ return nil if actions.empty?
274
+ a = actions.first
275
+ okay = true
276
+ unless a.meta?
277
+ playbooks.reverse.each do |playbook|
278
+ okay = validate_playbook playbook, a
279
+ break unless okay
280
+ end
281
+ end
282
+ if okay
283
+ performance_stack.push actions
284
+ proceed quietly: quietly
285
+ performance_stack.pop
286
+ end
287
+ a
288
+ end
289
+
290
+ def validate_playbook playbook, action
291
+ okay = true
292
+ playbook.validators.each { |v|
293
+ result = v.call(self, action.verb, action.parameters)
294
+ okay = (result != false)
295
+ break unless okay
296
+ }
297
+ okay
298
+ end
299
+
300
+ def buffer_stack
301
+ @buffer_stack ||= 0
302
+ end
303
+
304
+ def set_buffer_stack num
305
+ @buffer_stack = num
306
+ end
307
+
308
+ # @return [String]
309
+ def buffer
310
+ @buffer ||= ''
311
+ end
312
+
313
+ def append_buffer str
314
+ @buffer += str
315
+ end
316
+
317
+ def clear_buffer
318
+ @buffer = ''
319
+ end
320
+
321
+ def performance_stack
322
+ @performance_stack ||= []
323
+ end
324
+ end
325
+ end