Tags

React 19TypeScriptVite 7Tailwind CSS 4Framer Motionuse-gesturegamereacttypescripthistory

Whenabouts is a daily historical timeline puzzle published at scalargame.com/whenabouts as part of the ScalarGame platform. Five historical events appear one at a time — each with a title, description, and image. Scrub a horizontal year carousel to lock in your guess. There's no failure state: all five events always score something. The goal is to maximize total accuracy points across all five.

How It Works

Each puzzle is pre-authored and sharded into per-month JSON files (`public/puzzles/YYYY-MM.json`) that are precached by the service worker. No database round-trip is needed to play — puzzles load from cache on repeat visits and work fully offline.

Each puzzle has a tag (a content theme — e.g. "Space Exploration", "Ancient Rome") that groups the five events. The tag name appears in the masthead and in the share card.

Scrubber

The year carousel is a horizontal drag/swipe surface built with `@use-gesture/react` and the Pointer Events API. Three zoom levels — Century, Decade, Year — are reachable via pinch gesture or tap. BCE years are supported (negative integers rendered as "X BCE"). The scrubber bounds are set per-puzzle based on the range of events in that day's set.

After locking a guess the card reveals the true date, your accuracy grade, and your score for that event. On mobile the card auto-advances to the next event after ~2.8 seconds. On desktop, events transition via Framer Motion slide-in with `AnimatePresence mode="wait"`.

Scoring

Each event scores independently using an exponential decay formula:

Score = round(1000 × e^(−k × Δdays) × multiplier)

`Δdays` is fractional — a month-level guess scores as 1/12 of a year off. The decay constant `k` is set per event by `popularity_tier` (1–5): obscure events (tier 1) are forgiving (`k = 0.000003`), famous events (tier 5) are strict (`k = 0.0003`). Maximum 2000 points per event, 10,000 total.

GradeThresholdMultiplierEmoji
Perfect< 1 year off×2.0🟨
Exact Year1–2 years off×1.2🟩
Right Era2–10 years off×1.0🟧
Way Off> 10 years off×1.0🟥

Each grade returns a random reaction message from a curated set.

Share Format

Whenabouts — {tagName}

{playDate}

🟨🟩🟧🟧🟩

8,420 pts

The emoji grid is spoiler-free — it shows accuracy grades but not years or event names.

Content

Puzzles are pre-authored in a separate content repo and processed at build time. The Vite plugin `whenabouts-puzzle-split` shards the master JSON (~5.9MB) into per-month files (~200KB each), keeping the initial load fast. A `public/puzzles/dates.json` index lists all available `playDate` strings for the free-play random picker.

Each event carries:

  • `title`, `blurb`, optional `revealBlurb` (post-lock reveal text)
  • True `year`, optional `month` and `day`
  • `popularity_tier` (1–5) for scoring decay
  • `image_url` + `imageTone` (`warm`/`cool`/`neutral`) for placeholder stripe color when no image is available
  • `ringHue` — per-event accent color for the card border

Modes

  • Daily — counts toward streak and lifetime stats (games played, current streak, max streak, average score, high score, perfect placements)
  • Free Play — random past puzzle; `suppressStats: true` prevents any stats update

State

State is managed via a plain `useState` + localStorage hook (`useTimelineGame`) rather than Zustand. Daily state (`timeline_daily_state`) includes a `playDate` guard that detects stale state from a previous day on load. User stats (`timeline_user_stats`) update automatically on the `isComplete` transition. Free-play state keys as `timeline_freeplay_{playDate}` with the suppress flag set.