# Re: Tournament Matchups (#105)

**URL:** <https://www.ruby-forum.com/t/re-tournament-matchups-105/225935>\
**Category:** Ruby\
**Created:** [December 10, 2006, 10:14pm UTC](https://www.ruby-forum.com/t/re-tournament-matchups-105/225935 "2006-12-10T22:14:04Z")\
**Posts on this page:** 2\
**Page:** 1

<div class="post-metadata">

**Author:** ![arachnd](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/a/278dde/32.png) [@arachnd](https://www.ruby-forum.com/u/arachnd)\
**Post date:** [December 10, 2006, 10:14pm UTC](https://www.ruby-forum.com/t/re-tournament-matchups-105/225935/1 "2006-12-10T22:14:04Z")

</div>

(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.  
,\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

---

<div class="post-metadata">

**Author:** ![arachnd](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/a/278dde/32.png) [@arachnd](https://www.ruby-forum.com/u/arachnd)\
**Post date:** [December 11, 2006, 2:52am UTC](https://www.ruby-forum.com/t/re-tournament-matchups-105/225935/2 "2006-12-11T02:52:35Z")

</div>

OK, this is my updated version of the chart producing script that is in  
full compliance with the rules…those pesky things. All byes will be  
in the first round. The file is attached as the wrapping is messing it  
up. My other submission, that is 56 lines compared to this 463-liner,  
does not have the problem that was fixed in this version.

The sample output from the quiz examples (with 2 FREE!):  
daniel@daniel-desktop:~$ ./tournie.rb -n 6 -c  
1  
-----+  
|—+  
-----+ | 1  
bye ±—+  
| |—+  
4 ±—+ |  
-----+ | 4 |  
|—+ |  
-----+ | 1  
5 ±—+  
| |—\> 1  
2 ±—+  
-----+ | 2  
|—+ |  
-----+ | 2 |  
bye ±—+ |  
| |—+  
3 ±—+  
-----+ | 3  
|—+  
-----+  
6

daniel@daniel-desktop:~$ ./tournie.rb -n 8 -c  
1  
—+  
|—+  
—+ | 1  
8 ±—+  
| |—+  
4 ±—+ |  
—+ | 4 |  
|—+ |  
—+ | 1  
5 ±—+  
| |—\> 1  
2 ±—+  
—+ | 2  
|—+ |  
—+ | 2 |  
7 ±—+ |  
| |—+  
3 ±—+  
—+ | 3  
|—+  
—+  
6

daniel@daniel-desktop:~$ ./tournie.rb -f tournie.csv -c  
Red Devils  
------------+  
|—+  
------------+ | Red Devils  
Underdogs ±------------+  
| |—+  
Metrostart ±------------+ |  
------------+ | Metrostart |  
|—+ |  
------------+ | Red Devils  
Giants ±------------+  
| |—\> Red Devils  
Mets ±------------+  
------------+ | Mets  
|—+ |  
------------+ | Mets |  
Jets ±------------+ |  
| |—+  
Yankees ±------------+  
------------+ | Yankees  
|—+  
------------+  
Red Sox

daniel@daniel-desktop:~$ ./tournie.rb -f tournie.csv -c -5 c  
Red Devils  
------------+  
|—+  
------------+ | Red Devils  
Underdogs ±------------+  
| |—+  
Metrostart ±------------+ |  
------------+ | Metrostart |  
|—+ |  
------------+ | Red Devils  
Giants ±------------+  
| |—\> Red Devils  
Mets ±------------+  
------------+ | Mets  
|—+ |  
------------+ | Mets |  
Jets ±------------+ |  
| |—+  
Yankees ±------------+  
------------+ | Yankees  
|—+  
------------+  
Red Sox
