EXP-011 · Case study Running For fun · Added Sep 2026

fifa-rivals

Made for fun, to make FIFA games at the office more competitive. ELO leaderboard, winstreaks, head-to-head, and share-ready stat cards, all computed in the browser from one JSON file. Kept light on purpose: no framework, no backend, no build. The hard part was making the ELO feel fair.

01Overview

fifa-rivals started as a joke to make FIFA games with my office friends more competitive. Every match gets recorded in a single data.json file, and everything else — leaderboard, streaks, head-to-head, club aggregates — is computed in the browser from that data.

02What Was Built

  • ELO leaderboard — base 1200, K=32, computed in date order. Deterministic: same data, same ratings every time
  • Two leagues — ELO, form, and streaks are computed per league. Cross-league matches count as Exhibitions: shown in history, but move no ELO
  • Head-to-head — full matrix of all players plus per-pair duel details
  • Club stats — per-club aggregates (global and per player) with real club crests
  • Stat cards — each player can download a 1080×1350 PNG stat card, share-ready for Instagram and WhatsApp
  • Records — longest winstreak, biggest win, highest-scoring match, most frequent rival

03Making ELO Fair

This was the part that took the most tries. A plain ELO formula is easy to write, but getting ratings that everyone at the office accepted as fair was not. Where it landed: ratings are recomputed from scratch in date order so the result never depends on when you look, each league keeps its own ELO, and cross-league matches count as exhibitions that move no rating.

04Why No Framework

It is a for-fun project, so it should stay light. The whole site is plain HTML, CSS, and vanilla JS — no build step, no dependencies, no backend. Stats live in a pure computation module (stats.js) shared between the browser and a plain Node test file. Updating the site means editing one JSON file and pushing — Caddy serves it straight from disk.