tl;dr It's deployed at https://ncaa-predictor.onrender.com
First step was getting the bracket done. The NCAA Tournament is broken into 4 regions, each containing 16 teams seeded 1 through 16.
The 1 seeded team, plays the 16 seeded team, the 2 plays 15, etc. The tournament is organized to favor the better seeded team, so 1 and 2 are on opposite sides of the bracket. In the end, the first round looks something like this
@matchups [
[1, 16],
[8, 9],
[5, 12],
[4, 13],
[6, 11],
[3, 14],
[7, 10],
[2, 15]
]
Next, we recursivlely resolve the games, until there is only 1 left.
def resolve([ _ | _ ] = matchups) do
matchups
|> Enum.map(fn [team_a, team_b] -> resolve(team_a, team_b) end)
|> Enum.chunk_every(2)
|> resolve()
end
Enum.chunk_every/2
is especially helpful here.
Next, we implement a naive approach, assuming the better seeded team wins
def resolve({name_a, seed_a} = team_a, {name_b, seed_b} = team_b) do
if seed_a < seed_b do
IO.puts "#{name_a} beats #{name_b}"
team_a
else
IO.puts "#{name_b} beats #{name_a}"
team_b
end
end
And our base case
def resolve([{name, _seed} = team]) do
IO.puts "#{name} wins!"
team
end
Next, we just do this for each of the 4 regions, and put them in a tournament with eachother
def play() do
final_four = for region <- ["WEST", "EAST", "SOUTH", "MIDWEST"] do
winner = resolve(@matchups)
{region, winner}
end
resolve(final_four)
end
And that's basically it!
But, better seeds don't always win. So I found some data on http://mcubed.net/ncaab/seeds.shtml, parsed it and came up with a map that shows the percentage of times a seed beats a given seed.
For the first seed, it looks like this
@data %{
1 => %{
1 => 50.0,
2 => 53.3,
3 => 62.5,
4 => 70.7,
5 => 83.3,
6 => 68.8,
7 => 85.7,
8 => 80.2,
9 => 90.0,
10 => 85.7,
11 => 57.1,
12 => 100.0,
13 => 100.0,
14 => 0.0,
15 => 0.0,
16 => 99.3
},
2 => %{ ... }
}
For some cases, like against 14 and 15 seeds, there is no data, so in that case we assume the better seed wins.
Our resolve/2
function, now looks like
def resolve({_, team_a} = a, {_, team_b} = b) do
team_a_win_pct = @data[team_a][team_b]
{winner, loser} =
if :rand.uniform() * 100 < team_a_win_pct or (team_a_win_pct == 0.0 and team_a < team_b) do
{a, b}
else
{b, a}
end
seed_text =
if team_a_win_pct != 0.0 do
winner_pct = @data[elem(winner, 1)][elem(loser, 1)]
"#{elem(winner, 0)} beats #{elem(loser, 0)} seeds #{winner_pct}% of the time"
else
"No data for #{elem(winner, 0)} vs #{elem(loser, 0)}. Assuming #{elem(winner, 0)} wins"
end
IO.puts(
String.pad_trailing("#{elem(winner, 0)} beats #{elem(loser, 0)}", 21) <> "\t" <> seed_text
)
winner
end
And that's basically it for the CLI! You can check it out on Github if you'd like to check out the final version.
Now, for the deploy. I wrote a basic Plug router (no Phoenix for a project this small)
defmodule NCAA.Server do
use Plug.Router
plug(:match)
plug(:dispatch)
get "/" do
{:ok, pid} = StringIO.open("")
NCAA.play(pid)
resp_text = StringIO.flush(pid)
StringIO.close(pid)
resp_text
send_resp(conn, 200, resp_text)
end
match _ do
send_resp(conn, 404, "not found")
end
end
You can see we are passing in a pid
to our NCAA.play
function. This is because want to capture everything written, so we can send it back to the client (instead of to STDOUT).
That means all of our IO.puts(string)
functions change to IO.puts(pid, string)
. Very straight-forward.
After the winner is calculated, we capture the string with StringIO.flush
, and close the process. Then we just send it to the client with send_resp
.
Next we need to make sure our server starts up. First in our mix.exs
we change def application
to
def application do
[
mod: {NCAA.Application, []},
extra_applications: [:logger]
]
end
Then create NCAA.Application
defmodule NCAA.Application do
@moduledoc false
use Application
require Logger
def start(_type, _args) do
port =
case Mix.env() do
:prod ->
80
_ ->
Logger.info("Starting application at http://localhost:4000")
4000
end
children = [
{Plug.Cowboy, scheme: :http, plug: NCAA.Server, options: [port: port]}
]
opts = [strategy: :one_for_one, name: NCAA.Supervisor]
Supervisor.start_link(children, opts)
end
end
In prod we start in port 80, otherwise 4000 is fine.
I deployed this to render. The build script is simply mix deps.get && mix compile
and the run script is just mix run --no-halt
.
Easy peasy. You can check out at at https://ncaa-predictor.onrender.com for the duration of the tournament, for just clone it yourself from Github and run it with mix run --no-halt
.
Thanks for reading.
Top comments (0)