A still appears, you narrow the year in stages, and every correct narrowing banks a point while you play at wyit.film.
- Status
- live at wyit.film
- Monthly cost
- $0
- Infra
- static CDN, 2 GETs per round
- Payload
- ~198 KB gzip cold first round
- Performance
- Lighthouse 97 perf
- Corpus
- 4,028 films, 1915–2025
Address space
FilmGrab stills filtered for year + intact image are assigned offline to slots
000000–004027 by a deterministic seeded shuffle, with each slot backing two
CDN files:
datasets/items/v1/{slotId}.json — ~230 bytes: title, year, edges
images/items/v1/{slotId}.webp — ≤1024px wide, q76
The WebP caps are a cell-network budget: 1–2s to first painted image. ≤1024px wide at q76 is what fit.
The six-digit ID is the lookup protocol. Both URLs derive from it, so the image
fetch starts the moment a slot exists, in parallel with metadata rather than
after a separate lookup. The client ships with slotCount = 4028 and a
pad-to-six function. Per-year indexes and manifests exist for tooling.
→ Offline validation (Celebrity Golf,
same static-corpus move).
Picking a slot
const buf = new Uint32Array(1)
crypto.getRandomValues(buf)
return buf[0] % total
Modulo bias exists (2³² ≢ 0 mod 4028); relative bias ~9×10⁻⁷ is negligible.
Distribution is uniform over films even though the calendar is not: the 2010s
hold 1,107 slots (27%); eight years have ≤2 films. Random mode excludes repeats
in-session; daily persists under daily:{dateKey}.
Daily mode swaps the pointer: daysSince(epoch, localMidnight) % slotCount
into a second versioned shuffle (seed "wyit-daily-v1"). Every player lands
on the same slot with no shared server state. I walked a permutation instead of
hash(date) % n so the cycle visits every slot before any repeat (~11 years of
dailies). Day index is the puzzle number for free. Local midnight is the day
boundary because that’s what Wordle established; with no backend, a global clock
would buy nothing anyway.
Accepted tradeoffs: device timezone can time-travel the puzzle, and scores are client-trust, so refresh-to-retry works with no shared server clock.
Address space
One localStorage schema keyed daily:{dateKey} covers mid-game resume,
return-visit summary, and share (full guess path stored as a Wordle-style emoji
grid).
Two requests and ~200 KB
- App shell: HTML 1.5 KB + CSS 8.7 KB + JS 165.7 KB gzipped
GET {slotId}.json(~230 B,max-age=300)GET {slotId}.webp(22.6 KB avg, 6.9–32.8,max-age=31536000, immutable)
An inline script in index.html draws the slot and preloads JSON + WebP before
React hydrates. The client then keeps 5+2 rounds ahead, eagerly preloads the
first three stills, and prefetches each slot’s four graph edges (baked offline).
We can prefetch in the time the user takes to finish a round; those depths are
what fits inside that window.
System fonts only (I didn’t want to ship a font download). JS is 84% of cold payload: one 505 KB raw bundle. At that size, splitting never came up.
The narrowing engine
Originally it was hard buckets: pick a century, then a decade, then a year. Being off by one was too unforgiving. Even now most people don’t score very high on any image. Runtime derives the year options shown on the drag pill from the current parent range under these four constraints:
- ≤4 options per range step
- no option narrower than 2 years mid-game
- adjacent options overlap ≥2 years
- final step: exact years, ≤6
initial: [1900, 2025],
steps: [
{ bins: 5, widthBins: 2, strideBins: 1, windows: 4 },
{ bins: 9, widthBins: 3, strideBins: 2, windows: 4 },
{ bins: 9, widthBins: 3, strideBins: 2, windows: 4 },
],
exactWidth: 5, maxSteps: 5,
Step one on the full range:
1900–1949 · 1925–1975 · 1950–2000 · 1976–2025. Each pick becomes the next
parent; past the explicit steps an adaptive rule subdivides until the range fits
in 5 years / ≤6 candidates. Scoring: each correct narrowing banks one point; a
wrong commitment ends the round.
I wanted a physical sense of things snapping into one of the 3–5 options, while implying a sliding overlapping range. There’s no reason it has to be a slider at all: the input is discrete, and buttons would work. The drag pill is there to make the overlapping-window structure felt.