gamefic 0.6.1 → 1.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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/bin/gamefic +3 -0
  3. data/lib/gamefic/character.rb +42 -6
  4. data/lib/gamefic/director/parser.rb +25 -25
  5. data/lib/gamefic/engine/tty.rb +14 -2
  6. data/lib/gamefic/engine.rb +8 -7
  7. data/lib/gamefic/grammar/gender.rb +1 -1
  8. data/lib/gamefic/grammar/pronouns.rb +53 -8
  9. data/lib/gamefic/grammar/verbs.rb +1 -0
  10. data/lib/gamefic/grammar/word_adapter.rb +31 -18
  11. data/lib/gamefic/html.rb +27 -13
  12. data/lib/gamefic/plot/article_mount.rb +1 -1
  13. data/lib/gamefic/plot/scene_mount.rb +27 -110
  14. data/lib/gamefic/{snapshots.rb → plot/snapshot.rb} +60 -29
  15. data/lib/gamefic/plot/you_mount.rb +1 -1
  16. data/lib/gamefic/plot.rb +56 -29
  17. data/lib/gamefic/query/base.rb +3 -1
  18. data/lib/gamefic/scene/active.rb +11 -18
  19. data/lib/gamefic/scene/base.rb +21 -0
  20. data/lib/gamefic/scene/conclusion.rb +11 -0
  21. data/lib/gamefic/scene/custom.rb +21 -0
  22. data/lib/gamefic/scene/multiple_choice/input.rb +11 -0
  23. data/lib/gamefic/scene/multiple_choice.rb +73 -0
  24. data/lib/gamefic/scene/passive.rb +17 -0
  25. data/lib/gamefic/scene/pause.rb +24 -0
  26. data/lib/gamefic/scene/question.rb +21 -0
  27. data/lib/gamefic/scene/yes_or_no.rb +30 -0
  28. data/lib/gamefic/scene.rb +10 -120
  29. data/lib/gamefic/script/base.rb +7 -2
  30. data/lib/gamefic/script.rb +4 -0
  31. data/lib/gamefic/shell/command/base.rb +38 -0
  32. data/lib/gamefic/shell/command/play.rb +51 -0
  33. data/lib/gamefic/shell/command.rb +4 -0
  34. data/lib/gamefic/shell/registry.rb +13 -0
  35. data/lib/gamefic/shell.rb +14 -71
  36. data/lib/gamefic/source/file.rb +1 -1
  37. data/lib/gamefic/source.rb +5 -0
  38. data/lib/gamefic/tester.rb +0 -1
  39. data/lib/gamefic/version.rb +1 -1
  40. data/lib/gamefic.rb +1 -6
  41. metadata +69 -61
  42. data/lib/gamefic/scene/concluded.rb +0 -22
  43. data/lib/gamefic/scene/multiplechoice.rb +0 -74
  44. data/lib/gamefic/scene/paused.rb +0 -26
  45. data/lib/gamefic/scene/yesorno.rb +0 -43
