Reference implementation for the Phoenix Architecture. Work in progress. aicoding.leaflet.pub/
ai coding crazy
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 416 lines 13 kB view raw
1import { createServer, IncomingMessage, ServerResponse } from 'node:http'; 2import { createHash } from 'node:crypto'; 3import { EventEmitter } from 'node:events'; 4 5export interface Player { 6 id: string; 7 connected: boolean; 8 lastSeen: number; 9} 10 11export interface GameState { 12 grid: number[][]; 13 players: Map<string, Player>; 14 gameEndTime: number; 15} 16 17export interface ConnectionMessage { 18 type: 'init' | 'player_update'; 19 data: { 20 playerId?: string; 21 grid?: number[][]; 22 players?: Array<{ id: string; connected: boolean }>; 23 remainingTime?: number; 24 playerCount?: number; 25 }; 26} 27 28export interface WebSocketLike { 29 send(data: string): void; 30 close(): void; 31 on(event: 'message' | 'close', handler: (data?: any) => void): void; 32} 33 34export class ConnectionHandler extends EventEmitter { 35 private server: ReturnType<typeof createServer>; 36 private connections = new Map<string, WebSocketLike>(); 37 private players = new Map<string, Player>(); 38 private gameState: GameState; 39 private cleanupInterval: NodeJS.Timeout; 40 private port: number; 41 42 constructor(port = 3000) { 43 super(); 44 this.port = port; 45 this.gameState = { 46 grid: Array(10).fill(null).map(() => Array(10).fill(0)), 47 players: this.players, 48 gameEndTime: Date.now() + 300000, // 5 minutes default 49 }; 50 51 this.server = createServer(this.handleHttpRequest.bind(this)); 52 this.cleanupInterval = setInterval(this.cleanupDisconnectedPlayers.bind(this), 1000); 53 } 54 55 private handleHttpRequest(req: IncomingMessage, res: ServerResponse): void { 56 if (req.method === 'GET' && req.url === '/') { 57 res.writeHead(200, { 'Content-Type': 'text/html' }); 58 res.end(this.generateGameUI()); 59 return; 60 } 61 62 if (req.url === '/ws' && req.headers.upgrade === 'websocket') { 63 this.handleWebSocketUpgrade(req, res); 64 return; 65 } 66 67 res.writeHead(404); 68 res.end('Not Found'); 69 } 70 71 private handleWebSocketUpgrade(req: IncomingMessage, res: ServerResponse): void { 72 const key = req.headers['sec-websocket-key']; 73 if (!key) { 74 res.writeHead(400); 75 res.end('Bad Request'); 76 return; 77 } 78 79 const acceptKey = this.generateWebSocketAcceptKey(key); 80 res.writeHead(101, { 81 'Upgrade': 'websocket', 82 'Connection': 'Upgrade', 83 'Sec-WebSocket-Accept': acceptKey, 84 }); 85 86 const socket = res.socket; 87 if (!socket) return; 88 89 const playerId = this.generatePlayerId(); 90 const wsConnection = this.createWebSocketConnection(socket, playerId); 91 92 this.connections.set(playerId, wsConnection); 93 this.addPlayer(playerId); 94 this.sendInitialState(wsConnection, playerId); 95 this.broadcastPlayerCount(); 96 } 97 98 private generateWebSocketAcceptKey(key: string): string { 99 const magic = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; 100 return createHash('sha1').update(key + magic).digest('base64'); 101 } 102 103 private createWebSocketConnection(socket: any, playerId: string): WebSocketLike { 104 const connection = { 105 send: (data: string) => { 106 const frame = this.createWebSocketFrame(data); 107 socket.write(frame); 108 }, 109 close: () => { 110 socket.end(); 111 }, 112 on: (event: string, handler: (data?: any) => void) => { 113 if (event === 'message') { 114 socket.on('data', (buffer: Buffer) => { 115 const message = this.parseWebSocketFrame(buffer); 116 if (message) handler(message); 117 }); 118 } else if (event === 'close') { 119 socket.on('close', handler); 120 socket.on('end', handler); 121 } 122 } 123 }; 124 125 connection.on('close', () => { 126 this.handlePlayerDisconnect(playerId); 127 }); 128 129 return connection; 130 } 131 132 private createWebSocketFrame(data: string): Buffer { 133 const payload = Buffer.from(data, 'utf8'); 134 const payloadLength = payload.length; 135 136 let frame: Buffer; 137 if (payloadLength < 126) { 138 frame = Buffer.allocUnsafe(2 + payloadLength); 139 frame[0] = 0x81; // FIN + text frame 140 frame[1] = payloadLength; 141 payload.copy(frame, 2); 142 } else if (payloadLength < 65536) { 143 frame = Buffer.allocUnsafe(4 + payloadLength); 144 frame[0] = 0x81; 145 frame[1] = 126; 146 frame.writeUInt16BE(payloadLength, 2); 147 payload.copy(frame, 4); 148 } else { 149 frame = Buffer.allocUnsafe(10 + payloadLength); 150 frame[0] = 0x81; 151 frame[1] = 127; 152 frame.writeUInt32BE(0, 2); 153 frame.writeUInt32BE(payloadLength, 6); 154 payload.copy(frame, 10); 155 } 156 157 return frame; 158 } 159 160 private parseWebSocketFrame(buffer: Buffer): string | null { 161 if (buffer.length < 2) return null; 162 163 const firstByte = buffer[0]; 164 const secondByte = buffer[1]; 165 166 if ((firstByte & 0x80) === 0) return null; // Not final frame 167 168 const opcode = firstByte & 0x0f; 169 if (opcode !== 0x01) return null; // Not text frame 170 171 const masked = (secondByte & 0x80) === 0x80; 172 let payloadLength = secondByte & 0x7f; 173 let offset = 2; 174 175 if (payloadLength === 126) { 176 if (buffer.length < 4) return null; 177 payloadLength = buffer.readUInt16BE(2); 178 offset = 4; 179 } else if (payloadLength === 127) { 180 if (buffer.length < 10) return null; 181 payloadLength = buffer.readUInt32BE(6); 182 offset = 10; 183 } 184 185 if (masked) { 186 if (buffer.length < offset + 4 + payloadLength) return null; 187 const maskKey = Buffer.from(buffer.subarray(offset, offset + 4)); 188 offset += 4; 189 const payload = Buffer.from(buffer.subarray(offset, offset + payloadLength)); 190 191 for (let i = 0; i < payload.length; i++) { 192 payload[i] ^= maskKey[i % 4]; 193 } 194 195 return payload.toString('utf8'); 196 } else { 197 if (buffer.length < offset + payloadLength) return null; 198 return Buffer.from(buffer.subarray(offset, offset + payloadLength)).toString('utf8'); 199 } 200 } 201 202 private generatePlayerId(): string { 203 let id: string; 204 do { 205 id = Math.random().toString(16).substring(2, 8).padStart(6, '0'); 206 } while (this.players.has(id)); 207 return id; 208 } 209 210 private addPlayer(playerId: string): void { 211 const player: Player = { 212 id: playerId, 213 connected: true, 214 lastSeen: Date.now(), 215 }; 216 this.players.set(playerId, player); 217 this.emit('playerJoined', playerId); 218 } 219 220 private handlePlayerDisconnect(playerId: string): void { 221 const player = this.players.get(playerId); 222 if (player) { 223 player.connected = false; 224 player.lastSeen = Date.now(); 225 } 226 this.connections.delete(playerId); 227 } 228 229 private cleanupDisconnectedPlayers(): void { 230 const now = Date.now(); 231 const toRemove: string[] = []; 232 233 for (const [playerId, player] of this.players) { 234 if (!player.connected && now - player.lastSeen > 5000) { 235 toRemove.push(playerId); 236 } 237 } 238 239 if (toRemove.length > 0) { 240 for (const playerId of toRemove) { 241 this.players.delete(playerId); 242 this.emit('playerLeft', playerId); 243 } 244 this.broadcastPlayerCount(); 245 } 246 } 247 248 private sendInitialState(connection: WebSocketLike, playerId: string): void { 249 const message: ConnectionMessage = { 250 type: 'init', 251 data: { 252 playerId, 253 grid: this.gameState.grid, 254 players: Array.from(this.players.values()).map(p => ({ 255 id: p.id, 256 connected: p.connected, 257 })), 258 remainingTime: Math.max(0, this.gameState.gameEndTime - Date.now()), 259 }, 260 }; 261 connection.send(JSON.stringify(message)); 262 } 263 264 private broadcastPlayerCount(): void { 265 const connectedCount = Array.from(this.players.values()).filter(p => p.connected).length; 266 const message: ConnectionMessage = { 267 type: 'player_update', 268 data: { 269 playerCount: connectedCount, 270 }, 271 }; 272 273 const messageStr = JSON.stringify(message); 274 for (const connection of this.connections.values()) { 275 connection.send(messageStr); 276 } 277 } 278 279 private generateGameUI(): string { 280 return `<!DOCTYPE html> 281<html lang="en"> 282<head> 283 <meta charset="UTF-8"> 284 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 285 <title>Phoenix VCS Game</title> 286 <style> 287 body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background: #f0f0f0; } 288 .container { max-width: 800px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; } 289 .grid { display: grid; grid-template-columns: repeat(10, 40px); gap: 2px; margin: 20px 0; } 290 .cell { width: 40px; height: 40px; border: 1px solid #ccc; display: flex; align-items: center; justify-content: center; cursor: pointer; } 291 .cell:hover { background: #e0e0e0; } 292 .status { margin: 10px 0; padding: 10px; background: #f8f8f8; border-radius: 4px; } 293 .players { margin: 20px 0; } 294 .player { padding: 5px; margin: 2px 0; background: #e8f4f8; border-radius: 3px; } 295 </style> 296</head> 297<body> 298 <div class="container"> 299 <h1>Phoenix VCS Game</h1> 300 <div class="status"> 301 <div>Player ID: <span id="playerId">Connecting...</span></div> 302 <div>Players Online: <span id="playerCount">0</span></div> 303 <div>Time Remaining: <span id="timeRemaining">--:--</span></div> 304 </div> 305 <div class="grid" id="gameGrid"></div> 306 <div class="players"> 307 <h3>Players</h3> 308 <div id="playerList"></div> 309 </div> 310 </div> 311 <script> 312 const ws = new WebSocket('ws://localhost:${this.port}/ws'); 313 let gameState = { grid: [], players: [], remainingTime: 0 }; 314 315 ws.onmessage = (event) => { 316 const message = JSON.parse(event.data); 317 if (message.type === 'init') { 318 document.getElementById('playerId').textContent = message.data.playerId; 319 gameState.grid = message.data.grid; 320 gameState.players = message.data.players; 321 gameState.remainingTime = message.data.remainingTime; 322 updateUI(); 323 } else if (message.type === 'player_update') { 324 document.getElementById('playerCount').textContent = message.data.playerCount; 325 } 326 }; 327 328 function updateUI() { 329 updateGrid(); 330 updatePlayerList(); 331 updateTimer(); 332 } 333 334 function updateGrid() { 335 const grid = document.getElementById('gameGrid'); 336 grid.innerHTML = ''; 337 for (let i = 0; i < 10; i++) { 338 for (let j = 0; j < 10; j++) { 339 const cell = document.createElement('div'); 340 cell.className = 'cell'; 341 cell.textContent = gameState.grid[i][j] || ''; 342 grid.appendChild(cell); 343 } 344 } 345 } 346 347 function updatePlayerList() { 348 const list = document.getElementById('playerList'); 349 list.innerHTML = ''; 350 gameState.players.forEach(player => { 351 const div = document.createElement('div'); 352 div.className = 'player'; 353 div.textContent = player.id + (player.connected ? ' (online)' : ' (offline)'); 354 list.appendChild(div); 355 }); 356 } 357 358 function updateTimer() { 359 const minutes = Math.floor(gameState.remainingTime / 60000); 360 const seconds = Math.floor((gameState.remainingTime % 60000) / 1000); 361 document.getElementById('timeRemaining').textContent = 362 minutes.toString().padStart(2, '0') + ':' + seconds.toString().padStart(2, '0'); 363 } 364 365 setInterval(() => { 366 if (gameState.remainingTime > 0) { 367 gameState.remainingTime -= 1000; 368 updateTimer(); 369 } 370 }, 1000); 371 </script> 372</body> 373</html>`; 374 } 375 376 public listen(): Promise<void> { 377 return new Promise((resolve) => { 378 this.server.listen(this.port, () => { 379 resolve(); 380 }); 381 }); 382 } 383 384 public close(): Promise<void> { 385 return new Promise((resolve) => { 386 clearInterval(this.cleanupInterval); 387 for (const connection of this.connections.values()) { 388 connection.close(); 389 } 390 this.server.close(() => { 391 resolve(); 392 }); 393 }); 394 } 395 396 public getPlayerCount(): number { 397 return Array.from(this.players.values()).filter(p => p.connected).length; 398 } 399 400 public getPlayers(): Player[] { 401 return Array.from(this.players.values()); 402 } 403 404 public updateGameState(grid: number[][], gameEndTime: number): void { 405 this.gameState.grid = grid; 406 this.gameState.gameEndTime = gameEndTime; 407 } 408} 409 410/** @internal Phoenix VCS traceability — do not remove. */ 411export const _phoenix = { 412 iu_id: '287d727e4c54fc45b5ea5e2484392a34a4b0750386cdb6f88404dcff44b70aa3', 413 name: 'Connection Handling', 414 risk_tier: 'medium', 415 canon_ids: [6 as const], 416} as const;