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
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: ad70329086d7b1e9dd06a8459c5b1ee95952ab5453f63afd4e7a87df6ba90e69
4
+ data.tar.gz: 443a8642312dd1da33612a78ce1598723a9336b46e55b840636c2f41804eb8c5
5
5
  SHA512:
6
- metadata.gz: 12f2199a4d803e7a179b49e1d50f6e83571f21d601e67828546de918c65b3da0ee97790e66ee5f73f34b1a901d515284e3c8bd030ff0dc5d816678b7c9a0ba48
7
- data.tar.gz: 31118c91be341821662fe8624b85924b196c52f9b2bcd0c758f5af13fc9d1dceaa76f204d6f6f3d2e274ef049153b416dcd2648a207e4ad07ea02ea4b9b440c4
6
+ metadata.gz: 6c207722b0d00906579e9a221d0f8fad831a8f35694ac2252569d427d2846e113beda49c3e160083013284e6f5c85354f9959b49fceb4383100c451de61dc882
7
+ data.tar.gz: 899b4605d74e236a6904d80a6218c6c607c78594357124bb2b36ea12557bee1c485e76bde2eb8ed1f29359f7d6a6f32ca1d7c2294b0d56f9d1109ad195f7c890
data/.gitignore ADDED
@@ -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
data/.rubocop.yml ADDED
@@ -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
data/.solargraph.yml ADDED
@@ -0,0 +1,5 @@
1
+ include:
2
+ - lib/**/*.rb
3
+ exclude:
4
+ - spec/**/*
5
+ - test/**/*
data/CHANGELOG.md ADDED
@@ -0,0 +1,10 @@
1
+ # 2.1.0 - June 21, 2021
2
+ - Remove redundant MultipleChoice prompt
3
+ - Deprecate Scene::Custom
4
+
5
+ # 2.0.3 - December 14, 2020
6
+ - Remove unused Index class
7
+ - Active#conclude accepts data argument
8
+
9
+ # 2.0.2 - April 25, 2020
10
+ - 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.
data/README.md ADDED
@@ -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.
data/Rakefile ADDED
@@ -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
data/gamefic.gemspec ADDED
@@ -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
data/lib/gamefic.rb CHANGED
@@ -1,9 +1,11 @@
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/serialize'
7
9
  require 'gamefic/element'
8
10
  require 'gamefic/entity'
9
11
  require 'gamefic/active'
@@ -12,9 +14,7 @@ require "gamefic/scene"
12
14
  require "gamefic/query"
13
15
  require "gamefic/action"
14
16
  require "gamefic/syntax"
15
- require "gamefic/plot"
17
+ require 'gamefic/world'
18
+ require 'gamefic/scriptable'
19
+ require 'gamefic/plot'
16
20
  require 'gamefic/subplot'
17
- require "gamefic/engine"
18
- require "gamefic/user"
19
-
20
- 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,27 +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
 
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]
93
108
  def hidden?
94
109
  verb.to_s.start_with?('_')
95
110
  end
96
111
 
97
- def executor
98
- @executor
99
- end
100
-
101
- def order_key
102
- @order_key ||= 0
103
- end
104
-
112
+ # @return [Integer]
105
113
  def rank
106
114
  if @rank.nil?
107
115
  @rank = 0
108
- queries.each { |q|
116
+ queries.each do |q|
109
117
  @rank += (q.rank + 1)
110
- }
118
+ end
111
119
  @rank -= 1000 if verb.nil?
112
120
  end
113
121
  @rank
@@ -116,45 +124,43 @@ module Gamefic
116
124
  def valid? actor, objects
117
125
  return false if objects.length != queries.length
118
126
  i = 0
119
- queries.each { |p|
127
+ queries.each do |p|
120
128
  return false unless p.include?(actor, objects[i])
121
129
  i += 1
122
- }
130
+ end
123
131
  true
124
132
  end
125
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]
126
140
  def attempt actor, tokens
127
- i = 0
128
141
  result = []
129
142
  matches = Gamefic::Query::Matches.new([], '', '')
130
- queries.each { |p|
131
- 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 == ''
132
145
  matches = p.resolve(actor, "#{matches.remaining} #{tokens[i]}".strip, continued: (i < queries.length - 1))
133
146
  return nil if matches.objects.empty?
147
+ accepted = matches.objects.select { |o| p.accept?(o) }
148
+ return nil if accepted.empty?
134
149
  if p.ambiguous?
135
- result.push matches.objects
150
+ result.push accepted
136
151
  else
137
- return nil if matches.objects.length > 1
138
- result.push matches.objects[0]
152
+ return nil if accepted.length != 1
153
+ result.push accepted.first
139
154
  end
140
- i += 1
141
- }
142
- self.new(actor, result)
155
+ end
156
+ new(actor, result)
143
157
  end
144
158
 
145
159
  protected
146
160
 
147
- def verb= sym
148
- @verb = sym
149
- end
161
+ attr_writer :verb
150
162
 
151
- def meta= bool
152
- @meta = bool
153
- end
154
-
155
- def order_key= num
156
- @order_key = num
157
- end
163
+ attr_writer :meta
158
164
  end
159
165
  end
160
166
  end
@@ -1,280 +1,331 @@
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
+ 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