gamefic 1.6.0 → 2.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (105) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +12 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +16 -0
  5. data/.solargraph.yml +5 -0
  6. data/CHANGELOG.md +6 -0
  7. data/Gemfile +7 -0
  8. data/LICENSE +20 -0
  9. data/README.md +28 -0
  10. data/Rakefile +10 -0
  11. data/gamefic.gemspec +27 -0
  12. data/lib/gamefic.rb +11 -8
  13. data/lib/gamefic/action.rb +68 -58
  14. data/lib/gamefic/active.rb +331 -0
  15. data/lib/gamefic/actor.rb +8 -0
  16. data/lib/gamefic/command.rb +9 -7
  17. data/lib/gamefic/core_ext/array.rb +27 -49
  18. data/lib/gamefic/core_ext/string.rb +25 -16
  19. data/lib/gamefic/describable.rb +37 -22
  20. data/lib/gamefic/element.rb +47 -0
  21. data/lib/gamefic/entity.rb +24 -48
  22. data/lib/gamefic/{matchable.rb → keywords.rb} +52 -50
  23. data/lib/gamefic/messaging.rb +43 -45
  24. data/lib/gamefic/node.rb +14 -5
  25. data/lib/gamefic/plot.rb +73 -85
  26. data/lib/gamefic/plot/darkroom.rb +80 -0
  27. data/lib/gamefic/plot/host.rb +42 -46
  28. data/lib/gamefic/plot/snapshot.rb +14 -214
  29. data/lib/gamefic/query.rb +15 -17
  30. data/lib/gamefic/query/base.rb +51 -42
  31. data/lib/gamefic/query/children.rb +0 -0
  32. data/lib/gamefic/query/descendants.rb +2 -2
  33. data/lib/gamefic/query/external.rb +18 -0
  34. data/lib/gamefic/query/family.rb +3 -7
  35. data/lib/gamefic/query/matches.rb +75 -67
  36. data/lib/gamefic/query/parent.rb +0 -0
  37. data/lib/gamefic/query/siblings.rb +0 -0
  38. data/lib/gamefic/query/text.rb +12 -12
  39. data/lib/gamefic/query/tree.rb +17 -0
  40. data/lib/gamefic/scene.rb +1 -5
  41. data/lib/gamefic/scene/{active.rb → activity.rb} +4 -6
  42. data/lib/gamefic/scene/base.rb +77 -13
  43. data/lib/gamefic/scene/conclusion.rb +0 -2
  44. data/lib/gamefic/scene/custom.rb +0 -2
  45. data/lib/gamefic/scene/multiple_choice.rb +18 -16
  46. data/lib/gamefic/scene/multiple_scene.rb +29 -20
  47. data/lib/gamefic/scene/pause.rb +7 -2
  48. data/lib/gamefic/scene/yes_or_no.rb +21 -9
  49. data/lib/gamefic/scriptable.rb +88 -0
  50. data/lib/gamefic/serialize.rb +223 -0
  51. data/lib/gamefic/subplot.rb +47 -51
  52. data/lib/gamefic/syntax.rb +15 -13
  53. data/lib/gamefic/version.rb +3 -3
  54. data/lib/gamefic/world.rb +18 -0
  55. data/lib/gamefic/world/callbacks.rb +135 -0
  56. data/lib/gamefic/world/commands.rb +184 -0
  57. data/lib/gamefic/world/entities.rb +98 -0
  58. data/lib/gamefic/{plot → world}/playbook.rb +245 -236
  59. data/lib/gamefic/world/players.rb +37 -0
  60. data/lib/gamefic/world/scenes.rb +226 -0
  61. metadata +40 -108
  62. data/bin/gamefic +0 -9
  63. data/lib/gamefic/character.rb +0 -232
  64. data/lib/gamefic/character/state.rb +0 -12
  65. data/lib/gamefic/engine.rb +0 -7
  66. data/lib/gamefic/engine/base.rb +0 -66
  67. data/lib/gamefic/engine/tty.rb +0 -24
  68. data/lib/gamefic/grammar.rb +0 -13
  69. data/lib/gamefic/grammar/conjugator.rb +0 -20
  70. data/lib/gamefic/grammar/gender.rb +0 -11
  71. data/lib/gamefic/grammar/person.rb +0 -10
  72. data/lib/gamefic/grammar/plural.rb +0 -13
  73. data/lib/gamefic/grammar/pronouns.rb +0 -105
  74. data/lib/gamefic/grammar/tense.rb +0 -6
  75. data/lib/gamefic/grammar/verb_set.rb +0 -43
  76. data/lib/gamefic/grammar/verbs.rb +0 -26
  77. data/lib/gamefic/grammar/word_adapter.rb +0 -49
  78. data/lib/gamefic/plot/articles.rb +0 -22
  79. data/lib/gamefic/plot/callbacks.rb +0 -127
  80. data/lib/gamefic/plot/commands.rb +0 -121
  81. data/lib/gamefic/plot/entities.rb +0 -88
  82. data/lib/gamefic/plot/players.rb +0 -15
  83. data/lib/gamefic/plot/scenes.rb +0 -149
  84. data/lib/gamefic/plot/theater.rb +0 -73
  85. data/lib/gamefic/plot/you_mount.rb +0 -22
  86. data/lib/gamefic/script.rb +0 -13
  87. data/lib/gamefic/script/base.rb +0 -42
  88. data/lib/gamefic/script/file.rb +0 -14
  89. data/lib/gamefic/script/text.rb +0 -14
  90. data/lib/gamefic/shell.rb +0 -76
  91. data/lib/gamefic/source.rb +0 -14
  92. data/lib/gamefic/source/base.rb +0 -12
  93. data/lib/gamefic/source/file.rb +0 -23
  94. data/lib/gamefic/source/text.rb +0 -16
  95. data/lib/gamefic/tester.rb +0 -19
  96. data/lib/gamefic/text.rb +0 -8
  97. data/lib/gamefic/text/ansi.rb +0 -53
  98. data/lib/gamefic/text/html.rb +0 -68
  99. data/lib/gamefic/text/html/conversions.rb +0 -250
  100. data/lib/gamefic/text/html/entities.rb +0 -9
  101. data/lib/gamefic/tty.rb +0 -10
  102. data/lib/gamefic/user.rb +0 -8
  103. data/lib/gamefic/user/base.rb +0 -15
  104. data/lib/gamefic/user/buffer.rb +0 -32
  105. data/lib/gamefic/user/tty.rb +0 -54
