gamefic 1.4.1 → 1.5.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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/lib/gamefic.rb +1 -2
  3. data/lib/gamefic/character.rb +31 -45
  4. data/lib/gamefic/director/delegate.rb +46 -24
  5. data/lib/gamefic/director/order.rb +7 -0
  6. data/lib/gamefic/director/parser.rb +11 -11
  7. data/lib/gamefic/engine/base.rb +2 -3
  8. data/lib/gamefic/entity.rb +8 -26
  9. data/lib/gamefic/plot.rb +38 -242
  10. data/lib/gamefic/plot/callbacks.rb +101 -0
  11. data/lib/gamefic/plot/command_mount.rb +70 -40
  12. data/lib/gamefic/plot/entities.rb +82 -0
  13. data/lib/gamefic/plot/host.rb +46 -0
  14. data/lib/gamefic/plot/playbook.rb +192 -0
  15. data/lib/gamefic/plot/players.rb +15 -0
  16. data/lib/gamefic/plot/scene_mount.rb +69 -31
  17. data/lib/gamefic/plot/snapshot.rb +20 -5
  18. data/lib/gamefic/scene/active.rb +8 -1
  19. data/lib/gamefic/scene/base.rb +4 -26
  20. data/lib/gamefic/scene/custom.rb +53 -3
  21. data/lib/gamefic/scene/multiple_choice.rb +1 -0
  22. data/lib/gamefic/scene/yes_or_no.rb +1 -1
  23. data/lib/gamefic/scene_data/multiple_scene.rb +0 -4
  24. data/lib/gamefic/shell.rb +0 -1
  25. data/lib/gamefic/source/file.rb +1 -1
  26. data/lib/gamefic/stage.rb +10 -2
  27. data/lib/gamefic/subplot.rb +70 -53
  28. data/lib/gamefic/syntax.rb +9 -2
  29. data/lib/gamefic/tester.rb +1 -1
  30. data/lib/gamefic/text.rb +8 -0
  31. data/lib/gamefic/{ansi.rb → text/ansi.rb} +12 -15
  32. data/lib/gamefic/text/html.rb +68 -0
  33. data/lib/gamefic/text/html/conversions.rb +250 -0
  34. data/lib/gamefic/text/html/entities.rb +9 -0
  35. data/lib/gamefic/tty.rb +2 -0
  36. data/lib/gamefic/user/tty.rb +2 -4
  37. data/lib/gamefic/version.rb +1 -1
  38. metadata +12 -8
  39. data/lib/gamefic/direction.rb +0 -46
  40. data/lib/gamefic/html.rb +0 -68
  41. data/lib/gamefic/html_to_ansi.rb +0 -185
  42. data/lib/gamefic/plot/entity_mount.rb +0 -45
  43. data/lib/gamefic/rule.rb +0 -18
@@ -14,6 +14,7 @@ module Gamefic
14
14
 
15
15
  def start actor
16
16
  data = start_data_for(actor)
17
+ data.options.clear
17
18
  do_start_block actor, data
18
19
  tell_options actor, data
19
20
  end
@@ -15,7 +15,7 @@ module Gamefic
15
15
  if data.yes? or data.no?
16
16
  this_scene = actor.scene
17
17
  do_finish_block actor, data
18
- actor.cue :active if (actor.scene == this_scene and actor.next_scene.nil?)
18
+ #actor.cue :active if (actor.scene == this_scene and actor.next_scene.nil?)
19
19
  else
20
20
  actor.tell data.invalid_message
21
21
  end
@@ -1,8 +1,6 @@
1
1
  module Gamefic
2
2
 
3
3
  class SceneData::MultipleScene < SceneData::MultipleChoice
4
- attr_accessor :selection
5
- attr_accessor :number
6
4
  def options
7
5
  scene_map.keys
8
6
  end
@@ -15,8 +13,6 @@ module Gamefic
15
13
  scene_map[choice]
16
14
  end
17
15
 
18
- private
19
-
20
16
  def scene_map
21
17
  @scene_map ||= {}
