System Active // V3.0.0
LOC: 23.8103° N
All posts
Jun 26, 2026·7 min read

2,147,483,648 World Cups, One Weekend

StoryMathWorld CupWeekend Project

With the 2026 World Cup Round of 32 about to be set, I wanted to build something themed around it. My first instinct was the obvious one: a predictor. Pick your team, see their odds of winning it all. Every sports site already does this and it felt flat the moment I wrote it down.

What kept nagging at me instead was a framing question. Once the Round of 32 is locked, the knockout stage is just 31 single-elimination matches. That means the whole tournament can only unfold in 2^31 distinct ways. That's a real, countable number: 2,147,483,648. Not "millions of possible outcomes" as a vague phrase. An exact number, and every one of those 2.1 billion timelines has an exact probability once you know each team's strength.

That's a much more interesting app than a predictor. Not "who wins" but "how many realities are still alive, and which one are we in."

The Doctor Strange hook

Once I had the framing, the reference wrote itself. In Infinity War, Doctor Strange looks through 14,000,605 possible futures to find the one where they win. That's a number small enough to be funny next to 2,147,483,648. The app leans into it directly: right now, before a single match is played, you're looking at a superposition of 2.1 billion brackets. Every real result the tournament produces collapses that number, exactly in half, until only one timeline is left standing.

So the product became: a live counter of how many World Cup realities remain, that ticks down as actual results come in.

The math: Elo, Poisson, then exact DP

The whole thing rests on keeping three layers cleanly separated, each one consuming only the output of the one before it.

1. Team strength (Elo). Every team gets a rating from the World Football Elo system. This is the only place where "how good is this team" lives.

2. Match model (Poisson). From the Elo gap between two teams, I derive an expected-goals rate for each side using a supremacy split: roughly 130 Elo points of edge is worth about one extra goal. Each side's goal count is modeled as a Poisson variable, and summing over the joint distribution of scorelines gives win probability, draw probability, the single most likely scoreline, and a confidence score, all from the same underlying numbers, so they can never contradict each other.

3. Bracket math (exact dynamic programming). This is the part that made the project worth building. Because the bracket is fixed going in, you don't need Monte Carlo simulation to answer "what's the chance this team wins it all." You can compute it exactly with a bottom-up DP over the bracket tree: the probability a team wins its round-of-32 match, times the probability its potential opponents get there too, weighted by matchup odds, recursively up to the final. It runs in well under a millisecond in the browser, and it's exact, not sampled.

Here's what that collapse looks like in numbers, stage by stage:

StageMatches decidedRealities still alive
Round of 32 set (kickoff)0 / 312,147,483,648
After the Round of 3216 / 3132,768
After the Round of 1624 / 31128
After the Quarterfinals28 / 318
After the Semifinals30 / 312
After the Final31 / 311

Every completed match fixes one binary choice and halves what's left. That halving is the whole mechanic of the site.

Architecture: slow model, fast bracket

I split the system into two layers that update on very different clocks.

engine/ (Python, slow-moving)          web/ (Next.js + TypeScript, live)
----------------------------          -----------------------------------
Elo ratings                           reads model.json (committed)
   |                                  reads results.json (live, as matches finish)
Poisson goal model                          |
   |                                  exact bracket DP: superposition + collapse
advance-probability matrix            re-run on every click
   |  writes
model.json  ------------------------------->

The Python side only runs when I want to retune the model, and it writes a static model.json bundle. The TypeScript side never touches Elo or Poisson directly, it just reads the bundle and does the exact combinatorics live, in the browser, on every result update or hypothetical click. That separation means swapping in a better-trained model later (something trained on Kaggle match data, say) is a one-file change. The bracket engine and the entire UI stay untouched.

Results themselves come from results.json, kept current three ways: a GitHub Action that polls football-data.org every six hours and opens a PR for me to review and merge, a one-line CLI command for manual entry by FIFA match number, or editing the JSON directly for anything unusual (like a penalty-shootout draw, which the API reports as a tie and I have to resolve by hand).

What the weekend actually looked like

The build itself moved fast once the math was settled, because the hard part was never the DP, it was making the UI communicate a number like 2.1 billion without it feeling abstract.

A few things that took longer than expected:

  • The champion ribbon. The bracket converges into a single "champion" banner at the center, and getting the flag and the country name to sit as one visually centered unit, without overlapping at different name lengths, took several passes. Small thing, disproportionate amount of fiddling.
  • Save-as-image. Turning the live bracket into a shareable static image (for posting outside the app) meant re-rendering the whole 32-team bracket to a canvas server-side, not just screenshotting the DOM.
  • Performance on the bracket-pool queries. Two fun modes, Chalk (draw a random bracket from the 50 most likely outcomes) and Chaos (draw one from the 50 most cursed, upset-everywhere outcomes), originally recomputed too much on every draw. Reworking how those pools were built brought that path down by roughly 85x.
  • Making the live counter actually live. Wiring the GitHub Action so a finished match becomes a merged PR becomes a redeployed counter, without me babysitting it every six hours.

None of that is exotic engineering. It's the normal shape of a weekend project: the core idea takes an afternoon to prove out, and then most of the remaining time goes into the fifteen small things that make it feel finished instead of like a prototype.

Why exact math instead of simulation

It would have been easier to just run 100,000 Monte Carlo simulations and report the frequencies. I didn't want to, for a specific reason: the whole premise of the app is "every number here is exact," and a simulated percentage undercuts that premise even if it's numerically close. If I'm telling someone their team has a 1 in 4,096 chance of a specific run to the final, I want that to be a fact derived from the model, not an estimate with sampling noise. The DP makes that true, and it's fast enough that there's no performance excuse not to.

Where it stands

It's live at bracket.aweshislam.com, static export on Vercel, no login, no backend beyond the results file. You can look up your team's round-by-round odds, check whether two teams can even meet before the final, or just watch the realities-remaining counter shrink as results land.

The tournament isn't over yet, so this post is only the build story. Once the final whistle blows I'll write the follow-up: how the model actually did, called-correctly tallies, exact-scoreline hits, and whatever the data says about whether Elo-plus-Poisson was good enough for a job like this.

Keep reading