gamefic 1.7.0 → 2.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.
Files changed (99) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +12 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +13 -0
  5. data/.solargraph.yml +5 -0
  6. data/Gemfile +7 -0
  7. data/LICENSE +20 -0
  8. data/README.md +28 -0
  9. data/Rakefile +10 -0
  10. data/gamefic.gemspec +27 -0
  11. data/lib/gamefic.rb +7 -6
  12. data/lib/gamefic/action.rb +38 -28
  13. data/lib/gamefic/active.rb +325 -280
  14. data/lib/gamefic/actor.rb +8 -5
  15. data/lib/gamefic/command.rb +9 -7
  16. data/lib/gamefic/core_ext/array.rb +24 -49
  17. data/lib/gamefic/core_ext/string.rb +25 -16
  18. data/lib/gamefic/describable.rb +21 -23
  19. data/lib/gamefic/element.rb +43 -31
  20. data/lib/gamefic/entity.rb +6 -12
  21. data/lib/gamefic/index.rb +121 -0
  22. data/lib/gamefic/{matchable.rb → keywords.rb} +52 -50
  23. data/lib/gamefic/messaging.rb +43 -44
  24. data/lib/gamefic/node.rb +14 -5
  25. data/lib/gamefic/plot.rb +69 -89
  26. data/lib/gamefic/plot/darkroom.rb +92 -264
  27. data/lib/gamefic/plot/host.rb +42 -48
  28. data/lib/gamefic/plot/snapshot.rb +5 -18
  29. data/lib/gamefic/query.rb +14 -18
  30. data/lib/gamefic/query/base.rb +30 -18
  31. data/lib/gamefic/query/children.rb +0 -0
  32. data/lib/gamefic/query/external.rb +18 -14
  33. data/lib/gamefic/query/family.rb +1 -7
  34. data/lib/gamefic/query/matches.rb +75 -67
  35. data/lib/gamefic/query/parent.rb +0 -0
  36. data/lib/gamefic/query/siblings.rb +0 -0
  37. data/lib/gamefic/query/text.rb +2 -1
  38. data/lib/gamefic/scene.rb +0 -2
  39. data/lib/gamefic/scene/activity.rb +24 -26
  40. data/lib/gamefic/scene/base.rb +64 -8
  41. data/lib/gamefic/scene/conclusion.rb +0 -2
  42. data/lib/gamefic/scene/custom.rb +0 -2
  43. data/lib/gamefic/scene/multiple_choice.rb +18 -3
  44. data/lib/gamefic/scene/multiple_scene.rb +29 -20
  45. data/lib/gamefic/scene/pause.rb +7 -2
  46. data/lib/gamefic/scene/yes_or_no.rb +21 -9
  47. data/lib/gamefic/scriptable.rb +87 -0
  48. data/lib/gamefic/serialize.rb +68 -0
  49. data/lib/gamefic/subplot.rb +29 -35
  50. data/lib/gamefic/syntax.rb +14 -13
  51. data/lib/gamefic/version.rb +3 -3
  52. data/lib/gamefic/world.rb +16 -0
  53. data/lib/gamefic/world/callbacks.rb +135 -0
  54. data/lib/gamefic/world/commands.rb +184 -0
  55. data/lib/gamefic/{plot → world}/entities.rb +30 -29
  56. data/lib/gamefic/{plot → world}/playbook.rb +255 -240
  57. data/lib/gamefic/world/players.rb +21 -0
  58. data/lib/gamefic/world/scenes.rb +226 -0
  59. metadata +41 -92
  60. data/bin/gamefic +0 -9
  61. data/lib/gamefic/engine.rb +0 -7
  62. data/lib/gamefic/engine/base.rb +0 -59
  63. data/lib/gamefic/engine/tty.rb +0 -24
  64. data/lib/gamefic/grammar.rb +0 -13
  65. data/lib/gamefic/grammar/conjugator.rb +0 -20
  66. data/lib/gamefic/grammar/gender.rb +0 -11
  67. data/lib/gamefic/grammar/person.rb +0 -10
  68. data/lib/gamefic/grammar/plural.rb +0 -13
  69. data/lib/gamefic/grammar/pronouns.rb +0 -106
  70. data/lib/gamefic/grammar/tense.rb +0 -6
  71. data/lib/gamefic/grammar/verb_set.rb +0 -43
  72. data/lib/gamefic/grammar/verbs.rb +0 -26
  73. data/lib/gamefic/grammar/word_adapter.rb +0 -49
  74. data/lib/gamefic/plot/articles.rb +0 -22
  75. data/lib/gamefic/plot/callbacks.rb +0 -126
  76. data/lib/gamefic/plot/commands.rb +0 -120
  77. data/lib/gamefic/plot/players.rb +0 -15
  78. data/lib/gamefic/plot/scenes.rb +0 -187
  79. data/lib/gamefic/plot/theater.rb +0 -73
  80. data/lib/gamefic/plot/you_mount.rb +0 -22
  81. data/lib/gamefic/script.rb +0 -13
  82. data/lib/gamefic/script/base.rb +0 -42
  83. data/lib/gamefic/script/file.rb +0 -14
  84. data/lib/gamefic/script/text.rb +0 -14
  85. data/lib/gamefic/shell.rb +0 -76
  86. data/lib/gamefic/source.rb +0 -14
  87. data/lib/gamefic/source/base.rb +0 -12
  88. data/lib/gamefic/source/file.rb +0 -23
  89. data/lib/gamefic/source/text.rb +0 -16
  90. data/lib/gamefic/tester.rb +0 -19
  91. data/lib/gamefic/text.rb +0 -8
  92. data/lib/gamefic/text/ansi.rb +0 -53
  93. data/lib/gamefic/text/html.rb +0 -68
  94. data/lib/gamefic/text/html/conversions.rb +0 -250
  95. data/lib/gamefic/text/html/entities.rb +0 -9
  96. data/lib/gamefic/tty.rb +0 -10
  97. data/lib/gamefic/user.rb +0 -7
  98. data/lib/gamefic/user/base.rb +0 -29
  99. data/lib/gamefic/user/tty.rb +0 -38
