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
@@ -0,0 +1,121 @@
1
+ require 'json'
2
+
3
+ module Gamefic
4
+ module Index
5
+ @@elements = []
6
+ @@stuck_length = 0
7
+
8
+ def initialize **data
9
+ data.each_pair do |k, v|
10
+ public_send "#{k}=", v
11
+ end
12
+ @@elements.push self
13
+ end
14
+
15
+ def to_serial
16
+ index = @@elements.index(self)
17
+ raise RuntimeError, "#{self} is not an indexed element" unless index
18
+ "#<ELE_#{index}>"
19
+ end
20
+
21
+ def destroy
22
+ @@elements.delete self unless Index.stuck?(self)
23
+ end
24
+
25
+ def self.elements
26
+ @@elements
27
+ end
28
+
29
+ def self.serials
30
+ result = []
31
+ @@elements.each do |e|
32
+ d = {}
33
+ d['class'] = e.class.to_s
34
+ e.instance_variables.each do |k|
35
+ v = e.instance_variable_get(k)
36
+ d[k] = v.to_serial
37
+ end
38
+ result.push d
39
+ end
40
+ result
41
+ end
42
+
43
+ def self.from_serial serial
44
+ if serial.is_a?(Hash) && serial['class']
45
+ klass = eval(serial['class'])
46
+ object = klass.allocate
47
+ serial.each_pair do |k, v|
48
+ next unless k.to_s.start_with?('@')
49
+ object.instance_variable_set(k, from_serial(v))
50
+ end
51
+ object
52
+ elsif serial.is_a?(Numeric)
53
+ serial
54
+ elsif serial.is_a?(String)
55
+ match = serial.match(/#<ELE_([0-9]+)>/)
56
+ return Gamefic::Index.elements[match[1].to_i] if match
57
+ match = serial.match(/#<SYM:([a-z0-9_\?\!]+)>/i)
58
+ return match[1].to_sym if match
59
+ serial
60
+ elsif serial.is_a?(Array)
61
+ result = serial.map { |e| from_serial(e) }
62
+ result = "#<UNKNOWN>" if result.any? { |e| e == "#<UNKNOWN>" }
63
+ result
64
+ elsif serial.is_a?(Hash)
65
+ result = {}
66
+ unknown = false
67
+ serial.each_pair do |k, v|
68
+ k2 = from_serial(k)
69
+ v2 = from_serial(v)
70
+ if k2 == "#<UNKNOWN>" || v2 == "#<UNKNOWN>"
71
+ unknown = true
72
+ break
73
+ end
74
+ result[k2] = v2
75
+ end
76
+ result = "#<UNKNOWN>" if unknown
77
+ result
78
+ elsif serial && serial != true
79
+ STDERR.puts "Unable to unserialize #{serial.class}"
80
+ nil
81
+ else
82
+ # true, false, or nil
83
+ serial
84
+ end
85
+ end
86
+
87
+ def self.unserialize serials
88
+ serials.each_with_index do |s, i|
89
+ next if elements[i]
90
+ klass = eval(s['class'])
91
+ klass.new
92
+ end
93
+ serials.each_with_index do |s, i|
94
+ s.each_pair do |k, v|
95
+ next unless k.to_s.start_with?('@')
96
+ next if v == "#<UNKNOWN>"
97
+ elements[i].instance_variable_set(k, from_serial(v))
98
+ end
99
+ end
100
+ elements
101
+ end
102
+
103
+ def self.stick
104
+ @@stuck_length = @@elements.length
105
+ end
106
+
107
+ def self.stuck
108
+ @@stuck_length
109
+ end
110
+
111
+ def self.clear
112
+ @@stuck_length = 0
113
+ @@elements.clear
114
+ end
115
+
116
+ def self.stuck? thing
117
+ index = @@elements.index(thing)
118
+ index && index <= @@stuck_length - 1
119
+ end
120
+ end
121
+ end
@@ -1,50 +1,52 @@
1
- module Gamefic
2
- module Matchable
3
- SPLIT_REGEXP = /[\s]+/
4
-
5
- # Get an array of keywords associated with this object.
6
- # The default implementation splits the value of self.to_s into an array.
7
- #
8
- # @return [Array<String>]
9
- def keywords
10
- self.to_s.downcase.split(SPLIT_REGEXP).uniq
11
- end
12
-
13
- # Determine if this object matches the provided description.
14
- # In a regular match, every word in the description must be a keyword.
15
- # Fuzzy matches accept words if a keyword starts with it, e.g., "red"
16
- # would be a fuzzy match for "reddish."
17
- #
18
- # @example
19
- # dog = "big red dog"
20
- # dog.extend Gamefic::Matchable
21
- #
22
- # dog.match?("red dog") #=> true
23
- # dog.match?("gray dog") #=> false
24
- # dog.match?("red do") #=> false
25
- #
26
- # dog.match?("re do", fuzzy: true) #=> true
27
- # dog.match?("red og", fuzzy: true) #=> false
28
- #
29
- # @return [Boolean]
30
- def match? description, fuzzy: false
31
- words = description.split(SPLIT_REGEXP)
32
- return false if words.empty?
33
- matches = 0
34
- available = keywords
35
- words.each { |w|
36
- if fuzzy
37
- available.each { |k|
38
- if k.gsub(/[^a-z0-9]/, '').start_with?(w.downcase.gsub(/[^a-z0-9]/, ''))
39
- matches +=1
40
- break
41
- end
42
- }
43
- else
44
- matches +=1 if available.include?(w.downcase)
45
- end
46
- }
47
- matches == words.length
48
- end
49
- end
50
- end
1
+ module Gamefic
2
+ module Keywords
3
+ SPLIT_REGEXP = /[\s]+/
4
+
5
+ # Get an array of keywords associated with this object.
6
+ # The default implementation splits the value of self.to_s into an array.
7
+ #
8
+ # @return [Array<String>]
9
+ def keywords
10
+ self.to_s.downcase.split(SPLIT_REGEXP).uniq
11
+ end
12
+
13
+ # Determine if this object matches the provided description.
14
+ # In a regular match, every word in the description must be a keyword.
15
+ # Fuzzy matches accept words if a keyword starts with it, e.g., "red"
16
+ # would be a fuzzy match for "reddish."
17
+ #
18
+ # @example
19
+ # dog = "big red dog"
20
+ # dog.extend Gamefic::Matchable
21
+ #
22
+ # dog.specified?("red dog") #=> true
23
+ # dog.specified?("gray dog") #=> false
24
+ # dog.specified?("red do") #=> false
25
+ #
26
+ # dog.specified?("re do", fuzzy: true) #=> true
27
+ # dog.specified?("red og", fuzzy: true) #=> false
28
+ #
29
+ # @param description [String] The description to be compared
30
+ # @param fuzzy [Boolean] Use fuzzy matching (default is false)
31
+ # @return [Boolean]
32
+ def specified? description, fuzzy: false
33
+ words = description.split(SPLIT_REGEXP)
34
+ return false if words.empty?
35
+ matches = 0
36
+ available = keywords
37
+ words.each { |w|
38
+ if fuzzy
39
+ available.each { |k|
40
+ if k.gsub(/[^a-z0-9]/, '').start_with?(w.downcase.gsub(/[^a-z0-9]/, ''))
41
+ matches +=1
42
+ break
43
+ end
44
+ }
45
+ else
46
+ matches +=1 if available.include?(w.downcase)
47
+ end
48
+ }
49
+ matches == words.length
50
+ end
51
+ end
52
+ end
@@ -1,44 +1,43 @@
1
- module Gamefic
2
- module Messaging
3
- # Send a message to the entity.
4
- # This method will automatically wrap the message in HTML paragraphs.
5
- # To send a message without paragraph formatting, use #stream instead.
6
- #
7
- # @param message [String]
8
- def tell(message)
9
- message = "<p>#{message.strip}</p>"
10
- # This method uses String#gsub instead of String#gsub! for
11
- # compatibility with Opal.
12
- message = message.gsub(/[ \t\r]*\n[ \t\r]*\n[ \t\r]*/, '</p><p>')
13
- message = message.gsub(/[ \t]*\n[ \t]*/, ' ')
14
- p_set_messages messages + message
15
- end
16
-
17
- # Send a message to the Character as raw text.
18
- # Unlike #tell, this method will not wrap the message in HTML paragraphs.
19
- #
20
- # @param message [String]
21
- def stream(message)
22
- p_set_messages messages + message.strip
23
- end
24
-
25
- # @return [String]
26
- def messages
27
- @messages ||= ''
28
- end
29
-
30
- def output
31
- messages
32
- end
33
-
34
- def flush
35
- p_set_messages '' unless messages.empty?
36
- end
37
-
38
- private
39
-
40
- def p_set_messages str
41
- @messages = str
42
- end
43
- end
44
- end
1
+ module Gamefic
2
+ module Messaging
3
+ # Send a message to the entity.
4
+ # This method will automatically wrap the message in HTML paragraphs.
5
+ # To send a message without paragraph formatting, use #stream instead.
6
+ #
7
+ # @param message [String]
8
+ def tell(message)
9
+ @messages = @messages.to_s + format(message)
10
+ end
11
+
12
+ # Send a message to the Character as raw text.
13
+ # Unlike #tell, this method will not wrap the message in HTML paragraphs.
14
+ #
15
+ # @param message [String]
16
+ def stream(message)
17
+ @messages = @messages.to_s + message
18
+ end
19
+
20
+ # Get all the currently buffered messages consolidated in a single string.
21
+ #
22
+ # @return [String]
23
+ def messages
24
+ @messages ||= ''
25
+ end
26
+
27
+ # Clear the buffered messages.
28
+ #
29
+ def flush
30
+ @messages = ''
31
+ end
32
+
33
+ private
34
+
35
+ def format message
36
+ "<p>#{message.strip}</p>"
37
+ .gsub(/[ \t\r]*\n[ \t\r]*\n[ \t\r]*/, "</p><p>")
38
+ .gsub(/[ \t]*\n[ \t]*/, ' ')
39
+ .gsub(/<p>[\s]*<p>/, '<p>')
40
+ .gsub(/<\/p>[\s]*<\/p>/, '</p>')
41
+ end
42
+ end
43
+ end
@@ -1,31 +1,37 @@
1
1
  # Exception raised when setting a node's parent would cause
2
2
  # a circular reference, e.g., A -> A or A -> B -> A
3
- class CircularNodeReferenceError < Exception
4
- end
3
+ class CircularNodeReferenceError < RuntimeError; end
5
4
 
6
5
  module Gamefic
7
-
8
6
  module Node
7
+ # An array of the object's children.
8
+ #
9
9
  # @return [Array]
10
10
  def children
11
11
  @children ||= []
12
12
  @children.clone
13
13
  end
14
14
 
15
+ # Get a flat array of all descendants.
16
+ #
15
17
  # @return [Array]
16
18
  def flatten
17
19
  array = Array.new
18
20
  children.each { |child|
19
21
  array = array + recurse_flatten(child)
20
22
  }
21
- return array
23
+ array
22
24
  end
23
25
 
26
+ # The object's parent.
27
+ #
24
28
  # @return [Object]
25
29
  def parent
26
30
  @parent
27
31
  end
28
32
 
33
+ # Set the object's parent.
34
+ #
29
35
  def parent=(node)
30
36
  return if node == @parent
31
37
  if node == self
@@ -49,6 +55,10 @@ module Gamefic
49
55
  end
50
56
  end
51
57
 
58
+ # Determine if external objects can interact with this object's children.
59
+ # For example, a game can designate that the contents of a bowl are
60
+ # accessible, while the contents of a locked safe are not.
61
+ #
52
62
  # @return [Boolean]
53
63
  def accessible?
54
64
  true
@@ -81,5 +91,4 @@ module Gamefic
81
91
  return array
82
92
  end
83
93
  end
84
-
85
94
  end
@@ -1,133 +1,113 @@
1
- require 'gamefic/tester'
2
- require 'gamefic/source'
3
- require 'gamefic/script'
4
1
  require 'gamefic/query'
5
2
 
6
3
  module Gamefic
7
-
4
+ # A plot controls the game narrative and manages the world model.
5
+ # Authors typically build plots through scripts that are executed in a
6
+ # special container called a stage. All of the elements that compose the
7
+ # narrative (characters, locations, scenes, etc.) reside in the stage's
8
+ # scope. Game engines use the plot to receive game data and process user
9
+ # input.
10
+ #
8
11
  class Plot
9
- autoload :Scenes, 'gamefic/plot/scenes'
10
- autoload :Commands, 'gamefic/plot/commands'
11
- autoload :Entities, 'gamefic/plot/entities'
12
- autoload :Articles, 'gamefic/plot/articles'
13
- autoload :YouMount, 'gamefic/plot/you_mount'
14
12
  autoload :Snapshot, 'gamefic/plot/snapshot'
15
13
  autoload :Darkroom, 'gamefic/plot/darkroom'
16
14
  autoload :Host, 'gamefic/plot/host'
17
- autoload :Players, 'gamefic/plot/players'
18
- autoload :Playbook, 'gamefic/plot/playbook'
19
- autoload :Callbacks, 'gamefic/plot/callbacks'
20
- autoload :Theater, 'gamefic/plot/theater'
21
15
 
22
- attr_reader :commands, :imported_scripts, :source
16
+ # @return [Hash]
17
+ attr_reader :metadata
23
18
 
24
- # TODO: Metadata could use better protection
25
- attr_accessor :metadata
26
- include Theater
27
- include Gamefic, Tester, Players, Scenes, Commands, Entities
28
- include Articles, YouMount, Snapshot, Host, Callbacks
19
+ include World
20
+ include Scriptable
21
+ # @!parse extend Scriptable::ClassMethods
22
+ include Snapshot
23
+ include Host
29
24
 
30
- # @param source [Source::Base]
31
- def initialize(source = nil)
32
- @source = source || Source::Text.new({})
33
- @working_scripts = []
34
- @imported_scripts = []
35
- @running = false
36
- post_initialize
25
+ # @param structure [Gamefic::Structure]
26
+ # @param metadata [Hash]
27
+ def initialize metadata: {}
28
+ Gamefic::Index.clear
29
+ @metadata = metadata
30
+ run_scripts
31
+ mark_static_entities
32
+ Gamefic::Index.stick
37
33
  end
38
34
 
39
35
  def player_class cls = nil
40
36
  @player_class = cls unless cls.nil?
41
- @player_class
42
- end
43
-
44
- # @return [Gamefic::Plot::Playbook]
45
- def playbook
46
- @playbook ||= Gamefic::Plot::Playbook.new
37
+ @player_class ||= Gamefic::Actor
47
38
  end
48
39
 
49
- def running?
50
- @running
51
- end
52
-
53
- # Get an Array of all scripts that have been imported into the Plot.
54
- #
55
- # @return [Array<Script>] The imported scripts
56
- def imported_scripts
57
- @imported_scripts ||= []
58
- end
59
-
60
- def post_initialize
61
- # TODO: Should this method be required by extended classes?
62
- end
63
-
64
40
  # Get an Array of the Plot's current Syntaxes.
65
41
  #
66
42
  # @return [Array<Syntax>]
67
43
  def syntaxes
68
44
  playbook.syntaxes
69
45
  end
70
-
46
+
71
47
  # Prepare the Plot for the next turn of gameplay.
72
- # This method is typically called by the Engine that manages game execution.
48
+ # This method is typically called by the Engine that manages game
49
+ # execution.
50
+ #
73
51
  def ready
74
52
  playbook.freeze
75
- @running = true
76
- # Call the initial state to make sure it's set
77
- initial_state
78
53
  call_ready
79
54
  call_player_ready
80
- p_subplots.each { |s| s.ready }
55
+ subplots.each { |s| s.ready }
56
+ players.each do |p|
57
+ p.state.replace(
58
+ p.scene.state
59
+ .merge({
60
+ messages: p.messages,
61
+ last_prompt: p.last_prompt,
62
+ last_input: p.last_input
63
+ })
64
+ .merge(p.state)
65
+ )
66
+ p.output.replace(p.state)
67
+ p.state.clear
68
+ end
81
69
  end
82
-
70
+
83
71
  # Update the Plot's current turn of gameplay.
84
- # This method is typically called by the Engine that manages game execution.
72
+ # This method is typically called by the Engine that manages game
73
+ # execution.
74
+ #
85
75
  def update
86
76
  entities.each { |e| e.flush }
87
77
  call_before_player_update
88
- p_players.each { |p|
78
+ players.each do |p|
89
79
  p.performed nil
80
+ next unless p.scene
81
+ p.last_input = p.queue.last
82
+ p.last_prompt = p.scene.prompt
90
83
  p.scene.update
91
- }
92
- p_entities.each { |e| e.update }
84
+ if p.scene.is_a?(Scene::Conclusion)
85
+ player_conclude_procs.each do |proc|
86
+ proc.call p
87
+ end
88
+ end
89
+ end
93
90
  call_player_update
94
91
  call_update
95
- p_subplots.each { |s| s.update unless s.concluded? }
96
- p_subplots.delete_if { |s| s.concluded? }
92
+ subplots.each { |s| s.update unless s.concluded? }
93
+ subplots.delete_if { |s| s.concluded? }
97
94
  end
98
95
 
99
- def tell entities, message, refresh = false
96
+ # Send a message to a group of entities.
97
+ #
98
+ # @param entities [Array<Entity>]
99
+ # @param message [String]
100
+ def tell entities, message
100
101
  entities.each { |entity|
101
- entity.tell message, refresh
102
+ entity.tell message
102
103
  }
103
104
  end
104
-
105
- # Load a script into the current Plot.
106
- # This method is similar to Kernel#require, except that the script is
107
- # evaluated within the Plot's context via #stage.
108
- #
109
- # @param path [String] The path to the script being evaluated
110
- # @return [Boolean] true if the script was loaded by this call or false if it was already loaded.
111
- def script path
112
- imported_script = source.export(path)
113
- if imported_script.nil?
114
- raise LoadError.new("cannot load script -- #{path}")
115
- end
116
- if !@working_scripts.include?(imported_script) and !imported_scripts.include?(imported_script)
117
- @working_scripts.push imported_script
118
- # @hack Arguments need to be in different order if source returns proc
119
- if imported_script.read.kind_of?(Proc)
120
- stage &imported_script.read
121
- else
122
- stage imported_script.read, imported_script.absolute_path
123
- end
124
- @working_scripts.pop
125
- imported_scripts.push imported_script
126
- true
127
- else
128
- false
129
- end
130
- end
131
105
  end
106
+ end
132
107
 
108
+ module Gamefic
109
+ # @yieldself [Gamefic::Plot]
110
+ def self.script &block
111
+ Gamefic::Plot.script &block
112
+ end
133
113
  end