gamefic 2.4.0 → 3.0.0
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.
- checksums.yaml +4 -4
- data/.github/workflows/rspec.yml +41 -40
- data/.rspec-opal +2 -0
- data/.solargraph.yml +20 -3
- data/CHANGELOG.md +9 -0
- data/Rakefile +11 -1
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/gamefic.gemspec +5 -2
- data/lib/gamefic/action.rb +52 -183
- data/lib/gamefic/active/cue.rb +25 -0
- data/lib/gamefic/active/epic.rb +68 -0
- data/lib/gamefic/active/messaging.rb +43 -0
- data/lib/gamefic/active/take.rb +69 -0
- data/lib/gamefic/active.rb +95 -192
- data/lib/gamefic/actor.rb +2 -0
- data/lib/gamefic/block.rb +28 -0
- data/lib/gamefic/command.rb +16 -6
- data/lib/gamefic/core_ext/array.rb +4 -4
- data/lib/gamefic/core_ext/string.rb +10 -5
- data/lib/gamefic/describable.rb +39 -65
- data/lib/gamefic/dispatcher.rb +63 -32
- data/lib/gamefic/entity.rb +44 -19
- data/lib/gamefic/logging.rb +32 -0
- data/lib/gamefic/messenger.rb +66 -0
- data/lib/gamefic/narrative.rb +104 -0
- data/lib/gamefic/node.rb +44 -53
- data/lib/gamefic/plot.rb +60 -93
- data/lib/gamefic/props/default.rb +41 -0
- data/lib/gamefic/props/multiple_choice.rb +65 -0
- data/lib/gamefic/props/pause.rb +11 -0
- data/lib/gamefic/props/yes_or_no.rb +21 -0
- data/lib/gamefic/props.rb +10 -0
- data/lib/gamefic/query/base.rb +45 -126
- data/lib/gamefic/query/general.rb +46 -0
- data/lib/gamefic/query/result.rb +20 -0
- data/lib/gamefic/query/scoped.rb +41 -0
- data/lib/gamefic/query/text.rb +30 -31
- data/lib/gamefic/query.rb +7 -15
- data/lib/gamefic/response.rb +118 -0
- data/lib/gamefic/rulebook/calls.rb +90 -0
- data/lib/gamefic/rulebook/events.rb +79 -0
- data/lib/gamefic/rulebook/hooks.rb +57 -0
- data/lib/gamefic/rulebook/scenes.rb +68 -0
- data/lib/gamefic/rulebook.rb +139 -0
- data/lib/gamefic/scanner.rb +103 -0
- data/lib/gamefic/scene/activity.rb +9 -17
- data/lib/gamefic/scene/conclusion.rb +6 -5
- data/lib/gamefic/scene/default.rb +88 -0
- data/lib/gamefic/scene/multiple_choice.rb +14 -69
- data/lib/gamefic/scene/pause.rb +9 -13
- data/lib/gamefic/scene/yes_or_no.rb +6 -46
- data/lib/gamefic/scene.rb +11 -7
- data/lib/gamefic/scope/base.rb +44 -0
- data/lib/gamefic/scope/children.rb +16 -0
- data/lib/gamefic/scope/family.rb +20 -0
- data/lib/gamefic/scope/myself.rb +13 -0
- data/lib/gamefic/scope/parent.rb +13 -0
- data/lib/gamefic/scope/siblings.rb +14 -0
- data/lib/gamefic/scope.rb +8 -0
- data/lib/gamefic/scriptable/actions.rb +156 -0
- data/lib/gamefic/scriptable/entities.rb +76 -0
- data/lib/gamefic/scriptable/events.rb +65 -0
- data/lib/gamefic/scriptable/proxy.rb +55 -0
- data/lib/gamefic/scriptable/queries.rb +73 -0
- data/lib/gamefic/scriptable/scenes.rb +162 -0
- data/lib/gamefic/scriptable.rb +167 -73
- data/lib/gamefic/snapshot.rb +36 -0
- data/lib/gamefic/stage.rb +51 -0
- data/lib/gamefic/subplot.rb +51 -79
- data/lib/gamefic/syntax/template.rb +67 -0
- data/lib/gamefic/syntax.rb +102 -83
- data/lib/gamefic/vault.rb +50 -0
- data/lib/gamefic/version.rb +1 -1
- data/lib/gamefic.rb +26 -15
- data/spec-opal/spec_helper.rb +24 -0
- metadata +91 -29
- data/lib/gamefic/element.rb +0 -46
- data/lib/gamefic/keywords.rb +0 -52
- data/lib/gamefic/messaging.rb +0 -43
- data/lib/gamefic/plot/darkroom.rb +0 -120
- data/lib/gamefic/plot/host.rb +0 -42
- data/lib/gamefic/plot/snapshot.rb +0 -27
- data/lib/gamefic/query/children.rb +0 -9
- data/lib/gamefic/query/descendants.rb +0 -15
- data/lib/gamefic/query/external.rb +0 -39
- data/lib/gamefic/query/family.rb +0 -18
- data/lib/gamefic/query/itself.rb +0 -13
- data/lib/gamefic/query/matches.rb +0 -75
- data/lib/gamefic/query/parent.rb +0 -9
- data/lib/gamefic/query/siblings.rb +0 -13
- data/lib/gamefic/query/tree.rb +0 -17
- data/lib/gamefic/scene/base.rb +0 -142
- data/lib/gamefic/scene/multiple_scene.rb +0 -29
- data/lib/gamefic/serialize.rb +0 -196
- data/lib/gamefic/world/callbacks.rb +0 -135
- data/lib/gamefic/world/commands.rb +0 -181
- data/lib/gamefic/world/entities.rb +0 -98
- data/lib/gamefic/world/playbook.rb +0 -233
- data/lib/gamefic/world/players.rb +0 -37
- data/lib/gamefic/world/scenes.rb +0 -228
- data/lib/gamefic/world.rb +0 -18
@@ -0,0 +1,139 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'gamefic/rulebook/calls'
|
4
|
+
require 'gamefic/rulebook/events'
|
5
|
+
require 'gamefic/rulebook/hooks'
|
6
|
+
require 'gamefic/rulebook/scenes'
|
7
|
+
|
8
|
+
module Gamefic
|
9
|
+
# A collection of rules that define the behavior of a narrative.
|
10
|
+
#
|
11
|
+
# Rulebooks provide a way to separate narrative data from code. This
|
12
|
+
# separation is necessary to ensure that the game state can be serialized in
|
13
|
+
# snapshots.
|
14
|
+
#
|
15
|
+
class Rulebook
|
16
|
+
# @return [Calls]
|
17
|
+
attr_reader :calls
|
18
|
+
|
19
|
+
# @return [Events]
|
20
|
+
attr_reader :events
|
21
|
+
|
22
|
+
# @return [Hooks]
|
23
|
+
attr_reader :hooks
|
24
|
+
|
25
|
+
# @return [Scenes]
|
26
|
+
attr_reader :scenes
|
27
|
+
|
28
|
+
# @return [Narrative]
|
29
|
+
attr_reader :narrative
|
30
|
+
|
31
|
+
# @param narrative [Narrative]
|
32
|
+
def initialize(narrative)
|
33
|
+
@narrative = narrative
|
34
|
+
@calls = Calls.new
|
35
|
+
@events = Events.new
|
36
|
+
@hooks = Hooks.new
|
37
|
+
@scenes = Scenes.new
|
38
|
+
end
|
39
|
+
|
40
|
+
def freeze
|
41
|
+
super
|
42
|
+
[@calls, @events, @hooks, @scenes].each(&:freeze)
|
43
|
+
self
|
44
|
+
end
|
45
|
+
|
46
|
+
# @return [Array<Response>]
|
47
|
+
def responses
|
48
|
+
@calls.responses
|
49
|
+
end
|
50
|
+
|
51
|
+
# @return [Array<Syntax>]
|
52
|
+
def syntaxes
|
53
|
+
@calls.syntaxes
|
54
|
+
end
|
55
|
+
|
56
|
+
# An array of all the verbs available in the rulebook. This list only
|
57
|
+
# includes verbs that are explicitly defined in reponses. It excludes
|
58
|
+
# synonyms that might be defined in syntaxes (see #synonyms).
|
59
|
+
#
|
60
|
+
# @example
|
61
|
+
# rulebook.respond :verb { |_| nil }
|
62
|
+
# rulebook.interpret 'synonym', 'verb'
|
63
|
+
# rulebook.verbs #=> [:verb]
|
64
|
+
#
|
65
|
+
# @return [Array<Symbol>]
|
66
|
+
def verbs
|
67
|
+
@calls.verbs
|
68
|
+
end
|
69
|
+
|
70
|
+
# An array of all the verbs defined in responses and any synonyms defined
|
71
|
+
# in syntaxes.
|
72
|
+
#
|
73
|
+
# @example
|
74
|
+
# rulebook.respond :verb { |_| nil }
|
75
|
+
# rulebook.interpret 'synonym', 'verb'
|
76
|
+
# rulebook.synonyms #=> [:synonym, :verb]
|
77
|
+
#
|
78
|
+
def synonyms
|
79
|
+
@calls.synonyms
|
80
|
+
end
|
81
|
+
|
82
|
+
# Get an array of all the responses that match a list of verbs.
|
83
|
+
#
|
84
|
+
# @param verbs [Array<Symbol>]
|
85
|
+
# @return [Array<Response>]
|
86
|
+
def responses_for *verbs
|
87
|
+
@calls.responses_for *verbs
|
88
|
+
end
|
89
|
+
|
90
|
+
# Get an array of all the syntaxes that match a lit of verbs.
|
91
|
+
#
|
92
|
+
# @param words [Array<Symbol>]
|
93
|
+
# @return [Array<Syntax>]
|
94
|
+
def syntaxes_for *synonyms
|
95
|
+
@calls.syntaxes_for *synonyms
|
96
|
+
end
|
97
|
+
|
98
|
+
def run_ready_blocks
|
99
|
+
events.ready_blocks.each { |blk| Stage.run narrative, &blk }
|
100
|
+
end
|
101
|
+
|
102
|
+
def run_update_blocks
|
103
|
+
events.update_blocks.each { |blk| Stage.run narrative, &blk }
|
104
|
+
end
|
105
|
+
|
106
|
+
def run_before_actions action
|
107
|
+
hooks.run_before action, narrative
|
108
|
+
end
|
109
|
+
|
110
|
+
def run_after_actions action
|
111
|
+
hooks.run_after action, narrative
|
112
|
+
end
|
113
|
+
|
114
|
+
def run_conclude_blocks
|
115
|
+
events.conclude_blocks.each { |blk| Stage.run narrative, &blk }
|
116
|
+
end
|
117
|
+
|
118
|
+
def run_player_conclude_blocks player
|
119
|
+
events.player_conclude_blocks.each { |blk| Stage.run(narrative) { blk.call(player) } }
|
120
|
+
end
|
121
|
+
|
122
|
+
def run_player_output_blocks player, output
|
123
|
+
events.player_output_blocks.each { |blk| Stage.run(narrative) { blk.call(player, output) } }
|
124
|
+
end
|
125
|
+
|
126
|
+
def empty?
|
127
|
+
calls.empty? && hooks.empty? && scenes.empty? && events.empty?
|
128
|
+
end
|
129
|
+
|
130
|
+
def script
|
131
|
+
narrative.class.included_blocks.select(&:script?).each { |blk| Stage.run(narrative, &blk.code) }
|
132
|
+
end
|
133
|
+
|
134
|
+
def script_with_defaults
|
135
|
+
script
|
136
|
+
scenes.with_defaults narrative
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module Gamefic
|
2
|
+
# A module for matching objects to tokens.
|
3
|
+
#
|
4
|
+
module Scanner
|
5
|
+
NEST_REGEXP = / in | on | of | from | inside | from inside /
|
6
|
+
|
7
|
+
# The result of an attempt to scan objects against a token in a Scanner. It
|
8
|
+
# provides an array of matching objects, the text that matched them, and the
|
9
|
+
# text that remains unmatched.
|
10
|
+
#
|
11
|
+
class Result
|
12
|
+
# The scanned objects
|
13
|
+
#
|
14
|
+
# @return [Array<Object>]
|
15
|
+
attr_reader :scanned
|
16
|
+
|
17
|
+
# The scanned token
|
18
|
+
#
|
19
|
+
# @return [String]
|
20
|
+
attr_reader :token
|
21
|
+
|
22
|
+
# The matched objects
|
23
|
+
#
|
24
|
+
# @return [Array<Object>]
|
25
|
+
attr_reader :matched
|
26
|
+
|
27
|
+
# The remaining (unmatched) portion of the token
|
28
|
+
#
|
29
|
+
# @return [String]
|
30
|
+
attr_reader :remainder
|
31
|
+
|
32
|
+
def initialize scanned, token, matched, remainder
|
33
|
+
@scanned = scanned
|
34
|
+
@token = token
|
35
|
+
@matched = matched
|
36
|
+
@remainder = remainder
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Scan entities against a token.
|
41
|
+
#
|
42
|
+
# @param objects [Array<Gamefic::Entity>]
|
43
|
+
# @param token [String]
|
44
|
+
# @return [Result]
|
45
|
+
def self.scan objects, token
|
46
|
+
# @note Theoretically, scanned objects only have to implement two
|
47
|
+
# methods:
|
48
|
+
# * #keywords => [Array<String>]
|
49
|
+
# * #children => [Array<#keywords, #children>]
|
50
|
+
|
51
|
+
words = token.keywords
|
52
|
+
available = objects.clone
|
53
|
+
filtered = []
|
54
|
+
if nested?(token) && objects.all?(&:children)
|
55
|
+
denest(objects, token)
|
56
|
+
else
|
57
|
+
words.each_with_index do |word, idx|
|
58
|
+
tested = select_strict(available, word)
|
59
|
+
tested = select_fuzzy(available, word) if tested.empty?
|
60
|
+
return Result.new(objects, token, filtered, words[idx..].join(' ')) if tested.empty?
|
61
|
+
|
62
|
+
filtered = tested
|
63
|
+
available = filtered
|
64
|
+
end
|
65
|
+
Result.new(objects, token, filtered, '')
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
class << self
|
70
|
+
private
|
71
|
+
|
72
|
+
def select_strict available, word
|
73
|
+
available.select { |obj| obj.keywords.include?(word) }
|
74
|
+
end
|
75
|
+
|
76
|
+
def select_fuzzy available, word
|
77
|
+
available.select { |obj| obj.keywords.any? { |wrd| wrd.start_with?(word) } }
|
78
|
+
end
|
79
|
+
|
80
|
+
def nested?(token)
|
81
|
+
token.match(NEST_REGEXP)
|
82
|
+
end
|
83
|
+
|
84
|
+
def denest(objects, token)
|
85
|
+
parts = token.split(NEST_REGEXP)
|
86
|
+
current = parts.pop
|
87
|
+
last_result = scan(objects, current)
|
88
|
+
until parts.empty?
|
89
|
+
current = "#{parts.last} #{current}"
|
90
|
+
result = scan(last_result.matched, current)
|
91
|
+
break if result.matched.empty?
|
92
|
+
|
93
|
+
parts.pop
|
94
|
+
last_result = result
|
95
|
+
end
|
96
|
+
return Result.new(objects, token, [], '') if last_result.matched.empty? || last_result.matched.length > 1
|
97
|
+
return last_result if parts.empty?
|
98
|
+
|
99
|
+
denest(last_result.matched.first.children, parts.join(' '))
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -1,21 +1,13 @@
|
|
1
|
-
|
2
|
-
# Active Scenes handle the default command prompt, where input is parsed
|
3
|
-
# into an Action performed by the Character. This is the default scene in
|
4
|
-
# a Plot.
|
5
|
-
#
|
6
|
-
class Scene::Activity < Scene::Base
|
7
|
-
def post_initialize
|
8
|
-
self.type = 'Activity'
|
9
|
-
end
|
1
|
+
# frozen_string_literal: true
|
10
2
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
3
|
+
module Gamefic
|
4
|
+
module Scene
|
5
|
+
# A scene that accepts player commands for actors to perform.
|
6
|
+
#
|
7
|
+
class Activity < Default
|
8
|
+
def finish actor, props
|
9
|
+
super
|
10
|
+
actor.perform props.input
|
19
11
|
end
|
20
12
|
end
|
21
13
|
end
|
@@ -1,9 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Gamefic
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
@type ||= 'Conclusion'
|
4
|
+
module Scene
|
5
|
+
# A scene that ends an actor's participation in a narrative.
|
6
|
+
#
|
7
|
+
class Conclusion < Default
|
7
8
|
end
|
8
9
|
end
|
9
10
|
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Gamefic
|
4
|
+
module Scene
|
5
|
+
# The base class for scenes. Authors can instantiate this class directly
|
6
|
+
# and customize it with on_start and on_finish blocks.
|
7
|
+
#
|
8
|
+
class Default
|
9
|
+
# @return [Symbol]
|
10
|
+
attr_reader :name
|
11
|
+
|
12
|
+
# @param name [Symbol]
|
13
|
+
# @param narrative [Narrative]
|
14
|
+
# @param on_start [Proc, nil]
|
15
|
+
# @param on_finish [Proc, nil]
|
16
|
+
# @yieldparam [self]
|
17
|
+
def initialize name, narrative, on_start: nil, on_finish: nil
|
18
|
+
@name = name
|
19
|
+
@narrative = narrative
|
20
|
+
@start_blocks = []
|
21
|
+
@finish_blocks = []
|
22
|
+
@start_blocks.push on_start if on_start
|
23
|
+
@finish_blocks.push on_finish if on_finish
|
24
|
+
yield(self) if block_given?
|
25
|
+
end
|
26
|
+
|
27
|
+
# @return [String]
|
28
|
+
def type
|
29
|
+
@type ||= self.class.to_s.sub(/^Gamefic::Scene::/, '')
|
30
|
+
end
|
31
|
+
|
32
|
+
def new_props(**context)
|
33
|
+
self.class.props_class.new(name, type, **context)
|
34
|
+
end
|
35
|
+
|
36
|
+
def on_start &block
|
37
|
+
@start_blocks.push block
|
38
|
+
end
|
39
|
+
|
40
|
+
def on_finish &block
|
41
|
+
@finish_blocks.push block
|
42
|
+
end
|
43
|
+
|
44
|
+
# @param actor [Gamefic::Actor]
|
45
|
+
# @param props [Props::Default]
|
46
|
+
# @return [void]
|
47
|
+
def start actor, props
|
48
|
+
actor.output[:scene] = to_hash
|
49
|
+
actor.output[:prompt] = props.prompt
|
50
|
+
end
|
51
|
+
|
52
|
+
# @param actor [Gamefic::Actor]
|
53
|
+
# @param props [Props::Default]
|
54
|
+
# @return [void]
|
55
|
+
def finish actor, props
|
56
|
+
props.input = actor.queue.shift
|
57
|
+
end
|
58
|
+
|
59
|
+
def run_start_blocks actor, props
|
60
|
+
@start_blocks.each { |blk| Stage.run(@narrative, actor, props, &blk) }
|
61
|
+
end
|
62
|
+
|
63
|
+
def run_finish_blocks actor, props
|
64
|
+
@finish_blocks.each { |blk| Stage.run(@narrative, actor, props, &blk) }
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.props_class
|
68
|
+
@props_class ||= Props::Default
|
69
|
+
end
|
70
|
+
|
71
|
+
def conclusion?
|
72
|
+
is_a?(Conclusion)
|
73
|
+
end
|
74
|
+
|
75
|
+
def to_hash
|
76
|
+
{ name: name, type: type }
|
77
|
+
end
|
78
|
+
|
79
|
+
class << self
|
80
|
+
protected
|
81
|
+
|
82
|
+
def use_props_class klass
|
83
|
+
@props_class = klass
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -1,79 +1,24 @@
|
|
1
|
-
|
2
|
-
# Provide a list of options and process the selection in the scene's finish
|
3
|
-
# block. After the scene is finished, the :active scene will be cued unless
|
4
|
-
# some other scene has already been prepared or cued.
|
5
|
-
#
|
6
|
-
# The finish block's input parameter receives a MultipleChoice::Input object
|
7
|
-
# instead of a String.
|
8
|
-
#
|
9
|
-
class Scene::MultipleChoice < Scene::Base
|
10
|
-
# The zero-based index of the selected option.
|
11
|
-
#
|
12
|
-
# @return [Integer]
|
13
|
-
attr_reader :index
|
1
|
+
# frozen_string_literal: true
|
14
2
|
|
15
|
-
|
16
|
-
|
17
|
-
#
|
18
|
-
|
19
|
-
|
20
|
-
# The full text of the selected option.
|
3
|
+
module Gamefic
|
4
|
+
module Scene
|
5
|
+
# A scene that presents a list of choices and processes the player's input.
|
6
|
+
# If the input is not a valid choice, the scene gets recued.
|
21
7
|
#
|
22
|
-
|
23
|
-
|
8
|
+
class MultipleChoice < Default
|
9
|
+
use_props_class Props::MultipleChoice
|
24
10
|
|
25
|
-
|
26
|
-
|
27
|
-
def post_initialize
|
28
|
-
self.type = 'MultipleChoice'
|
29
|
-
self.prompt = 'Enter a choice:'
|
30
|
-
end
|
31
|
-
|
32
|
-
def finish
|
33
|
-
get_choice
|
34
|
-
if selection.nil?
|
35
|
-
actor.tell invalid_message
|
36
|
-
else
|
11
|
+
def start actor, props
|
37
12
|
super
|
13
|
+
actor.output[:options] = props.options
|
38
14
|
end
|
39
|
-
end
|
40
15
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
def options
|
45
|
-
@options ||= []
|
46
|
-
end
|
47
|
-
|
48
|
-
# The text to display when an invalid selection is received.
|
49
|
-
#
|
50
|
-
# @return [String]
|
51
|
-
def invalid_message
|
52
|
-
@invalid_message ||= 'That is not a valid choice.'
|
53
|
-
end
|
54
|
-
|
55
|
-
def state
|
56
|
-
super.merge options: options
|
57
|
-
end
|
58
|
-
|
59
|
-
private
|
16
|
+
def finish actor, props
|
17
|
+
super
|
18
|
+
return if props.index
|
60
19
|
|
61
|
-
|
62
|
-
|
63
|
-
@number = input.to_i
|
64
|
-
@index = number - 1
|
65
|
-
@selection = options[index]
|
66
|
-
else
|
67
|
-
i = 0
|
68
|
-
options.each { |o|
|
69
|
-
if o.casecmp(input).zero?
|
70
|
-
@selection = o
|
71
|
-
@index = i
|
72
|
-
@number = index + 1
|
73
|
-
break
|
74
|
-
end
|
75
|
-
i += 1
|
76
|
-
}
|
20
|
+
actor.tell format(props.invalid_message, input: props.input)
|
21
|
+
actor.recue
|
77
22
|
end
|
78
23
|
end
|
79
24
|
end
|
data/lib/gamefic/scene/pause.rb
CHANGED
@@ -1,17 +1,13 @@
|
|
1
|
-
|
2
|
-
# Pause for user input.
|
3
|
-
#
|
4
|
-
class Scene::Pause < Scene::Base
|
5
|
-
def post_initialize
|
6
|
-
self.type = 'Pause'
|
7
|
-
self.prompt = 'Press enter to continue...'
|
8
|
-
end
|
1
|
+
# frozen_string_literal: true
|
9
2
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
3
|
+
module Gamefic
|
4
|
+
module Scene
|
5
|
+
# Pause a scene. This rig simply runs on_start and waits for user input
|
6
|
+
# before proceeding to on_finish. The user input itself is ignored by
|
7
|
+
# default.
|
8
|
+
#
|
9
|
+
class Pause < Default
|
10
|
+
use_props_class Props::Pause
|
15
11
|
end
|
16
12
|
end
|
17
13
|
end
|
@@ -1,51 +1,11 @@
|
|
1
|
-
|
2
|
-
# Prompt the user to answer "yes" or "no". The scene will accept variations
|
3
|
-
# like "YES" or "n" and normalize the answer to "yes" or "no" in the finish
|
4
|
-
# block. After the scene is finished, the :active scene will be cued if no
|
5
|
-
# other scene has been prepared or cued.
|
6
|
-
#
|
7
|
-
class Scene::YesOrNo < Scene::Base
|
8
|
-
attr_writer :invalid_message
|
9
|
-
|
10
|
-
def post_initialize
|
11
|
-
self.type = 'YesOrNo'
|
12
|
-
self.prompt = 'Yes or No:'
|
13
|
-
end
|
14
|
-
|
15
|
-
# True if the actor's answer is Yes.
|
16
|
-
# Any answer beginning with letter Y is considered Yes.
|
17
|
-
#
|
18
|
-
# @return [Boolean]
|
19
|
-
def yes?
|
20
|
-
input.to_s[0,1].downcase == 'y' or input.to_i == 1
|
21
|
-
end
|
22
|
-
|
23
|
-
# True if the actor's answer is No.
|
24
|
-
# Any answer beginning with letter N is considered No.
|
25
|
-
#
|
26
|
-
# @return [Boolean]
|
27
|
-
def no?
|
28
|
-
input.to_s[0,1].downcase == 'n' or input.to_i == 2
|
29
|
-
end
|
1
|
+
# frozen_string_literal: true
|
30
2
|
|
31
|
-
|
32
|
-
|
3
|
+
module Gamefic
|
4
|
+
module Scene
|
5
|
+
# A specialized MultipleChoice scene that only accepts Yes or No.
|
33
6
|
#
|
34
|
-
|
35
|
-
|
36
|
-
@invalid_message ||= 'Please enter Yes or No.'
|
37
|
-
end
|
38
|
-
|
39
|
-
def finish
|
40
|
-
if yes? or no?
|
41
|
-
super
|
42
|
-
else
|
43
|
-
actor.tell invalid_message
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
def state
|
48
|
-
super.merge options: ['Yes', 'No']
|
7
|
+
class YesOrNo < MultipleChoice
|
8
|
+
use_props_class Props::YesOrNo
|
49
9
|
end
|
50
10
|
end
|
51
11
|
end
|
data/lib/gamefic/scene.rb
CHANGED
@@ -1,11 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Gamefic
|
4
|
+
# Narratives use scenes to process game turns. The start of a scene defines
|
5
|
+
# the output to be sent to the player. The finish processes player input.
|
6
|
+
#
|
2
7
|
module Scene
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
autoload :YesOrNo, 'gamefic/scene/yes_or_no'
|
8
|
+
require 'gamefic/scene/default'
|
9
|
+
require 'gamefic/scene/activity'
|
10
|
+
require 'gamefic/scene/multiple_choice'
|
11
|
+
require 'gamefic/scene/pause'
|
12
|
+
require 'gamefic/scene/yes_or_no'
|
13
|
+
require 'gamefic/scene/conclusion'
|
10
14
|
end
|
11
15
|
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Gamefic
|
4
|
+
module Scope
|
5
|
+
# The base class for a Scoped query's scope.
|
6
|
+
#
|
7
|
+
class Base
|
8
|
+
attr_reader :context
|
9
|
+
|
10
|
+
# @param [Gamefic::Entity]
|
11
|
+
def initialize context
|
12
|
+
@context = context
|
13
|
+
end
|
14
|
+
|
15
|
+
# @param [Array<Gamefic::Entity>]
|
16
|
+
def matches
|
17
|
+
[]
|
18
|
+
end
|
19
|
+
|
20
|
+
# @param [Gamefic::Entity]
|
21
|
+
def self.matches context
|
22
|
+
new(context).matches
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.precision
|
26
|
+
0
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
# Return an array of the entity's accessible descendants.
|
32
|
+
#
|
33
|
+
# @param [Entity]
|
34
|
+
# @return [Array<Entity>]
|
35
|
+
def subquery_accessible entity
|
36
|
+
return [] unless entity&.accessible?
|
37
|
+
|
38
|
+
entity.children.flat_map do |c|
|
39
|
+
[c] + subquery_accessible(c)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Gamefic
|
4
|
+
module Scope
|
5
|
+
# The Children scope returns an entity's children and all accessible
|
6
|
+
# descendants.
|
7
|
+
#
|
8
|
+
class Children < Base
|
9
|
+
def matches
|
10
|
+
context.children.flat_map do |c|
|
11
|
+
[c] + subquery_accessible(c)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Gamefic
|
4
|
+
module Scope
|
5
|
+
# The Family scope returns an entity's parent, siblings, and descendants.
|
6
|
+
#
|
7
|
+
class Family < Base
|
8
|
+
def matches
|
9
|
+
result = context.parent ? [context.parent] : []
|
10
|
+
result.concat subquery_accessible(context.parent)
|
11
|
+
result.delete context
|
12
|
+
context.children.each do |c|
|
13
|
+
result.push c
|
14
|
+
result.concat subquery_accessible(c)
|
15
|
+
end
|
16
|
+
result.uniq
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|