@@ -1,5 +1,4 @@
1
1
  module Gamefic
2
-
3
2
  # A Conclusion ends the Plot (or the character's participation in it).
4
3
  #
5
4
  class Scene::Conclusion < Scene::Custom
@@ -7,5 +6,4 @@ module Gamefic
7
6
  @type ||= 'Conclusion'
8
7
  end
9
8
  end
10
-
11
9
  end
@@ -1,9 +1,7 @@
1
1
  module Gamefic
2
-
3
2
  # A Custom Scene allows for complete configuration of its behavior upon
4
3
  # instantiation. It is suitable for direct instantiation or subclassing.
5
4
  #
6
5
  class Scene::Custom < Scene::Base
7
6
  end
8
-
9
7
  end
@@ -1,5 +1,4 @@
1
1
  module Gamefic
2
-
3
2
  # Provide a list of options and process the selection in the scene's finish
4
3
  # block. After the scene is finished, the :active scene will be cued unless
5
4
  # some other scene has already been prepared or cued.
@@ -8,9 +7,21 @@ module Gamefic
8
7
  # instead of a String.
9
8
  #
10
9
  class Scene::MultipleChoice < Scene::Custom
10
+ # The zero-based index of the selected option.
11
+ #
12
+ # @return [Integer]
11
13
  attr_reader :index
14
+
15
+ # The one-based index of the selected option.
16
+ #
17
+ # @return [Number]
12
18
  attr_reader :number
19
+
20
+ # The full text of the selected option.
21
+ #
22
+ # @return [String]
13
23
  attr_reader :selection
24
+
14
25
  attr_writer :invalid_message
15
26
 
16
27
  def post_initialize
@@ -28,10 +39,16 @@ module Gamefic
28
39
  end
29
40
  end
30
41
 
42
+ # The array of available options.
43
+ #
44
+ # @return [Array<String>]
31
45
  def options
32
46
  @options ||= []
33
47
  end
34
48
 
49
+ # The text to display when an invalid selection is received.
50
+ #
51
+ # @return [String]
35
52
  def invalid_message
36
53
  @invalid_message ||= 'That is not a valid choice.'
