gamefic 1.5.1 → 1.6.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 (70) hide show
  1. checksums.yaml +4 -4
  2. data/lib/gamefic.rb +1 -3
  3. data/lib/gamefic/action.rb +140 -79
  4. data/lib/gamefic/character.rb +120 -53
  5. data/lib/gamefic/character/state.rb +12 -0
  6. data/lib/gamefic/core_ext/array.rb +53 -11
  7. data/lib/gamefic/core_ext/string.rb +1 -0
  8. data/lib/gamefic/describable.rb +37 -11
  9. data/lib/gamefic/engine/base.rb +17 -4
  10. data/lib/gamefic/engine/tty.rb +4 -0
  11. data/lib/gamefic/entity.rb +4 -15
  12. data/lib/gamefic/matchable.rb +50 -0
  13. data/lib/gamefic/messaging.rb +45 -0
  14. data/lib/gamefic/node.rb +16 -0
  15. data/lib/gamefic/plot.rb +27 -33
  16. data/lib/gamefic/plot/{article_mount.rb → articles.rb} +22 -22
  17. data/lib/gamefic/plot/callbacks.rb +30 -4
  18. data/lib/gamefic/plot/{command_mount.rb → commands.rb} +121 -121
  19. data/lib/gamefic/plot/entities.rb +3 -3
  20. data/lib/gamefic/plot/host.rb +3 -3
  21. data/lib/gamefic/plot/playbook.rb +74 -30
  22. data/lib/gamefic/plot/scenes.rb +149 -0
  23. data/lib/gamefic/plot/snapshot.rb +14 -39
  24. data/lib/gamefic/plot/theater.rb +73 -0
  25. data/lib/gamefic/query.rb +6 -19
  26. data/lib/gamefic/query/base.rb +127 -246
  27. data/lib/gamefic/query/children.rb +6 -7
  28. data/lib/gamefic/query/descendants.rb +15 -0
  29. data/lib/gamefic/query/family.rb +19 -7
  30. data/lib/gamefic/query/itself.rb +13 -0
  31. data/lib/gamefic/query/matches.rb +67 -11
  32. data/lib/gamefic/query/parent.rb +6 -7
  33. data/lib/gamefic/query/siblings.rb +10 -7
  34. data/lib/gamefic/query/text.rb +39 -35
  35. data/lib/gamefic/scene.rb +1 -1
  36. data/lib/gamefic/scene/active.rb +12 -6
  37. data/lib/gamefic/scene/base.rb +56 -5
  38. data/lib/gamefic/scene/conclusion.rb +3 -0
  39. data/lib/gamefic/scene/custom.rb +0 -83
  40. data/lib/gamefic/scene/multiple_choice.rb +54 -32
  41. data/lib/gamefic/scene/multiple_scene.rb +11 -6
  42. data/lib/gamefic/scene/pause.rb +3 -4
  43. data/lib/gamefic/scene/yes_or_no.rb +23 -9
  44. data/lib/gamefic/script/base.rb +4 -0
  45. data/lib/gamefic/subplot.rb +22 -19
  46. data/lib/gamefic/syntax.rb +7 -15
  47. data/lib/gamefic/user/base.rb +7 -13
  48. data/lib/gamefic/user/buffer.rb +7 -0
  49. data/lib/gamefic/user/tty.rb +13 -12
  50. data/lib/gamefic/version.rb +1 -1
  51. metadata +11 -37
  52. data/lib/gamefic/director.rb +0 -23
  53. data/lib/gamefic/director/delegate.rb +0 -126
  54. data/lib/gamefic/director/order.rb +0 -17
  55. data/lib/gamefic/director/parser.rb +0 -137
  56. data/lib/gamefic/keywords.rb +0 -67
  57. data/lib/gamefic/plot/query_mount.rb +0 -9
  58. data/lib/gamefic/plot/scene_mount.rb +0 -182
  59. data/lib/gamefic/query/ambiguous_children.rb +0 -5
  60. data/lib/gamefic/query/expression.rb +0 -47
  61. data/lib/gamefic/query/many_children.rb +0 -7
  62. data/lib/gamefic/query/plural_children.rb +0 -14
  63. data/lib/gamefic/query/self.rb +0 -10
  64. data/lib/gamefic/scene_data.rb +0 -10
  65. data/lib/gamefic/scene_data/base.rb +0 -12
  66. data/lib/gamefic/scene_data/multiple_choice.rb +0 -19
  67. data/lib/gamefic/scene_data/multiple_scene.rb +0 -21
  68. data/lib/gamefic/scene_data/yes_or_no.rb +0 -18
  69. data/lib/gamefic/serialized.rb +0 -24
  70. data/lib/gamefic/stage.rb +0 -106
