Bash Shell Script: Building Your March Madness Bracket

I must admit that I don't really follow basketball. But, I do like to engage with folks at work, and every spring I've always felt a little left out when my work colleagues fill out their NCAA March Madness basketball brackets. If your office is like mine, it seems everyone gets very excited to build their brackets and follow the basketball games and play in an office pool.

I wanted to play too, but I just didn't know enough about the teams to make an informed decision to fill out my March Madness bracket. So a few years ago, I found another way. I wrote a program to build my basketball bracket for me.

Computational models that predict the outcomes of sports games are not new. A little Googling will uncover methods such as the Pythagorean Expectation Model or other algorithms that build on previous team performance to predict future game outcomes. But that requires some research into sports statistics and following each team throughout the season. That's too much work for me, so I used a different method that should be familiar to many of my fellow nerds: the “Dungeons and Dragons Model”.

That's right! You can apply a simple D16 method to build a March Madness basketball bracket. How scientific is this? Probably not very, but it's enough to give me a stake in following March Madness basketball, but not enough that I feel particularly saddened if my bracket doesn't perform well. That's good enough for me.

Let's build a shell script to build an NCAA March Madness basketball bracket. I'll create the script under these assumptions:

  1. The NCAA March Madness basketball brackets are seeded with the NCAA's ranking of 64 college basketball teams, divided into four regions, and ranked #1 through #16.

  2. The NCAA March Madness basketball brackets are always initialized with the same contests: #1 vs #16, #8 vs #9, #5 vs #12, and so on.

  3. A #1 ranked team should perform better than a #16 team, but a #8 team should perform about the same as a #9 team.

With these assumptions, let's examine the D16 method. In a tabletop role-playing game, you might throw a 1D16 to determine the outcome of an encounter. You would compare the value of the 1D16 to a player's statistic, such as Dexterity or Strength. This kind of throw assumes a “probability” or “likelihood” of an outcome based on the relative strength of the player.

Similarly, you can compare the outcome of a 1D16 to a team's NCAA ranking to determine the outcome of a team's performance. A #1 team should be a strong team, so let's assume the #1 team has 15 out of 16 “chances” to win, and one out of sixteen “chances” to lose. Without any other inputs, the #1 team would win if the 1D16 value is two or greater, and the #1 team would lose if the 1D16 value is one.

So, you can throw a 1D16 to determine if team “A” wins, and a 1D16 to determine if team “B” loses, or vice versa. If the two agree, you know the outcome of the game.

In bash, you generate a random number every time you reference the $RANDOM environment variable. The variable returns a value between 0 and 32,767, but you want a number between 1 and 16. You can reduce the random number's range to 16 values by using the modulo operator. Using modulo 16 returns a value between 0 and 15. Adjusting that to a number between 1 and 16 is simple addition:


d16=$(( ( $RANDOM % 16 ) + 1 ))

Here's a bash function that assumes two inputs are the NCAA rankings of two teams, team A and team B. Using the D16 method, the function predicts the winner of a game and returns the winning team in the function's exit value:


function guesswinner {
        rankA=$1
        rankB=$2

        d16A=$(( ( $RANDOM % 16 ) + 1 ))
        d16B=$(( ( $RANDOM % 16 ) + 1 ))

        if [ $d16A -gt $rankA -a $d16B -le $rankB ] ; then
                # team A wins and team B loses
                return $rankA
        elif [ $d16A -le $rankA -a $d16B -gt $rankB ] ; then
                # team A loses and team B wins
                return $rankB
        else
                # no winner
                return 0
        fi
}

Of course, the D16 method assumes the two outcomes agree. Although this method works most of the time, it's possible that neither results in a winner. A simple workaround is to try again. I find the outcomes agree within one or a few throws, but for an evenly matched game, such as a #1 team against a #2 team, you might have to give up after too many attempts.

With that assumption, let's write a bash function to call guesswinner repeatedly until the two outcomes agree. The function prints the match-up, prints the winner, and returns the winning team via the exit value:


function winner {
        teamA=$1
        teamB=$2

        echo -n "$teamA vs $teamB : "

        count=0

        # iterate and return winner, if found

        while [ $count -lt 10 ] ; do
                guesswinner $teamA $teamB
                win=$?

        if [ $win -gt 0 ] ; then
                # winner found
                echo $win
                return $win
        fi

                count=$(( $count + 1 ))
        done

        # no winner found, return a default winner

        echo "=$teamA"
        return $teamA
}

The = in the last echo statement helps you see if the function was unable to determine a winner after ten attempts.

