gamefic 1.6.0 → 2.0.3

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 (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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: ce1dd2ec32bb2872b9c18a40cfdf92ac745506a4
4
- data.tar.gz: 264aa06dc79054f22d07fe4552b6341c05a4c82e
2
+ SHA256:
3
+ metadata.gz: 1ac7da649347939f4c5baadc676151b8a8c73714552c48a290de75bd072bb801
4
+ data.tar.gz: 6d83150ed62df24a4056e3e54ff5b499bc5f7a69ebe8d5cfaddb261f64573af4
5
5
  SHA512:
6
- metadata.gz: ad1365b02676307134c0a141dbe9003989093b2e78f590affd2c5bbaa237758ae1357e3c156551e36f65b22c44bd0f81a06b940e655075bf529dcea59b75fc6a
7
- data.tar.gz: fc741d054f2d4e187d6e51fc96ef4a59354dfc6bea21c39adfcf7a45a5e48195fcae2017c3543bd56d9e4dfbd89bfb584e4a3e8349145bb869878cbd178eb12b
6
+ metadata.gz: e068317caa9fbfb1f8a73bde4725f5df3f3bcad95ba87d44c8f973759cdc991e0fbbcea874fd451eacbd6ffd2ea2ade9144e61f2d358f31fec6a7f1e97ca3a10
7
+ data.tar.gz: 1ab813a5cb14307bc844c42424c9503bae6f73a48d12979a5369812619e5a2e80dfab2ac6181401045511286743fc20f35968bd46e5071cf24f296e3c1c20e36
@@ -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,16 @@
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
14
+
15
+ Style/MethodDefParentheses:
16
+ Enabled: false
@@ -0,0 +1,5 @@
1
+ include:
2
+ - lib/**/*.rb
3
+ exclude:
4
+ - spec/**/*
5
+ - test/**/*
@@ -0,0 +1,6 @@
1
+ # 2.0.3 - December 14, 2020
2
+ - Remove unused Index class
3
+ - Active#conclude accepts data argument
4
+
5
+ # 2.0.2 - April 25, 2020
6
+ - Improved snapshot serialization
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,17 +1,20 @@
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'
7
+ require 'gamefic/describable'
8
+ require 'gamefic/serialize'
9
+ require 'gamefic/element'
6
10
  require 'gamefic/entity'
7
- require 'gamefic/character'
11
+ require 'gamefic/active'
12
+ require 'gamefic/actor'
8
13
  require "gamefic/scene"
9
14
  require "gamefic/query"
10
15
  require "gamefic/action"
11
16
  require "gamefic/syntax"
12
- require "gamefic/plot"
17
+ require 'gamefic/world'
18
+ require 'gamefic/scriptable'
19
+ require 'gamefic/plot'
13
20
  require 'gamefic/subplot'
14
- require "gamefic/engine"
15
- require "gamefic/user"
16
-
17
- require 'gamefic/version'
@@ -1,32 +1,35 @@
1
1
  module Gamefic
2
- # Exception raised when the Action's proc arity is not compatible with the
3
- # number of queries
4
- class ActionArgumentError < ArgumentError
5
- end
6
-
7
2
  class Action
8
- attr_reader :parameters
9
-
10
- def initialize actor, parameters
3
+ # An array of objects on which the action will operate, e.g., an entity
4
+ # that is a direct object of a command.
5
+ #
6
+ # @return [Array<Object>]
7
+ attr_reader :arguments
8
+ alias parameters arguments
9
+
10
+ def initialize actor, arguments
11
11
  @actor = actor
12
- @parameters = parameters
12
+ @arguments = arguments
13
13
  @executed = false
14
14
  end
15
15
 
16
- # @todo Determine whether to call them parameters, arguments, or both.
17
- def arguments
18
- parameters
19
- end
20
-
16
+ # Perform the action.
17
+ #
21
18
  def execute
22
19
  @executed = true
23
- self.class.executor.call(@actor, *@parameters) unless self.class.executor.nil?
20
+ self.class.executor.call(@actor, *arguments) unless self.class.executor.nil?
24
21
  end
25
22
 
23
+ # True if the #execute method has been called for this action.
24
+ #
25
+ # @return [Boolean]
26
26
  def executed?
27
27
  @executed
28
28
  end
29
29
 
30
+ # The verb associated with this action.
31
+ #
32
+ # @return [Symbol] The symbol representing the verb
30
33
  def verb
31
34
  self.class.verb
32
35
  end
@@ -39,34 +42,39 @@ module Gamefic
39
42
  self.class.rank
40
43
  end
41
44
 
45
+ # True if the action is flagged as meta.
46
+ #
47
+ # @return [Boolean]
42
48
  def meta?
43
49
  self.class.meta?
44
50
  end
45
51
 
46
- def order_key
47
- self.class.order_key
48
- end
49
-
50
- def self.subclass verb, *q, meta: false, order_key: 0, &block
52
+ # @param verb [Symbol]
53
+ # @param queries [Array<Gamefic::Query::Base>]
54
+ # @param meta [Boolean]
55
+ # @return [Class<Action>]
56
+ def self.subclass verb, *queries, meta: false, &block
51
57
  act = Class.new(self) do
52
58
  self.verb = verb
53
59
  self.meta = meta
54
- self.order_key = order_key
55
- q.each { |q|
60
+ queries.each do |q|
56
61
  add_query q
57
- }
62
+ end
58
63
  on_execute &block
59
64
  end
60
- if !block.nil? and act.queries.length + 1 != block.arity and block.arity > 0
61
- raise ActionArgumentError.new("Number of parameters is not compatible with proc arguments")
65
+ if !block.nil? && act.queries.length + 1 != block.arity && block.arity > 0
66
+ raise ArgumentError.new("Number of parameters is not compatible with proc arguments")
62
67
  end
63
68
  act
64
69
  end
65
70
 
66
71
  class << self
67
- def verb
68
- @verb
69
- end
72
+ attr_reader :verb
73
+
74
+ # The proc to call when the action is executed
75
+ #
76
+ # @return [Proc]
77
+ attr_reader :executor
70
78
 
71
79
  def meta?
72
80
  @meta ||= false
@@ -87,23 +95,27 @@ module Gamefic
87
95
 
88
96
  def signature
89
97
  # @todo This is clearly unfinished
90
- "#{verb} #{queries.map{|m| m.signature}.join(',')}"
98
+ "#{verb} #{queries.map {|m| m.signature}.join(', ')}"
91
99
  end
92
100
 
93
- def executor
94
- @executor
95
- end
96
-
97
- def order_key
98
- @order_key ||= 0
101
+ # True if this action is not intended to be performed directly by a
102
+ # character.
103
+ # If the action is hidden, users should not be able to perform it with a
104
+ # direct command. By default, any action whose verb starts with an
105
+ # underscore is hidden.
106
+ #
107
+ # @return [Boolean]
108
+ def hidden?
109
+ verb.to_s.start_with?('_')
99
110
  end
100
111
 
112
+ # @return [Integer]
101
113
  def rank
102
114
  if @rank.nil?
103
115
  @rank = 0
104
- queries.each { |q|
116
+ queries.each do |q|
105
117
  @rank += (q.rank + 1)
106
- }
118
+ end
107
119
  @rank -= 1000 if verb.nil?
108
120
  end
109
121
  @rank
@@ -112,45 +124,43 @@ module Gamefic
112
124
  def valid? actor, objects
113
125
  return false if objects.length != queries.length
114
126
  i = 0
115
- queries.each { |p|
127
+ queries.each do |p|
116
128
  return false unless p.include?(actor, objects[i])
117
129
  i += 1
118
- }
130
+ end
119
131
  true
120
132
  end
121
133
 
134
+ # Return an instance of this Action if the actor can execute it with the
135
+ # provided tokens, or nil if the tokens are invalid.
136
+ #
137
+ # @param action [Gamefic::Entity]
138
+ # @param tokens [Array<String>]
139
+ # @return [self, nil]
122
140
  def attempt actor, tokens
123
- i = 0
124
141
  result = []
125
142
  matches = Gamefic::Query::Matches.new([], '', '')
126
- queries.each { |p|
127
- return nil if tokens[i].nil? and matches.remaining == ''
143
+ queries.each_with_index do |p, i|
144
+ return nil if tokens[i].nil? && matches.remaining == ''
128
145
  matches = p.resolve(actor, "#{matches.remaining} #{tokens[i]}".strip, continued: (i < queries.length - 1))
129
146
  return nil if matches.objects.empty?
147
+ accepted = matches.objects.select { |o| p.accept?(o) }
148
+ return nil if accepted.empty?
130
149
  if p.ambiguous?
131
- result.push matches.objects
150
+ result.push accepted
132
151
  else
133
- return nil if matches.objects.length > 1
134
- result.push matches.objects[0]
152
+ return nil if accepted.length != 1
153
+ result.push accepted.first
135
154
  end
136
- i += 1
137
- }
138
- self.new(actor, result)
155
+ end
156
+ new(actor, result)
139
157
  end
140
158
 
141
159
  protected
142
160
 
143
- def verb= sym
144
- @verb = sym
145
- end
161
+ attr_writer :verb
146
162
 
147
- def meta= bool
148
- @meta = bool
149
- end
150
-
151
- def order_key= num
152
- @order_key = num
153
- end
163
+ attr_writer :meta
154
164
  end
155
165
  end
156
166
  end
@@ -0,0 +1,331 @@
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
+ clear_buffer if buffer_stack == 0
121
+ set_buffer_stack buffer_stack + 1
122
+ self.perform *command
123
+ set_buffer_stack buffer_stack - 1
124
+ buffer
125
+ end
126
+
127
+ # Perform an action.
128
+ # This is functionally identical to the `perform` method, except the
129
+ # action must be declared as a verb with a list of parameters. Use
130
+ # `perform` if you need to parse a string as a command.
131
+ #
132
+ # The command will be executed immediately regardless of the entity's
133
+ # state.
134
+ #
135
+ # @example
136
+ # character.execute :take, @key
137
+ #
138
+ # @return [Gamefic::Action]
139
+ def execute(verb, *params, quietly: false)
140
+ actions = []
141
+ playbooks.reverse.each { |p| actions.concat p.dispatch_from_params(self, verb, params) }
142
+ execute_stack actions, quietly: quietly
143
+ end
144
+
145
+ # Proceed to the next Action in the current stack.
146
+ # This method is typically used in Action blocks to cascade through
147
+ # multiple implementations of the same verb.
148
+ #
149
+ # @example Proceed through two implementations of a verb
150
+ # introduction do |actor|
151
+ # actor[:has_eaten] = false # Initial value
152
+ # end
153
+ #
154
+ # respond :eat do |actor|
155
+ # actor.tell "You eat something."
156
+ # actor[:has_eaten] = true
157
+ # end
158
+ #
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<Scene::Base>]
191
+ # @param data [Hash] Additional scene data
192
+ def cue new_scene, **data
193
+ @next_scene = nil
194
+ if new_scene.nil?
195
+ @scene = nil
196
+ else
197
+ @scene = new_scene.new(self, **data)
198
+ @scene.start
199
+ end
200
+ end
201
+
202
+ # Prepare a scene to be started for this character at the beginning of the
203
+ # next turn. As opposed to #cue, a prepared scene will not start until the
204
+ # current scene finishes.
205
+ #
206
+ # @param new_scene [Class<Scene::Base>]
207
+ # @oaram data [Hash] Additional scene data
208
+ def prepare new_scene, **data
209
+ @next_scene = new_scene
210
+ @next_options = data
211
+ end
212
+
213
+ # Return true if the character is expected to be in the specified scene on
214
+ # the next turn.
215
+ #
216
+ # @return [Boolean]
217
+ def will_cue? scene
218
+ (@scene.class == scene and @next_scene.nil?) || @next_scene == scene
219
+ end
220
+
221
+ # Cue a conclusion. This method works like #cue, except it will raise a
222
+ # NotConclusionError if the scene is not a Scene::Conclusion.
223
+ #
224
+ # @param new_scene [Class<Scene::Base>]
225
+ # @oaram data [Hash] Additional scene data
226
+ def conclude new_scene, **data
227
+ raise NotConclusionError unless new_scene <= Scene::Conclusion
228
+ cue new_scene, **data
229
+ end
230
+
231
+ # True if the character is in a conclusion.
232
+ #
233
+ # @return [Boolean]
234
+ def concluded?
235
+ !scene.nil? && scene.kind_of?(Scene::Conclusion)
236
+ end
237
+
238
+ # Record the last action the entity executed. This method is typically
239
+ # called when the entity performs an action in response to user input.
240
+ #
241
+ def performed action
242
+ action.freeze
243
+ @last_action = action
244
+ end
245
+
246
+ def accessible?
247
+ false
248
+ end
249
+
250
+ def inspect
251
+ to_s
252
+ end
253
+
254
+ # Track the entity's performance of a scene.
255
+ #
256
+ def entered scene
257
+ klass = (scene.kind_of?(Gamefic::Scene::Base) ? scene.class : scene)
258
+ entered_scenes.push klass unless entered_scenes.include?(klass)
259
+ end
260
+
261
+ # Determine whether the entity has performed the specified scene.
262
+ #
263
+ # @return [Boolean]
264
+ def entered? scene
265
+ klass = (scene.kind_of?(Gamefic::Scene::Base) ? scene.class : scene)
266
+ entered_scenes.include?(klass)
267
+ end
268
+
269
+ private
270
+
271
+ # @return [Array<Gamefic::Scene::Base>]
272
+ def entered_scenes
273
+ @entered_scenes ||= []
274
+ end
275
+
276
+ # @param actions [Array<Gamefic::Action>]
277
+ # @param quietly [Boolean]
278
+ def execute_stack actions, quietly: false
279
+ return nil if actions.empty?
280
+ a = actions.first
281
+ okay = true
282
+ unless a.meta?
283
+ playbooks.reverse.each do |playbook|
284
+ okay = validate_playbook playbook, a
285
+ break unless okay
286
+ end
287
+ end
288
+ if okay
289
+ performance_stack.push actions
290
+ proceed quietly: quietly
291
+ performance_stack.pop
292
+ end
293
+ a
294
+ end
295
+
296
+ def validate_playbook playbook, action
297
+ okay = true
298
+ playbook.validators.each { |v|
299
+ result = v.call(self, action.verb, action.parameters)
300
+ okay = (result != false)
301
+ break unless okay
302
+ }
303
+ okay
304
+ end
305
+
306
+ def buffer_stack
307
+ @buffer_stack ||= 0
308
+ end
309
+
310
+ def set_buffer_stack num
311
+ @buffer_stack = num
312
+ end
313
+
314
+ # @return [String]
315
+ def buffer
316
+ @buffer ||= ''
317
+ end
318
+
319
+ def append_buffer str
320
+ @buffer += str
321
+ end
322
+
323
+ def clear_buffer
324
+ @buffer = ''
325
+ end
326
+
327
+ def performance_stack
328
+ @performance_stack ||= []
329
+ end
330
+ end
331
+ end