37
54
  end
@@ -69,7 +86,5 @@ module Gamefic
69
86
  list += "</ol>"
70
87
  actor.tell list
71
88
  end
72
-
73
89
  end
74
-
75
90
  end
@@ -1,20 +1,29 @@
1
- module Gamefic
2
-
3
- class Scene::MultipleScene < Scene::MultipleChoice
4
- def option_map
5
- @option_map ||= {}
6
- end
7
-
8
- def map option, scene
9
- options.push option
10
- option_map[option] = scene
11
- end
12
-
13
- def finish
14
- get_choice
15
- unless selection.nil?
16
- actor.prepare option_map[selection]
17
- end
18
- end
19
- end
20
- end
1
+ module Gamefic
2
+ class Scene::MultipleScene < Scene::MultipleChoice
3
+ def option_map
4
+ @option_map ||= {}
5
+ end
6
+
7
+ # @param option [String]
8
+ # @param scene [Class<Gamefic::Scene::Base>]
9
+ def map option, scene
10
+ options.push option
11
+ option_map[option] = scene
12
+ end
13
+
14
+ def finish
15
+ get_choice
16
+ unless selection.nil?
17
+ actor.prepare option_map[selection]
18
+ end
19
+ end
20
+
21
+ def state
22
+ entered = {}
23
+ option_map.each_pair do |k, v|
24
+ entered[k] = actor.entered?(v)
25
+ end
26
+ super.merge entered: entered
27
+ end
28
+ end
29
+ end
@@ -1,5 +1,4 @@
1
1
  module Gamefic
2
-
3
2
  # Pause for user input.
4
3
  #
5
4
  class Scene::Pause < Scene::Custom
@@ -7,6 +6,12 @@ module Gamefic
7
6
  self.type = 'Pause'
8
7
  self.prompt = 'Press enter to continue...'
9
8
  end
9
+
10
+ class << self
11
+ def tracked?
12
+ @tracked = true if @tracked.nil?
13
+ @tracked
14
+ end
15
+ end
10
16
  end
11
-
12
17
  end
@@ -1,32 +1,41 @@
1
1
  module Gamefic
2
-
3
2
  # Prompt the user to answer "yes" or "no". The scene will accept variations
4
3
  # like "YES" or "n" and normalize the answer to "yes" or "no" in the finish
5
4
  # block. After the scene is finished, the :active scene will be cued if no
6
5
  # other scene has been prepared or cued.
7
6
  #
8
7
  class Scene::YesOrNo < Scene::Custom
8
+ attr_writer :invalid_message
9
+
9
10
  def post_initialize
10
11
  self.type = 'YesOrNo'
11
- self.prompt = 'Yes or No?'
12
+ self.prompt = 'Yes or No:'
12
13
  end
13
14
 
15
+ # True if the actor's answer is Yes.
16
+ # Any answer beginning with letter Y is considered Yes.
17
+ #
18
+ # @return [Boolean]
14
19
  def yes?
15
- input.to_s[0,1].downcase == 'y'
20
+ input.to_s[0,1].downcase == 'y' or input.to_i == 1
16
21
  end
17
22
 
23
+ # True if the actor's answer is No.
24
+ # Any answer beginning with letter N is considered No.
25
+ #
26
+ # @return [Boolean]
18
27
  def no?
19
- input.to_s[0,1].downcase == 'n'
28
+ input.to_s[0,1].downcase == 'n' or input.to_i == 2
20
29
  end
21
30
 
31
+ # The message sent to the user for an invalid answer, i.e., the input
32
+ # could not be resolved to either Yes or No.
33
+ #
34
+ # @return [String]
22
35
  def invalid_message
23
36
  @invalid_message ||= 'Please enter Yes or No.'
24
37
  end
25
38
 
26
- def prompt
27
- @prompt ||= 'Yes or No?'
28
- end
29
-
30
39
  def finish
31
40
  if yes? or no?
32
41
  super
@@ -34,6 +43,9 @@ module Gamefic
34
43
  actor.tell invalid_message
35
44
  end
36
45
  end
37
- end
38
46
 
