Skip to content

arcadekit glossary ​

The words the packages, docs, specs and findings use. One vocabulary for the whole workspace.

Declaration ​

one part of a game as a named class, made by a builder: class LevelSolved extends Game.Event("LevelSolved", { ... }) {}. It carries its definition and its types and runs nothing by itself; a Layer or a loop hosts it. The kit's declarations are Game.Event, Game.Command, Game.Rejection, Game.Simulation (arcadekit/game), Game.Engine (arcadekit/game/browser), Content and Asset (arcadekit/content) (ADR-0020). Avoid: definition, for the class; a definition is the object a builder takes.

Simulation ​

the DOM-free half of a game, its rules, declared with Game.Simulation: what a run's state is made from (create), how one step advances it (step), its step input as a Schema, the events it reports, the commands it takes with a handler for each (apply), and its sample. Its functions are plain code. Push-puzzle's is PushPuzzle. Game.Loop steps one, Game.Engine runs it in the browser, and a scenario checks it. Survivors still uses the older GameSimulation.

Command ​

a decision a player makes outside what their device holds, named in the imperative and declared with Game.Command: an upgrade pick, "continue lesson". An app sends one (engine.send), and the loop applies it at the start of the next step, before step, through the simulation's apply handler of its tag. A run changes only at a step: by its input and by commands. The rules may refuse one, with a rejection. Not a Foldkit Command, which is how an app causes a side effect.

Rejection ​

a reason the rules refuse a command, named for what is wrong and declared with Game.Rejection: "offer closed". A command lists the rejections it can end with, its apply handler returns one in place of changing the state, and the sender's engine.send fails with it. Nothing happened in the run, so it is not an event and only the sender hears of it. A command that never reached the rules (no run, or the run ended first) is dropped, which is not a rejection: CommandDropped. Avoid: error, for a rejection; a refused command is the rules working.

Scenario ​

a specification of a simulation that runs it (arcadekit/game/scenario): given a run, after these steps and commands, it reported these events and refused these commands. It steps the real rules through Game.Loop, and fails if the run reported an event or refused a command the scenario didn't check, or if a command it sent was never applied. Avoid: story, which is Foldkit's Story, the test of an app's update; and scenario for survivors' long run, the fixed 20,000-step run its determinism tests pin.

GameSimulation ​

the older contract for the DOM-free half of a game (arcadekit/runtime): how to create a run's state, advance it by one step, drain its events and sample it. Survivors exports one from its root entry (survivorsSimulation). The runtime never looks inside the state.

GameDefinition ​

a game as the browser runs it (arcadekit/runtime/browser): its GameSimulation plus its GamePresentation, which reads input from the viewport and draws into it. Survivors builds it in its ./browser entry as { ...simulation, createInput, createView }; a declared game has a Game.Engine in its place.

GameLoop ​

runs a GameSimulation (arcadekit/runtime): one run at a time, its fixed clock, its events and samples. Game.Loop (arcadekit/game) runs a declared simulation on it and adds what the declaration owns: its events, its commands and its sample. It owns the step order and the time policy; time and input come from its caller. GameLoop.frame is the seam where stepping adapters plug in: the browser engine's frame loop, tests and replays, and #25's timer or lockstep runner. The engine's FrameSource is its own frame clock: it times the engine's frames, which step by real time and then draw. It is not where stepping adapters plug in (ADR-0010).

Run ​

one play-through, from start to stop, the next start or a crash. A survivors run is one attempt to survive; a push-puzzle run is one attempt at one level. Restarting starts a new run with the same config. Each run has its own state object, made by the genre's create, so a view can tell runs apart by identity. A genre can end its run itself (survivors' runEnded event, push-puzzle's LevelSolved). The runtime keeps stepping the run until stop, the next start or a crash, and those steps do nothing.

Step ​