@@ -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,45 +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
- #user.send message
15
- p_set_messages messages + message
16
- end
17
-
18
- # Send a message to the Character as raw text.
19
- # Unlike #tell, this method will not wrap the message in HTML paragraphs.
20
- #
21
- # @param message [String]
22
- def stream(message)
23
- p_set_messages messages + message.strip
24
- end
25
-
26
- # @return [String]
27
- def messages
28
- @messages ||= ''
29
- end
30
-
31
- def output
32
- messages
33
- end
34
-
35
- def flush
36
- p_set_messages '' unless messages.empty?
37
- end
38
-
39
- private
40
-
41
- def p_set_messages str
42
- @messages = str
43
- end
44
- end
45
- 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,123 +1,111 @@
1
- # TODO: JSON support is currently experimental.
2
- #require 'gamefic/entityloader'
3
- require 'gamefic/tester'
4
- require 'gamefic/source'
5
- require 'gamefic/script'
6
1
  require 'gamefic/query'
7
2
 
8
3
  module Gamefic
9
-
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
+ #
10
11
  class Plot
11
- autoload :Scenes, 'gamefic/plot/scenes'
12
- autoload :Commands, 'gamefic/plot/commands'
13
- autoload :Entities, 'gamefic/plot/entities'
14
- autoload :Articles, 'gamefic/plot/articles'
15
- autoload :YouMount, 'gamefic/plot/you_mount'
16
12
  autoload :Snapshot, 'gamefic/plot/snapshot'
13
+ autoload :Darkroom, 'gamefic/plot/darkroom'
17
14
  autoload :Host, 'gamefic/plot/host'