@@ -0,0 +1,73 @@
1
+ module Gamefic
2
+
3
+ # Provide a list of options and process the selection in the scene's finish
4
+ # block. After the scene is finished, the :active scene will be cued unless
5
+ # some other scene has already been prepared or cued.
6
+ #
7
+ # The finish block's input parameter receives a MultipleChoice::Input object
8
+ # instead of a String.
9
+ #
10
+ class Scene::MultipleChoice < Scene::Custom
11
+ autoload :Input, 'gamefic/scene/multiple_choice/input'
12
+
13
+ def initialize config = {}
14
+ super
15
+ @options = (config[:options] || [])
16
+ @invalid_choice_message = config[:invalid_choice_message]
17
+ end
18
+
19
+ def start actor
20
+ @current_options = @options.clone
21
+ @start.call(actor, @current_options) unless @start.nil?
22
+ tell_choices actor
23
+ end
24
+
25
+ def finish actor, input
26
+ this_scene = actor.scene
27
+ index = nil
28
+ choice = nil
29
+ if input.strip =~ /[0-9]+/ and input.to_i > 0
30
+ index = input.to_i - 1
31
+ choice = @current_options[index]
32
+ else
33
+ index = 0
34
+ @current_options.each { |o|
35
+ if o.casecmp(input).zero?
36
+ choice = o
37
+ break
38
+ end
39
+ index += 1
40
+ }
41
+ end
42
+ if choice.nil?
43
+ actor.tell invalid_choice_message
44
+ tell_choices actor
45
+ else
46
+ input_object = Input.new(input, index, choice)
47
+ super actor, input_object
48
+ actor.cue :active if (actor.scene == this_scene and actor.next_scene.nil?)
49
+ end
50
+ end
51
+
52
+ def prompt
53
+ @prompt ||= "Enter a choice:"
54
+ end
55
+
56
+ def invalid_choice_message
57
+ @invalid_choice_message ||= "That's not a valid selection."
58
+ end
59
+
60
+ private
61
+
62
+ def tell_choices actor
63
+ list = '<ol class="multiple_choice">'
64
+ @current_options.each { |o|
65
+ list += "<li>#{o}</li>"
66
+ }
67
+ list += "</ol>"
68
+ actor.tell list
69
+ end
70
+
71
+ end
72
+
73
+ end
@@ -0,0 +1,17 @@
1
+ module Gamefic
2
+
3
+ # Passive Scenes immediately cue another scene after they're finished. If no
4
+ # scene has been cued or prepared, it defaults to the :active scene.
5
+ #
6
+ class Scene::Passive < Scene::Custom
7
+ def initialize &block
8
+ @start = block
9
+ end
10
+ def start actor
11
+ this_scene = actor.scene
12
+ super
13
+ actor.cue :active if (actor.scene == this_scene and actor.next_scene.nil?)
14
+ end
15
+ end
16
+
17
+ end
@@ -0,0 +1,24 @@
1
+ module Gamefic
2
+
3
+ # Wait for input. After the scene is finished (e.g., the player presses
4
+ # Enter), the :active scene will be cued if no other scene has been prepared
5
+ # or cued.
6
+ #
7
+ class Scene::Pause < Scene::Custom
8
+ def initialize prompt = nil, &block
9
+ @prompt = prompt
10
+ @start = block
11
+ end
12
+ def start actor
13
+ @start_scene = actor.scene
14
+ super
15
+ end
16
+ def finish actor, input
17
+ actor.cue :active if (actor.scene == @start_scene and actor.next_scene.nil?)
18
+ end
19
+ def prompt
20
+ @prompt ||= "Press Enter to continue..."
21
+ end
22
+ end
23
+
24
+ end
@@ -0,0 +1,21 @@
1
+ module Gamefic
2
+
3
+ # Question Scenes handle a string of arbitrary input. Examples include
4
+ # asking for a password, a destination, or a topic of conversation. The
5
+ # finish block is solely responsible for processing the answer.
6
+ # After the scene is finished, the :active scene will automatically be cued
7
+ # if no other scene has been cued or prepared.
8
+ #
9
+ class Scene::Question < Scene::Custom
10
+ def initialize prompt, &block
11
+ @prompt = prompt
12
+ @finish = block
13
+ end
14
+ def finish actor, input
15
+ this_scene = actor.scene
16
+ super
17
+ actor.cue :active if (actor.scene == this_scene and actor.next_scene.nil?)
18
+ end
19
+ end
20
+
21
+ end
@@ -0,0 +1,30 @@
1
+ module Gamefic
2
+
3
+ # Prompt the user to answer "yes" or "no". The scene will accept variations
4
+ # like "YES" or "n" and normalize the answer to "yes" or "no" in the finish
5
+ # block. After the scene is finished, the :active scene will be cued if no
6
+ # other scene has been prepared or cued.
7
+ #
8
+ class Scene::YesOrNo < Scene::Custom
9
+ def initialize prompt = nil, &block
10
+ @prompt = prompt
11
+ @finish = block
12
+ end
13
+ def finish actor, input
14
+ answer = nil
15
+ if input.downcase[0, 1] == "y"
16
+ answer = "yes"
17
+ elsif input.downcase[0, 1] == "n"
18
+ answer = "no"
19
+ end
20
+ if answer.nil?
21
+ actor.tell "Please enter Yes or No."
22
+ else
23
+ this_scene = actor.scene
24
+ @finish.call actor, answer
25
+ actor.cue :active if (actor.scene == this_scene and actor.next_scene.nil?)
26
+ end
27
+ end
28
+ end
29
+
30
+ end
data/lib/gamefic/scene.rb CHANGED
@@ -1,125 +1,15 @@
1
1
  module Gamefic
2
2
 
