projects

VeePeenini, Part 15: Wrapped — the Payoff at the Whistle

· Vitor Pontual · 7 min read

VeePeenini, Part 15: Wrapped — the Payoff at the Whistle

The World Cup ended today. So did VeePeenini’s first tournament, and I’d been sitting on a small secret for six weeks about how I wanted it to end.

Back when the group stage kicked off, I made myself a rule I wrote about in Part 7: no new features during the Cup. Only stabilize and bugfix. A live game with real friends and real standings is not the place to be shipping ambition. I held to it almost completely. There was exactly one exception, and it’s the reason this post exists.

The one thing I built while I’d sworn not to

After weeks of daily packs, blown predictions, and a group chat that turned genuinely feral over a 1-0, I knew what the group deserved at the end: not a final leaderboard and a shrug, but a payoff. Something personal. A recap that turns a season of spreadsheet math into a story you’d actually want to look back on. A send-off.

So while everyone else saw me only fixing things, I was quietly building the backend for it. Three parts, all built early on purpose:

  • The compute. A computeFullTime pass that rolls up everything a player did across the whole tournament — stickers collected, album completion, exact-score snipes, head-to-head duels, trades, the lot.
  • A reveal-gate. The recap stays locked until the tournament is genuinely over — every fixture finished. Before that, it’s admin-only; everyone else who wanders to the URL gets a themed “unlocks when the final whistle blows” screen. I could preview it early with a force flag, but nobody else could peek.
  • An immutable snapshot. The moment the Cup completes, each player’s numbers freeze into a stored snapshot and never move again. It’s a keepsake, and a keepsake you can still edit isn’t one.

I built it early precisely so it could reveal itself at the whistle with no scramble from me on the day. Which, given how the day actually went, was a good instinct.

Anonymous is the whole game

Here’s the design call I’m proudest of, and the one that took the most discipline: the recap never names another player.

That sounds like a limitation. It’s the entire point. This is a game for nine friends. The fun is the rivalry, but the trust is that I’m not going to broadcast that someone finished dead last in exact scores, or expose the exact gap between two people who’ve been needling each other for a month. So every single stat is framed as where you landed in the field, never versus a named person.

You were 2nd overall. 1st in the album. 7th for exact scores. 3rd in duels. All true at once, each one stating its own category so the numbers read as a profile and not a contradiction. The charts race “you vs the leader” and “you vs the field” — never “you vs [a specific friend]”. An early draft had named callouts in it. I dropped them all. In a friend group, the relative standing is the interesting part; the named comparison is just a way to make someone feel small.

Making the charts feel good

A Wrapped archetype card Your archetype, revealed with confetti — the emotional thesis of the whole recap on one card.

The recap opens on the site’s dark language — near-black, gold hero numbers with a glow, mono labels, the Mila mascot showing up as a recurring character — and taps through like a Spotify Wrapped: story cards, an energy-bar progress meter, gold confetti on exactly two moments (a top-three finish and the archetype reveal). Empty stats get dropped so nobody taps through a card that just says zero. It ends by naming your archetype — The Sniper, The Collector, and a few others — and hands you a shareable stat card you can drop straight into the group chat.

The archetypes and the card frame came together fast. The race charts did not.

My first instinct was a bump chart — the classic rank-over-time tangle of lines where everyone swaps positions. On paper it’s the obvious choice. On our real data, with real ties, it was abstract noise. You couldn’t feel anything in it. I killed it and went concrete instead: cumulative lines that just climb. Your gold line, thick and glowing, against a faint “leader” line, both drawn slowly enough to actually watch, with a caption the data writes itself (“you closed all 48 — pipped to the line”). Concrete beat clever here.

The rest was the unglamorous polish that decides whether a thing feels finished: making the giant hero number shrink to fit its column instead of blowing out the layout, honoring reduced-motion so the whole thing freezes gracefully for anyone who wants that, and getting the draw timing to feel deliberate rather than laggy.

Shipping it live, during the actual final

The gate meant the recap could reveal itself at the whistle. The finale was where I decided it should do more than that — and, honestly, where I was still building while the trophy was being lifted.

Three things went out during the match:

  • The whole-field points race. The two-line version was good, but the finale wanted spectacle. I rebuilt it so every player is a colour, each line drawn from zero over a slow twelve-second build — the season replayed in one shot. Still no names on the lines; you know which one is yours because it’s the one that glows.
  • A champion slide. The winner gets their own moment, with confetti.
  • A dashboard banner. The instant the final whistle registered, a banner appeared on everyone’s home screen inviting them into their Wrapped. No URL to share, no announcement to write. It just showed up, the way it should.

The whole-field points race Every player a colour, drawn from zero — the season replayed in one shot. No names on the lines; you know yours because it glows. (Legend anonymized.)

And a bug worth admitting, because this series has never pretended I don’t write them. On the first cut of the whole-field race, the lines didn’t animate at all — they just snapped to their final shape, instantly, the whole point of a twelve-second build gone. It was a Motion gotcha: the animation library skips the entrance animation on the initial mount unless you tell it not to, and I hadn’t. I only caught it by watching the actual render frames instead of trusting that “it should animate.” It should have. It didn’t. Watching real frames, not reading the code and nodding, is what found it. Fixed it live, mid-final.

The shareable Wrapped card The stat card you can drop straight into the group chat. (Anonymized.)

What the whistle froze

So here’s where VeePeenini’s first tournament actually landed. When the final whistle blew today, nine snapshots froze at once. Each friend has their own Wrapped now — their archetype, their ranks, their season drawn as a line climbing out of zero — and it will read exactly the same in a year as it does tonight. Nobody’s numbers name anybody else’s. It’s personal, it’s private, and it’s done.

I’ve written a lot in this series about the boring stuff being what matters — the migrations, the move, the alerts that had never fired. This is the one part that was allowed to be purely for joy. It’s also the one feature I let myself break my own no-new-features rule for, and I’d make that trade again. The standings kept the group honest all tournament. The Wrapped is the part that lets them look back and smile.

Everything else in this series had to be trustworthy first and pleasant second. The Wrapped got to be the other way around: pure payoff, the group’s reward for six weeks of caring too much about a 1-0. The Cup is over, and nine snapshots are frozen. That’s a good place to stop.