one fixed-length tick of the simulation (1/60 s by default), run by GameLoop. Steps are numbered from 0 in each run (stepIndex), and an input source reads each one's input by that number. A slow frame can run several steps to catch up; an input source's endStep runs after each one, and its endFrame once per frame that ran at least one step. A step must be deterministic: the same state and input give the same result, in every JavaScript engine. It changes the state in place and returns nothing, may allocate what it reports (an event is rare), and allocates nothing per entity. So a genre's step uses only exact arithmetic (+ - * /, Math.sqrt, Math.floor and the like), never an implementation-approximated Math function or **; survivors' math.ts has exact hypot2, sin, cos and powInt, and a lint rule enforces it (ADR-0011). A step is plain code: it runs no Effect and reads no clock or random source of its own, so its time comes in as an argument and its randomness from the run's Rng (ADR-0017).

Turn ​

one player action in a turn-based genre: a move or an undo in push-puzzle. A step holds at most one turn; most steps hold none. Don't use "step" for a turn or "turn" for a step.

Frame ​

one GameLoop.frame call: zero or more steps, then one endFrame (if it stepped), the event flush and the sample check. In the browser it is one animation frame (requestAnimationFrame), after which the view draws once. In a replay it is usually one step. Don't call a step a frame.

Event ​

a rare, meaningful fact a run reports to the outside ("level solved", "leveled up"), reported by a step and delivered by the loop after the frame's steps. Never per-entity data. An event's payload is the data it carries. Each genre declares its events once, in its root entry's events.ts, and apps use them in their Model and Messages. A declared simulation's events are Game.Event classes, each also its own Schema, reported with the step's report (push-puzzle's LevelSolved); under the older GameSimulation they are plain objects with a type, pushed into an EventBuffer (survivors). A sample is the runtime's read of the state for the UI (a HUD), also delivered as an event: every so many milliseconds on the time GameLoop's caller passes in (wall-clock time in the browser), or right after the game's version number changes (a turn-based HUD). Push-puzzle's sample is its Hud event; survivors' is a hud event that carries a HudSample. Avoid: snapshot for a sample.

Snapshot ​

a copy of a run's whole state that could be restored. The runtime has none (see finding 11). Don't call a sample a snapshot.

Replay ​

a deterministic re-run: the same seed and input script give the same run. Both genres' replay tests check it, running the simulation through the loop the demos run (GameLoop, or Game.Loop over it); survivors' pins a recorded state hash and push-puzzle's a recorded end state. The push-puzzle demo's "Replay" button only starts the level again.

Input source ​

where each step's input comes from (StepInput): a device, a script or, for #25, a buffer of confirmed inputs. read(step) gives one step's input before that step. A genre's browser InputSource is a StepInput that owns resources, usually wrapping an InputController, and ignores the step number; a script indexes by it. A source that reads presses forgets them after each step (endStep), so one press reaches one step.

View ​

a genre's GameView, part of its GamePresentation (arcadekit/runtime/browser). It draws a run into the viewport once per browser frame, after the frame's steps, and never changes the run's state. A test or replay that steps a GameLoop has no view. Not Foldkit's view function, which draws the app's UI. Avoid: renderer, which is Stage's WebGLRenderer.

Cue counter ​

a running total in a genre's state of one moment that makes a sound, such as survivors' hits (in World.cues) or push-puzzle's pushes (in Puzzle.cues). The step bumps it and never reads it; it only goes up during a run (push-puzzle's undo leaves it alone and bumps undos), and the genre's state hash leaves it out (push-puzzle has no hash; its replay test checks the end state without them). A sound view (SurvivorsSoundView, PushPuzzleSoundView) is a second GameView that compares each counter with the value it saw last frame and plays a sound for each one that rose (ADR-0014). Not an event.

Audio system ​

what an app and its sound views play sound through (AudioSystem, arcadekit/audio; the Audio service). Playing is a plain call that never fails: a sound before the first gesture unlocks audio, while the tab is hidden, past its voice cap or sooner than its minimum gap is dropped. Every sound plays through the sfx or music bus, and both through master. A sound view puts it beside the drawing view with combineViews. The app owns the sound files and the table of cue to sound; a genre ships none. Avoid: sound manager.

Viewport ​

