Monorepo for Aesthetic.Computer
aesthetic.computer
1#!/usr/bin/env node
2// UDP Multiplayer Test Script (WebSocket Version)
3// Tests the 1v1:move channel via WebSocket on localhost session server
4//
5// Note: This tests via WebSocket since geckos.io client requires a browser.
6// The actual UDP path is tested by running the game in two browser windows.
7//
8// Usage:
9// cd session-server && npm run dev # Start session server first
10// node tests/udp-multiplayer-test.mjs
11
12import WebSocket from "ws";
13
14const SESSION_URL = process.env.SESSION_URL || "wss://localhost:8889";
15const NUM_FAKE_PLAYERS = 2;
16const UPDATE_INTERVAL_MS = 50; // 20 updates per second
17
18console.log("🧪 Multiplayer Position Test (WebSocket)");
19console.log("=========================================");
20console.log(`📡 Connecting to: ${SESSION_URL}`);
21console.log(`👥 Simulating ${NUM_FAKE_PLAYERS} players\n`);
22console.log("ℹ️ Note: This tests WebSocket path. For UDP, test with two browser windows.\n");
23
24const players = [];
25
26class FakePlayer {
27 constructor(id) {
28 this.id = id;
29 this.handle = `test_player_${id}`;
30 this.pos = { x: Math.random() * 10 - 5, y: 1.6, z: Math.random() * 10 - 5 };
31 this.rot = { x: 0, y: Math.random() * 360, z: 0 };
32 this.ws = null;
33 this.connected = false;
34 this.receivedMoves = 0;
35 this.wsId = null;
36 }
37
38 async connect() {
39 return new Promise((resolve, reject) => {
40 this.ws = new WebSocket(SESSION_URL);
41
42 const timeout = setTimeout(() => {
43 reject(new Error(`Player ${this.id} connection timeout`));
44 }, 10000);
45
46 this.ws.on("open", () => {
47 clearTimeout(timeout);
48 this.connected = true;
49 console.log(`✅ Player ${this.id} (${this.handle}) WebSocket connected`);
50 });
51
52 this.ws.on("message", (data) => {
53 try {
54 const msg = JSON.parse(data.toString());
55
56 // Handle connection acknowledgment
57 if (msg.type === "connected" || msg.type === "connected:already") {
58 this.wsId = msg.id;
59 console.log(`🆔 Player ${this.id} assigned WebSocket ID: ${this.wsId}`);
60
61 // Send join message
62 this.send("1v1:join", {
63 handle: this.handle,
64 pos: this.pos,
65 rot: this.rot,
66 health: 100
67 });
68 resolve();
69 }
70
71 // Handle moves from other players
72 if (msg.type === "1v1:move" && msg.id !== this.wsId) {
73 this.receivedMoves++;
74 if (this.receivedMoves <= 3 || this.receivedMoves % 100 === 0) {
75 console.log(`📨 Player ${this.id} received move from ${msg.id}:`,
76 `pos(${msg.content.pos.x.toFixed(2)}, ${msg.content.pos.y.toFixed(2)}, ${msg.content.pos.z.toFixed(2)})`);
77 }
78 }
79
80 // Handle join from other players
81 if (msg.type === "1v1:join" && msg.id !== this.wsId) {
82 console.log(`👋 Player ${this.id} sees ${msg.content.handle} joined`);
83 }
84 } catch (e) {
85 // Ignore non-JSON messages
86 }
87 });
88
89 this.ws.on("error", (err) => {
90 console.error(`❌ Player ${this.id} WebSocket error:`, err.message);
91 reject(err);
92 });
93
94 this.ws.on("close", () => {
95 this.connected = false;
96 console.log(`🔌 Player ${this.id} disconnected`);
97 });
98 });
99 }
100
101 send(type, content) {
102 if (!this.connected || !this.ws) return;
103 this.ws.send(JSON.stringify({ type, content }));
104 }
105
106 sendPosition() {
107 if (!this.connected) return;
108
109 // Simulate movement
110 this.pos.x += (Math.random() - 0.5) * 0.1;
111 this.pos.z += (Math.random() - 0.5) * 0.1;
112 this.rot.y += Math.random() * 2;
113
114 this.send("1v1:move", {
115 pos: this.pos,
116 rot: this.rot
117 });
118 }
119
120 disconnect() {
121 if (this.ws) {
122 this.ws.close();
123 this.connected = false;
124 }
125 }
126
127 getStats() {
128 return {
129 id: this.id,
130 handle: this.handle,
131 wsId: this.wsId,
132 connected: this.connected,
133 receivedMoves: this.receivedMoves
134 };
135 }
136}
137
138async function runTest() {
139 // Create fake players
140 for (let i = 0; i < NUM_FAKE_PLAYERS; i++) {
141 players.push(new FakePlayer(i));
142 }
143
144 // Connect all players
145 console.log("🔗 Connecting players...\n");
146 try {
147 await Promise.all(players.map(p => p.connect()));
148 } catch (err) {
149 console.error("❌ Failed to connect all players:", err.message);
150 process.exit(1);
151 }
152
153 console.log("\n✅ All players connected!\n");
154 console.log("📤 Starting position updates...\n");
155
156 // Start sending position updates
157 const updateInterval = setInterval(() => {
158 players.forEach(p => p.sendPosition());
159 }, UPDATE_INTERVAL_MS);
160
161 // Run for 5 seconds then print stats
162 await new Promise(resolve => setTimeout(resolve, 5000));
163
164 clearInterval(updateInterval);
165
166 console.log("\n📊 Test Results:");
167 console.log("================");
168 players.forEach(p => {
169 const stats = p.getStats();
170 console.log(`Player ${stats.id} (${stats.handle}): ${stats.receivedMoves} moves received`);
171 });
172
173 // Calculate expected moves
174 const expectedMovesPerPlayer = (5000 / UPDATE_INTERVAL_MS) * (NUM_FAKE_PLAYERS - 1);
175 console.log(`\n📈 Expected moves per player: ~${expectedMovesPerPlayer}`);
176
177 const avgReceived = players.reduce((sum, p) => sum + p.getStats().receivedMoves, 0) / players.length;
178 const successRate = (avgReceived / expectedMovesPerPlayer * 100).toFixed(1);
179 console.log(`📈 Average received: ${avgReceived.toFixed(0)} (${successRate}% success rate)`);
180
181 // Cleanup
182 console.log("\n🧹 Cleaning up...");
183 players.forEach(p => p.disconnect());
184
185 console.log("✅ Test complete!\n");
186 process.exit(0);
187}
188
189// Handle errors
190process.on("unhandledRejection", (err) => {
191 console.error("❌ Unhandled error:", err);
192 process.exit(1);
193});
194
195runTest();