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
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