import assert from "node:assert/strict"; import test from "node:test"; import { FURNITURE, MOVERS_BOT_IDS, MOVERS_PHYSICS_BACKEND, MOVERS_SNAPSHOT_RATE, MOVERS_TICK_RATE, moversGame, } from "../dist/index.js"; function inputPacket(sequence, targetTick, input = {}) { return { sequence, targetTick, observedTick: Math.max(0, targetTick - 1), input: { forward: 0, strafe: 0, sprint: false, grab: false, throwItem: false, closeDoors: false, ...input, }, }; } test("Bad Movers is a fourth complete game built through defineMultiplayerGame", () => { assert.equal(moversGame.tickRateHz, MOVERS_TICK_RATE); assert.equal(moversGame.snapshotRateHz, MOVERS_SNAPSHOT_RATE); const server = moversGame.createServer(); assert.equal(server.currentState.players.filter((player) => player.bot).length, MOVERS_BOT_IDS.length); assert.equal(server.currentState.furniture.length, Object.keys(FURNITURE).length); assert.deepEqual(MOVERS_PHYSICS_BACKEND, { name: "Box3D", version: "0.1.0", bindingVersion: "0.2.0", runtime: "WebAssembly SIMD", solver: "single-threaded deterministic", subSteps: 4, }); server.addPlayer(1); server.addPlayer(2); assert.equal(server.currentState.players.find((player) => player.id === 1).team, "yellow"); assert.equal(server.currentState.players.find((player) => player.id === 2).team, "blue"); }); test("real Box3D gravity advances full 3D furniture transforms", () => { const server = moversGame.createServer(); const box = server.currentState.furniture.find((item) => item.kind === "mystery-box"); Object.assign(box, { x: 0, y: 8, z: 0, velocityX: 0, velocityY: 0, velocityZ: 0, }); for (let tick = 0; tick < 12; tick += 1) server.step(); assert.ok(box.y < 7, `expected Box3D gravity to drop the box, got y=${box.y}`); assert.ok(box.velocityY < 0); assert.ok(Number.isFinite(box.rotationX + box.rotationY + box.rotationZ + box.rotationW)); }); test("independent Box3D authorities produce the same deterministic match", () => { const first = moversGame.createServer(); const second = moversGame.createServer(); for (let tick = 0; tick < 600; tick += 1) { first.step(); second.step(); } assert.deepEqual(first.currentState, second.currentState); }); test("only the owning mover receives exact stamina", () => { const server = moversGame.createServer(); server.addPlayer(1); server.addPlayer(2); const yellowView = server.createSnapshot(1, 0).state; const blueView = server.createSnapshot(2, 0).state; assert.equal(yellowView.players.find((player) => player.id === 1).stamina, 100); assert.equal(yellowView.players.find((player) => player.id === 2).stamina, null); assert.equal(blueView.players.find((player) => player.id === 1).stamina, null); assert.equal(blueView.players.find((player) => player.id === 2).stamina, 100); assert.equal("nextEventId" in yellowView, false); }); test("furniture only scores when a mover closes their own truck doors", () => { const server = moversGame.createServer(); server.addPlayer(1); const player = server.currentState.players.find((candidate) => candidate.id === 1); const urn = server.currentState.furniture.find((item) => item.kind === "urn"); Object.assign(player, { x: -34, z: 0 }); Object.assign(player, { carryingId: urn.id, grabbing: false }); Object.assign(urn, { x: -35, z: 0, damage: 10, carriedBy: player.id }); assert.equal(server.currentState.yellowScore, 0); assert.equal(server.submitInput(1, inputPacket(1, 1, { closeDoors: true })).accepted, true); const events = server.step().events; assert.equal(urn.securedBy, "yellow"); assert.equal(server.currentState.yellowScore, Math.round(FURNITURE.urn.value * 0.9)); assert.ok(events.some((event) => event.type === "doors" && event.team === "yellow")); assert.ok(events.some((event) => event.type === "secured" && event.itemId === urn.id)); }); test("Bad Movers bots can complete physical deliveries headlessly", () => { const server = moversGame.createServer(); let secured = 0; for (let tick = 0; tick < 2_400; tick += 1) { for (const event of server.step().events) { if (event.type === "secured") secured += 1; } } assert.ok(secured >= 2, `expected bot deliveries, saw ${secured}`); assert.ok(server.currentState.yellowScore + server.currentState.blueScore > 0); assert.ok(server.currentState.furniture.every((item) => Number.isFinite(item.x + item.z + item.damage))); }); test("Bad Movers input and state round-trip through the generic protocol", () => { const server = moversGame.createServer(); server.addPlayer(7); const client = moversGame.createClient(); client.initialize(7, server.createSnapshot(7, 10)); const packet = client.createInput({ forward: 1, strafe: -0.5, sprint: true, grab: true, throwItem: false, closeDoors: false, }, 1); const decoded = moversGame.protocol.decodeClient( moversGame.protocol.encodeClient({ kind: "input", packet }), ); assert.equal(decoded.kind, "input"); assert.deepEqual(decoded.packet, packet); assert.equal(server.submitInput(7, decoded.packet).accepted, true); server.step(); client.step(); client.reconcile(server.createSnapshot(7, 20)); assert.ok(client.currentState.players.some((player) => player.id === 7)); });