22
18
  end
data/lib/gamefic/shell.rb CHANGED
@@ -70,7 +70,6 @@ module Gamefic
70
70
  plot = Plot.new(Source::File.new(File.join(directory, 'scripts')))
71
71
  plot.script 'main'
72
72
  plot.metadata = YAML.load_file File.join(directory, 'metadata.yaml')
73
- puts ""
74
73
  Engine::Tty.start(plot)
75
74
  end
76
75
  end
@@ -16,7 +16,7 @@ module Gamefic
16
16
  end
17
17
  }
18
18
  }
19
- raise "Script #{path} not found"
19
+ raise LoadError.new("cannot load script -- #{path}")
20
20
  end
21
21
  end
22
22
 
data/lib/gamefic/stage.rb CHANGED
@@ -3,16 +3,17 @@ module Gamefic
3
3
  module Stage
4
4
  def stage *args, &block
5
5
  s = generate_stage
6
-
7
6
  if block.nil?
8
7
  s.module_eval(*args)
9
8
  else
10
9
  s.module_exec(*args, &block)
11
10
  end
12
11
  end
12
+
13
13
  private
14
+
14
15
  def generate_stage
15
- return @stage if !@stage.nil?
16
+ return @stage unless @stage.nil?
16
17
 
17
18
  exposed = self.class.exposed_methods.keys
18
19
  mounted = self.class.mounted_modules.keys
@@ -44,6 +45,7 @@ module Gamefic
44
45
 
45
46
  return @stage
46
47
  end
48
+
47
49
  module ClassMethods
48
50
  def mount *args
49
51
  args.each { |a|
@@ -51,22 +53,28 @@ module Gamefic
51
53
  mounted_modules[a] = nil
52
54
  }
53
55
  end
56
+
54
57
  def expose *args
55
58
  args.each { |a|
56
59
  exposed_methods[a] = nil
57
60
  }
58
61
  end
62
+
59
63
  def exposed_methods
60
64
  @@exposed_methods ||= from_superclass(:exposed_methods, {}).dup
61
65
  end
66
+
62
67
  def mounted_modules
63
68
  @@mounted_modules ||= from_superclass(:mounted_modules, {}).dup
64
69
  end
70
+
65
71
  private
72
+
66
73
  def from_superclass(m, default = nil)
67
74
  superclass.respond_to?(m) ? superclass.send(m) : default
68
75
  end
69
76
  end
77
+
70
78
  def self.included(base)
71
79
  base.extend(ClassMethods)
72
80
  end
@@ -1,82 +1,99 @@
1
+ require 'gamefic/plot'
2
+
1
3
  module Gamefic
2
4
 
3
5
  class Subplot
4
- module Element
5
- attr_reader :subplot
6
- end
7
- module Feature
8
- def subplots
9
- p_subplots.clone
10
- end
11
- private
12
- def p_subplots
13
- @p_subplots ||= []
14
- end
15
- end
16
- module Host
17
- # Get an array of all the current subplots.
18
- #
19
- # @return [Array<Subplot>]
20
- def subplots
21
- p_subplots.clone
22
- end
23
-
24
- # Start a new subplot based on the provided class.
25
- #
26
- # @param [Class] The class of the subplot to be created (Subplot by default)
27
- # @return [Subplot]
28
- def branch subplot_class, feature:nil, &block
29
- subplot = subplot_class.new(self, feature: feature, &block)
30
- p_subplots.push subplot
31
- subplot
6
+ include Stage
7
+
8
+ attr_reader :plot
9
+ attr_writer :denied_message
10
+
11
+ mount Plot::Entities
12
+ mount Plot::CommandMount
13
+ mount Plot::Callbacks
14
+ mount Plot::SceneMount
15
+ expose :plot, :conclude
16
+
17
+ class << self
18
+ def start_proc
19
+ @start_proc
32
20
  end
33
-
21
+
34
22
  private
35
- def p_subplots
36
- @p_subplots ||= []
23
+
24
+ def on_start &block
25
+ @start_proc = block
37
26
  end
