# StuCent Mini-Game Author Guide

Paste this whole document into your AI (Claude, ChatGPT, Gemini, or any other), then ask it
to build a game. When it is done, submit the three files at the upload page.

You bring your own AI. StuCent stores, serves, and teaches with the result.

**Every game MUST:** read `game.config.maxPoints` and scale its score to it, call
`game.end({ score, maxScore })` **exactly once**, use **no external URLs**, and be playable
with a finger on a tablet.

You will produce **three separate files**:

| File | Contains | Must NOT contain |
|---|---|---|
| `game.html` | Markup only | `<style>` or `<script>` tags |
| `game.css` | All styling | — |
| `game.js` | All logic | — |

---

## 1. The runtime contract

StuCent serves your game in a sandboxed iframe with two globals already injected.
**Do not redefine them.**

- **`root`** — the DOM accessor. It is `document`. Use `root.getElementById(...)` /
  `root.querySelector(...)`.
- **`game`** — `{ config, ready(), start(), stop(), end() }`.

Get these four right or scoring breaks:

1. **Scale the score to `game.config.maxPoints`.** The platform delivers the activity's
   `maxPoints` on `game.config` (default 100 if absent). Design your point economy so that
   completing the DESIGNED levels totals `maxScore`, then scale:

       const maxScore = (game.config && game.config.maxPoints) || 100;
       const score = Math.round(points * maxScore / DESIGNED_TOTAL);

   The server clamps the **room** score (mastery/stars) to `maxPoints`, but keeps the raw
   run score for the **leaderboard** (sanity-capped at 10x `maxPoints`) — so an endless
   game reporting past `maxScore` is correct and lets players compete beyond 100%. A game
   that reports out of a hardcoded 100 into a 50-point activity is still wrong (halved).

2. **Call `game.end({ score, maxScore })` exactly once.** Guard with a boolean flag — a
   double call double-reports. Nothing is recorded until it fires. You may also pass
   `timeTaken` (milliseconds) and `success`.

3. **Stars shown in-game are cosmetic.** The server recomputes stars from score% vs the
   activity thresholds (default 50 / 70 / 90). Design so a competent player at the target
   age reaches ~70-90%, a struggling player still clears 50%.

4. **Timers and loops use `requestAnimationFrame` + wall-clock deltas — never
   `setInterval`.** In a backgrounded tab `setInterval` throttles to ~1fps, so an interval
   countdown drifts. Track time with `Date.now()` deltas inside the rAF loop.

## 2. Security and sandbox (hard rules)

- **No external URLs** — no CDNs, web fonts, remote images, or API calls. The sandbox is
  `allow-scripts` on an opaque origin. Anything remote silently fails.
- **Inline every asset** — emoji, CSS shapes, inline `<svg>`, or `data:` URIs. There is no
  file upload, so `./assets/...` paths will NOT resolve.
- **No** `localStorage` / `sessionStorage`, `<form>`, `eval`, `new Function`, or
  string-argument timers.
- Font stack: `system-ui, -apple-system, sans-serif`.

## 3. Touch-first and responsive (hard rules)

- Minimum touch target 44px (48px+ for ages 5-10) — as an ABSOLUTE floor, not a percentage.
  Percentage-sized targets shrink below the floor on phones. If the visual element must be
  small (grid dots), resolve input by NEAREST cell on the container so the effective target
  is the full cell.
- Gameplay inputs use **pointer** events (`pointerdown`) for latency. Chrome buttons
  (Start, Play Again, mute) use `click` so keyboard and switch-access users can operate
  them. No hover-required, drag-only, right-click, or keyboard-only controls.
- Size with `vw` / `vh` / `%` / `rem`, not fixed px. Fit any orientation, and give every
  full-viewport screen `overflow-y: auto` — a How-to screen taller than a landscape phone
  with `overflow: hidden` locks the player out of the game entirely.
- Canvas games: the canvas SHRINKS on phones (often to ~0.45 scale). Draw teaching and
  problem text at >=24px internal size. Check the rendered scale at 375px wide AND in
  landscape — if DOM controls (keypads) crowd the canvas below ~0.4 scale, switch to a
  side-by-side landscape layout with a media query.
- `touch-action: none` on the play area to stop scroll-jank.

