Skip to content

jengine-chase

A maze chase: four pursuers that each pick a different tile to aim at, scatter and chase waves, and the cornering that makes a maze feel like one.

KindProject package: linked into the builds of the projects that list it
Sourcepackages/jengine-chase
Version0.1.0
Entry pointjengine_chase::ChasePackage
Script namespaceChase
Listed byprojects/arcade-classics

List it in the top-level "packages" array of project.jengine:

{
"packages": [
{ "name": "jengine-chase", "source": { "type": "path", "path": "../../packages/jengine-chase" } }
]
}

Then rebuild the editor viewport for the project (jengine viewport-build projects/<name>, or the editor’s Rebuild viewport banner). Native and web builds link it on their own.

A scene that uses its components must be a v2 scene ("version": 2): a v1 scene has no field for a package component and keeps it as inert custom data.

A script can check that the package is linked with if Chase == nil then return end.

Scene keyRust type
chase_gameChaseGamepackage only: needs a v2 scene

Commands run in every runtime and return nothing; a package publishes what scripts read onto components (see the notes below). The flat name also works.

CallFlat name
Chase.new_game(e)chase_new_game
Chase.level(e, n)chase_level
Chase.quirk(e, on)chase_quirk
Chase.input(e, dx, dy)chase_input
  • projects/arcade-classics/assets/scenes/amber.scene

From packages/jengine-chase/README.md.

A maze chase: four pursuers that each aim at a different tile, a clock that turns them all round, and a tunnel they crawl through.

Used by projects/arcade-classics (Amber Maze).

ModuleWhat it is
boardThe maze — walls, food, the tunnel that joins to itself, and the one door only a chaser may use.
rulesThe game. Four target tiles and one movement rule, as pure functions.
chaseOne tick of it — lanes, the wave clock, the pen, the pill.
contentMazes, chasers and levels as data tables. A maze is an ASCII drawing.

A maze chase looks like the simplest game in an arcade and is the only one whose enemies are a design. Four chasers that all ran at you would be one chaser drawn four times, and the game would be either trivial or impossible depending only on how fast you made them. What makes it playable is that each one wants a different tile:

RuleAims atWhat a player feels
shadowyour tilethe one you can read, so you learn the rules from it
speedyfour tiles ahead of youcuts you off, so running at it works
bashfulthe line from shadow through two ahead of you, doubledpincers with the red one, and neither of them planned it
pokeyyou, until it is within shy tiles — then its own cornerloses its nerve exactly as it arrives

None of them can see round a corner. None knows where the others are, except that bashful reads shadow’s tile — which is why eating the red one changes how the cyan one behaves, a thing the arcade never told anybody.

There is no pathfinding in this package. The whole movement rule is:

// At the centre of every tile, of the ways out that are not straight back
// the way you came, take the one whose next tile is nearest the target.
let goal = rules::target(rule, sight, def.shy, quirk);
let dir = rules::choose(&board, tile, facing, goal, Pass::Chaser);

Ties go up, then left, then down, then right. That fixed order is not a detail — it is why the maze is learnable, and why two chasers with the same target do the same thing instead of drifting apart.

Because targeting is a pure function of a few tiles, tests/rules.rs holds all four rules to their arithmetic rather than to a screenshot.

The up-overflow bug is reproduced on purpose

Section titled “The up-overflow bug is reproduced on purpose”

The original’s “step a tile forward” routine added to the column as well as the row when the direction was up, so four ahead of an upward-facing player is four up and four left. It was never fixed. Every player who ever learned the maze learned it with the bug in place, and the safe spots in the corners exist because of it.