@@ -0,0 +1,12 @@
1
+ module Gamefic
2
+ class Character
3
+ module State
4
+ def state
5
+ @state = {}
6
+ @state.merge! scene.state unless scene.nil?
7
+ @state.merge! output: messages
8
+ @state
9
+ end
10
+ end
11
+ end
12
+ end
@@ -1,4 +1,15 @@
1
1
  class Array
2
+ # Get a subset of the array that matches the argument.
3
+ # If the argument is a Class or Module, the elements must be of the type.
4
+ # If the argument is a Symbol, it should be a method for which the elements must return true.
5
+ # If the argument is an Object, the elements must equal the object.
6
+ #
7
+ # @example
8
+ # animals = ['dog', 'cat', nil]
9
+ # animals.that_are(String) #=> ['dog', 'cat']
10
+ # animals.that_are('dog') #=> ['dog']
11
+ # animals.that_are(:nil?) #=> [nil]
12
+ #
2
13
  # @return [Array]
3
14
  def that_are(cls)
4
15
  if (cls.kind_of?(Class) or cls.kind_of?(Module))
@@ -12,6 +23,10 @@ class Array
12
23
  return Array.new
13
24
  end
14
25
  end
26
+
27
+ # Get a subset of the array that does not match the argument.
28
+ # See Array#that_are for information about how arguments are evaluated.
29
+ #
15
30
  # @return [Array]
16
31
  def that_are_not(cls)
17
32
  if (cls.kind_of?(Class) or cls.kind_of?(Module))
@@ -22,26 +37,50 @@ class Array
22
37
  return self.clone - [cls]
23
38
  end
24
39
  end
40
+
41
+ # Get a random element from the array.
42
+ # @deprecated Use Array#sample instead.
43
+ #
25
44
  def random
26
45
  return self[rand(self.length)]
27
46
  end
47
+
48
+ # Pop a random element from the array.
49
+ # @deprecated Use Array#pop_sample instead.
50
+ #
28
51
  def pop_random
52
+ pop_sample
53
+ end
54
+
55
+ # Pop a random element from the array.
56
+ #
57
+ def pop_sample
29
58
  delete_at(rand(self.length))
30
59
  end
60
+
61
+ # @todo Candidate for deprecation
31
62
  # @return [Array]
32
- def shuffle
33
- self.sort { |a, b|
34
- rand(3) <=> rand(3)
35
- }
36
- end
63
+ #def shuffle
64
+ # self.sort { |a, b|
65
+ # rand(3) <=> rand(3)
66
+ # }
67
+ #end
68
+
69
+ # @todo Candidate for deprecation
37
70
  # @return [Array]
38
- def shuffle!
39
- self.sort! { |a, b|
40
- rand(3) <=> rand(3)
41
- }
42
- end
43
- # Get a string representation of the array that separated elements with
71
+ #def shuffle!
72
+ # self.sort! { |a, b|
73
+ # rand(3) <=> rand(3)
74
+ # }
75
+ #end
76
+
77
+ # Get a string representation of the array that separates elements with
44
78
  # commas and adds a conjunction before the last element.
79
+ #
80
+ # @example
81
+ # animals = ['a dog', 'a cat', 'a mouse']
82
+ # animals.join_and #=> 'a dog, a cat, and a mouse'
83
+ #
45
84
  # @return [String]
46
85
  def join_and(sep = ', ', andSep = ' and ', serial = true)
47
86
  if self.length < 3
@@ -51,6 +90,9 @@ class Array
51
90
  start.join(sep) + "#{serial ? sep.strip : ''}#{andSep}#{self.last}"
52
91
  end
53
92
  end
93
+
94
+ # @see Array#join_and
95
+ #
54
96
  # @return [String]
55
97
  def join_or(sep = ', ', orSep = ' or ', serial = true)
56
98
  join_and(sep, orSep, serial)
@@ -1,4 +1,5 @@
1
1
  class String
2
+ include Gamefic::Matchable
2
3
  # Capitalize the first letter without changing the rest of the string.