the DOM element the view draws a run into and the input source reads input from. The runtime calls it element in attach; the view gets it as container and the input source as surface. In a Foldkit app, gameViewport (arcadekit/foldkit) renders it, and engineEntry keeps the engine attached to it while it is in the document. engineEntry takes onError, the Message sent if the engine fails to load or to attach, or crashes. One per document.

Lockstep ​

how peers play one run together (#25, arcadekit/net): each sends only its input, and every peer runs step N once it has every player's input for step N, so all runs stay identical without sending state. The kit's lockstep is delay-based: no prediction, no rollback and no snapshots; a step whose inputs haven't arrived waits. Anything that changes a lockstep run (an upgrade pick, a pause, a player's removal) is an input scheduled for a step, never a modify. Peers compare a state hash every so many steps, and a mismatch (a desync) stops the run with an error naming the step and the hashes (ADR-0012).

Host ​

the peer at the centre of a lockstep run's star. It runs the lobby, sends the seed and content hash at the start, receives every peer's input, decides when a step's inputs are complete and relays the merged input to everyone. It alone removes a player stepping has stalled on for about 10 s (silent, closed, or too slow to keep up), by scheduling the removal as input. It also removes a peer whose input doesn't decode. It never ends a run on a peer's word: a peer's desync claim counts only as that peer's own hash, which the host compares with its own. A hidden tab on a desktop browser isn't one: its worker keeps it stepping. A page the browser freezes or suspends (a mobile browser in the background) is. The host is also a player, and a peer.

Peer ​

one player's browser in a lockstep run, the host included. A peer that isn't the host connects only to the host: it sends its own input there and steps on the merged inputs the host relays. Avoid: client, which suggests the host simulates for it; every peer runs the whole simulation.

Input delay ​

how many steps ahead a peer schedules its local input: input sampled now is for step N + inputDelay, so it usually reaches every peer before that step is due and stepping doesn't wait. A few steps (2 to 6 steps is 33 to 100 ms at 60 Hz), tuned by measurement. Not latency, which is how long a message takes to arrive.

Lockstep session ​

one peer's side of a lockstep run (LockstepSession, arcadekit/net), made with hostLockstep or joinLockstep. In an Effect program, hostLockstepScoped and joinLockstepScoped make one its scope owns (ScopedLockstepSession): closing the scope stops it listening, its time is Effect's Clock, and run(ticker) ticks it on a Ticker ((onTick) => stop) until it stops, failing with its LockstepError. It owns a GameLoop and steps it on a timer: the caller calls tick(nowMs), and each tick runs the steps that time says are due and whose inputs are all confirmed. In the browser the Co-op client runs that timer in a dedicated Web Worker (the run's scope terminates the worker and frees its script), so a hidden tab on a desktop browser keeps stepping at full rate and holds no one up; only drawing stops. The Co-op client ticks its session on that worker and again on each animation frame, before drawing. A page the browser freezes or suspends stops its worker too, and the drop rule handles it. It reads the local player's input inputDelay steps ahead and reports waitingFor while a step waits. Its seams are a Transport and an input codec. Its caller also gives it combine (every player's input to one step's input) and the genre's hash; for a game whose step input is PlayerInputs, combine is the kit's combinePlayerInputs, which is what the Co-op client passes.

Confirmed step ​

what the host decided for one step (ConfirmedStep): every remaining player's input, in player order with the host first, and the players removed at that step. Every peer, the host included, steps on confirmed steps only, decoded from the same wire form, and reads them by step number through StepInput.read.

Transport ​

how a lockstep session's messages travel (Transport, arcadekit/net): text to a peer by id, and a listener for what arrives. It may lose, delay, duplicate and reorder messages; the session resends what isn't acknowledged. It has two adapters: the in-memory network for tests (createMemoryNetwork, with seeded latency, jitter and loss), and WebRTC in the browser (linksTransport in arcadekit/net/browser), which sends over each peer link's unreliable, unordered data channel. Not the signalling worker, which only introduces peers.

one WebRTC connection between two peers of a lockstep run, open (PeerLink, arcadekit/net/browser): an unreliable, unordered lockstep channel for the Transport, and a reliable, ordered messages channel for what is said around the run (a lobby's picks, its start). The signalling client (the scoped hostRoom and joinRoom) makes them through the signalling worker: a host gets one per guest, a guest one to the host. The room's scope owns every link, the run's too, so closing it closes them; PeerLink.closed succeeds once a link has closed or its connection has failed. Avoid: connection, for the whole link.

Signalling worker ​

the hosted service that introduces a lockstep run's peers so they can open WebRTC data channels (arcadekit/net/worker, ADR-0012). It is the kit's, for Cloudflare, and an app's Worker is a few lines over it: for the survivors demo, the same Cloudflare Worker that serves the site, on the same origin (apps/survivor-demo/worker). The host opens a room and gets its join code; each guest joins with the code; the worker relays WebRTC offers, answers and ICE candidates between the host and each guest (never guest to guest, as in the star) until the host starts the run. A room holds at most its room size, which the app sets where it sets up the worker (4 unless it says otherwise, 16 at most), admits no one after the start (no late join), and ends when the host leaves or after 10 minutes. It never carries game input. Its messages are the Schemas of arcadekit/net/signalling. If it can't set a new room's expiry alarm, it refuses the host (503) and keeps no room. One Cloudflare Durable Object per join code, declared by the app (the demo's alchemy.run.ts, with Alchemy). Avoid: server, lobby server, matchmaker.

Lobby ​

where a co-op run's players gather before it starts: the host's room and its peer links, each player's lobby pick, and the host's start. It is the Co-op client's, with its messages on each peer link's reliable channel; its screens are the app's. The host can start once there are at least two players and at most the game's limit, each with a pick. No one joins a run after the lobby (no late join).

Lobby pick ​

what a player chooses in the lobby, as a string id from the list the Co-op game gives for its content: a hero, in survivors. At the start the host replaces an empty or unknown one with the first in the list, and a guest whose content lacks one of the run's picks refuses to start. The picks, in player order, make the run's config. Not a pick, which answers a survivors offer.

Co-op game ​

a game as the Co-op client runs it. It has two halves. Its Co-op rules (CoopRules, arcadekit/net) are DOM-free: the GameSimulation, whose step input is PlayerInputs, and its lockstep parts: the input codec, the state hash, the lobby picks and the config they make, and what the local player's seat needs (how to fill its input, when an answer is settled, who is choosing, what the player hears of an event, and which event ends the run). The CoopGame (arcadekit/net/browser) adds the content to load and the presentation. A genre exports its rules from its root entry and builds the game in its ./browser entry (survivorsCoopRules, makeSurvivorsCoopGame). Its content must be JSON data, since peers compare its hash.

Co-op client ​

the kit module that runs a Co-op game for one player (makeCoopClient, arcadekit/net/browser): the lobby, then the lockstep run, drawn into the viewport while attached. Each host or join is one seat, which owns its room, lobby and run; hosting or joining again, or leaving, closes it before the next one speaks. The app drives it with commands (host, join, choose, start, answer, pause, leave) and hears one ordered stream of events: the lobby, the start, the run's events for the local player, who the run waits on, and at most one failure, with a reason an app can re-word. A Foldkit app hands it to engineEntry as it would an engine (ADR-0019). Avoid: co-op engine; an Engine runs a single-player game.

Answer ​

what the local player tells a co-op run besides its device's input, such as a survivors upgrade pick. The Co-op client holds it on every step's input until the game says it is settled: it can no longer apply. A player who owes one is choosing.

Join code ​

the six letters a host reads out so guests can find its room, such as K7MQ2X; letters and digits that can't be confused (no 0/O, 1/I/L), typed in any case. The worker picks it; it names the room only while the room lives.

Hero ​

one player's character in a survivors run (Hero, hero.ts). Each player has one, at its slot: the player's place in player order, host first in lockstep (SurvivorsConfig.heroIds, 1 to 4). A hero owns its body, stats, weapons, passives and upgrade offer; the run owns everything else (clock, team XP, horde, gems, projectiles, Rng). Survivors' rules visit heroes in slot order, and an enemy chases the nearest playing hero, the lowest slot winning a tie (ADR-0013). Avoid: player for the hero; the player is the person or peer.

Team XP ​

survivors' one XP pool and level for the whole run (World.level, xp, xpNext). Any hero's gem feeds it, and each level gained is a level-up for every hero.

Offer ​

the upgrade choices a survivors hero picks from after a level-up (Hero.offer). Every playing hero gets its own, numbered together by World.offerSeq. The run is frozen while any hero has one open and resumes in the step that closes the last, by a pick or by its player leaving. A pick names the offer it answers (HeroInput.pick), so a late or repeated one does nothing. Single-player picks through chooseUpgrade and modify; lockstep, through step input, as the Co-op client's answer.

Down ​

a survivors hero at 0 HP. It stays where it fell, does nothing and isn't chased; there is no revive. The run is lost once no hero is playing. With one hero, down is dead.

Left ​

a survivors hero whose player left the run, at the step lockstep removed them (SurvivorsInput.leaving, from a confirmed step's removed players). It is no longer drawn or chased, and its open offer closes.

Engine ​

the Effect-facing API of a running game: attach to an element, listen to events, start, pause, stop, and send a command. A genre declares one with Game.Engine (arcadekit/game/browser): its simulation, the view and input that draw it and read its input, and what those are made from (uses: Effects run on each attach, such as the app's audio or a sprite sheet's load). The class is the engine service's key and its layer provides it, so the engine exists before its assets and an attach loads them. Push-puzzle's is PushPuzzleEngine. What follows is the older GameEngine (arcadekit/runtime/browser), which survivors still uses and which has modify in place of send. attach runs its frame loop for as long as it stays attached, and fails with GameCrashed if a frame throws (a crash: a bug in the game, reported to the player like a failed attach). A crash ends the run, as stop does. Each genre declares its own engine service. A genre whose game draws from loaded assets declares an engine loader instead (EngineLoader, makeEngineLoader). Its load makes the engine once from the loaded sheet. There is no engine before then, and engineOf reaches it.

Content ​

what a game is played with, as data: its levels, heroes, weapons. A genre declares it with Content (arcadekit/content): its Schema, the file each part comes from, and the checks the Schema can't express. The class is the key of the service that loads it once per app (MyContent.layer(source), MyContent.load); MyContent.read(source) loads it for a test with the same checks. Push-puzzle's is PushPuzzleContent.

Asset ​

something a game draws or plays from that the app loads once, such as a sprite sheet, declared with Asset (arcadekit/content). The genre declares it with how to free it, the app says where it comes from (MySheet.layer(load)), and MySheet.load gives it: the first success is kept, a failed load can be run again, and the app's end frees it. Push-puzzle's is PushPuzzleSheet.

Content source ​

where content files come from (ContentSource), such as memorySource in tests or fetchJsonSource in the browser. A genre's content definition (ContentDefinition) is its schema, the file each part comes from, and the checks the schema can't express, as one value. A genre's content service, built with makeContentLoader from the definition and a source, loads and checks the files once per app; a failed load isn't kept, so it can be retried. Tests load the same definition with loadContent, so they run the same checks.

Pool ​

a fixed-capacity set of entities stored as one typed array per field (createPool in arcadekit/core). Removing swaps the last entity in, so pool indices are not stable. An identified pool (createIdentifiedPool) also gives each entity a stable id, for a genre that must name one entity across removals (undo, tweening).

SpatialGrid ​

a uniform grid in arcadekit/core for "which points are near here?", rebuilt every step from continuous positions. Not a tile map.

Tile map ​

a genre's grid of static cells (wall, floor, hole), stored in typed arrays and indexed by column and row (row * cols + col). It describes the level, not where entities are. Push-puzzle's lives in the genre package.

Mesh instances ​

copies of one mesh drawn in a single draw call (MeshInstances in arcadekit/render-three), for a 3D view that draws one mesh many times, such as a crowd of enemies or a floor of tiles. Both demos draw pixel art instead, with sprites. Each instance gets a position, a uniform scale, a rotation around the vertical axis and a color. Not a pool: mesh instances belong to the view and hold only what it draws. The view sets them from the state it reads.

Sprite sheet ​

one PNG of pixel art plus the JSON Aseprite exports for it (SpriteSheet, loadSpriteSheet in arcadekit/render-three): its frames, each with a rect and a duration, and its tags, named runs of frames that are animations (slime/move). frameAt gives the frame a tag shows at a time. The app names its sheet and hands its load to the genre's engine layer. LoadContent loads it with the content and shows a failure on the loading screen, whose "Try again" runs LoadContent again. The engine loader keeps it and frees it when the app ends: a view draws from a sheet but never frees it, and disposeObject skips its texture (ADR-0015). A content entry's sprite names a tag (a survivors hero's names the prefix of its walk, idle and down tags); without one, a generic sprite is drawn in the entry's color. Avoid: atlas, texture for the whole sheet.

Sprites ​

copies of frames from one sprite sheet drawn in a single draw call (InstancedSprites in arcadekit/render-three), as quads facing a pixel camera (PixelCamera): an orthographic camera looking straight down, with a whole number of screen pixels per sprite pixel and its position snapped to screen pixels, so the art never shimmers. Each sprite gets a position on the ground, a frame, a flip, a tint, a flash, an alpha, a lift (drawn higher, sorted at its feet) and a layer; within a layer, lower on screen draws in front. Like mesh instances, they belong to the view.

Save ​

what a demo app keeps between sessions: records by level or hero id, such as a level's best move count or a hero's best time. It is UI state, not simulation state, so no genre package knows about it. Each app reads it once at startup and writes it on the genre's end-of-run event (survivors' runEnded, push-puzzle's LevelSolved), never per frame and never from a step. It lives in localStorage under one versioned key per game (arcadekit/<game>/save). A write re-reads the stored save and keeps the best of both. A save that fails to decode (corrupt, or an unknown version) is copied to <key>/corrupt and read as empty. arcadekit/save (makeSaveStore) does all of that; each app passes its key, the stored field name, its record Schema and its record merge (how two records for one id become one), and wraps the result in its own Save service. See ADR-0009. Avoid: snapshot, which is a whole run's state.

Kit ​

the shared code that genre packages and demo apps build on, published as one package, arcadekit. A primitive is one of the kit's building blocks, such as createPool or InputController.

Entry ​

one subpath export of a package, and what an import names: arcadekit/core, arcadekit/runtime/browser. The kit has twenty-one, in ten groups: core, input, content, save, audio, render-three, runtime, game, net and foldkit, some with a ./browser entry for what needs the DOM and a ./testing entry for what only tests import, game with a ./scenario entry for scenarios, and net with a ./worker entry for what needs Cloudflare. An entry imports only the entries below it (ADR-0018). A genre package's root entry is its DOM-free one.

Genre package ​

a package that implements one kind of game on the kit (@arcadekit/survivors, @arcadekit/push-puzzle). Each entry exports what an app uses and nothing else; rules internals stay in their modules. Push-puzzle's is declarations: its root entry has its events, its simulation and its content, and ./browser its engine with the sheet asset and the cue table the app provides. What follows describes survivors' shape, on the older contract. The root entry holds the content definition and service, the GameSimulation with the state, config and step input an app names, and the event payload Schemas, with no DOM or Three.js (its tsconfig.root.json checks it with no DOM types; keeping Three.js out is a convention); its ./browser entry holds the engine, the GameDefinition, and the cue table, art and view types an app wires. A genre with co-op also exports its Co-op rules from the root entry and its Co-op game from ./browser.

Finding ​

a place where a genre had to change, work around, copy or bypass a shared primitive, or where a shared primitive's name or docs assume one genre. A primitive a genre simply doesn't use is not a finding. See docs/push-puzzle-findings.md and docs/co-op-findings.md.

Message, Command, Subscription, Mount and resources are Foldkit's words, used in the apps. An app turns events into Messages, so a Message is not an event.

Released under the MIT License.