3
- # SceneManagers handle the creation and execution of player scenes.
4
- #
5
- # @example Create a scene that lets the player select a name.
6
- # scene_managers[:get_name] = SceneManager.new do |manager|
7
- # manager.state = "Active" # Tell the Engine that this scene accepts input
8
- # manager.prompt = "Enter your name:"
9
- # manager.start do |actor, data|
10
- # actor.tell "Let's start with a formal introduction."
11
- # end
12
- # manager.finish do |actor, data|
13
- # actor[:name] = data.input
14
- # actor.tell "Howdy, #{actor[:name]}!"
15
- # data.next_cue = :active # Proceed to the default :active scene
16
- # end
17
- # end
18
- #
19
- class SceneManager
20
- attr_accessor :state
21
- attr_writer :prompt
22
-
23
- def initialize &block
24
- yield self if block_given?
25
- end
26
-
27
- # Get the SceneData class that provide data about the current event to a
28
- # Scene instance.
29
- #
30
- def data_class
31
- SceneData
32
- end
33
-
34
- # Get the name that describes this scene's state.
35
- # Two common values for the state are Active and Passive. If a scene is
36
- # Active, it is capable of accepting user input. If it is Passive, it
37
- # is probably not interactive (e.g., a cutscene) and will usually cue
38
- # an Active scene in order to continue gameplay.
39
- #
40
- # @return [String] The name of the state.
41
- def state
42
- @state ||= 'Passive'
43
- end
44
-
45
- # Get the Scene class that the SceneManager uses to prepare a scene for
46
- # the plot.
47
- #
48
- def scene_class
49
- Scene
50
- end
51
-
52
- # Define a Block to be executed when the scene starts. The Engine should
53
- # execute this block before the player is queried for input.
54
- #
55
- # @yieldparam [Character]
56
- # @yieldparam [SceneData]
57
- def start &block
58
- @start = block
59
- end
60
-
61
- # Define a Block to be executed when the scene finishes. The engine should
62
- # process user input in this block.
63
- #
64
- # @yieldparam [Character]
65
- # @yieldparam [SceneData]
66
- def finish &block
67
- @finish = block
68
- end
69
-
70
- # Prepare a new Scene for execution.
71
- #
72
- # @return [Scene]
73
- def prepare key
74
- scene_class.new(self, key)
75
- end
76
-
77
- # Get the prompt to display to the user when requesting input.
78
- #
79
- # @return [String]
80
- def prompt
81
- @prompt ||= ">"
82
- end
83
- end
84
-
85
- class SceneData
86
- attr_accessor :input, :prompt, :next_cue
3
+ module Scene
4
+ autoload :Base, 'gamefic/scene/base'
5
+ autoload :Custom, 'gamefic/scene/custom'
6
+ autoload :Active, 'gamefic/scene/active'
7
+ autoload :Passive, 'gamefic/scene/passive'
8
+ autoload :Pause, 'gamefic/scene/pause'
9
+ autoload :Conclusion, 'gamefic/scene/conclusion'
10
+ autoload :MultipleChoice, 'gamefic/scene/multiple_choice'
11
+ autoload :YesOrNo, 'gamefic/scene/yes_or_no'
12
+ autoload :Question, 'gamefic/scene/question'
87
13
  end
88
14
 
89
- class Scene
90
- attr_reader :data, :state, :key
91
-
92
- def initialize(manager, key)
93
- @manager = manager
94
- @start = manager.instance_variable_get(:@start)
95
- @finish = manager.instance_variable_get(:@finish)
96
- @state = manager.state
97
- @data = manager.data_class.new
98
- @data.prompt = manager.prompt
99
- @key = key
100
- end
101
-
102
- # Start the scene. This method is typically called by the Plot.
103
- #
104
- # @param actor [Character] The Scene's Character.
105
- def start actor
106
- return if @start.nil?
107
- @data.input = nil
108
- @start.call actor, @data
109
- end
110
-
111
- # Finish the scene. This method is typically called by the Plot.
112
- # The Finish method is responsible for processing any input received
113
- # from players.
114
- #
115
- # @param actor [Character] The Scene's Character.
116
- # @param input [String] Input received from the Character (e.g., a player command).
117
- def finish actor, input
118
- @data.next_cue ||= :active
119
- return if @finish.nil?
120
- @data.input = input
121
- @finish.call actor, @data
122
- end
123
- end
124
-
125
15
  end
@@ -11,18 +11,23 @@ module Gamefic
11
11
  def read
12
12
  raise "#read must be defined in subclasses"
13
13
  end
14
- # Get the script's path
14
+ # Get the script's path.
15
15
  #
16
16
  # @return [String]
17
17
  def path
18
18
  raise "#path must be defined in subclasses"
19
19
  end
20
- # Get the absolute path of the script's original file
20
+ # Get the absolute path of the script's original file, or its URL for
21
+ # sources that are not file-based.
21
22
  #
22
23
  # @return [String]
23
24
  def absolute_path
