How to test a multiplayer game locally with bots and play money
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.
To test a Net Forge multiplayer game on your own computer, run npm run play -- --players 2 to open two players in one room, and npm test to have bots play it headless and report what broke. Both run entirely on your machine: no wallet, no chain, no Net Forge servers, and coins are play money.
npm create netforge@latest my-game -- --template arena --yes
cd my-game
npm run play -- --players 2
npm testThe tools
| Command | What it does |
|---|---|
npm run play | Builds the client with your src/game.ts, starts the local room host and opens http://127.0.0.1:4663 |
npm run play -- --players 3 | Opens three tabs, each a separate player in the same room |
npm run play -- --host 0.0.0.0 | Lets phones and laptops on your network join |
npm run play -- --port 5000 --no-open | Another port, and print the link instead of opening a browser |
npm test | Compiles src/game.ts as the play build does and plays it headless with bots |
npm test -- --minutes 5 --bots 4 --seed 2 | A longer run, more bots, another seed |
npm run check | The same playtest, printed as JSON for scripts and coding agents |
1. Play several players at once
Each tab or device that opens the link is its own player with its own input, prediction and view. Every tab joins the open room until it is full, then a new room opens (the local host holds up to 8). Seat bots join the arena shooter while at least one person is in the room, so one tab already has opponents.
To try a phone, run npm run play -- --host 0.0.0.0, then open the printed link on the phone with localhost replaced by your computer's network address, in the page address and in its server parameter.
2. Run the playtest bots
npm testThe test compiles src/game.ts exactly as the play build does, so a disallowed import or an invalid declaration fails here first, with its file, line and reason. Then bots play the game with no browser and no server, and it prints a short report:
Compiling src/game.ts (arena shooter)...
Playtest: 2 bots for 3 minutes of game time...
kills 41, downs 41, wave 1, top level 0
earn rules fired 2 times (first_blood 2)
0.10 ms per tick on this computer
PASS: the game compiles and the playtest checks pass.It exits 1 when the game does not compile, a playtest check fails (for example nobody scores, no earn rule fires, or one of the genre's own checks fails), or the game softlocks: nothing changes for 30 seconds of game time.
3. Read the JSON in scripts and agents
npm run checkprints the whole result: ok, template, problems (one line each) and report with ticks, bots, kills, deaths, wave, maxLevel, earned per rule, softlock, msPerTick and p99MsPerTick, plus genre measures such as the seconds to the first threat and the first reward. A coding agent should run it after every change and fix each line in problems.
4. Write your own playtest
The same harness is the engine's engine.test.run. Template packages use it in their own tests with named assertions:
import { engine } from "@netforge/engine";
export async function playtest(): Promise<void> {
const r = await engine.test.run({
ticks: 30 * 180,
bots: 4,
seed: 3,
assert: [
{ name: "kills", check: (x) => x.kills > 500 },
{ name: "levels", check: (x) => x.maxLevel >= 5 },
{ name: "earn emitted", check: (x) => x.earnEmitted > 0 },
{ name: "no softlock", check: (x) => !x.softlock },
],
});
if (r.failed.length > 0) throw new Error(`playtest failed: ${r.failed.join(", ")}`);
console.info(`${r.msPerTick.toFixed(2)} ms per tick, peak ${r.peakEnemies} enemies`);
}Call it after your game module has been loaded (the template's manifest loads src/game.ts). Measured with this harness on an Apple M2 Pro and Node 22: the survivor template holds 2,000 enemies with 4 bots at 2.22 ms per tick on average (p99 5.21 ms), and the arena shooter runs 4 bots for 4 minutes at 60 Hz at 0.10 ms per tick.
How it works
kit-play builds the template's browser client with your src/game.ts in place of the template's own, and bundles a local room host from the engine's open simulation and netcode: one world, a fixed tick, one input queue per player and per-player delta snapshots at 20 Hz. Sign-in, the referee and every money route answer 403, so earn rules fire and show but pay nothing. Bots play through the same simulation as people, and in arena games they never earn. See Testing with playtest bots.
Alternatives
- Colyseus Playground lets you connect test clients to Colyseus rooms from the browser and inspect their state.
- Playwright drives real browsers from tests, which suits checking menus and page flows around a game.
- Vitest runs fast TypeScript tests in Node. Net Forge's own template tests run
engine.test.runinside Vitest.
Pick npm test when you want a bot playtest of the actual game rules, with no browser, in seconds.
FAQ
How do I test multiplayer on one computer?
Open the game in two or more tabs or windows. npm run play -- --players 2 opens them for you, and each one is a separate player in the same room.
Do the bots need a browser or a GPU?
No. npm test runs the simulation headless in Node with no renderer, so it runs in CI and in a coding agent's terminal.
Is any real money involved in local testing?
No. Coins are play money, there is no wallet and no chain, and the local host makes no calls to Net Forge servers.
Why did my test fail with a softlock?
Nothing changed for 30 seconds of game time: no kills, no damage and no movement. It usually means enemies cannot reach players, a wave never ends, or a rule stopped the run. The report's numbers show where it stalled.
Can I make the test reproducible?
Yes. Runs are deterministic for a seed, so npm test -- --seed 2 gives the same result every time. Change the seed to explore other runs.
Where does progress go in local play?
Into memory. Achievements and looks bought with play money last until kit-play stops.