NET FORGE DOCS

Testing a game with playtest bots

Last updated Checked against @netforge/engine 0.2.0View as Markdown

Net Forge, the open-source multiplayer browser game engine built for AI coding agents, is published on npm under @netforge with source at github.com/netnetdev/net-forge.

engine.test.run plays your declared game with bots, headless, at full speed, and reports what happened: kills, deaths, waves, levels reached, which earn rules fired, whether the game softlocked, the server cost per tick and design metrics such as how long until the first threat. In a project made with npm create netforge, npm test runs it on your src/game.ts, and npm run check prints the report as JSON.

npm test
npm run check

No browser, server or network is involved, so it takes seconds and is the check a coding agent should run after every edit.

What the report contains

FieldMeaning
kills, deaths, wave, maxLevelWhat the bots achieved over the run.
runs.won, runs.lostRuns that ended in a win or a loss.
earnedHow many times each earn rule paid (play money).
earnEmittedEarn events the game emitted.
softlocktrue if 30 seconds of game time passed with no kill and no hit.
msPerTick, p99MsPerTickServer cost of one simulation tick, mean and 99th percentile. The budget at 30 Hz is 33 ms.
peakEnemiesThe most enemies alive at once.
funDesign metrics: seconds to the first threat and the first reward, level at 1, 3, 6 and 9 minutes, deaths in the first minute, share of spawns that appeared on screen, share of deaths that were telegraphed.
rules.checksFor board genres: each rules check with a pass or fail line.
failedThe names of assertions that failed. Empty means pass.

Write your own assertions

test/playtest.ts
import { engine } from "@netforge/engine";

const report = await engine.test.run({
  ticks: 30 * 180, // three minutes at 30 Hz
  bots: 4,
  seed: 3,
  bot: "human", // reacts 200 ms late, kites, reads cards for a second
  assert: [
    { name: "nobody down in the first minute", check: (r) => (r.fun?.deathsBeforeMinute ?? 0) === 0 },
    { name: "levels come fast", check: (r) => r.maxLevel >= 5 },
    { name: "an earn rule fired", check: (r) => r.earnEmitted > 0 },
    { name: "no softlock", check: (r) => !r.softlock },
  ],
});
if (report.failed.length) throw new Error(`playtest failed: ${report.failed.join(", ")}`);

Import your game module before the call (for example import "../src/game.ts";) so its declarations are registered. seed makes a run repeatable: the same seed and the same game play out the same way (only the timing fields depend on the machine).

Stress the server

Pass enemies to hold a crowd at a fixed size and measure what one tick costs:

test/stress.ts
import { engine } from "@netforge/engine";

const r = await engine.test.run({ ticks: 30 * 30, bots: 4, seed: 4, enemies: 2000 });
console.log(`peak ${r.peakEnemies} enemies, ${r.msPerTick.toFixed(2)} ms/tick mean, p99 ${r.p99MsPerTick.toFixed(2)} ms`);

With the survivor template on an Apple M2 Pro under Node 22 this printed peak 2000 enemies, 2.22 ms/tick mean, p99 5.21 ms.

The design rules the bots measure

The engine ships a design playbook (GAME-DESIGN.md in @netforge/engine) built from measured play. Among its rules: the first reward within about 10 seconds, real danger by 0:30, nobody dies in the first minute, enemies never spawn on screen, and big hits are telegraphed for 0.5 to 1 second. The fun metrics in the report check these, so a change that makes the opening empty or unfair shows up as numbers, not as a feeling after a play session.

Bots in multiplayer

The same bots fill empty seats in rooms that ask for them (the arena shooter's players.bots). Seat bots never earn: killing one scores and appears in the kill feed but pays no earn rule, and a "top of the round" rule pays only when another person was in the round. See How to make a .io-style arena shooter.

FAQ

How do I test a multiplayer browser game without opening browsers?

Run npm test. The playtest plays your game headless with several bots at full speed and reports kills, progress, earn rules fired, softlocks and the cost per tick. Use npm run play -- --players 4 when you want to see it in real tabs.

What does softlock mean in the report?

That the run went 30 seconds of game time with nothing happening: no kill and no hit. It usually means enemies stopped spawning or cannot reach the players.

Are playtest results repeatable?

Yes. With the same seed, game and engine version, a run plays out the same way. The msPerTick timings depend on the machine.

Can a coding agent read the results?

Yes. npm run check prints the result as JSON: whether it passed, one line per problem, and the full report.