Tutorial 9 · Rules — Times, Targets & Scores

This page explains the game's bookkeeper — the part that keeps time, score and goals. It answers the two questions every game asks: "how am I doing?" and "have I won yet?". The Manor below runs all of it — 90 seconds on the clock, 100 points a trinket, 25 × every second left:

The whole configuration

const rules = new Rules({
  timer: {
    seconds: 90,
    perRoom: false,     // true = the Manic Miner air supply, reset every room;
                        // false = one Bruce Lee global clock
    onExpire: 'die',    // 'die' costs a life and the clock re-arms;
                        // 'end' is game over, rules.gameOver goes true
  },
  scoring: {            // the points table, paid on rules.event(name)
    collect: 100,       // an item's own `points` overrides (the golden trinket)
    kill: 250,
    roomVisit: 50,      // first entry into each room (the start room is free)
    targetComplete: 500,
    secondLeft: 25,     // × seconds remaining, paid once on completion
  },
  targets: [            // the win conditions — ALL must be met
    { type: 'collect-all' },                       // every item in the world
    { type: 'collect', count: 3, tag: 'key' },     // n items, optionally by tag
    { type: 'reach', room: 'the-vault' },          // set foot in a room
    { type: 'visit', rooms: ['attic', 'cellar'] }, // set foot in all of them
    { type: 'survive', seconds: 60 },              // stay alive this long
  ],
});

Everything is optional. Omit targets and you get the classic default — collect everything. Omit timer and time is still counted (for the HUD and time bonuses); it just never runs out.

Wiring: one bind, one tick

rules.bind(world, player);   // after world.attach(player, spawn)
// then, once per update:
rules.tick();

bind subscribes to the World's events, so room visits and collects score themselves — each item's points and tag flow through. With the player bound, a non-fatal countdown expiry hurts them as { type: 'time' } — which routes through the normal contact event, so yes, a shield can cancel time itself. Anything the World doesn't know about, you report yourself:

ninjaFelled && rules.event('kill');            // scores from the table
rules.event('kill', { points: 400 });          // or override per-event

Targets and gates: the lock and the key

Give a target an id, and a World gate can demand it:

targets: [{ id: 'six-trinkets', type: 'collect', count: 6 }]
// in a room definition:
{ type: 'gate', x, y, w, h, goal: 'six-trinkets' }

bind hands the World a goalCheck hook, and the gate stays shut — solid tiles, remember — until targetDone('six-trinkets') says otherwise. That's the Manor's vault: the portcullis over the loft ladder is a rule, rendered. A gate naming an unknown id never opens, loudly visible in play — a typo fails on the first playtest, not in production.

Reading the state

For the HUD: rules.score, rules.timeLeft (seconds, or null with no timer), rules.elapsed, rules.complete, rules.gameOver, and each target's progress/done. For the moments, events (with matching single-listener onX props): 'score', 'event', 'target-complete', 'complete' (all targets met — you won), 'expire', 'game-over'.

What you've learned

Times, targets and scores are declared, not programmed — a Rules object is the game's win contract in JSON shape, and gates make objectives physical. This is the last module: you've now read the whole engine.