24
25
  raise "#absolute_path must be defined in subclasses"
25
26
  end
27
+ # Script objects are equal if their relative paths are the same. Note that
28
+ # they are still considered equal if their absolute paths are different,
29
+ # or even if they come from different types of sources.
30
+ #
26
31
  # @param other[Script::Base]
27
32
  # @return [Boolean]
28
33
  def==(other)
@@ -1,5 +1,9 @@
1
1
  module Gamefic
2
2
 
3
+ # Script classes provide code to be executed in Plots. They are accessed
4
+ # through Source classes, e.g., a Source::Text object is used to find
5
+ # Source::Files.
6
+ #
3
7
  module Script
4
8
  autoload :Base, 'gamefic/script/base'
5
9
  autoload :File, 'gamefic/script/file'
@@ -0,0 +1,38 @@
1
+ require 'gamefic/shell'
2
+ require 'slop'
3
+
4
+ class Gamefic::Shell::Command::Base
5
+ # Execute the command
6
+ #
7
+ def run input
8
+ raise "Unimplemented command"
9
+ end
10
+
11
+ # Get the options for the command
12
+ #
13
+ # @return [Slop::Options]
14
+ def options
15
+ @options ||= Slop::Options.new
16
+ end
17
+
18
+ # Get the help documentation for this command.
19
+ #
20
+ # @return [String]
21
+ def help
22
+ optons.to_s
23
+ end
24
+
25
+ protected
26
+
27
+ # @return [Slope::Result]
28
+ def parse input
29
+ parser.parse input
30
+ end
31
+
32
+ private
33
+
34
+ # @return [Slop::Parser]
35
+ def parser
36
+ @parser ||= Slop::Parser.new(options)
37
+ end
38
+ end
@@ -0,0 +1,51 @@
1
+ require 'zip'
2
+ require 'tmpdir'
3
+ require 'gamefic/engine/tty'
4
+ require 'gamefic/shell'
5
+ require 'yaml'
6
+
7
+ class Gamefic::Shell::Command::Play < Gamefic::Shell::Command::Base
8
+ include Gamefic
9
+
10
+ def run input
11
+ result = parse input
12
+ file = result.arguments[1]
13
+ raise "File not specified." if file.nil?
14
+ raise "'#{file}' does not exist." if !File.exist?(file)
15
+ raise "'#{file}' is a directory." if File.directory?(file)
16
+ play file
17
+ end
18
+
19
+ private
20
+
21
+ def decompress(zipfile, destination)
22
+ Zip::File.open(zipfile) do |z|
23
+ z.each do |entry|
24
+ FileUtils.mkdir_p File.join(destination, File.dirname(entry.name))
25
+ full_path = File.join(destination, entry.name)
26
+ if !File.exist?(full_path)
27
+ entry.extract full_path
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ def play file
34
+ Dir.mktmpdir 'gamefic_' do |dir|
35
+ puts "Loading..."
36
+ story = Plot.new(Source::File.new(File.join(dir, 'scripts')))
37
+ begin
38
+ decompress file, dir
39
+ rescue Exception => e
40
+ puts "'#{file}' does not appear to be a valid Gamefic file."
41
+ #puts "Error: #{e.message}"
42
+ #exit 1
43
+ end
44
+ story.script 'main'
45
+ story.metadata = YAML.load_file File.join(dir, 'metadata.yaml')
46
+ engine = Tty::Engine.new story
47
+ puts "\n"
48
+ engine.run
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,4 @@
1
+ module Gamefic::Shell::Command
2
+ autoload :Base, 'gamefic/shell/command/base'
3
+ autoload :Play, 'gamefic/shell/command/play'
4
+ end
@@ -0,0 +1,13 @@
1
+ module Gamefic
2
+ class Shell
3
+ module Registry
4
+ @commands = {}
5
+ def self.register cmd, cls
6
+ @commands[cmd] = cls
7
+ end
8
+ def self.get_command_class cmd
9
+ @commands[cmd]
10
+ end
11
+ end
12
+ end
13
+ end
data/lib/gamefic/shell.rb CHANGED
@@ -1,82 +1,25 @@
1
- require 'zip'
2
- require 'tmpdir'
3
- require 'getoptlong'
4
- require 'gamefic/engine/tty'
5
-
6
1
  module Gamefic
7
2
 
8
3
  class Shell
9
- attr_accessor :argv
4
+ autoload :Command, 'gamefic/shell/command'
5
+
10
6
  def initialize
7
+ @commands = {}
8
+ end
11
9
 