47
+ def state
48
+ super.merge options: ['Yes', 'No']
49
+ end
50
+ end
39
51
  end
@@ -0,0 +1,87 @@
1
+ module Gamefic
2
+ # The Scriptable module provides a clean room (aka "theater") for scripts.
3
+ #
4
+ # @!method stage(*args, &block)
5
+ # Execute a block of code in a subset of the owner's scope.
6
+ #
7
+ # The provided code is evaluated inside a clean room object that has its
8
+ # own instance variables and access to the owner's public methods. The proc
9
+ # can accept the method call's arguments.
10
+ #
11
+ # @example Execute a block of code
12
+ # stage {
13
+ # puts 'Hello'
14
+ # }
15
+ #
16
+ # @example Execute a block of code with arguments
17
+ # stage 'hello' { |message|
18
+ # puts message # <- prints 'hello'
19
+ # }
20
+ #
21
+ # @example Use an instance variable
22
+ # stage { @message = 'hello'" }
23
+ # stage { puts @message } # <- prints 'hello'
24
+ #
25
+ # @yieldpublic [Gamefic::Plot]
26
+ # @return [Object] The value returned by the executed code
27
+ #
28
+ # @!method theater
29
+ # The object that acts as an isolated namespace for staged code.
30
+ # @return [Object]
31
+ #
32
+ # @!parse alias cleanroom theater
33
+ module Scriptable
34
+ module ClassMethods
35
+ # An array of blocks that were added by the `script` class method.
36
+ #
37
+ # @return [Array<Proc>]
38
+ def blocks
39
+ @blocks ||= []
40
+ end
41
+
42
+ # Add a block to be executed by the instance's `stage` method.
43
+ #
44
+ # Note that `script` does not execute the block instantly, but stores
45
+ # it in the `blocks` array to be executed later.
46
+ #
47
+ # @yieldpublic [Gamefic::Plot]
48
+ def script &block
49
+ blocks.push block
50
+ end
51
+ end
52
+
53
+ def self.included klass
54
+ klass.extend ClassMethods
55
+ end
56
+
57
+ private
58
+
59
+ # Execute all the scripts that were added by the `script` class method.
60
+ #
61
+ def run_scripts
62
+ self.class.blocks.each { |blk| stage &blk }
63
+ end
64
+ end
65
+ end
66
+
67
+ # @note #stage and #theater are implemented this way so the clean room object
68
+ # defines its classes and modules in the root namespace.
69
+ Gamefic::Scriptable.module_exec do
70
+ define_method :stage do |*args, &block|
71
+ theater.instance_exec *args, &block
72
+ end
73
+
74
+ define_method :theater do
75
+ @theater ||= begin
76
+ instance = self
77
+ theater ||= Object.new
78
+ theater.instance_exec do
79
+ define_singleton_method :method_missing do |symbol, *args, &block|
80
+ instance.public_send :public_send, symbol, *args, &block
81
+ end
82
+ end
83
+ theater
84
+ end
85
+ end
86
+ alias cleanroom theater
87
+ end
@@ -0,0 +1,68 @@
1
+ module Gamefic
2
+ module Serialize
3
+ def to_serial
4
+ {
5
+ 'class' => self.class.to_s
6
+ }.merge serialize_instance_variables
7
+ end
8
+ end
9
+ end
10
+
11
+ class Object
12
+ def to_serial
13
+ return self if [true, false, nil].include?(self)
14
+ # @todo This warning is a little too spammy. Set up a logger so it can be
15
+ # limited to an info or debug level.
16
+ # STDERR.puts "Unable to convert #{self} to element"
17
+ "#<UNKNOWN>"
18
+ end
19
+
20
+ def serialize_instance_variables
21
+ result = {}
22
+ instance_variables.each do |k|
23
+ result[k.to_s] = instance_variable_get(k).to_serial
24
+ end
25
+ result
26
+ end
27
+ end
28
+
29
+ class Symbol
30
+ def to_serial
31
+ "#<SYM:#{self}>"
32
+ end
33
+ end
34
+
35
+ class String
36
+ def to_serial
37
+ self
38
+ end
39
+ end
40
+
41
+ class Numeric
42
+ def to_serial
43
+ self
44
+ end
45
+ end
46
+
47
+ class Array
48
+ def to_serial
49
+ map do |e|
50
+ s = e.to_serial
51
+ return "#<UNKNOWN>" if s == "#<UNKNOWN>"
52
+ s
53
+ end
54
+ end
55
+ end
56
+
57
+ class Hash
58
+ def to_serial
59
+ result = {}
60
+ each_pair do |key, value|
61
+ k2 = key.to_serial
62
+ v2 = value.to_serial
63
+ return "#<UNKNOWN>" if k2 == "#<UNKNOWN>" || v2 == "#<UNKNOWN>"
64
+ result[k2] = v2
65
+ end
66
+ result
67
+ end
68
+ end
@@ -1,39 +1,29 @@
1
1
  require 'gamefic/plot'
