diff --git a/lib/dialogue.rb b/lib/dialogue.rb new file mode 100644 index 0000000..5ccc010 --- /dev/null +++ b/lib/dialogue.rb @@ -0,0 +1,110 @@ +require_relative './gardner' + +# Dialogue is the module for traversing an existing tree. +module Dialogue + + # Format and display the trunk + # + # @param trunk [Hash] The trunk hash + # @param debug [Boolean] The status of showing debug information + def self.display_trunk(trunk, debug=false) + 40.times { print "-" } + puts "\n[ Trunk ]\n" if debug + puts "\n#{trunk["trunk"]}" + 40.times { print "-" } + puts "\n" + end + + # Format and display a branch and the options + # + # @param branch [Hash] A branch data set + # @param branch_no [Integer] The branch number + # @param debug [Boolean] Status of showing debug information + def self.display_branch(branch, branch_no, debug=false) + puts "\n[ Branch: #{branch_no} ]" if debug + puts "\n#{branch["desc"]}\n\n" + + branch["options"].each_pair do |k,v| + puts "\t#{k}: #{v.keys[0]}" + puts "\t\t[ Goes to branch #{v.values[0]} ]\n" if debug + end + end + + # Speaker holds the functionality for going through a dialogue tree. + class Speaker + # The file, which should be a dialogue tree YAML file. + attr_accessor :file + # Status of verbose/debug mode. True = on; false = off. + attr_accessor :debug + + def initialize(file="", debug=false) + @file = file + @debug = debug + end + + # Conversation handles navigating the tree, until the option to end is + # reached. + def conversation() + tree = Gardner.prune_trunk(@file) + + Dialogue.display_trunk(tree[0], false) + branches = Gardner.prune_branches(tree[1]) + + next_branch = 1 + until next_branch == 0 do + next_branch = talk(branches[next_branch], next_branch) + end + + puts "\n#{branches[0]["desc"]}" + exit + end + + # Talk displays a branch, the options, and prompts for a response. + # + # @param branch [Hash] A branch data set + # @param branch_no [Integer] The branch number + # @return [Integer] The number of the next branch + def talk(branch, branch_no) + # If there are no options on this branch, we assume it's a terminal + # branch. Return 0, and end the program. + if branch["options"].empty? + puts "\n#{branch["desc"]}\n\n" + return 0 + end + + Dialogue.display_branch(branch, branch_no, @debug) + + response = get_response(branch) + + unless response == 0 + puts "\n" + 10.times { print "*" } + puts "\n(Your choice: #{branch["options"][response].keys[0]})" + response = branch["options"][response].values[0].to_i + end + + return response + end + + # Get a response for the displayed branch + # + # @param branch [Hash] A branch data set + # @return [Integer] the next branch + def get_response(branch) + valid_options = branch["options"].keys.join(", ") + + print "\n[#{valid_options}]> " + STDOUT.flush + response = STDIN.gets.chomp.to_i + + until branch["options"].keys.include?(response) or response == 0 + print "[## Invalid options. " + print "Valid options are #{valid_options}, or 0 to exit." + print "\n[#{valid_options}]> " + response = STDIN.gets.chomp.to_i + end + + return response + end + end +end diff --git a/lib/sapling/gardner.rb b/lib/gardner.rb similarity index 54% rename from lib/sapling/gardner.rb rename to lib/gardner.rb index 5437883..e6184bf 100644 --- a/lib/sapling/gardner.rb +++ b/lib/gardner.rb @@ -3,10 +3,10 @@ require 'yaml' # Gardner is the module for working with a dialogue tree file module Gardner - # Parse the branch + # Parse the tree array into an array of numbered branches, and ordered leaves. # # @param tree [Array] The dialogue tree - # @return [Array] The array of options on the branch. + # @return [Array] An array of numbered branches, with numbered leaves def self.prune_branches(tree) branches = { 0 => { "desc" => "Thanks for using Sapling!" } } tree.each do |b| @@ -16,13 +16,12 @@ module Gardner end return branches - end - # Parse the options + # Parse the leaves of a branch into a numbered hash of options. # # @param leaves [Array] The option of leaf hashes - # @return [Hash] A has of options + # @return [Hash] A numbered hash of options def self.prune_leaves(leaves) x = 1 options = {} @@ -35,34 +34,15 @@ module Gardner end return options - end - # Parse the trunk - # The trunk is like the introduction to the tree. + # Parse the trunk of the tree. # # @param tree [Hash] The entire tree - # @return [Hash] The tree without the trunk + # @return [Array] The trunk, and the remainder of the tree def self.prune_trunk(tree) trunk = tree.shift - 40.times { print "-" } - puts "\n#{trunk["trunk"]}" - 40.times { print "-" } - puts "\n" - - return tree + return [trunk,tree] end - - # The main method for Sapling. From here, the tree is grown. - # - # @param tree [File] The dialogue tree file - # @return [Hash] The final, constructed data set - def self.grow(tree) - trunk = Gardner.prune_trunk(tree) - branches = Gardner.prune_branches(trunk) - - return branches - end - end diff --git a/lib/planter.rb b/lib/planter.rb new file mode 100644 index 0000000..df38ee0 --- /dev/null +++ b/lib/planter.rb @@ -0,0 +1,210 @@ +require_relative './dialogue' +require_relative './gardner' +require_relative './utility' + +# Planter is the module for creating or editing a tree. +module Planter + + # In-memory tree + class Plot + + # The tree, trunk, and branches + attr_accessor :tree, :trunk, :branches + + # Edit the trunk of the tree + def edit_trunk + puts "Current Trunk:\n" + Dialogue.display_trunk(@trunk, true) + print "\n[ =EDITING= ](CTRL-C to abort)> " + STDOUT.flush + begin + new_trunk = STDIN.gets.to_s + rescue Interrupt + puts "\n**Aborting edit**\n\n" + new_trunk = @trunk["trunk"] + end + @trunk["trunk"] = new_trunk + end + + # Edit a branch on the tree + # + # @param branch [Integer] The number of the branch to be edited. + def edit_branch(branch_no) + puts "Current Branch:\n" + Dialogue.display_branch(@branches[branch_no], branch_no, true) + print "\n[ =EDITING= ](CTRL-C to abort)> " + STDOUT.flush + begin + new_branch = STDIN.gets.to_s + rescue Interrupt + puts "\n**Aborting edit**\n\n" + new_branch = @branches[branch_no]["desc"] + end + @branches[branch_no]["desc"] = new_branch + end + + # Edit a leaf on a branch, grasshopper + # + # @param branch [Integer] The number of the branch to be edited. + # @param leaf [Hash] The leaf hash to be edited. + def edit_leaf(branch, leaf) + + end + end + + # Utilities for editing specific parts of a tree. + class Spade + + # The file we parse into a tree + attr_writer :file + + def initialize(file) + @file = file + end + + # Establish and populate a new Plot (in-memory tree), then control the flow + # of editing the Plot + def plant + @plot = Plot.new + @plot.tree = @file + @plot.trunk = @file.shift + @plot.branches = Gardner.prune_branches(@file) + + next_branch = dig(1) + until next_branch == 0 do + next_branch = dig(next_branch) + end + + puts "\n#{@plot.branches[0]["desc"]}" + exit + end + + # Function for displaying a single branch in debug mode. We also always + # display the trunk, since otherwise it's displayed a single time then gone + # forever (until next time). + # + # @param branch_no [Integer] The number of the branch to be displayed. + def dig(branch_no) + branch = @plot.branches[branch_no] + + Dialogue.display_trunk(@plot.trunk, true) + Dialogue.display_branch(branch, branch_no, true) + + response = get_response(branch) + to_branch = parse_response(response, branch_no) + + return to_branch + end + + # Get a response for the displayed branch + # + # @param branch [Hash] A branch data set + # @return [Integer] the next branch + def get_response(branch) + total_branches = @plot.branches.count - 1 + valid_options = ["1-#{total_branches}","t","a","b","x","l","s","q"] + print_options = valid_options.join(",") + + print "\n[#{print_options}]> " + STDOUT.flush + response = STDIN.gets.chomp.to_s.downcase + + until valid_options.include?(response) or response.to_i.between?(1,total_branches) + print "[## Invalid response. " + print "Valid options are #{print_options}" + print "\n[#{print_options}]> " + response = STDIN.gets.chomp.to_s.downcase + end + + return response + end + + # Parse the response from get_response + # + # @param response [String] The option selected + # @param branch_no [Integer] The currently-displayed branch + # @return [Integer] the branch to display + def parse_response(response, branch_no) + 10.times { print "*" } + print "\n(Your choice: " + + if response.to_i >= 1 + print "Change to branch #{response.to_i})\n\n" + return response.to_i + + end + + case response.to_s.downcase + when "t" + print "Edit the trunk.)\n\n" + @plot.edit_trunk + return branch_no + when "a" + print "Add a new branch.)\n\n" + return branch_no + when "b" + print "Edit the current branch.)\n\n" + @plot.edit_branch(branch_no) + return branch_no + when "x" + print "Delete the current branch.)\n\n" + return branch_no + when "l" + print "Edit leaves of current branch.)\n\n" + return branch_no + when "s" + print "Save changes.)\n\n" + return branch_no + when "q" + print "Quit without saving.)\n\n" + print "Unsaved changes will be lost. Still quit? [y/n]> " + verify = STDIN.gets.chomp.to_s.downcase + + return 0 if verify == "y" + return branch_no + else + print "Unknown option. Returning to current branch.)\n\n" + return branch_no + end + end + end +end + +=begin + +Process: +- User selects to create/edit a tree + - If the user presented a file, use it as the tree file + - If the user failed to provide a file, create a new tree file in the current directory + +- Check if the file is empty. + - If so, assume a new tree + - If not, verify formatting + +- If new tree, prompt for trunk +- If existing tree, display trunk and first branch + +- At this point, editing is the same + - Prompt provides options: + - #: Go to that branch number + - T: Modify the tree trunk + - A: Add a new branch (append to list of branches) + - B: Modify the current branch description + - X: Delete the current branch (does this renumber branches?) + - L: Modify the current leaves, respond with leaf prompt + - S: Save changes + - Q: Quit + +- Example prompt: + [ 0-5,T,A,B,X,L,S,Q ]> +- Leaf prompt: + +Details: + +- Regardless of the file existing, the user will be editing a Hash, not the actual YAML file +- Use Gardner to build and interact with the tree +- Use Planter to modify the tree "in memory" (aka, the hash) +- Use Dialog to interact with the tree "in memory", to test-run it +- After each edit option, display the current branch with the new changes. + +=end diff --git a/lib/sapling/dialogue.rb b/lib/sapling/dialogue.rb deleted file mode 100644 index bbae8b9..0000000 --- a/lib/sapling/dialogue.rb +++ /dev/null @@ -1,79 +0,0 @@ -require_relative './gardner' - -# Dialogue is the module for traversing an existing tree. -module Dialogue - - # Spealer holds the functionality for viewing and going through a dialogue - # tree. - class Speaker - # The file, which should be a dialogue tree YAML file. - attr_accessor :file - - # Status of verbose/debug mode. True = on; false = off. - attr_accessor :debug - - def initialize - @file = "" - @debug = false - end - - # Conversation handles navigating the tree, until the option to end is - # reached. - def conversation() - tree = Gardner.grow(@file) - - 10.times { print "*" } - puts "\n[ Branch: 1 ]" if @debug - next_branch = talk(tree[1]) - until next_branch == 0 do - puts "\n[ Branch: #{next_branch} ]" if @debug - next_branch = talk(tree[next_branch]) - end - - puts "\n#{tree[0]["desc"]}" - exit - end - - # Talk displays a branch, the options, and prompts for a response - # - # @param branch [Hash] A branch data set - # @return [Integer] The number of the next branch - def talk(branch) - - # If there are no options on this branch, we assume it's a terminal - # branch. Return 0, and end the program. - if branch["options"].empty? - puts "\n#{branch["desc"]}\n\n" - return 0 - end - - valid_options = branch["options"].keys.join(", ") - - puts "\n#{branch["desc"]}\n\n" - branch["options"].each_pair do |k,v| - puts "\t#{k}: #{v.keys[0]}" - puts "\t\t [ Goes to branch #{v.values[0]} ]" if @debug - end - - print "\n[#{valid_options}]> " - STDOUT.flush - response = STDIN.gets.chomp.to_i - - until branch["options"].keys.include?(response) or response == 0 - print "[## Invalid options. " - print "Valid options are #{valid_options}, or 0 to exit." - print "\n[#{valid_options}]> " - response = STDIN.gets.chomp.to_i - end - - - puts "\n" - 10.times { print "*" } - return 0 if response == 0 - puts "\n(Your choice: #{branch["options"][response].keys[0]})" - return branch["options"][response].values[0].to_i - end - - end - -end diff --git a/lib/sapling/planter.rb b/lib/sapling/planter.rb deleted file mode 100644 index 4c30739..0000000 --- a/lib/sapling/planter.rb +++ /dev/null @@ -1,117 +0,0 @@ -require_relative './dialogue' -require_relative './gardner' -require_relative './utility' - -# Planter is the module for creating or editing a tree. -module Planter - - class Plot - # The tree, trunk, and branches - attr_accessor :tree, :trunk, :branches - - end - - # Utilities for editing specific parts of a tree. - class Spade - - # The file we parse into a tree - attr_writer :file - - # Establish a new Plot, which is basically an object for storing information - # for us. From here, we start gardening. - def plant - @plot = Plot.new - @plot.tree = @file - @plot.trunk = @file.shift.values[0] - @plot.branches = Gardner.prune_branches(@file) - - dig(1) - end - - # Function for displaying a single branch in debug mode. We also always - # display the trunk, since otherwise it's displayed a single time then gone - # forever (until next time). - # - # @param branch_no [Integer] The number of the branch to be displayed. - def dig(branch_no) - branch = @plot.branches[branch_no] - - # Print the trunk - 40.times { print "-" } - puts "\n[ Trunk ]\n#{@plot.trunk}" - 40.times { print "-" } - puts "\n" - 10.times { print "*" } - - # Print the branch and options - puts "\n[ Branch: #{branch_no} ]" - puts "\n#{branch["desc"]}\n\n" - unless branch["options"].empty? - branch["options"].each_pair do |k,v| - puts "\t#{k}: #{v.keys[0]}" - puts "\t\t [ Goes to branch #{v.values[0]} ]" - end - end - end - - # Edit the trunk of the tree - def edit_trunk - - end - - # Edit a branch on the tree - # - # @param branch [Integer] The number of the branch to be edited. - def edit_branch(branch) - - end - - # Edit a leaf on a branch, grasshopper - # - # @param branch [Integer] The number of the branch to be edited. - # @param leaf [Hash] The leaf hash to be edited. - def edit_leaf(branch, leaf) - - end - - end - -end - -=begin - -Process: -- User selects to create/edit a tree - - If the user presented a file, use it as the tree file - - If the user failed to provide a file, create a new tree file in the current directory - -- Check if the file is empty. - - If so, assume a new tree - - If not, verify formatting - -- If new tree, prompt for trunk -- If existing tree, display trunk and first branch - -- At this point, editing is the same - - Prompt provides options: - - B # - Go to Branch # - - T - Edit Trunk - - D - Edit Current Branch description - - X - Delete Current Branch - - L A - Add a new Leaf - - L # D - Edit Leaf # description - - L # B - Edit Leaf # branch destination - - L # X - Remove Leaf # - - S - Save Changes (write to the file) - - Q - Quit without saving - -Details: - -- Regardless of the file existing, the user will be editing a Hash, not the actual YAML file - - Use Gardner to build the tree (either existing or skeleton) - - Use Dialog to interact with the tree - - Overwrite prompt with Planter prompt - - On changes, re-build the tree, and restart dialogue from most recent branch -- After each edit option, display the current branch with the new changes. - -=end diff --git a/lib/sapling/utility.rb b/lib/utility.rb similarity index 99% rename from lib/sapling/utility.rb rename to lib/utility.rb index 12b13f1..576122a 100644 --- a/lib/sapling/utility.rb +++ b/lib/utility.rb @@ -55,5 +55,4 @@ def verify_tree(file) end results.include?(false) ? false : true - end diff --git a/lib/sapling.rb b/sapling.rb similarity index 84% rename from lib/sapling.rb rename to sapling.rb index 890f562..5e3c9ae 100644 --- a/lib/sapling.rb +++ b/sapling.rb @@ -3,19 +3,14 @@ require 'optparse' require 'yaml' -require_relative 'sapling/dialogue' -require_relative 'sapling/gardner' -require_relative 'sapling/planter' -require_relative 'sapling/utility' +Dir[File.join(__dir__, 'lib', '*.rb')].each { |file| require file } # Sapling is the main module for the program. From here, the rest of the world # starts building. module Sapling - # CLI is the class for option parsing, and the gateway to the program, on the # command line class CLI - # Option parsing, and gateway to either reading and traversing a tree, or # editing/creating a tree. def talk(options) @@ -43,8 +38,7 @@ module Sapling end puts "Welcome to Sapling, a Dialogue Tree Utility.\n" - speaker = Dialogue::Speaker.new - speaker.file = YAML.load_file(ARGV[0]) + speaker = Dialogue::Speaker.new(YAML.load_file(ARGV[0]),false) speaker.conversation end @@ -64,14 +58,13 @@ module Sapling end puts "Welcome to Sapling, a Dialogue Tree Utility.\n" - gardner = Planter::Spade.new - gardner.file = tree + gardner = Planter::Spade.new(tree) gardner.plant end end - # Hacky way of dealing with bad options + # Handle bad options gracefully begin opt_parser.parse!(options) rescue OptionParser::InvalidOption