18
- autoload :Players, 'gamefic/plot/players'
19
- autoload :Playbook, 'gamefic/plot/playbook'
20
- autoload :Callbacks, 'gamefic/plot/callbacks'
21
- autoload :Theater, 'gamefic/plot/theater'
22
15
 
23
- attr_reader :commands, :imported_scripts, :source
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
16
+ # @return [Hash]
17
+ attr_reader :metadata
29
18
 
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
37
- end
19
+ attr_reader :static
38
20
 
39
- # @return [Gamefic::Plot::Playbook]
40
- def playbook
41
- @playbook ||= Gamefic::Plot::Playbook.new
42
- end
21
+ include World
22
+ include Scriptable
23
+ # @!parse extend Scriptable::ClassMethods
24
+ include Snapshot
25
+ include Host
26
+ include Serialize
43
27
 
44
- def running?
45
- @running
46
- end
28
+ exclude_from_serial [:@static]
47
29
 
48
- # Get an Array of all scripts that have been imported into the Plot.
49
- #
50
- # @return [Array<Script>] The imported scripts
51
- def imported_scripts
52
- @imported_scripts ||= []
53
- end
54
-
55
- def post_initialize
56
- # TODO: Should this method be required by extended classes?
30
+ # @param metadata [Hash]
31
+ def initialize metadata: {}
32
+ @metadata = metadata
33
+ run_scripts
34
+ @static = [self] + scene_classes + entities
57
35
  end
58
-
36
+
59
37
  # Get an Array of the Plot's current Syntaxes.
60
38
  #
61
39
  # @return [Array<Syntax>]
62
40
  def syntaxes
63
41
  playbook.syntaxes
64
42
  end
65
-
43
+
66
44
  # Prepare the Plot for the next turn of gameplay.
67
- # This method is typically called by the Engine that manages game execution.
45
+ # This method is typically called by the Engine that manages game
46
+ # execution.
47
+ #
68
48
  def ready
69
49
  playbook.freeze
70
- @running = true
71
50
  call_ready
72
51
  call_player_ready
73
- p_subplots.each { |s| s.ready }
52
+ subplots.each { |s| s.ready }
53
+ players.each do |p|
54
+ p.state.replace(
55
+ p.scene.state
56
+ .merge({
57
+ messages: p.messages,
58
+ last_prompt: p.last_prompt,
59
+ last_input: p.last_input,
60
+ queue: p.queue
61
+ })
62
+ .merge(p.state)
63
+ )
64
+ p.output.replace(p.state)
65
+ p.state.clear
66
+ end
74
67
  end
75
-
68
+
76
69
  # Update the Plot's current turn of gameplay.
77
- # This method is typically called by the Engine that manages game execution.
70
+ # This method is typically called by the Engine that manages game
71
+ # execution.
72
+ #
78
73
  def update
79
74
  entities.each { |e| e.flush }
80
75
  call_before_player_update
81
- p_players.each { |p| p.scene.update }
82
- p_entities.each { |e| e.update }
76
+ players.each do |p|
77
+ p.performed nil
78
+ next unless p.scene
79
+ p.last_input = p.queue.last
80
+ p.last_prompt = p.scene.prompt
81
+ p.scene.update
82
+ if p.scene.is_a?(Scene::Conclusion)
83
+ player_conclude_procs.each do |proc|
84
+ proc.call p
85
+ end
86
+ end
87
+ end
83
88
  call_player_update
84
89
  call_update
85
- p_subplots.each { |s| s.update unless s.concluded? }
86
- p_subplots.delete_if { |s| s.concluded? }
90
+ subplots.delete_if(&:concluded?)
91
+ subplots.each(&:update)
87
92
  end
88
93
 
89
- def tell entities, message, refresh = false
94
+ # Send a message to a group of entities.
95
+ #
96
+ # @param entities [Array<Entity>]
97
+ # @param message [String]
98
+ def tell entities, message
90
99
  entities.each { |entity|
91
- entity.tell message, refresh
100
+ entity.tell message
92
101
  }
93
102
  end