38
27
  end
39
-
40
- attr_reader :plot, :entities, :players
41
-
42
- def initialize plot, feature:nil
28
+
29
+ def initialize plot, introduce: nil
43
30
  @plot = plot
44
- @entities = []
45
- @players = []
46
31
  @concluded = false
47
- post_initialize
48
- yield(self) if block_given?
49
- introduce feature unless feature.nil?
32
+ stage &self.class.start_proc unless self.class.start_proc.nil?
33
+ playbook.freeze
34
+ self.introduce introduce unless introduce.nil?
35
+ end
36
+
37
+ def default_scene
38
+ plot.default_scene
39
+ end
40
+
41
+ def default_conclusion
42
+ plot.default_conclusion
43
+ end
44
+
45
+ def playbook
46
+ @playbook ||= plot.playbook.dup
50
47
  end
51
- def post_initialize
48
+
49
+ # HACK: Always assume subplots are running for the sake of entity destruction
50
+ def running?
51
+ true
52
52
  end
53
- def make *args
54
- e = plot.make(*args)
55
- e.extend Gamefic::Subplot::Element
56
- e.instance_variable_set(:@subplot, self)
57
- entities.push e
58
- e
53
+
54
+ def denied_message
55
+ @denied_message ||= 'You are already involved in another subplot.'
59
56
  end
57
+
60
58
  def introduce player
61
- player.send(:p_subplots).push self
62
- players.push player
59
+ if plot.subbed?(player)
60
+ player.tell denied_message
61
+ else
62
+ super
63
+ end
63
64
  end
65
+
64
66
  def exeunt player
65
- player.send(:p_subplots).delete self
66
- players.delete player
67
+ player.playbook = plot.playbook
68
+ p_players.delete player
67
69
  end
70
+
68
71
  def conclude
69
- concluded = true
72
+ @concluded = true
70
73
  entities.each { |e|
71
- e.destroy
74
+ destroy e
72
75
  }
73
76
  players.each { |p|
74
77
  exeunt p
75
78
  }
76
79
  end
80
+
77
81
  def concluded?
78
82
  @concluded
79
83
  end
84
+
85
+ def ready
86
+ conclude if players.empty?
87
+ return if concluded?
88
+ playbook.freeze
89
+ call_ready
90
+ call_player_ready
91
+ end
92
+
93
+ def update
94
+ call_player_update
95
+ call_update
96
+ end
80
97
  end
81
98
 
82
99
  end
@@ -60,7 +60,10 @@ module Gamefic
60
60
  m = text.match(@regexp)
61
61
  return nil if m.nil?
62
62
  arguments = []
63
- @replace.to_s.split_words.each { |r|
63
+ # HACK: Skip the first word if the verb is not nil.
64
+ # This is ugly.
65
+ b = @verb.nil? ? 0 : 1
66
+ @replace.to_s.split_words[b..-1].each { |r|
64
67
  if r.match(/^\{\$[0-9]+\}$/)
65
68
  arguments.push m[r[2..-2].to_i]
66
69
  else
@@ -70,6 +73,10 @@ module Gamefic
70
73
  Command.new @verb, arguments
71
74
  end
72
75
 
76
+ def accept? text
77
+ !text.match(@regexp).nil?
78
+ end
79
+
73
80
  # Get a signature that identifies the form of the Syntax.
74
81
  # Signatures are used to compare Syntaxes to each other.
75
82
  #
@@ -106,7 +113,7 @@ module Gamefic
106
113
  }
107
114
  cb <=> ca
108
115
  }
109
- matches
116
+ matches
110
117
  end
111
118
  end
112
119
 
@@ -9,7 +9,7 @@ module Gamefic
9
9
  end
10
10
  def run_test name, actor
11
11
  queue = []
12
- actor.plot.stage actor, queue, &test_procs[name]
12
+ stage actor, queue, &test_procs[name]
13
13
  actor.queue.push *queue
14
14
  actor[:test_queue_length] = queue.length
15
15
  actor[:test_queue_scene] = actor.scene