2
2
 
3
3
  module Gamefic
4
-
5
- class Subplot
6
- include Plot::Theater
7
- include Plot::Entities
8
- include Plot::Commands
9
- include Plot::Callbacks
10
- include Plot::Scenes
11
- include Plot::Articles
4
+ class Subplot #< Container
5
+ include World
6
+ include Scriptable
7
+ # @!parse extend Scriptable::ClassMethods
12
8
 
13
9
  # @return [Gamefic::Plot]
14
10
  attr_reader :plot
15
11
 
16
- class << self
17
- attr_reader :start_proc
18
-
19
- protected
20
-
21
- def on_start &block
22
- @start_proc = block
23
- end
24
- end
25
-
26
- def initialize plot, introduce: nil, next_cue: nil
12
+ # @param plot [Gamefic::Plot]
13
+ # @param introduce [Gamefic::Actor]
14
+ # @param next_cue [Class<Gamefic::Scene::Base>]
15
+ def initialize plot, introduce: nil, next_cue: nil, **more
27
16
  @plot = plot
28
17
  @next_cue = next_cue
29
18
  @concluded = false
30
- stage &self.class.start_proc unless self.class.start_proc.nil?
19
+ configure more
20
+ run_scripts
31
21
  playbook.freeze
32
22
  self.introduce introduce unless introduce.nil?
33
23
  end
34
24
 
35
- def add_entity e
36
- @p_entities.push e
25
+ def players
26
+ @players ||= []
37
27
  end
38
28
 
39
29
  def subplot
@@ -58,25 +48,21 @@ module Gamefic
58
48
  ent
59
49
  end
60
50
 
61
- # HACK: Always assume subplots are running for the sake of entity destruction
62
- def running?
63
- true
64
- end
65
-
66
51
  def exeunt player
52
+ player_conclude_procs.each { |block| block.call player }
67
53
  player.playbooks.delete playbook
68
54
  player.cue (@next_cue || default_scene)
69
- p_players.delete player
55
+ players.delete player
70
56
  end
71
57
 
72
58
  def conclude
73
59
  @concluded = true
74
- entities.each { |e|
75
- destroy e
76
- }
77
- players.each { |p|
78
- exeunt p
79
- }
60
+ # Players needed to exit first in case any player_conclude procs need to
61
+ # interact with the subplot's entities.
62
+ players.each { |p| exeunt p }
63
+ # @todo I'm not sure why rejecting nils is necessary here. It's only an
64
+ # issue in Opal.
65
+ entities.reject(&:nil?).each { |e| destroy e }
80
66
  end
81
67
 
82
68
  def concluded?
@@ -84,6 +70,9 @@ module Gamefic
84
70
  end
85
71
 
86
72
  def ready
73
+ # @todo We might not want to conclude subplots without players. There
74
+ # might be cases where a subplot gets created with the intention of
75
+ # introducing players in a later turn.
87
76
  conclude if players.empty?
88
77
  return if concluded?
89
78
  playbook.freeze
@@ -95,6 +84,11 @@ module Gamefic
95
84
  call_player_update
96
85
  call_update
97
86
  end
98
- end
99
87
 
88
+ # Subclasses can override this method to handle additional configuration
89
+ # options.
90
+ #
91
+ def configure more
92
+ end
93
+ end
100
94
  end