## 4. Judging and state integrity (hard rules)

1. **Judge actions against the state at ACT time, not resolution time.** Capture the aim
   when the shot fires, the target when typing starts. Anything judged later against
   `state.current*` is both exploitable (adjust after acting) and unfair (punished for a
   change the player did not make).
2. **Changing the question invalidates the old question's artifacts.** Sweep or re-check
   in-flight items (falling notes, queued cards) when the problem changes — a stale
   distractor can EQUAL the new answer, punishing a correct response.
3. **The picture must not leak the answer.** No target drawn on the answer ray, no
   colour-coding by the value that decides matches, no generation tell (correct and
   distractor items must draw from overlapping ranges/denominators). Ask: can a player who
   ignores the maths succeed by looks alone? Then the mechanic is not teaching.
4. **Feedback must be SEEN to teach.** Verify the geometry at every possible answer. A
   reveal, ray, or explanation drawn off-canvas (for example an off-centre origin whose far
   angles leave the picture) is silence. State-based tests will not catch this — check the
   pixels where the feedback should appear, across the full answer range.
5. **Deferred actions need locks and a drain.** If merges or removals resolve on a delay,
   block input until they have actually run (a counter, not just a timer), and drain the
   pending queue inside `finish()` — points earned in the final moment must count, and a
   pending removal must not trigger a false game-over.

## 5. Difficulty and score depth (hard rules)

A leaderboard only works if scores SPREAD. A game that ends the moment the core objective
is met (for example "catch 12 and stop") lets every decent player tie at max score, and the
leaderboard collapses to the time tiebreak. Design for depth:

1. **Difficulty must progress.** Structure play as levels or waves: every N successes
   advance the level. Advance the **content** (a new target, sneakier near-miss distractors,
   more simultaneous items, deeper variants of the skill) — not only the physics.
2. **Cap the physical ramps.** Speed and spawn rate may rise for the first levels but MUST
   cap at a ceiling that stays humanly playable for the target age (younger = lower
   ceiling). Past the cap, ramp cognitive difficulty instead. A game that becomes
   physically impossible teaches nothing and reads as unfair.
3. **Front-load the points; make 100% expensive.** Weight scoring so completing the core
   objective (level 1) lands ~50% (first star), solid multi-level play lands 70-90%, and
   only sustained excellence at the hardest levels reaches 100%.
   Example weights for 5 levels of 10 catches: 5 / 2 / 1.5 / 1 / 0.5 points per catch
   (= 50 / 20 / 15 / 10 / 5 per level, 100 total).
4. **Do not stop at 100% — go endless.** Beating the final designed level must NOT end the
   game: enter an endless tail (cycle or permute the hardest content, keep the physics at
   the cap, keep tightening the cognitive difficulty) and keep adding points past
   `maxScore`. Report the raw total in `game.end` — the room score clamps at `maxPoints`
   server-side, but the leaderboard keeps the raw run score, so players who already have
   100% and 3 stars still compete for the top spot. The endless tail ends via lives or the
   time cap.
5. **Always bound the session.** A lose condition (lives, board locked up, base breached —
   whatever fits the genre) AND a hard time cap APPLIED PER TARGET AGE (ages 5-7 approx
   120s, ages 8-10 approx 150s, older approx 180s — do not default everyone to the
   ceiling). Every path (endless tail, lose condition, time up) must reach the single
   `finish()`.
6. **Announce level changes in-game** (a banner: "Level 2! Now catch = 1/3") — a difficulty
   shift the player does not notice reads as a bug, especially when the target changes.

## 6. Game feel — "juice" (hard rules)

A silent, static game reads as broken. Minimums for every game:

1. **Feedback within ~100ms of every player action.** Success → particle burst plus a scale
   "pop" on the actor. Damage or mistake → brief screen shake plus flash. Level-up →
   celebratory burst. All canvas or CSS drawn — no assets needed. Respect
   `prefers-reduced-motion` (skip shake and particles, keep colour and text cues).

