Back to Build Kit

Game Configuration

GAME-CONFIG.md • Config + Reporting Contract

MANDATORY Contract

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 scaling map 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:

  1. Pretty-printed so I can read it
  2. 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 a result.

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

Example A — Quiz-style game
{
  "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
  }
}
Example B — WASD / Mission game
{
  "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

meta
engine, mode (levels | missions | endless)
game
id, name, subject, category
progression
levelCount, unitsPerLevel, passThreshold, lives
difficultyCurve
startTier, maxTier, rampEvery, scaling
adaptive
(optional) enabled, window, up/down thresholds
mastery
stars[], replayable
scoring
base, streakBonus, timeBonus, hintPenalty
help
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

Quiz path

questions array with questionText, chosenAnswer, correctAnswer, isCorrect, timeTakenMs, etc.

Mission path

questions array with challenge, outcome, attempts, timeMs.

Always wrap with: bridge.result({ user, game, summary, questions, completedAt })
Progress: bridge.progress({ questionIndex, totalQuestions, score, streak })

6. What lib/ shares

Shared: 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

Convert the attached HTML game into a Svelte 5 game that obeys SKILL.md, PARENT-BRIDGE.md, RESPONSIVE-UX.md, and GAME-CONFIG.md.

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.
Integrated from src/data/config.md + src/data/gameconfig.md