10
+ def register cmd, cls
11
+ @commands[cmd] = cls
12
12
  end
13
+
13
14
  def execute
14
- if ARGV.length == 0
15
- ARGV.push 'help'
16
- end
17
- cmd = ARGV.shift
18
- case cmd
19
- when 'play'
20
- play ARGV.shift
21
- when 'help'
22
- help ARGV.shift
23
- else
24
- play cmd
15
+ command = ARGV[0]
16
+ cls = @commands[command]
17
+ if cls.nil?
18
+ Gamefic::Shell::Command::Play.new.run(['play'] + ARGV)
19
+ else
20
+ cls.new.run ARGV
25
21
  end
26
22
  end
27
- private
28
- def play file
29
- if !File.exist?(file)
30
- puts "'#{file}' does not exist."
31
- exit 1
32
- end
33
- if File.directory?(file)
34
- puts "'#{file}' is not a Gamefic file."
35
- exit 1
36
- end
37
- Dir.mktmpdir 'gamefic_' do |dir|
38
- puts "Loading..."
39
- story = Plot.new(Source.new(dir + '/scripts'))
40
- begin
41
- decompress file, dir
42
- rescue Exception => e
43
- puts "'#{file}' does not appear to be a valid Gamefic file."
44
- puts e.backtrace
45
- exit 1
46
- end
47
- story.load dir + '/main'
48
- engine = Tty::Engine.new story
49
- puts "\n"
50
- engine.run
51
- end
52
- end
53
- def help command
54
- shell_script = File.basename($0)
55
- case command
56
- when "play"
57
- puts <<EOS
58
- #{shell_script} play [file]
59
- Play a Gamefic file on the command line.
60
- EOS
61
- when nil, "help"
62
- puts <<EOS
63
- #{shell_script} play [file] - play a Gamefic file
64
- #{shell_script} help - display this message
65
- #{shell_script} help [command] - display info about command
66
- EOS
67
- else
68
- puts "Unrecognized command '#{command}'"
69
- exit 1
70
- end
71
- end
72
- def decompress(zipfile, destination)
73
- Zip::File.open(zipfile) do |z|
74
- z.each do |entry|
75
- FileUtils.mkdir_p "#{destination}/#{File.dirname(entry.name)}"
76
- entry.extract "#{destination}/#{entry.name}"
77
- end
78
- end
79
- end
80
23
  end
81
-
24
+
82
25
  end
@@ -10,7 +10,7 @@ module Gamefic
10
10
  def export path
11
11
  @directories.each { |directory|
12
12
  @@extensions.each { |ext|
13
- abs_file = directory + '/' + path + ext
13
+ abs_file = File.join(directory, path + ext)
14
14
  if File.file?(abs_file)
15
15
  return Script::File.new(abs_file, path)
16
16
  end
@@ -1,5 +1,10 @@
1
1
  module Gamefic
2
2
 
3
+ # Plots use Source classes to fetch scripts to be executed. The most common
4
+ # type of Source class is Source::File, which searches for scripts in
5
+ # a predefined list of directories on the filesystem, similar to the way
6
+ # that Kernel#require works.
7
+ #
3
8
  module Source
4
9
  autoload :Base, 'gamefic/source/base'
5
10
  autoload :File, 'gamefic/source/file'
@@ -13,7 +13,6 @@ module Gamefic
13
13
  actor[:test_queue] = queue
14
14
  actor[:test_queue_length] = queue.length
15
15
  actor[:test_queue_scene] = actor.scene
16
- actor[:testing] = true
17
16
  end
18
17
  end
19
18
 
@@ -1,3 +1,3 @@
1
1
  module Gamefic
2
- VERSION = '0.6.1'
2
+ VERSION = '1.0.0'
3
3
  end
data/lib/gamefic.rb CHANGED
@@ -7,11 +7,6 @@ require 'gamefic/serialized'
7
7
  require 'gamefic/entity'
8
8
  require 'gamefic/character'
9
9
  require "gamefic/scene"
10
- require "gamefic/scene/active"
11
- require "gamefic/scene/concluded"
12
- require "gamefic/scene/paused"
13
- require "gamefic/scene/multiplechoice"
14
- require "gamefic/scene/yesorno"
15
10
  require "gamefic/query"
16
11
  require "gamefic/action"
17
12
  require "gamefic/syntax"
@@ -20,4 +15,4 @@ require "gamefic/director"
20
15
  require "gamefic/plot"
21
16
  require "gamefic/engine"
22
17
  require "gamefic/direction"
23
- require "gamefic/snapshots"
18
+ require "gamefic/version"