A stack of little brain games. Spork is one app — one home screen — that hosts a growing shelf of distinct games, the way Sporcle offers Quizzes and Acrostic, Chess Attack, Quizzle, and Steps as separate games with their own rules. Not one engine wearing hats: genuinely different games, unified by a shared shell and a common feel. Play instantly as a guest — no account required.
Shipping today, six games: Quizzes (Sporcle-style, across nine interaction formats — each its own daily game), Steps (word ladder), Acrostic (spell a hidden word from clue initials), Quizzle (wager pub-quiz), Chess Attack (real mate-in-N puzzles), and Flashcards (AI-generated decks). Live Trivia (real-time multiplayer) is on the roadmap. One fresh puzzle of each type every day, generated + validated automatically — see Where quizzes come from.
Spork is built as a shell + game islands: the platform (home, guest identity, streaks, the AI
generation pipeline, admin dashboard, quality/CI rig) is shared; each game is a self-contained island
under src/games/<game>/ that plugs into it. Adding a whole new game is a new island, not a
rewrite — and within a game like Quizzes, adding a new format is just new data or a new renderer.
Play: open spork.jpc.io in any browser (installable as a PWA), or get the iOS app via TestFlight.
Architecture, quality bar, and toolchain descend from the stoop app via flashstack — the same Ionic + Amplify Gen2 stack, strict CI gates, and Gherkin-first → full-e2e workflow.
Sporcle isn't one game — it's a destination for many bite-sized brain games, each with its own mechanic, sharing an audience and a habit. Spork chases the same shape at two levels:
Level 1 — a shelf of distinct games. Six ship today — Quizzes, Steps, Acrostic, Quizzle, Chess Attack, and Flashcards — with Live Trivia on the roadmap. These are not variations of one engine: a spaced-repetition card deck, a chess variant, a word ladder, and a timed quiz have nothing mechanically in common. What they share is the platform: one guest-first home, streaks, a consistent look, and the build/quality rig. Each game is an island you can add without touching the others — the payoff is a habit-forming stack, not a single hit.
Level 2 — within a game, breadth for free. The first game, Quizzes, is itself broad (~1.5M Sporcle quizzes span dozens of formats). Here the bet is one small engine, a handful of renderers, and an ocean of data:
- One content model. Every quiz — a world map, a flag grid, a "name the Beatles" foursome, the
periodic table — is a
Quizplus universalAnswerrows. One shape, discriminated by apromptKind+ grouping (mirroring how Sporcle's owngame+entrytables work under the hood). - A few renderers. How you're prompted (map region, image, text clue, grid cell, nothing) and how you answer (type, click, pick, arrange) are small, swappable axes over the same engine.
- An ocean of data. Content comes from curated templates (maps — the topology is the answer set, so nothing is hallucinated) and AI generation (Claude writes typed/MC/picture quizzes on demand) — see Where quizzes come from.
The engineering discipline that makes both levels cheap is the same: get the seams right (a sharp platform/game boundary; a universal answer model) so breadth — more games, more formats — comes almost for free.
Each is a real, independent game. Six ship today; Live Trivia is on the roadmap. Each is its own
island under src/games/<game>/ reusing the shared shell.
| Game | Status | What it is |
|---|---|---|
| Quizzes | ✅ shipping | Sporcle-style "name them all against a clock," across 9 interaction formats (see Quiz types). |
| Flashcards | ✅ shipping | AI-generated decks studied straight through to a score (SM-2 for signed-in players). Folded in from flashstack. |
| Acrostic | ✅ shipping | A word puzzle: solve clues whose first letters spell a hidden word, revealed a letter at a time. |
| Steps | ✅ shipping | Word ladder — transform a start word into a target one letter at a time, each step a real word. |
| Chess Attack | ✅ shipping | Real mate-in-1/2/3 puzzles on a full board (chess.js) — find the forced checkmate; the defender replies. |
| Quizzle | ✅ shipping | A pub-quiz where you wager on your confidence per answer — points ride on the bet. |
| Live Trivia | ⬜ planned | Real-time multiplayer rounds — everyone answers the same question on a shared clock, live leaderboard. |
These differ on every axis that matters — input (type/click/drag/move a piece), scoring (found-set / wager / win-condition), session (solo-vs-clock / real-time-multiplayer). That diversity is the point: the shared platform is what makes carrying all of them viable, not a pretense that they're one engine.
Spork's Quizzes game targets every Sporcle-style interaction format. Each one reuses the same
Answer row and play engine, differing only along three axes — prompt (what you see), input
(how you answer), and scoring (what "correct" means):
| Type | You… | Prompt | Input | Scoring |
|---|---|---|---|---|
| Classic | type answers to reveal a hidden list | none | type | membership |
| Map | type a place → its region fills in on an SVG map | region | type | membership |
| Picture Box | identify people/things from images by typing | image | type | membership |
| Multiple Choice | pick the correct option, one question at a time | text | pick | membership |
| Clickable | click the correct tiles out of a displayed set | text/img | click | membership |
| Picture Click | click the right spot on a single image (map/diagram) | region | click | membership |
| Slideshow | answer one prompt per slide, advancing through a deck | text/img | type | membership |
| Sortable | drop each item into its correct bucket/category | text | arrange | bucketing |
| Order Up | arrange items into the correct sequence / ranking | text | arrange | sequence |
On top of these sit scoring variants that reuse a type's renderer with one engine flag — Minefield (one wrong answer ends the run), Blitz (race a short clock), Alphabet (one answer per letter). (These are all within the Quizzes game — the separate games like Acrostic and Chess Attack live at Level 1.)
Each is its own daily game on the home shelf (a Map puzzle and an Order Up puzzle feel like different
games), all reusing the one Answer row + play engine:
Map — fill the map![]() |
Classic — name them all![]() |
Multiple Choice![]() |
Picture Box — AI art![]() |
Find It — click the map![]() |
Spot It — click the spot![]() |
Slideshow![]() |
Sort It — buckets![]() |
In Order — sequence![]() |
- Prompt (
Answer.promptKind):NONE·TEXT(a clue) ·IMAGE(a media key) ·REGION(an SVG/id) ·CELL(a grid coordinate). PlusAnswer.groupKeyto tie several rows to one prompt (a four-member "foursome"). - Input (
Quiz.inputMode):type·pick·click·arrange. - Scoring (
Quiz.scoringMode):membership(find them all — the found-set engine) ·sequence(Order Up) ·bucketing(Sortable) ·elimination(Minefield).
Most types are just a combination of these plus a renderer component. membership types share one
engine; sequence/bucketing/elimination are deliberate engine variants, not forced fits.
Three sources, all feeding the same Quiz + Answer load path:
- Curated templates — map quizzes, where the topology is the answer set (reconciled once at
build time from
world-atlas+i18n-iso-countries, so nothing is hallucinated); and Chess Attack, whose mate-in-N puzzles come from the Lichess puzzle DB (CC0), each re-verified with chess.js to be a real forced mate. (An LLM can't reliably compose sound forced mates, so chess is template-backed, not generated.) - AI generation —
generateQuiz(mode, topic)has Claude author a quiz on demand (tool-forced structured output); PICTURE_BOX quizzes also get one Bedrock-drawn image per answer. This is the primary way the library grows: type a topic, get a playable quiz. - Automatic daily puzzles — a scheduled Lambda (
amplify/dailyingest, EventBridge "every day") generates one fresh, PUBLISHED puzzle per generative game type each day — the five text quiz types plus Steps, Acrostic, Quizzle, and Chess — with zero manual effort. It reuses each game's own prompt → parse → validate trio (never trusting the LLM; it retries on a bad candidate) and rotates topics deterministically by day. So the home shelf is never stale.
Every AI path is independently validated before anything is stored: the generator proposes, a pure validator (the same one its build-time fixtures use) verifies the puzzle is well-formed and solvable — a word ladder is a real ladder, an acrostic's initials actually spell the word, a chess solution legally captures the king — and rejects + retries otherwise. Nothing unvalidated reaches a player.
The same generation runs at build time to produce the committed seed fixtures
(npm run gen:quiz-fixtures), so the sandbox always has a consistent, known set of quizzes for
demos and e2e — no live LLM call during seed.
Scraping / import — paused. An earlier plan was to scrape and import a large external quiz corpus. With AI generation covering breadth, that's deferred — we may revisit a small importer later, but it's not needed to grow the library. (Any imported content would remain the property of its authors; import would be for content we have the right to use.)
| Layer | Choice |
|---|---|
| Mobile / Web client | Ionic 8 + React 19 + TypeScript (strict) |
| Native shell | Capacitor (iOS / Android) + installable PWA |
| Bundler | Vite |
| Backend | AWS Amplify Gen2 — Cognito (guest identity) + AppSync (GraphQL) + DynamoDB |
| Maps | react-simple-maps + world-atlas topology + i18n-iso-countries (build-time) |
| Chess | chess.js — legal moves, defender replies, checkmate detection (Chess Attack) |
| Testing | Vitest + Istanbul coverage (unit) · Playwright + playwright-bdd Gherkin (e2e) |
| AI | Bedrock Claude (tool-forced structured output) for generated quiz content |
┌─────────────────────── Spork (platform shell) ───────────────────────┐
│ Home game shelf · guest identity · streaks · admin · quality/CI │
└───────────────┬───────────────────────────────┬──────────────────────┘
│ │
┌──────────────▼─────────────┐ ┌─────────────▼──────────────┐
│ Quizzes (this repo) │ │ Flashcards │
│ │ │ Deck → Card, SM-2 study │
│ generateQuiz ─┬─ template │ └────────────────────────────┘
│ (mode fork) │ (MAP: no LLM, from topology fixture)
│ └─ generative (Bedrock: typed/MC/picture …)
│ │
│ Quiz (PUBLISHED) ──► Answer[] {promptKind, promptValue,
│ │ groupKey, display, accepted[]}
│ ▼
│ play engine: normalize → match → found-set + timer + score
│ renderer registry: RENDERERS[quiz.mode] (Map shipped; others land here)
│ │
│ ▼ best score per-device (localStorage — guest-only)
└─────────────────────────────────────────────────────────────┘
| Model | Purpose |
|---|---|
Quiz |
Published unit: topic, mode, inputMode, scoringMode, time limit, renderConfig JSON. |
Answer |
Universal row: promptKind, promptValue, groupKey, display, accepted[], options. |
Category |
Discover shelves (browsable rows + order/labels), shared across games. |
GenerationRun |
One template/AI generation run — powers the admin dashboard (game + mode). |
Deck / Card |
The Flashcards game (inherited): spaced-repetition study. |
Best scores for the guest-only Quizzes game live on the device (localStorage), not in a
per-user model — no account needed to play or to keep a personal best.
git clone https://github.com/johnpc/spork
cd spork
npm install
npm run dev # Vite dev server (or: ionic serve)The Amplify Gen2 backend lives in amplify/. Spin up a personal cloud sandbox (deploys
to your AWS account and writes amplify_outputs.json):
npx ampx sandbox
npm run gen:map-template # (re)build the world-countries map fixture
npm run seed # seed categories, a demo deck, and the World Countries quiznpm run gen:icons # regenerate PWA + iOS (light/dark/tinted) + Android icons
# from assets/icon.png (light) & assets/icon-dark.png (dark)Every gate runs in CI on PRs to main, and the blocking gates also run locally on every commit via a
Husky pre-commit hook.
| Command | What it checks |
|---|---|
npm run lint |
ESLint — including no-explicit-any: error (no any, ever) |
npm run format:check |
Prettier formatting |
npm run check:lines |
File-length discipline — every .ts/.tsx source file stays ≤ 100 lines |
npm run check:features |
Every .feature file is mapped to a CI acceptance area (no silently-unrun specs) |
npm run test:coverage |
Vitest unit tests with an 80% floor (statements/branches/functions/lines) |
npm run crap |
CRAP score per function — fails any function over 15 |
npm run build |
TypeScript + Vite production build |
npm run test:e2e |
Gherkin acceptance tests via Playwright + playwright-bdd |
npm run quality |
Runs the full local gate in sequence |
All acceptance tests are written as Gherkin .feature files — never raw spec code. Every data-reading
flow asserts on rendered real (seeded) data (e.g. a map region actually filling in), not just
navigation. The fix for low coverage is always a new test, never an exclusion.
The web build is an installable PWA (public/manifest.json, maskable icons). Capacitor wraps it
for the stores: .github/workflows/ios-deploy.yml archives + uploads to TestFlight;
android-deploy.yml builds and publishes an APK — both after CI succeeds on main. The iOS app icon
ships light, dark, and tinted variants.
- Bundle id:
com.johncorser.spork - Required repo secrets:
AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY,TEST_USERNAME,TEST_PASSWORD,ASC_KEY_ID,ASC_ISSUER_ID,ASC_KEY_CONTENT,TEAM_ID.
| # | Milestone | Status |
|---|---|---|
| 1 | Platform fork + guest-only Quizzes game + Map mode (template-backed, e2e verified) | ✅ |
| 2 | Universal 3-axis model + all 9 Sporcle quiz formats (renderer + seed + e2e each) | ✅ |
| 3 | AI generation for typed/MC/picture quizzes (Bedrock generative branch) | ✅ |
| 4 | Quiz import/scrape pipeline — paused (AI generation covers breadth) | ⏸️ |
| 5 | Discovery: browse/search/categories, popularity, per-device history | ⬜ |
| 6 | New games (own islands): Steps ✅, Acrostic ✅, Chess Attack ✅, Quizzle ✅; Live Trivia next | 🔨 |
| 7 | Daily model — one puzzle of each type per day, play-once; auto-published nightly | ✅ |
| 8 | iOS TestFlight beta (join) | ✅ |
| 9 | Daily images for Picture Box / Spot It (Stability + S3 on the schedule) | ⬜ |
The code in this repository is provided as a personal/experimental project — see the repo for terms. Quiz content is not covered: any imported or third-party quiz data and trademarks (Sporcle and others) remain the property of their respective owners, and the import capability described above is intended only for content one has the right to use.