94
-
95
- # Load a script into the current Plot.
96
- # This method is similar to Kernel#require, except that the script is
97
- # evaluated within the Plot's context via #stage.
98
- #
99
- # @param path [String] The path to the script being evaluated
100
- # @return [Boolean] true if the script was loaded by this call or false if it was already loaded.
101
- def script path
102
- imported_script = source.export(path)
103
- if imported_script.nil?
104
- raise LoadError.new("cannot load script -- #{path}")
105
- end
106
- if !@working_scripts.include?(imported_script) and !imported_scripts.include?(imported_script)
107
- @working_scripts.push imported_script
108
- # @hack Arguments need to be in different order if source returns proc
109
- if imported_script.read.kind_of?(Proc)
110
- stage &imported_script.read
111
- else
112
- stage imported_script.read, imported_script.absolute_path
113
- end
114
- @working_scripts.pop
115
- imported_scripts.push imported_script
116
- true
117
- else
118
- false
119
- end
120
- end
121
103
  end
104
+ end
122
105
 
106
+ module Gamefic
107
+ # @yieldself [Gamefic::Plot]
108
+ def self.script &block
109
+ Gamefic::Plot.script &block
110
+ end
123
111
  end
@@ -0,0 +1,80 @@
1
+ module Gamefic
2
+ # Create and restore plot snapshots.
3
+ #
4
+ class Plot
5
+ class Darkroom
6
+ # @return [Gamefic::Plot]
7
+ attr_reader :plot
8
+
9
+ def initialize plot
10
+ @plot = plot
11
+ end
12
+
13
+ # Create a snapshot of the plot.
14
+ #
15
+ # @return [Hash]
16
+ def save
17
+ index = plot.static + plot.players
18
+ plot.to_serial(index)
19
+ {
20
+ 'program' => {}, # @todo Metadata for version control, etc.
21
+ 'index' => index.map do |i|
22
+ if i.is_a?(Gamefic::Serialize)
23
+ {
24
+ 'class' => i.class.to_s,
25
+ 'ivars' => i.serialize_instance_variables(index)
26
+ }
27
+ else
28
+ i.to_serial(index)
29
+ end
30
+ end
31
+ }
32
+ end
33
+
34
+ # Restore a snapshot.
35
+ #
36
+ # @param snapshot [Hash]
37
+ def restore snapshot
38
+ # @todo Use `program` for verification
39
+
40
+ plot.subplots.each(&:conclude)
41
+ plot.subplots.clear
42
+
43
+ index = plot.static + plot.players
44
+ snapshot['index'].each_with_index do |obj, idx|
45
+ next if index[idx]
46
+ elematch = obj['class'].match(/^#<ELE_([\d]+)>$/)
47
+ if elematch
48
+ klass = index[elematch[1].to_i]
49
+ else
50
+ klass = Gamefic::Serialize.string_to_constant(obj['class'])
51
+ end
52
+ index.push klass.allocate
53
+ end
54
+
55
+ snapshot['index'].each_with_index do |obj, idx|
56
+ if index[idx].class.to_s != obj['class']
57
+ STDERR.puts "MISMATCH: #{index[idx].class} is not #{obj['class']}"
58
+ STDERR.puts obj.inspect
59
+ end
60
+ obj['ivars'].each_pair do |k, v|
61
+ next if k == '@subplots'
62
+ uns = v.from_serial(index)
63
+ next if uns == "#<UNKNOWN>"
64
+ index[idx].instance_variable_set(k, uns)
65
+ end
66
+ if index[idx].is_a?(Gamefic::Subplot)
67
+ index[idx].extend Gamefic::Scriptable
68
+ index[idx].instance_variable_set(:@theater, nil)
69
+ index[idx].send(:run_scripts)
70
+ index[idx].players.each do |pl|
71
+ pl.playbooks.push index[idx].playbook unless pl.playbooks.include?(index[idx].playbook)
72
+ end
73
+ index[idx].instance_variable_set(:@static, [index[idx]] + index[idx].scene_classes + index[idx].entities)
74
+ plot.subplots.push index[idx]
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end