With these two functions, it's very simple to run through all the first-round games to determine winners, then iterate through those winners to build the rest of the basketball bracket. A few echo statements help you to follow each round in the bracket. The function returns the winner of the bracket via the return value:


function playbracket {
        echo -e '\nround 1\n'

        winner 1 16
        round1A=$?

        winner 8 9
        round1B=$?

        winner 5 12
        round1C=$?

        winner 4 13
        round1D=$?

        winner 6 11
        round1E=$?

        winner 3 14
        round1F=$?

        winner 7 10
        round1G=$?

        winner 2 15
        round1H=$?

        echo -e '\nround 2\n'

        winner $round1A $round1B
        round2A=$?

        winner $round1C $round1D
        round2B=$?

        winner $round1E $round1F
        round2C=$?

        winner $round1G $round1H
        round2D=$?

        echo -e '\nround 3\n'

        winner $round2A $round2B
        round3A=$?

        winner $round2C $round2D
        round3B=$?

        echo -e '\nround 4\n'

        winner $round3A $round3B

        return $?
}

Finally, you need only call the playbracket function for each of the four regions. You are left with the “Final Four” with the winners of each bracket, but I'll leave the final determination of those contests for you to resolve on your own:


#!/bin/bash

function guesswinner {
        ...
}
function winner {
        ...
}
function playbracket {
        ...
}

echo -e '\n___ MIDWEST ___'

playbracket

echo -e '\n___ EAST ___'

playbracket

echo -e '\n___ WEST ___'

playbracket

echo -e '\n___ SOUTH ___'

playbracket

Every time you run the script, you will generate a fresh NCAA March Madness basketball bracket. It's entirely random, based on a D16 prediction similar to Dungeons and Dragons, so each iteration of the bracket will be different. Here's my sample run:


$ ./basketball.sh 

___ MIDWEST ___

round 1

1 vs 16 : 1
8 vs 9 : 8
5 vs 12 : 5
4 vs 13 : 4
6 vs 11 : 6
3 vs 14 : 3
7 vs 10 : 10
2 vs 15 : 2

round 2

1 vs 8 : 1
5 vs 4 : 5
6 vs 3 : 6
10 vs 2 : 2

round 3

1 vs 5 : 1
6 vs 2 : 2

round 4

1 vs 2 : 1

___ EAST ___

round 1

1 vs 16 : 1
8 vs 9 : 8
5 vs 12 : 5
4 vs 13 : 4
6 vs 11 : 6
3 vs 14 : 3
7 vs 10 : 7
2 vs 15 : 2

round 2

1 vs 8 : 8
5 vs 4 : 4
6 vs 3 : 3
7 vs 2 : 7

round 3

8 vs 4 : 4
3 vs 7 : 7

round 4

4 vs 7 : 7

___ WEST ___

round 1

1 vs 16 : 1
8 vs 9 : 8
5 vs 12 : 5
4 vs 13 : 4
6 vs 11 : 6
3 vs 14 : 3
7 vs 10 : 7
2 vs 15 : 2

round 2

1 vs 8 : 1
5 vs 4 : 5
6 vs 3 : 6
7 vs 2 : 2

round 3

1 vs 5 : 1
6 vs 2 : 2

round 4

1 vs 2 : 1

___ SOUTH ___

round 1

1 vs 16 : 1
8 vs 9 : 8
5 vs 12 : 12
4 vs 13 : 4
6 vs 11 : 11
3 vs 14 : 3
7 vs 10 : 10
2 vs 15 : 2

round 2

1 vs 8 : 1
12 vs 4 : 4
11 vs 3 : 3
10 vs 2 : 2

round 3

1 vs 4 : 1
3 vs 2 : 3

round 4

1 vs 3 : 3

In my experience, the D16 prediction works pretty well for the first few rounds, but often predicts the #1 team will make it to the fourth round. It's not a very scientific method, but I'll share that my computer-generated brackets usually fare well compared to others in my office.

The point of using a script to build your NCAA March Madness basket bracket isn't to take away the fun of the game. On the contrary, since I don't have much familiarity with basketball, building my bracket programmatically allows me to participate in the office basketball pool. It's entertaining without requiring much familiarity with sports statistics. My script gives me a reason to follow the games, but without the emotional investment if my bracket doesn't perform well. And that's good enough for me.

Jim Hall is an open source software advocate and developer, probably best known as the founder of FreeDOS. Jim is also very active in usability testing for open source software projects like GNOME. At work, Jim is CEO of Hallmentum, an IT executive consulting company that helps CIOs and IT Leaders with strategic planning and organizational development.

Load Disqus comments