2. **Sound, synthesised with the WebAudio API** — audio FILES cannot load in the sandbox,
   but oscillators work. Create the AudioContext inside the first `pointerdown` (autoplay
   policy) and include a mute toggle (>=44px). Recipe:

       var actx = null, muted = false;
       function beep(freq, ms, type, vol, delaySec) {
         if (muted || !actx) return;
         var t = actx.currentTime + (delaySec || 0);
         var o = actx.createOscillator(), g = actx.createGain();
         o.type = type || "sine"; o.frequency.value = freq;
         g.gain.setValueAtTime(vol || 0.15, t);
         g.gain.exponentialRampToValueAtTime(0.001, t + ms / 1000);
         o.connect(g); g.connect(actx.destination);
         o.start(t); o.stop(t + ms / 1000);
       }
       // correct:  beep(660,90); beep(880,120,"sine",0.15,0.07) — raise pitch with the streak
       // wrong:    beep(150,200,"square",0.08)
       // level-up: 523/659/784 arpeggio, 90ms apart (use the delaySec argument, not timers)

3. **Animate numbers.** Score counts up (ease toward the real value each frame). Floating
   "+N" text at the point of the action. A visible combo indicator when a streak is active.

## 7. Learning mechanics (hard rules)

1. **Never punish silently — and reading is FREE.** On a wrong answer, show a 2.5-3.5s
   micro-explanation of WHY (an 8-10-year-old reads about 2 words per second — 1.5s is too
   short), and FREEZE or heavily slow the action (including spawning and anything that can
   be missed) while it shows. A hint the player is punished for reading teaches them to
   ignore hints. The explanation must use correct mathematical language ("the difference is
   always 3", not "differences go up by 3") and must only reference things still visible —
   redraw the evidence in the overlay if the original was removed.
2. **Combos reward fluency — in the endless tail.** A visible streak multiplier is great,
   but applied to the DESIGNED levels it breaks the front-loaded budget (flawless play
   reaches "100%" and 3 stars two levels early). Apply multipliers only past the designed
   total, or include the combo headroom in the budget maths.
3. **Adaptive rubber-banding.** Track rolling accuracy (last ~10 items): under ~40%, ease
   one notch within the level; over ~90%, tighten one notch. Never drop below the level's
   floor or exceed the physics cap. Key the easing to signals the player cannot fake cheaply
   (lost lives or breaches), not to wrong answers — otherwise deliberate wrongs buy an
   easier game.
4. **Help must actually help.** Hints and rubber-band assists must never recommend dead-end
   moves (a merge that creates an unusable tile, a pair that cannot join). If a "buy
   information" mechanic exists, the information must be genuinely needed — when the visible
   data already fully determines the answer, make peeks free scaffolding that costs the
   combo streak instead of points.
5. **Content pool >= 5x one run's needs**, drawn randomly each run — replays must differ.
6. **Match the cognitive act to the skill — prefer production over recognition.** Catching
   or tapping the right thing only trains recognition. Typing an answer, placing a point, or
   constructing a value trains more. Ask of every mechanic: what is the player's head DOING
   per interaction, and can they succeed without doing it? If a dial, a colour, or a picture
   can substitute for the maths, the game is not teaching (see section 4, rule 3).
7. **Ramp cognition, not just speed.** The best level changes alter WHAT the player must
   think (a new representation, an inverse task, missing-number form), not only how fast.
   Keep motor and timing load small enough that reflexes never dominate the mastery score —
   stars should certify knowledge, not thumbs.
8. **The hint's method must be the method the game rewards.** Do not preach the written
   column method while a timer forbids using it — give the scaffold (column layout, scratch
   space, a pause) or teach the mental strategy instead.
9. Optional: **learning-gated power-ups** (slow-mo, extra heart) earned by answering a bonus
   item — power comes from knowledge, not luck.

## 8. Required screens

How-to-Play (shown first, big Start button, age-appropriate; mention that levels get harder
and explain the point weighting — early levels earn the most, endless adds on top)
→ 3-2-1 countdown
→ Game (uniform HUD: score top-left, lives top-right, timer top-centre; current target and
level progress visible in the play area)
→ Win/Lose (stars, score, an **encouraging** message, and Play Again; never shaming).

## 9. JS skeleton (correct contract — adapt per genre)

    // === CONFIG / STATE ===
    var maxScore = (game.config && game.config.maxPoints) || 100; // scale target
    var CONFIG = { lives: 3, duration: 60 };                      // duration seconds, 0 = untimed
    var state = { correct: 0, total: 0, lives: CONFIG.lives, running: false, startMs: 0 };
    var ended = false;                                            // game.end() guard

    function screen(id) {
      root.querySelectorAll('.screen').forEach(function (s) { s.classList.remove('active'); });
      root.getElementById('screen-' + id).classList.add('active');
    }

    function begin() {                 // called by the Start button (pointerdown)
      screen('countdown');
      var n = 3, el = root.getElementById('countdown-number');
      el.textContent = n;
      var t0 = Date.now();
      (function tick() {               // rAF countdown, not setInterval
        var left = 3 - Math.floor((Date.now() - t0) / 1000);
        if (left <= 0) { play(); return; }
        el.textContent = left;
        requestAnimationFrame(tick);
      })();
    }

    function play() {
      screen('game'); state.running = true; state.startMs = Date.now();
      loop();
    }

    function loop() {                  // single rAF loop drives timer + render
      if (!state.running) return;
      if (CONFIG.duration > 0) {
        var remain = CONFIG.duration - Math.floor((Date.now() - state.startMs) / 1000);
        root.getElementById('hud-timer').textContent = Math.max(0, remain) + 's';
        if (remain <= 0) { return finish(); }
      }
      update(); render();
      requestAnimationFrame(loop);
    }

    function answer(isCorrect) {       // call from your input handlers
      state.total++;
      if (isCorrect) { state.correct++; }
      else if (--state.lives <= 0) { return finish(); }
      root.getElementById('hud-score').textContent = 'Score: ' + state.correct;
      root.getElementById('hud-lives').textContent = '❤'.repeat(Math.max(0, state.lives));
    }

    function finish() {
      if (ended) return; ended = true; state.running = false;
      var fraction = state.total ? state.correct / state.total : 0; // 0..1 performance
      var score = Math.round(fraction * maxScore);
      screen('result');
      root.getElementById('result-score').textContent = 'Score: ' + score + ' / ' + maxScore;
      game.end({ score: score, maxScore: maxScore, timeTaken: Date.now() - state.startMs });
    }

    // update() / render() hold your genre logic. Wire Start + Play-Again with pointerdown.
    root.getElementById('btn-start').addEventListener('pointerdown', begin);

## 10. Self-check before you submit

Run these over your generated source:

    grep -c "game.end(" game.js                                       # exactly 1
    grep -nE "https?://" game.html game.css game.js | grep -v w3.org  # must be empty
    grep -nE "setInterval|localStorage|new Function|eval\(" game.js   # must be empty
    grep -nE "game\.config" game.js                                   # score is scaled (>=1)
    grep -nE "mousedown|mousemove|mouseup" game.js                    # prefer pointer events

The upload page runs these same checks in your browser and will not let you submit until
they pass.

Then confirm by reading: How-to-Play first, 3-2-1 countdown, HUD present, win AND lose both
call `finish()`, all targets >=44px, content randomised, the game is actually winnable, and
difficulty progresses with capped physical ramps — full score requires sustained play at the
hardest level, not a fixed early checklist. Also confirm the juice and learning minimums:
WebAudio sounds with a mute toggle, particles and shake (gated on `prefers-reduced-motion`,
including CSS keyframe animations), a combo indicator, and a frozen-action micro-explanation
on every wrong answer.

**Viewport check (REQUIRED — CSS arithmetic is not a substitute for rendering):** play the
game at 375x667, 667x375 (landscape!), and 360x740. At every size: the Start button must be
reachable (scrolling counts), no horizontal overflow, every effective touch target >=44px,
and canvas text must stay readable (render scale >= ~0.4). Landscape is where full-viewport
screens and canvas/keypad layouts break first.

## 11. Genres

Pick the genre by the COGNITIVE VERB you want to train — "catch the right thing" trains
recognition, not procedure. Entries marked "Sample" link to a working, playable exemplar
that follows every rule in this guide — study it before authoring in that genre.

**Fluency and recall** (automaticity under time pressure):

- **collector** — items fall; catch the correct ones. On-screen left/right buttons or drag.
  [Sample: Fraction Catcher](https://stucent.s3.ap-southeast-1.amazonaws.com/assets/games/c7eeb5a8-7213-44c9-8962-0884c6f5c217/v5/index.html) — equivalent fractions, 5 levels plus endless.
- **rhythm** — answers fall in lanes on a beat (WebAudio metronome); tap the lane when the
  correct answer crosses the hit line; on-beat = full points; tempo ramps but CAPS.
  [Sample: Beat Tables](https://stucent.s3.ap-southeast-1.amazonaws.com/assets/games/ae48641b-0e8a-451d-aed9-a0e92fade79b/v2/index.html) — times tables, missing-number finale.
- **quiz-action** — timed questions; full-width answer buttons; shrinking timer bar.
- **tower-defense** — enemies march on the base carrying problems; the correct answer is the
  ammunition; waves escalate content and simultaneous enemies (speed caps). Scaffold the
  taught method in the input itself. The best endless-leaderboard genre — waves continue
  forever.
  [Sample: Number Fortress](https://stucent.s3.ap-southeast-1.amazonaws.com/assets/games/16070794-bf07-46c8-8ae1-a9e7be26ccea/v3/index.html) — column addition and subtraction.

**Concepts and relationships** (seeing structure):

- **merge** — 2048-style: tiles combine under a CONTENT rule (equal values join and add;
  complements make a whole). The merge rule IS the concept. Swipe plus arrow buttons.
  [Sample: Make One](https://stucent.s3.ap-southeast-1.amazonaws.com/assets/games/e2c7abbb-7ff4-4044-8c99-501c948ab42f/v2/index.html) — fraction equivalence and addition.
- **sorting** — drag or tap items into labelled bins (classification).
- **matching** — tap first then second to connect pairs.
- **card** — memory match, flip, or sort. Tap to flip.

**Procedures and sequencing** (steps in order):

- **builder** — assemble in order; snap-to-slot; highlight the next slot.
- **platformer** — tap-to-jump, auto-run; answer-gated checkpoints.
- **racing** — correct answers advance a progress track.

**Spatial reasoning**:

- **trajectory** — swing a CENTRED cannon (all 0-180 degrees must stay on screen) and fire
  at a HIDDEN target; each level changes the cognition — read the scale, calculate (180
  degree line), reproduce a drawn angle, estimate with no marks. Show the degree readout
  ONLY when the task is the calculation; a readout during estimation rounds turns them into
  dial-matching.
  [Sample: Angle Cannon](https://stucent.s3.ap-southeast-1.amazonaws.com/assets/games/779419eb-a09b-4356-859a-5b45b8d92ca2/v3/index.html) — angles on a straight line.
- **tracing** — finger-place or draw points and paths against a spatial rule (reflect across
  a mirror line, trace the construction); grid dots make scoring objective; resolve input by
  nearest cell or line. Include the INVERSE task too (find the mirror line of a complete
  figure), not only production from a given rule.
  [Sample: Mirror Painter](https://stucent.s3.ap-southeast-1.amazonaws.com/assets/games/5066fe6a-5f2c-4adf-81fc-033cedb53765/v3/index.html) — paint reflections AND find hidden, off-centre mirror lines.
- **puzzle** — arrange or snap pieces. Pointer drag plus snap, or tap-to-select then
  tap-to-place.

**Scientific method** (hypothesis testing):

- **guess-the-rule** — a hidden rule generates examples; the player gathers evidence and
  commits to a prediction the rule must confirm. Make the evidence SHAPE vary so gathering
  is a real decision: adjacent terms, terms k apart (divide the jump), or a single clue where
  a free peek is genuinely necessary. If the visible data always determines the rule, peeks
  are a trap, not experimentation.
  [Sample: Rule Detective](https://stucent.s3.ap-southeast-1.amazonaws.com/assets/games/7f48c096-0198-44e0-b865-0b59738f74c5/v3/index.html) — nth term from adjacent, gap, and sparse evidence.
- **memory** — show a pattern, hide it, repeat; a growing sequence.

## 12. Never

External URLs · mouse-only, hover-required, or drag-only controls · sub-44px targets ·
fixed-px layouts · shaming lose messages · skipping How-to-Play · reporting points the
player did not earn · calling `game.end()` more than once · `setInterval` loops · gambling
mechanics · unwinnable games · flat difficulty from start to finish · ending the game
because the player hit 100% (go endless instead) · uncapped speed ramps that become
physically impossible · completely silent games · wrong answers with no explanation.