rules::ahead takes a quirk flag, defaulting on. Turning it off makes a harder and measurably worse game — the corners stop working and there is nowhere to stand — which is worth being able to demonstrate, so Amber Maze puts it on the menu.

  • The tunnel. A row open at both ends joins to itself. A chaser crosses it at 40% speed while you cross at 80%, so it is the only guaranteed escape in the game. Take the speed difference away and the tunnel is just a corridor that wraps.
  • The gate. The one cell a chaser may cross and you may not. Without it the pen is either a prison or a doorway for everyone.
  • The red zones (^). Open ground a chaser may not turn up out of. Four of them make the ground around the pen survivable. The restriction is on a chaser loose in the maze only — one leaving or coming home has to go up through its own gate, and a red zone on that route would strand it for the rest of the game.

When the clock changes the mood, and when a pill goes off, every loose chaser turns round where it stands. rules::choose cannot return the way back at all; the about-face lives in the sim and overrides it.

That distinction is the whole thing. A merely permitted reversal would be ignored whenever forward happened to be the nearer way, so about half the turns a player is listening for would simply not happen — and nothing would look broken.

Straight from the arcade’s own table, including the two that look wrong:

  • hero_food is lower than hero. Eating costs you a fraction of a tile per dot, so a full corridor is slower than an empty one and a chaser closes on you exactly while you are doing what you came to do. Remove it and the last dots of a level stop being tense.
  • tunnel is lower than everything. See above. Content::check refuses a level where it is not, because a game whose tunnel is not an escape has quietly lost its only safety valve and nothing will say so.

The leader also speeds up as the maze empties (cruise1 / cruise2, the arcade’s “cruise elroy”): the chaser a player thinks of as the aggressive one is not aggressive at the start of a level, it becomes so. Most players have never noticed this and all of them play around it.

chase_game on one entity, in a "version": 2 scene:

"chase_game": {
"tables": { "mazes": "data/chase_mazes", "chasers": "data/chase_chasers",
"levels": "data/chase_levels" },
"seed": 7, "quirk": true, "extra_life": 10000
}
CallDoes
Chase.new_game(e)back to level one
Chase.input(e, dx, dy)one frame of intent; dy is positive up
Chase.level(e, n)jump to a level
Chase.quirk(e, on)the up-overflow bug, on or off

Scripts read ChaseState, ChaseActors, ChaseMaze (republished only when the food changes) and ChaseEvents. ChaseActors carries each chaser’s target tile, which is what lets a game draw the four targets in a debug view — and ten seconds of looking at that view explains the entire game to anybody, which no amount of prose does.

Checking a table is worth as much as checking a maze

Section titled “Checking a table is worth as much as checking a maze”

Content::check is public and tested (tests/tables.rs), unlike its equivalents in the sibling packages. A guard nobody has ever seen fire is not a guard; it reads as coverage. Each rule has a table that breaks exactly it, because every one of them catches something that would otherwise show up as a maze that plays oddly rather than one that fails to load:

  • a maze with no power pill — the chasers can never be turned, so the game has no offence in it at all, and it looks completely normal;
  • a level whose eyes are slower than a chaser — eating one becomes a punishment, inverting the point of the mechanic with a two-character number;
  • a chaser that waits for nothing yet is drawn inside the pen — it can never cross its own gate outward and spends the game bouncing off the inside of a wall. The only symptom is a game that feels slightly easy.

That last class is the reason the checks exist. The same shape of bug got through anyway once: release = 0.0 was meant to mean “no timer, use the dot count” and the pen read it as “leave now”, so three of the four walked out together on frame one and then moved in lockstep for the whole game. Nothing errored. It was found by reading a log in which three chasers shared a tile on every line, and it is pinned by a_chaser_waiting_on_dots_stays_in_until_the_dots_are_eaten.

cargo test -p jengine-chase — 72 across four files.

FileCovers
rules.rsthe four target rules, the quirk, the movement rule, tie-breaks, no-reversal, red zones, fleeing
board.rsthe ASCII legend, the tunnel, the gate, reachability, the two y conventions
game.rslanes and decisions-at-centres, the turn buffer, food, pills and chains, the pen, the wave clock, losing and winning
tables.rsevery message Content::check can produce