airframe.io / src /game /MultiplayerRoom.test.ts
LATTICEWORKS's picture
feat: integrate Google Earth 3D Tiles and optimize engine hooks
565f6e7
Raw
History Blame Contribute Delete
10.4 kB
// Run with: node --import tsx/esm src/game/MultiplayerRoom.test.ts
// tsx is already a devDependency; Node 22+ required.
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import RAPIER from "@dimforge/rapier3d-compat";
import { MultiplayerRoom } from "./MultiplayerRoom.js";
import { AmmoBelt } from "../types.js";
import { DEFAULT_AIRCRAFT } from "./aircraftData.js";
type ClientInputTuple = [
number, // seq
number, // pitch
number, // roll
number, // yaw
number, // throttleDelta
number, // boost
number, // airbrake
number, // primaryFire
number, // secondaryFire
number, // flaps
number // gearDeployed
];
async function testMultiplayerRoom() {
console.log("[*] Running MultiplayerRoom unit tests...");
// Initialize Rapier WASM before running tests to prevent async race conditions
await RAPIER.init();
const room = new MultiplayerRoom();
room.roomId = "test-room-123";
// Mock Colyseus Room environment methods and properties
const broadcastMessages: Array<{ type: string; message: any }> = [];
room.broadcast = (type: any, message: any) => {
broadcastMessages.push({ type: String(type), message });
};
const messageHandlers = new Map<string, (client: any, message: any) => void>();
room.onMessage = (type: any, callback: any) => {
messageHandlers.set(String(type), callback);
return {} as any;
};
const clientMessages: Array<{ sessionId: string; type: string; message: any }> = [];
const createMockClient = (sessionId: string, nickname = "Maverick") => {
return {
sessionId,
auth: {
nickname,
selectedPlaneId: "falcon-mk2",
customizations: { skin: "default" },
selectedBelt: AmmoBelt.Universal,
equippedMods: { "falcon-mk2": [] }
},
send: (type: string, message: any) => {
clientMessages.push({ sessionId, type, message });
}
} as any;
};
// Prevent background simulation thread from starting
room.setSimulationInterval = (_cb: any, _delay: any) => {
return { close: () => {} } as any;
};
// 0.5 Test onAuth authentication
console.log("[*] Testing Room authentication (onAuth)...");
const saveDir = fs.existsSync("/data") ? "/data/saves" : path.join(process.cwd(), "saves");
if (!fs.existsSync(saveDir)) {
fs.mkdirSync(saveDir, { recursive: true });
}
const tempToken = "test_token_auth_unit";
const tempSavePath = path.join(saveDir, `${tempToken}.json`);
fs.writeFileSync(tempSavePath, JSON.stringify({
nickname: "Iceman",
selectedPlaneId: "falcon-mk2",
customizations: { skin: "default" },
selectedBelt: AmmoBelt.Universal,
equippedMods: { "falcon-mk2": [] }
}));
try {
const mockAuthClient = {} as any;
const authResult = await room.onAuth(mockAuthClient, { token: tempToken, nickname: "Iceman" });
assert.strictEqual(authResult.nickname, "Iceman", "onAuth should successfully validate correct credentials");
const authResult2 = await room.onAuth(mockAuthClient, { token: tempToken, nickname: "Maverick" });
assert.strictEqual(authResult2.nickname, "Maverick", "onAuth should accept different nickname in options");
const guestResult = await room.onAuth(mockAuthClient, { token: "nonexistent_token", nickname: "Iceman" });
assert.strictEqual(guestResult.isGuest, true, "onAuth should return guest session for nonexistent token");
} finally {
if (fs.existsSync(tempSavePath)) {
fs.unlinkSync(tempSavePath);
}
}
// 1. Test onCreate
console.log("[*] Testing Room creation...");
await room.onCreate({ mapId: "island-chain", mode: 1 });
assert.ok(room.state, "MatchState should be initialized");
assert.strictEqual(room.state.matchEnded, false, "Match should not be ended");
assert.strictEqual(room.state.team1Score, 0, "Team 1 score should be 0");
assert.strictEqual(room.state.team2Score, 0, "Team 2 score should be 0");
// By default, fillWithBots spawns 8 bots (4 per team)
const botCount = Array.from((room as any).pilots.values()).filter((p: any) => p.isBot).length;
assert.strictEqual(botCount, 8, "Should initially spawn exactly 8 bots (4 per team)");
// 2. Test team balancing & bot eviction
console.log("[*] Testing team balancing & bot eviction...");
(room as any).pilots.clear();
assert.strictEqual((room as any).setup.balanceTeams((room as any).pilots), 1, "balanceTeams should return 1 when empty");
// Add 1 player to team 1
(room as any).pilots.set("p1", { isBot: false, team: 1 });
assert.strictEqual((room as any).setup.balanceTeams((room as any).pilots), 2, "balanceTeams should return 2 when team 1 has 1 player");
// Add 1 player to team 2
(room as any).pilots.set("p2", { isBot: false, team: 2 });
assert.strictEqual((room as any).setup.balanceTeams((room as any).pilots), 1, "balanceTeams should return 1 when teams are equal");
// Spawn a bot to team 1, and evict it
(room as any).setup.botSys.spawnBot(1, DEFAULT_AIRCRAFT[0].id, (room as any).pilots, (room as any).state.players, (id: string, p: any) => (room as any).physSys.addPhysicsBody(id, p), (t: number) => (t === 1 ? Math.PI / 2 : -Math.PI / 2));
const bot1 = Array.from((room as any).pilots.values()).find((p: any) => p.isBot && p.team === 1);
assert.ok(bot1, "Bot should be spawned on Team 1");
const botId = (bot1 as any).id;
assert.ok((room as any).pilots.has(botId), "Bot should be in pilots map");
assert.ok((room as any).physSys.rigidBodies.has(botId), "Bot should have a rigid body");
// Evict the bot
(room as any).setup.evictBot(1, (room as any).pilots, (room as any).physSys, (room as any).playerVoxelGrids);
assert.ok(!(room as any).pilots.has(botId), "Bot should be evicted from pilots map");
assert.ok(!(room as any).physSys.rigidBodies.has(botId), "Bot's rigid body should be cleaned up");
// Restore bots for next steps
(room as any).pilots.clear();
(room as any).setup.botSys.fillWithBots(
8, (room as any).pilots, (room as any).state.players,
(id: string, p: any) => (room as any).physSys.addPhysicsBody(id, p),
(t: number) => (t === 1 ? Math.PI / 2 : -Math.PI / 2)
);
// 3. Test Player joining
console.log("[*] Testing Player joining...");
const client1 = createMockClient("player_session_1", "Iceman");
// Mock balanceTeams to assign to Team 1
(room as any).setup.balanceTeams = () => 1;
room.onJoin(client1, { aircraftId: "falcon-mk2", skin: "default" });
assert.ok((room as any).pilots.has(client1.sessionId), "Player pilot should be registered");
assert.ok(room.state.players.has(client1.sessionId), "Player should be added to Colyseus state schema");
const playerPilot = (room as any).pilots.get(client1.sessionId);
assert.strictEqual(playerPilot.name, "Iceman", "Pilot name should match auth client data");
assert.strictEqual(playerPilot.team, 1, "Player should be assigned to Team 1");
assert.strictEqual(playerPilot.isBot, false, "Player should not be marked as bot");
// Verify welcome message was sent to client
const welcomeMessage = clientMessages.find(m => m.sessionId === client1.sessionId && m.type === "welcome");
assert.ok(welcomeMessage, "Welcome packet should be sent to joining client");
assert.strictEqual(welcomeMessage.message.assignedTeam, 1, "Welcome packet team should be 1");
// 4. Test Chat message handler
console.log("[*] Testing Chat message handler...");
const chatHandler = messageHandlers.get("chat");
assert.ok(chatHandler, "Chat handler should be registered");
chatHandler(client1, "Fox Two! Fox Two!");
const lastChatBroadcast = broadcastMessages.find(m => m.type === "chat");
assert.ok(lastChatBroadcast, "Chat message should be broadcast");
assert.strictEqual(lastChatBroadcast.message[1], "player_session_1", "Broadcasted sender ID should match");
assert.strictEqual(lastChatBroadcast.message[2], "Iceman", "Broadcasted sender name should match");
assert.strictEqual(lastChatBroadcast.message[3], "Fox Two! Fox Two!", "Broadcasted text should match");
// 5. Test Input Handling & Queuing edge cases
console.log("[*] Testing input queue sorting and limits...");
(room as any).inputSys.inputQueues.set(client1.sessionId, []);
// Enqueue 65 inputs to test the queue size ceiling of 60
for (let i = 0; i < 65; i++) {
const tuple: ClientInputTuple = [i, 0, 0, 0, 0.5, 0, 0, 0, 0, 0, 0];
(room as any).inputSys.enqueueInput(client1.sessionId, tuple);
}
const q = (room as any).inputSys.inputQueues.get(client1.sessionId);
assert.strictEqual(q.length, 60, "Input queue size should be capped at 60");
assert.strictEqual(q[0].seq, 5, "Oldest inputs (0-4) should have been shifted out");
assert.strictEqual(q[59].seq, 64, "Newest input (64) should be at the tail");
// Test out-of-order sorting
(room as any).inputSys.inputQueues.set(client1.sessionId, []);
const t5: ClientInputTuple = [5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
const t8: ClientInputTuple = [8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
const t10: ClientInputTuple = [10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
(room as any).inputSys.enqueueInput(client1.sessionId, t10);
(room as any).inputSys.enqueueInput(client1.sessionId, t5);
(room as any).inputSys.enqueueInput(client1.sessionId, t8);
const sortedQ = (room as any).inputSys.inputQueues.get(client1.sessionId);
assert.strictEqual(sortedQ[0].seq, 5);
assert.strictEqual(sortedQ[1].seq, 8);
assert.strictEqual(sortedQ[2].seq, 10);
// Test queue under-run handles and preserves command state but disables fire
(room as any).inputSys.inputQueues.set(client1.sessionId, []);
(room as any).projectiles = [];
(room as any).inputSys.lastInputs.set(client1.sessionId, {
pitch: 0.5, roll: 0.2, yaw: 0.1, throttleDelta: 0,
boost: true, airbrake: false, primaryFire: true, secondaryFire: true,
flaps: "combat", gearDeployed: false
});
const originalRandUnder = Math.random;
Math.random = () => 0; // force fire if triggered
try {
(room as any).tick(); // runs simulation tick, empty queue triggers under-run
} finally {
Math.random = originalRandUnder;
}
assert.strictEqual((room as any).projectiles.length, 0, "Queue under-run should disable weapon firing and spawn 0 projectiles");
console.log("[*] All tests passed successfully!");
}
testMultiplayerRoom().catch(e => {
console.error("FAIL MultiplayerRoom test:", e);
process.exit(1);
});