3
4
  # (String#capitalize makes the rest of the string lower-case.)
4
5
  def capitalize_first
@@ -1,38 +1,48 @@
1
- require 'gamefic/keywords'
1
+ #require 'gamefic/keywords'
2
2
  require 'gamefic/grammar'
3
3
 
4
4
  module Gamefic
5
5
 
6
- # Add a variety of text properties for naming, describing, and properly
7
- # referencing objects.
6
+ # Add a variety of text properties for naming, describing, and referencing
7
+ # objects.
8
8
  module Describable
9
9
  include Grammar::Person, Grammar::Plural
10
+ include Matchable
11
+
12
+ # @return [String]
10
13
  attr_reader :name
11
- attr_accessor :synonyms, :indefinite_article
12
- attr_writer :definite_article
13
-
14
+
15
+ # @return [String]
16
+ attr_reader :synonyms
17
+
18
+ # @return [String]
19
+ attr_reader :indefinite_article
20
+
21
+ # @return [String]
22
+ attr_reader :definite_article
23
+
14
24
  # Get a set of Keywords associated with the object.
15
25
  # Keywords are typically the words in the object's name plus its synonyms.
16
26
  #
17
27
  # @return [Keywords]
18
28
  def keywords
19
- Keywords.new "#{name} #{synonyms}"
29
+ @keywords ||= "#{definite_article} #{indefinite_article} #{name} #{synonyms}".downcase.split(Matchable::SPLIT_REGEXP).uniq
20
30
  end
21
-
31
+
22
32
  # Get the name of the object with an indefinite article.
23
33
  # Note: proper-named objects never append an article, though an article
24
34
  # may be included in its proper name.
25
35
  #
26
36
  # @return [String]
27
37
  def indefinitely
28
- ((proper_named? or indefinite_article == '') ? '' : "#{indefinite_article} ") + name
38
+ ((proper_named? or indefinite_article == '') ? '' : "#{indefinite_article} ") + name.to_s
29
39
  end
30
40
 
31
41
  # Get the name of the object with a definite article.
32
42
  # Note: proper-named objects never append an article, though an article
33
43
  # may be included in its proper name.
34
44
  def definitely
35
- ((proper_named? or definite_article == '') ? '' : "#{definite_article} ") + name
45
+ ((proper_named? or definite_article == '') ? '' : "#{definite_article} ") + name.to_s
36
46
  end
37
47
 
38
48
  # Get the definite article for this object.
@@ -42,6 +52,16 @@ module Gamefic
42
52
  @definite_article || "the"
43
53
  end
44
54
 
55
+ def definite_article= article
56
+ @keywords = nil
57
+ @definite_article = article
58
+ end
59
+
60
+ def indefinite_article= article
61
+ @keywords = nil
62
+ @indefinite_article = article
63
+ end
64
+
45
65
  # Is the object proper-named?
46
66
  # Proper-named objects typically do not add articles to their names when
47
67
  # referenced #definitely or #indefinitely, e.g., "Jane Doe" instead of
@@ -54,7 +74,7 @@ module Gamefic
54
74
 
55
75
  # Set whether the object has a proper name.
56
76
  #
57
- # @param [Boolean]
77
+ # @param bool [Boolean]
58
78
  def proper_named=(bool)
59
79
  if bool == true
60
80
  if @definite_article != nil
@@ -71,6 +91,7 @@ module Gamefic
71
91
  #
72
92
  # @param value [String]
73
93
  def name=(value)
94
+ @keywords = nil
74
95
  words = value.split_words
75
96
  if ['a','an'].include?(words[0].downcase)
76
97
  @indefinite_article = words[0].downcase
@@ -120,6 +141,11 @@ module Gamefic
120
141
  end
121
142
  end
122
143
 
144
+ def synonyms= text
145
+ @keywords = nil
146
+ @synonyms = text
147
+ end
148
+
123
149
  # Set the object's default description.
124
150
  # The default description is typically set in an object's initialization
125
151
  # to ensure that a non-empty string is available when a instance-specific
@@ -29,22 +29,35 @@ module Gamefic
29
29
  def run
30
30
  connect
31
31
  @plot.introduce @character
32
+ @user.update @character.state
32
33
  turn until @character.concluded?
33
- print @user.flush
34
+ #print @user.flush
34
35
  end
35
36
 