@@ -0,0 +1,8 @@
1
+ module Gamefic
2
+
3
+ module Text
4
+ autoload :Ansi, 'gamefic/text/ansi'
5
+ autoload :Html, 'gamefic/text/html'
6
+ end
7
+
8
+ end
@@ -1,11 +1,8 @@
1
1
  module Gamefic
2
2
 
3
- # Constants for ANSI codes, plus Extras for custom formatting.
4
- module Ansi
3
+ # Constants for ANSI codes, plus ExtraCodes for custom formatting.
4
+ module Text::Ansi
5
5
  module Code
6
- class Nonstandard < String
7
-
8
- end
9
6
  module Attribute
10
7
  NORMAL = 0
11
8
  BOLD = 1
@@ -35,19 +32,19 @@ module Gamefic
35
32
  WHITE = 47
36
33
  end
37
34
  module Extra
38
- BLOCK = Nonstandard.new("block")
39
- PRE = Nonstandard.new("pre")
40
- HREF = Nonstandard.new("href")
41
- IMAGE = Nonstandard.new("image")
42
- SRC = Nonstandard.new("src")
43
- UPPERCASE = Nonstandard.new("uppercase")
44
- COMMAND = Nonstandard.new("command")
45
- IGNORED = Nonstandard.new("ignored")
46
- LINE = Nonstandard.new("line")
35
+ BLOCK = :block
36
+ PRE = :pre
37
+ HREF = :href
38
+ IMAGE = :image
39
+ SRC = :src
40
+ UPPERCASE = :uppercase
41
+ COMMAND = :command
42
+ IGNORED = :ignored
43
+ LINE = :line
47
44
  end
48
45
  end
49
46
  def self.graphics_mode(*settings)
50
- ansi = settings.flatten.that_are_not(Code::Nonstandard)
47
+ ansi = settings.flatten.that_are(Fixnum)
51
48
  return '' if ansi.length == 0
52
49
  "\e[#{ansi.join(';')}m"
53
50
  end
