gamefic 1.7.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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