(Where it says attached, it will be attached in another email, the email was too big) Following is the version that makes a nice, configurable ASCII chart. The chart is basically an object that stores a nested hash of [x][y] values. Instead of just outputting the data as a string, as above, this version stores everything in Objects. Tournament has rounds which have matches which have teams. The matching logic is very similar to the above. The options for the program can be accessed by running it with no arguments (or --help or -h or -? or look below): daniel@daniel-desktop:~$ ./tournie.rb Usage: tournie.rb [options] Use one of the following options to determine the teams: -n, --numerical TEAMS TEAMS number of teams where 1 is the best and TEAMS is the worst. -f, --from-csv FILE CSV file FILE to get team data from. <rank>,<name>\n format And any number of these to determine the output format(s): -c, --[no-]chart Display an ASCII based chart of rounds -t, --[no-]text Display the rounds in a textual format, for example: Round 1: 1 vs. 8, 4 vs. 5... The following are completely optional: (the short names correspond with positions on the num-pad) -8, --chart-height HEIGHT Controls the vertical spacing on the chart, with a higher HEIGHT meaning more spacing. Defaults to 4, must be an integer above 2. -4, --spacing-left SPACE Controls the space to the left of the team names. Defaults to 3, must be a positive, non-negative integer. -6, --spacing-right SPACE Controls the space to the right of the team names. Defaults to 1, must be a positive, non-negative integer. -5, --team-alignment ALIGNMENT The alignment of team names on their lines. Defaults to [l]eft, can be [r]ight or [c]entered. Takes -6 but not -4 into account. -?, -h, --help Show this message The quiz examples, as decided by this version (and some others): daniel@daniel-desktop:~$ ./tournie.rb -n 6 -ct Round 1: 1 vs. bye, 4 vs. 5, 2 vs. bye, 3 vs. 6. Round 2: 1 vs. 4, 2 vs. 3. Round 3: 1 vs. 2. 1 -----+ |---+ -----+ | 1 bye +----+ | |---+ 4 +----+ | -----+ | 4 | |---+ | -----+ | 1 5 +----+ | |---> 1 2 +----+ -----+ | 2 |---+ | -----+ | 2 | bye +----+ | | |---+ 3 +----+ -----+ | 3 |---+ -----+ 6 daniel@daniel-desktop:~$ ./tournie.rb -n 8 -ct Round 1: 1 vs. 8, 4 vs. 5, 2 vs. 7, 3 vs. 6. Round 2: 1 vs. 4, 2 vs. 3. Round 3: 1 vs. 2. 1 ---+ |---+ ---+ | 1 8 +----+ | |---+ 4 +----+ | ---+ | 4 | |---+ | ---+ | 1 5 +----+ | |---> 1 2 +----+ ---+ | 2 |---+ | ---+ | 2 | 7 +----+ | | |---+ 3 +----+ ---+ | 3 |---+ ---+ 6 daniel@daniel-desktop:~$ ./tournie.rb --from-csv tournie.csv --chart --text --team-alignment c --spacing-right 5 Round 1: Red Devils vs. Underdogs, Metrostart vs. Giants, Mets vs. Jets, Yankees vs. Red Sox. Round 2: Red Devils vs. Metrostart, Mets vs. Yankees. Round 3: Red Devils vs. Mets. Red Devils ----------------+ |---+ ----------------+ | Red Devils Underdogs +-----------------+ | |---+ Metrostart +-----------------+ | ----------------+ | Metrostart | |---+ | ----------------+ | Red Devils Giants +-----------------+ | |---> RD Mets +-----------------+(RD change ----------------+ | Mets d by me |---+ | b/c wrapp ----------------+ | Mets | ing) Jets +-----------------+ | | |---+ Yankees +-----------------+ ----------------+ | Yankees |---+ ----------------+ Red Sox Where tournie.csv had the following: 5,Metrostart 4,Yankees 8,Red Sox 7,Giants 2,Red Devils 9,Jets 3,Mets 56,Underdogs Source of the program (I recommend viewing attachment because of wrapping): #! /usr/bin/ruby require 'enumerator' require 'optparse' require 'ostruct' class String # Alias for String#center that fits into the ljust, rjust naming scheme. def cjust(*args) self.center(*args) end # align(:r, 5) --> rjust(5). Alignment can be :r, :l, :c def align(alignment, *args) self.send((alignment.to_s + "just").to_sym, *args) end end class Numeric # Is the given number a power of self? # 16.isPowerOf(2) == true # 100.isPowerOf(2) == false def isPowerOf(other) i = 0 while (other ** i <= self) return true if other ** i == self i += 1 end false end def average(other) (self + other) / 2 end end # Rounds have matches which have a winning and loosing team. class Match def initialize(*teams) @teams = teams.sort setLoser end # The loser is defined as the team with the lowest ranking before the tournament. def setLoser @teams.last.eliminate end def winner @teams.find{|x| !x.eliminated?} end def loser @teams.find{|x| x.eliminated? } end # Return in the following format: <winner> vs. <looser> def to_s @teams.collect{|team| team.to_s}.join(" vs. ") end attr_reader :teams end # Tournaments have rounds, which have matches. class Round @@totalRounds = 0 def initialize() @matches = [] @roundNum = @@totalRounds += 1 end def addMatch(match) @matches.push(match) end # Prints the round in "Round x: <match>, <match>, etc." format. def to_s "Round #{@roundNum}: " + @matches.join(", ") + "." end # This changes the order of the matches so that in the next round, the most extreme teams will face off. # Assumes that the matches were previously sorted by favorability (asc or desc) def sort! sorted = [] while @matches.length > 0 sorted << @matches.shift << @matches.pop end @matches = sorted.compact self end attr_reader :roundNum, :matches end # Matches have teams which have info about themselves. class Team @@favored = [] @@currentRound = nil def initialize(name) @name = name @eliminated = false @rounds = [] @@favored.push(self) end # Remove a team from future rounds if they lost. def eliminate @eliminated = true @@favored.delete(self) #@@notEliminated -= 1 end # If a team has played in a certain round. def inRound?() @rounds.include? @@currentRound end # Add a round a team has played in def addPlayedRound() @rounds << @@currentRound self end def to_s @name end # Returns an array with teams not in the current round by favorability. def self.eligibleTeams() @@favored.select{|x| !x.inRound?}.sort end def <=>(other) @@favored.index(self) <=> @@favored.index(other) end def self.currentRound=(round) @@currentRound = round end attr_reader :name, :eliminated alias_method :"eliminated?", :eliminated end class Tournament # Recieves an aray of team names in order of ranking with the best first. def initialize(teams) @teams = teams.collect {|team| Team.new(team.to_s) } @rounds = [] end def createNextRound currentRound = Round.new() Team.currentRound = currentRound # The top teams have a "bye" opponent if the number of teams isn't a power of two. Bye opponents always lose. until (Team.eligibleTeams.length.isPowerOf(2)) currentRound.addMatch( Match.new( Team.eligibleTeams.first.addPlayedRound, Team.new("bye") ) ) end # Assign the rest of the teams to play their extreme opposites. while(Team.eligibleTeams.length > 1) currentRound.addMatch( Match.new( Team.eligibleTeams.last.addPlayedRound, Team.eligibleTeams.first.addPlayedRound ) ) end currentRound.sort! if currentRound.roundNum == 1 @rounds.push(currentRound) end def createAllRounds until (@teams.find_all{|x| !x.eliminated?}.length == 1) createNextRound end end def to_s @rounds.join("\n") end def toASCIIChart(chartHeightModifier, spacingLeft, spacingRight, alignment) # Everything goes into this array in output[x][y] format, which is then printed. The origin is in the top left. output = ASCIICoordinatePlane.new # This stores the midpoints of the existing games outputted so that the next round's matches will be aligned in between this round's matches. midpoints = Hash.new( Array.new ) midpoints[1] = [chartHeightModifier] @rounds.first.matches.each {midpoints[1].unshift( midpoints[1].first - (chartHeightModifier + 2) )} x = 2 # Every round is one column. @rounds.each do |round| # The longest team name. columnWidth = round.matches.collect{|match| match.teams}.flatten.collect{|team| team.name}.max{|a, b| a.length <=> b.length}.length + spacingRight connectTheDots = [] insertMidpoint = true # Every iteration makes 1 match appear. round.matches.reverse.each do |match| y = midpoints[round.roundNum].shift + 2 # The first team's name. output.set(x, y, match.teams[0].name.to_s.align(alignment, columnWidth)) # The line under that team's name. output.fill(x - spacingLeft, y -= 1, x + columnWidth, y, "-") output.set(x - spacingLeft, y, "+") output.fill(0, y, x - 1, y, " ") if round.roundNum == 1 # The connector to the next round. output.set(x + columnWidth + 1, y -= 1, "-" * 3) # Deals with the midpoints. if insertMidpoint midpoints[round.roundNum + 1].push(y) else midpoints[round.roundNum + 1].push( midpoints[round.roundNum + 1].pop.average(y) ) end insertMidpoint = !insertMidpoint # The line above the next team's name. output.fill(x - spacingLeft, y -= 1, x + columnWidth, y, "-") output.set(x - spacingLeft, y, "+") output.fill(0, y, x - 1, y, " ") if round.roundNum == 1 # The next team's name. output.set(x, y -= 1, match.teams[1].name.to_s.align(alignment, columnWidth)) # The line on the right of the match going vertically. output.vertLine(x + columnWidth, y + 3, y+1) # To connect the match and the next match to each other. connectTheDots.push(y+2) end x += columnWidth + 4 # Makes the lines vertically between matches. connectTheDots.each_slice(2) do |yvalues| starting = yvalues[0] if yvalues[1] ending = yvalues[1] else ending = starting end output.vertLine(x, starting, ending) end x += spacingLeft end # Print the winning team. output.set(x - spacingLeft, midpoints[@rounds.length].last, "> " + @rounds.last.matches[0].winner.name) output end attr_reader :rounds end # Represents a coordinate plane with the origin in the top left. Every position can store a character. class ASCIICoordinatePlane def initialize # Thank you Joel VanderWerf! @value = Hash.new {|h,k| h[k] = Hash.new {" "}} @maxx = 10 @miny = -10 end # Sets a specific character to a point, overflowing onto points to the right if neccessary. def set(x, y, string) 0.upto(string.length-1) do |index| @value[x][y] = string[index].chr x += 1 end @miny = y if y < @miny @maxx = x if x > @maxx end # Fill a horizontal line with a repeating character. def fillHorz(opts) set(opts[:startx], opts[:starty], opts[:string ] * (opts[:endx] - opts[:startx]).abs) end # Fill a vertical line with a repeating character. def fillVert(opts) yvalues = [opts[:starty], opts[:endy]] y = yvalues.max until (y < yvalues.min) set(opts[:startx], y, opts[:string]) y -= 1 end end # Fills a straight, non-diagonal line with a repeating character def fill(startx, starty, endx, endy, string) if startx == endx fillVert({:startx => startx, :endx => endx, :starty => starty, :endy => endy, :string => string}) else fillHorz({:startx => startx, :endx => endx, :starty => starty, :endy => endy, :string => string}) end end # Creates a vertical line with +'s for the line endings. def vertLine(x, starty, endy) fill(x, starty, x, endy, "|") [starty, endy].each {|y| set(x, y, "+") } end # Outputs the coordinate plane to a string with spaces where no character was entered. def to_s output = "" 0.downto(@miny) do |y| 0.upto(@maxx) do |x| output += @value[x][y] end output += "\n" end output end end class OptParser def self.parse(args) options = OpenStruct.new options.csv = nil options.numerical = nil options.league = nil options.chart = false options.chartheight = 4 options.spacingleft = 3 options.spacingright = 1 options.alignment = :l options.textual = false # When called with no options, show the help. args = ["-?"] if args.empty? opts = OptionParser.new do |opts| opts.banner = "Usage: tournie.rb [options]" opts.separator "" opts.separator "Use one of the following options to determine the teams:" # From 1 to a numerical value. opts.on("-n", "--numerical TEAMS", "TEAMS number of teams where 1 is the best", "and TEAMS is the worst.") do |n| options.numerical = n end # From a CSV file opts.on("-f", "--from-csv FILE", "CSV file FILE to get team data from.", "<rank>,<name>\\n format") do |file| options.csv = file end opts.separator "" opts.separator "And any number of these to determine the output format(s):" # Chart based representation opts.on("-c", "--[no-]chart", "Display an ASCII based chart of rounds") do |chartYesNoMaybeNaN| options.chart = chartYesNoMaybeNaN end # Textual representation opts.on("-t", "--[no-]text", "Display the rounds in a textual format,", "for example:", "Round 1: 1 vs. 8, 4 vs. 5...") do |text| options.textual = text end opts.separator "" opts.separator "The following are completely optional:" opts.separator "(the short names correspond with positions on the num-pad)" # Chart height modifier opts.on("-8", "--chart-height HEIGHT", "Controls the vertical spacing on the chart,", "with a higher HEIGHT meaning more spacing.", "Defaults to 4, must be an integer above 2.") do |heigh| options.chartheight = heigh.to_i end # Spacing to the left of team names. opts.on("-4", "--spacing-left SPACE", "Controls the space to the left of the team names.", "Defaults to 3, must be a positive, non-negative integer.") do |s| options.spacingleft = s.to_i end # Spacing to the right of team names. opts.on("-6", "--spacing-right SPACE", "Controls the space to the right of the team names.", "Defaults to 1, must be a positive, non-negative integer.") do |s| options.spacingright = s.to_i end # Alignment of team names. opts.on("-5", "--team-alignment ALIGNMENT", "The alignment of team names on their lines.", "Defaults to [l]eft, can be [r]ight or [c]entered.", "Takes -6 but not -4 into account.") do |s| options.alignment = s.to_sym end opts.on("-?", "-h", "--help", "Show this message") do puts opts exit end end opts.parse!(args) options end end # Parse command line arguments opts = OptParser.parse(ARGV) # Create the tournament. if (opts.numerical) tournie = Tournament.new((1..(opts.numerical.to_i)).to_a) else teams = Hash.new orderedTeams = Array.new f = File.new(opts.csv) f.each_line do |line| line.chomp! line =~ /^([0-9]+), *(.*)$/ teams[$1.to_i] = $2 end teams.keys.sort.each do |rank| orderedTeams.push(teams[rank]) end tournie = Tournament.new(orderedTeams) end # Do the logic tournie.createAllRounds # Display the tournament puts tournie.to_s if opts.textual puts tournie.toASCIIChart(opts.chartheight, opts.spacingleft, opts.spacingright, opts.alignment).to_s if opts.chart