A reusable card game engine for CodeSignal bespoke simulations. Learners play through multi-encounter campaigns, drafting cards between encounters and deploying architectures under constraints (budget, slots, prerequisites). Content is just JSON — one engine covers system design, frontend stacks, data pipelines, security architecture, ML infra, and more.
The first proof-of-concept campaigns are "System Design Journey" and "ML Platform Journey", but the engine is topic-agnostic.
- A campaign defines a sequence of encounters with increasing difficulty
- Before each encounter, learners draft new cards to add to their collection
- A scenario sets the context (e.g., "Your startup just got featured on Hacker News — build an architecture that handles the traffic spike")
- A hand of cards represents available choices — each with a resource cost, effects on metrics, prerequisites, and synergies/anti-synergies with other cards
- Learners drag cards onto a board with limited slots and a finite resource budget
- Deploy to see if the architecture meets the encounter goals — an LLM generates a brief architectural verdict after each deploy
- A limited pool of retries spans the whole campaign — spend them wisely
- The AI grader evaluates the full campaign via
RUBRIC.md, readingsolution.jsonwhich includes behavioral telemetry (attempt log, draft log)
Three JSON files = one campaign:
- Card deck (
client/public/data/cards/<deckId>.json) — cards withid,name,icon,type,cost,effects,prerequisites,synergies(including anti-synergies) - Scenario (
client/public/data/scenarios/<scenarioId>.json) — briefing text, base metrics, goals, resource budget, available cards from the deck - Campaign (
client/public/data/campaigns/<campaignId>.json) — sequence of encounters referencing scenarios and decks, draft pools, retry budget
Card icons can be emojis or image paths (e.g. /data/icons/deck-id/card-id.png). Use scripts/generate-icons.mjs to generate icons via Gemini API.
No code changes needed. See examples/system-design-basics/ for a complete task template.
server.js Node.js HTTP + WebSocket server (state persistence, content generation)
vite.config.js Vite build config (proxy, build output)
initial_state.json Default campaign/scenario to load on server start
client/
index.html Main HTML structure
app.js UI orchestrator — data loading, rendering coordination, state persistence
bespoke-template.css Template layout/utilities (do not edit)
card-game.css Game-specific style imports
modules/
engine.js Core game engine (pure logic, no DOM) — metrics, synergies, campaigns, telemetry
state.js Global UI state management
notify.js Toast notification system
validate.js Content validation utilities
content-generator.js AI-assisted content generation helpers
renderers/
hand.js Hand cards rendering and pointer-based drag-and-drop
board.js Board slots rendering and card removal
battle.js Deploy animation, results, LLM debrief, coaching hints
draft.js Card drafting UI between encounters
header.js Top bar (metrics, retries, encounter info)
overlays.js Full-screen overlays (encounter transitions, campaign summary)
card-detail.js Card detail modal
tooltip.js Card hover tooltips
shared.js Shared rendering utilities
authoring.js Content authoring panel
styles/ CSS files for each renderer
color-overrides.css Dark mode alert color overrides (local, not in design-system)
public/
data/cards/ Card deck JSON files
data/scenarios/ Scenario JSON files
data/campaigns/ Campaign JSON files
data/icons/ Generated card icon PNGs
data/prompts/ Content generation prompt templates
cosmo/ Cosmo mascot SVG assets
help-content.html Help modal content
design-system/ CodeSignal design system (submodule, do not edit)
scripts/
generate-icons.mjs Gemini-powered card icon generation with bg removal
remove-bg.mjs Standalone background removal for raw icon PNGs
examples/
system-design-basics/ Complete task template (rubric, shell scripts)
State flow: Client loads campaign → loads encounter scenario + deck → user drafts/places/removes cards → engine recalculates metrics → UI updates → auto-saves to POST /state (800ms debounce) → solution.json on disk.
Deploy debrief: On each deploy, an LLM generates a short architectural verdict (max 10 words). The prompt includes full board context — active synergies, anti-synergies, budget usage, unplayed cards, missed synergies, feasibility analysis, and board-to-winning-combo overlap — so the feedback is specific and educational without being too prescriptive.
AI grading flow: AI grader reads solution.json directly (contains full game state + behavioral telemetry) and evaluates against RUBRIC.md.
| Campaign | Encounters | Deck |
|---|---|---|
| System Design Journey | startup-launch → going-viral → production-scale | system-architecture |
| ML Platform Journey | first-model → model-in-production → platform-at-scale | ml-infrastructure |
| Deck | Cards | Icons |
|---|---|---|
| system-design-patterns | 20 cards (databases, caches, load balancers, CDNs, etc.) | Generated |
| ml-infrastructure | 12 ML-specific infrastructure cards | Generated |
nvm use 20 # Node.js >=20 required (Vite)
npm install
npm run start:dev # Vite dev server (port 3000) + API server (port 3031)npm run build # Output → dist/GitHub Actions workflow creates a release on push to main.
- Copy
examples/system-design-basics/as a starting point - Create or reuse card deck, scenario, and campaign JSON files
- Edit
initial_state.json— setcampaignId(orscenarioId+deckIdfor standalone) - Adjust
RUBRIC.mdfor your grading criteria
MIT
Currently POST /api/generate-icons writes PNG files to client/public/data/icons/<deckId>/
immediately on generation. This means icon files accumulate locally even when the deck is never
exported.
Planned improvement: decouple generation from disk writes:
POST /api/generate-iconsreturns{ iconData: "data:image/png;base64,..." }— no disk write. The client stores the data URL incard.icon; the authoring preview displays it normally.- New
POST /api/save-iconsendpoint accepts{ deckId, icons: [{ cardId, iconData }] }, writes PNGs to disk, and returns[{ cardId, iconPath }]. handleExportAllcallssave-iconsfirst, swaps data URLs → file paths in the deck JSON, then downloads — so exported JSON always references real paths, not embedded base64.