Config-Driven Games
One Contract, Any Gameplay
Every Smartboard WOW game reads customizable settings from config_json and communicates with the HueGames parent through hue-game-bridge.
The gameplay can be anything — the contract is what is shared.
Final Deliverable Instructions
As a final deliverable, also generate the catalog config_json for this game (the JSON I paste into the Supabase config_json column). It MUST follow GAME-CONFIG.md and its params keys must match EXACTLY what this game reads — do not invent fields the game ignores, and do not omit any knob the game needs.
Include and tune these for a deep, long session (not 30 seconds):
- progression: levelCount (aim high — 15–30), unitsPerLevel, passThreshold.
- difficultyCurve: startTier..maxTier, rampEvery, and a
scalingmap that ramps the real difficulty knobs across tiers. - randomness: make sure question/level generation is randomized each play from the params (seeded per level is fine) so no two runs are identical.
- adaptive (if the game supports it), mastery.stars, scoring, help.howToPlay.
Output it TWO ways:
- Pretty-printed so I can read it
- Minified on one line for pasting
Then give me a one-line note per params field explaining what it controls and its safe range.
0. The Philosophy
There is NO single shared engine. A WASD dungeon and a 3D molecule builder share nothing in how they play.
What every game DOES share is a thin contract:
- Config contract — the game reads everything customizable from
config_json. - Bridge contract — the game does start / pause / resume / stop, reports
progress, and reports aresult.
The one rule: would you want to change this per game without touching code? Yes → config. No → leave it in the code.
1. How the game receives its config
Game side — request on load, listen for the answer:
let CONFIG = { ...DEFAULT_CONFIG }, booted = false;
function applyConfig(incoming) {
CONFIG = deepMerge(DEFAULT_CONFIG, incoming || {});
booted = true;
applyConfigChrome();
resetGame();
}
addEventListener("message", e => {
if (e?.data?.type === "hue:config") applyConfig(e.data.config);
});
if (window.parent) {
window.parent.postMessage({ type: "hue:config-request" }, "*");
}
setTimeout(() => { if (!booted) applyConfig(null); }, 1200);Parent side — answers the request:
addEventListener("message", e => {
if (e?.data?.type !== "hue:config-request") return;
const row = getRowForFrame(e.source);
if (!row?.config_json) return;
const cfg = typeof row.config_json === "string"
? JSON.parse(row.config_json) : row.config_json;
e.source.postMessage({ type: "hue:config", config: cfg }, "*");
});2. The config shape = CORE + params
{
"schemaVersion": 1,
"meta": { "engine": "skip-sequence", "mode": "levels" },
"game": { "id": "skip-count", "name": "Skip Counting Rocket Launch", "subject": "math" },
"progression": { "levelCount": 8, "unitsPerLevel": 4, "passThreshold": 0.6 },
"difficultyCurve": { "startTier": 1, "maxTier": 5, "rampEvery": 2, "scaling": { ... } },
"mastery": { "stars": [ { "star":1, "minAccuracy":60 }, ... ] },
"scoring": { "base": 100, "streakBonus": 25 },
"help": { "voice": true, "howToPlay": "Fill the blanks to launch!" },
"params": {
"steps": [2, 5, 10],
"sequenceLength": 5,
"gapsPerSequence": 2,
"distractors": 2
}
}{
"schemaVersion": 1,
"meta": { "engine": "fraction-dungeon", "mode": "missions" },
"game": { "id": "frac-dungeon", "name": "Fraction Dungeon", "subject": "math" },
"progression": { "levelCount": 10, "unitsPerLevel": 3, "passThreshold": 0.6 },
"difficultyCurve": { "startTier": 1, "maxTier": 5, "scaling": { "enemyCount": { "1": 1, "5": 6 } } },
"params": {
"control": "wasd",
"gridSize": [12, 9],
"moveSpeed": 4.5,
"fog": true
}
}3. CORE field reference
engine, mode (levels | missions | endless)
id, name, subject, category
levelCount, unitsPerLevel, passThreshold, lives
startTier, maxTier, rampEvery, scaling
(optional) enabled, window, up/down thresholds
stars[], replayable
base, streakBonus, timeBonus, hintPenalty
voice, howToPlay, examples[]
4. params — the escape hatch
Free-form object the game defines. Put anything you'd tune per catalog row here:
- • generators / question rules
- • world settings (gridSize, moveSpeed, gravity)
- • content banks or contentUrl
- • toggles (fog, hints, timerVisible)
5. Reporting the result
questions array with questionText, chosenAnswer, correctAnswer, isCorrect, timeTakenMs, etc.
questions array with challenge, outcome, attempts, timeMs.
bridge.result({ user, game, summary, questions, completedAt })Progress:
bridge.progress({ questionIndex, totalQuestions, score, streak })6. What lib/ shares
bridge.ts, config.ts, scoring.ts, GameShell, Celebration, SoundManager, help panel, theme.Custom per game: the actual gameplay, movement, win conditions, and wow moment.
7. Definition of Done (config gate)
- ✓ Everything customizable lives in config (CORE + params)
- ✓ Config resolves via postMessage handshake + default fallback
- ✓ difficultyCurve ramps correctly
- ✓ Stars/mastery + replayable work
- ✓ Bridge ready/progress/result wired with correct field names
- ✓ PAUSE freezes timers AND render loops
- ✓ Different config rows produce different play without code changes
8. Paste-ready prompt
Apply the CONFIG CONTRACT: read everything tunable from config_json (CORE + params). Use the postMessage handshake. Wire hue-game-bridge correctly. Keep the shared lib/ as the contract layer; keep gameplay custom.