36
37
  def turn
37
38
  @plot.ready
38
- print @user.flush
39
+ unless @character.state[:options].nil?
40
+ list = '<ol class="multiple_choice">'
41
+ @character.state[:options].each { |o|
42
+ list += "<li><a href=\"#\" rel=\"gamefic\" data-command=\"#{o}\">#{o}</a></li>"
43
+ }
44
+ list += "</ol>"
45
+ @character.tell list
46
+ end
47
+ #print @user.flush
48
+ @user.update @character.state
49
+ #@character.flush
39
50
  if @character.queue.empty?
40
51
  receive
41
52
  end
42
53
  @plot.update
43
- print @user.flush
54
+ #print @user.flush
55
+ @user.update @character.state
56
+ #@character.flush
44
57
  end
45
58
 
46
59
  def receive
47
- print @character.scene.prompt_for(@character) + ' '
60
+ print @character.scene.prompt + ' '
48
61
  input = STDIN.gets
49
62
  @character.queue.push input unless input.nil?
50
63
  end
@@ -5,6 +5,10 @@ module Gamefic
5
5
  # Extend Engine::Base to connect with User::Tty, which provides ANSI
6
6
  # formatting for HTML.
7
7
  #
8
+ # @note Due to their dependency on io/console, User::Tty and Engine::Tty are
9
+ # not included in the core Gamefic library. `require gamefic/tty` if you
10
+ # need them.
11
+ #
8
12
  class Engine::Tty < Engine::Base
9
13
  def post_initialize
10
14
  self.user_class = Gamefic::User::Tty
@@ -1,19 +1,17 @@
1
1
  require "gamefic/node"
2
2
  require "gamefic/describable"
3
- require "gamefic/serialized"
3
+ require 'gamefic/messaging'
4
4
 
5
5
  module Gamefic
6
6
 
7
7
  class Entity
8
8
  include Node
9
9
  include Describable
10
- include Serialized
11
- extend Serialized::ClassMethods
10
+ include Messaging
12
11
  include Grammar::WordAdapter
13
-
12
+
14
13
  attr_reader :session
15
- serialize :name, :parent, :description
16
-
14
+
17
15
  def initialize(args = {})
18
16
  pre_initialize
19
17
  args.each { |key, value|
@@ -39,15 +37,6 @@ module Gamefic
39
37
  # raise NotImplementedError, "#{self.class} must implement post_initialize"
40
38
  end
41
39
 
42
- def tell(message)
43
- #TODO: Should this even be here? In all likelihood, only Characters receive tells, right?
44
- #TODO: On second thought, it might be interesting to see logs from an npc point of view.
45
- end
46
-
47
- def stream(message)
48
- # Unlike tell, this method sends raw data without formatting.
49
- end
50
-
51
40
  # Execute the entity's on_update blocks.
52
41
  # This method is typically called by the Engine that manages game execution.
53
42
  # The base method does nothing. Subclasses can override it.
@@ -0,0 +1,50 @@
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
@@ -0,0 +1,45 @@
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
data/lib/gamefic/node.rb CHANGED
@@ -11,6 +11,8 @@ module Gamefic
11
11
  @children ||= []
12
12
  @children.clone
13
13
  end
14
+
15
+ # @return [Array]
14
16
  def flatten
15
17
  array = Array.new
16
18
  children.each { |child|
@@ -18,9 +20,12 @@ module Gamefic
18
20
  }
19
21
  return array
20
22
  end
23
+
24
+ # @return [Object]
21
25
  def parent
22
26
  @parent
23
27
  end
28
+
24
29
  def parent=(node)
25
30
  return if node == @parent
26
31
  if node == self
@@ -43,19 +48,30 @@ module Gamefic
43
48
  end
44
49
  end
45
50
  end
51
+
52
+ # @return [Boolean]
53
+ def accessible?
54
+ true
55
+ end
56
+
46
57
  protected
58
+
47
59
  def add_child(node)
48
60
  children
49
61
  @children.push(node)
50
62
  end
63
+
51
64
  def rem_child(node)
52
65
  children
53
66
  @children.delete(node)
54
67
  end
68
+
55
69
  def concat_children(children)
56
70
  children.concat(children)
57
71
  end
72
+
58
73
  private
74
+
59
75
  def recurse_flatten(node)
60
76
  array = Array.new
61
77
  array.push(node)