@@ -0,0 +1,68 @@
1
+ require 'rexml/document'
2
+ require 'gamefic/text/html/entities'
3
+
4
+ module Gamefic
5
+ module Text
6
+ module Html
7
+ autoload :Conversions, 'gamefic/text/html/conversions'
8
+
9
+ # Convert ampersands to &amp;
10
+ #
11
+ # @param text [String]
12
+ # @return [String]
13
+ def self.fix_ampersands(text)
14
+ codes = []
15
+ ENTITIES.keys.each { |e|
16
+ codes.push e[1..-1]
17
+ }
18
+ piped = codes.join('|')
19
+ re = Regexp.new("&(?!(#{piped}))")
20
+ text.gsub(re, '&amp;\1')
21
+ end
22
+
23
+ # Encode a String with HTML entities
24
+ #
25
+ # @param text [String]
26
+ # @return [String]
27
+ def self.encode(text)
28
+ encoded = text
29
+ ENTITIES.each { |k, v|
30
+ encoded = encoded.gsub(v, k)
31
+ }
32
+ encoded
33
+ end
34
+
35
+ # Decode a String's HTML entities
36
+ #
37
+ # @param text [String]
38
+ # @return [String]
39
+ def self.decode(text)
40
+ ENTITIES.each { |k, v|
41
+ text = text.gsub(k, v)
42
+ }
43
+ text
44
+ end
45
+
46
+ # Parse a String into an XML document
47
+ #
48
+ # @param code [String]
49
+ # @return [REXML::Document]
50
+ def self.parse(code)
51
+ code = fix_ampersands(code).strip
52
+ last = nil
53
+ begin
54
+ doc = REXML::Document.new code
55
+ rescue REXML::ParseException => e
56
+ # Convert invalid < characters to &lt;
57
+ if e.source.buffer != last and e.source.buffer[0,1] == '<'
58
+ code = code[0,(code.length - e.source.buffer.length)] + '&lt;' + e.source.buffer[1..-1]
59
+ last = e.source.buffer
60
+ retry
61
+ end
62
+ raise e
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ end
@@ -0,0 +1,250 @@
1
+ require 'gamefic/text/ansi'
2
+ require 'gamefic/text/html'
3
+ require 'io/console'
4
+
5
+ module Gamefic
6
+
7
+ module Text::Html::Conversions
8
+ include Gamefic::Text
9
+
10
+ def self.html_to_ansi text, wrap: true, width: nil
11
+ return '' if text.strip == ''
12
+ output = ''
13
+ begin
14
+ doc = Html.parse("<body>#{text.gsub(/\r/, '').strip}</body>")
15
+ output = AnsiFormatter.new.format(doc) + Ansi.graphics_mode(Ansi::Code::Attribute::NORMAL)
16
+ output = Html.decode(output)
17
+ rescue REXML::ParseException => e
18
+ output = text.strip
19
+ end
20
+ calc_width = width || size[0]
21
+ if calc_width.nil? or !wrap
22
+ output
23
+ else
24
+ terminalize(output, calc_width - 1)
25
+ end
26
+ end
27
+
28
+ def self.html_to_text text, wrap: true, width: nil
29
+ text = html_to_ansi text, wrap: wrap, width: width
30
+ text.gsub(/\e\[([;\d]+)?m/, '').gsub(/\n +\n/, "\n\n")
31
+ end
32
+
33
+ class AnsiNode
34
+ include Gamefic::Text::Ansi::Code
35
+ attr_accessor :parent
36
+
37
+ def render
38
+ end
39
+
40
+ def in_block?
41
+ p = parent
42
+ while !p.nil?
43
+ return true if p.kind_of?(BlockNode)
44
+ p = p.parent
45
+ end
46
+ false
47
+ end
48
+ end
49
+ private_constant :AnsiNode
50
+
51
+ class TextNode < AnsiNode
52
+ @@prev_format = nil
53
+ attr_reader :text, :format
54
+ def initialize text, format
55
+ @text = text
56
+ @format = format
57
+ end
58
+ def render
59
+ return @text if format.include?(Extra::PRE)
60
+ index = parent.children.index(self)
61
+ if index > 0
62
+ prev = parent.children[index - 1]
63
+ if prev.kind_of?(TextNode) and prev.format == format
64
+ if prev.text.match(/ $/)
65
+ return @text.lstrip
66
+ else
67
+ return @text
68
+ end
69
+ end
70
+ end
71
+ if @@prev_format == format
72
+ @text
73
+ else
74
+ @prev_format = format
75
+ Gamefic::Text::Ansi.graphics_mode(*format) + @text
76
+ end
77
+ end
78
+ end
79
+ private_constant :TextNode
80
+
81
+ class ElementNode < AnsiNode
82
+ def children
83
+ @children ||= []
84
+ end
85
+ def append child
86
+ children.push child
87
+ child.parent = self
88
+ end
89
+ end
90
+ private_constant :ElementNode
91
+
92
+ class BlockNode < ElementNode
93
+ def render
94
+ output = ''
95
+ children.each { |c|
96
+ output += c.render
97
+ }
98
+ output = "\n" + output.strip unless in_block?
99
+ output
100
+ end
101
+ end
102
+ private_constant :BlockNode
103
+
104
+ class InlineNode < ElementNode
105
+ def render
106
+ output = ''
107
+ children.each { |c|
108
+ output += c.render
109
+ output += "\n" if c.kind_of?(BlockNode)
110
+ }
111
+ output
112
+ end
113
+ end
114
+ private_constant :InlineNode
115
+
116
+ class BreakNode < ElementNode
117
+ def render
118
+ output = ''
119
+ children.each { |c|
120
+ output += c.render
121
+ }
122
+ output + "\n"
123
+ end
124
+ end
125
+ private_constant :BreakNode
126
+
127
+ class AnsiFormatter
128
+ include Gamefic::Text::Ansi::Code
129
+ def format document
130
+ @document = document
131
+ @ansi_root = InlineNode.new
132
+ @list_index = []
133
+ format_recursively @document.root, @ansi_root, [Attribute::NORMAL]
134
+ output = @ansi_root.render
135
+ output += (@ansi_root.children.last.kind_of?(BlockNode) ? "\n" : "")
136
+ end
137
+
138
+ def format_recursively element, ansi_node, stack
139
+ if element.is_a?(REXML::Text)
140
+ append_text element, ansi_node, stack
141
+ else
142
+ current = []
143
+ case element.name
144
+ when 'b', 'strong', 'em'
145
+ current.push Attribute::BOLD
146
+ format_children element, ansi_node, stack + current
147
+ when 'i', 'u'
148
+ current.push Attribute::UNDERSCORE
149
+ format_children element, ansi_node, stack + current
150
+ when 'h1', 'h2', 'h3', 'h4', 'h5'
151
+ current.push Attribute::BOLD, Extra::UPPERCASE
152
+ format_paragraph element, ansi_node, stack + current
153
+ when 'p'
154
+ format_paragraph element, ansi_node, stack
155
+ when 'ol', 'ul'
156
+ @list_index.push 0
157
+ format_paragraph element, ansi_node, stack
158
+ @list_index.pop
159
+ when 'li'
160
+ format_list_item element, ansi_node, stack
161
+ when 'pre'
162
+ current.push Extra::PRE
163
+ format_children element, ansi_node, stack + current
164
+ when 'br'
165
+ ansi_node.append TextNode.new("\n", stack + [Extra::PRE])
166
+ else
167
+ format_children element, ansi_node, stack
168
+ end
169
+ end
170
+ end
171
+
172
+ def append_text element, ansi_node, stack
173
+ text = element.to_s
174
+ text.gsub!(/[\s]+/, ' ') unless stack.include?(Extra::PRE)
175
+ text.upcase! if stack.include?(Extra::UPPERCASE)
176
+ ansi_node.append TextNode.new(text, stack)
177
+ end
178
+
179
+ def format_children element, node, stack
180
+ element.each { |e|
181
+ format_recursively e, node, stack
182
+ }
183
+ end
184
+
185
+ def format_paragraph element, node, stack
186
+ paragraph = BlockNode.new
187
+ node.append paragraph
188
+ format_children element, paragraph, stack
189
+ end
190
+
191
+ def format_list_item element, node, stack
192
+ i = 0
193
+ unless @list_index.empty?
194
+ @list_index[-1] = @list_index[-1] + 1
195
+ i = @list_index[-1]
196
+ end
197
+ b = BreakNode.new
198
+ node.append b
199
+ if element.parent.name == 'ol'
200
+ b.append TextNode.new("#{i}. ", stack)
201
+ else
202
+ b.append TextNode.new("* ", stack)
203
+ end
204
+ format_children element, b, stack
205
+ end
206
+ end
207
+ private_constant :AnsiFormatter
208
+
209
+ def self.terminalize string, max_length
210
+ i = 0
211
+ output = ''
212
+ line_length = 0
213
+ while i < string.length
214
+ line_length += 1
215
+ char = string[i,1]
216
+ if char == "\e"
217
+ # Right now, graphics modes are the only supported ANSI sequences.
218
+ end_of_seq = string.index("m", i)
219
+ output += string[i..end_of_seq]
220
+ i = end_of_seq + 1
221
+ elsif char == " "
222
+ next_space = string.index(/[\s]/, i + 1)
223
+ if !next_space.nil? and line_length + (next_space - i) > max_length
224
+ output += "\n"
225
+ line_length = 0
226
+ else
227
+ output += char
228
+ end
229
+ i += 1
230
+ else
231
+ if char == "\n"
232
+ line_length = 0
233
+ end
234
+ output += char
235
+ i += 1
236
+ end
237
+ end
238
+ output
239
+ end
240
+
241
+ def self.size
242
+ begin
243
+ return STDOUT.winsize.reverse
244
+ rescue
245
+ return [nil,nil]
246
+ end
247
+ end
248
+ end
249
+
250
+ end