diff --git a/client.ts b/client.ts index cf3c81d..1a37f37 100644 --- a/client.ts +++ b/client.ts @@ -31,6 +31,7 @@ let pendingEvts: Point[] = []; let isSpectator = false; // is this client in spectator mode? let myId: number | null = null; let mySymbol: "x" | "o" | null = null; +let animationId: number; let canvasMsg: string = "Offline..."; function drawGridBackground(ctx: CanvasRenderingContext2D, origin: Point) { @@ -43,7 +44,7 @@ function drawGridBackground(ctx: CanvasRenderingContext2D, origin: Point) { } for (let y = 1; y < 3; ++y) { - ctx.moveTo(origin.x + 0, origin.y + y * CELL_SIZE); + ctx.moveTo(origin.x, origin.y + y * CELL_SIZE); ctx.lineTo(origin.x + GRID_SIZE, origin.y + y * CELL_SIZE); } @@ -53,13 +54,13 @@ function drawGridBackground(ctx: CanvasRenderingContext2D, origin: Point) { function resizeCanvas(ctx: CanvasRenderingContext2D) { ctx.canvas.width = window.innerWidth; ctx.canvas.height = window.innerHeight; - ctx.clearRect(0, 0, ctx.canvas.height, ctx. canvas.width); + ctx.clearRect(0, 0, ctx.canvas.width, ctx. canvas.height); } function coordToGridIndex(origin: Point, clientPos: Point): [number, number] | undefined { // Coord relative to origin of the grid (origin) const pt = { x: clientPos.x - origin.x, y: clientPos.y - origin.y }; - const gridIndex: [number, number] = [Math.floor(3 * pt.x / GRID_SIZE), Math.floor(3 * pt.y / GRID_SIZE)]; + const gridIndex: [number, number] = [Math.floor(pt.x / CELL_SIZE), Math.floor(pt.y / CELL_SIZE)]; if (gridIndex[0] >= 0 && gridIndex[0] <= 2 && gridIndex[1] >= 0 && gridIndex[1] <= 2) { return gridIndex; } @@ -169,7 +170,7 @@ function update(ctx: CanvasRenderingContext2D, time: number, ws: WebSocket) { handlePendingEvts(ws, gridOrigin); updateGridState(ctx, time, gridOrigin); - window.requestAnimationFrame(t => update(ctx, t, ws)); + animationId = window.requestAnimationFrame(t => update(ctx, t, ws)); } function init() { @@ -190,12 +191,26 @@ function init() { }); // websocket stuff - ws.onopen = (e) => { + ws.onopen = () => { console.log("connected to websocket server"); }; + ws.onerror = () => { + canvasMsg = "Connection error!"; + }; + + ws.onclose = () => { + canvasMsg = "Disconnected"; + }; + ws.onmessage = (evt) => { - const msg: Message = JSON.parse(evt.data); + let msg: Message; + try { + msg = JSON.parse(evt.data); + } catch { + console.debug("received non-JSON message, ignoring"); + return; + } console.log(msg); switch (msg.kind) { case "hello": { @@ -260,6 +275,7 @@ function init() { window.addEventListener('resize', () => resizeCanvas(ctx)); window.requestAnimationFrame(t => update(ctx, t, ws)); + window.addEventListener('beforeunload', () => cancelAnimationFrame(animationId)); } init(); diff --git a/server.ts b/server.ts index cc38098..e1758f9 100644 --- a/server.ts +++ b/server.ts @@ -1,14 +1,15 @@ import type { Message, Update, Hello, EndGame, Symbol, SymbolWithHue } from "common.js" import { WebSocket, WebSocketServer, MessageEvent } from "ws"; -const port = 1234 -const wss = new WebSocketServer({ port }); +const PORT = 1234; +const GRID_SIZE = 3; +const wss = new WebSocketServer({ port: PORT }); let grid = [0, 0, 0, 0, 0, 0, 0, 0, 0]; let hues = [0, 0, 0, 0, 0, 0, 0, 0, 0]; let endGame = false; -console.log(`waiting for connection on ws://localhost:${port}`); +console.log(`waiting for connection on ws://localhost:${PORT}`); interface Client { id: number, @@ -36,7 +37,12 @@ wss.on("connection", (ws, req) => { if (playerId === 0) { spectateData.push(undefined); } else { - const sym = clients.find(c => c.id === playerId)!.symbol; + const client = clients.find(c => c.id === playerId); + if (!client) { + spectateData.push(undefined); + continue; + } + const sym = client.symbol; const hue = hues[i]; spectateData.push({ symbol: sym, hue: hue }); } @@ -55,15 +61,25 @@ wss.on("connection", (ws, req) => { const helloMsg: Message = { kind: "hello", data: { id, symbol } as Hello - } + }; clients.push({id, ws, symbol}); ws.send(JSON.stringify(helloMsg)); const addr = req.headers["x-forwarded-for"] || req.socket.remoteAddress; console.log(`player #${id} connected with address ${addr}. total clients ${clients.length}`); ws.addEventListener("message", (event: MessageEvent) => { - const message = JSON.parse(event.data as string); + let message; + try { + message = JSON.parse(event.data as string); + } catch { + console.warn("received non-JSON message, ignoring"); + return; + } const {x, y} = message; + if (x < 0 || x >= GRID_SIZE || y < 0 || y >= GRID_SIZE) { + console.warn("received invalid move, ignoring"); + return; + } const hue = Math.floor(Math.random() * 361); // hue is a value in degrees const player = clients.find(x => x.ws === ws); if (!player) throw new Error("player not found"); @@ -93,12 +109,12 @@ wss.on("connection", (ws, req) => { } } - if (clients.length < 2 || player.id != currentPlayer?.id || endGame) { + if (clients.length < 2 || player.id !== currentPlayer?.id || endGame) { return; } - if (grid[y*3+x] === 0) { - grid[y*3+x] = player.id; + if (grid[y * GRID_SIZE + x] === 0) { + grid[y * GRID_SIZE + x] = player.id; hues[y*3+x] = hue; const msg = JSON.stringify({ kind: "update", @@ -151,7 +167,7 @@ wss.on("connection", (ws, req) => { } }); - ws.on("close", (code: number) => { + ws.on("close", () => { spectators = spectators.filter(s => s.readyState !== WebSocket.CLOSED); const isClientDisconnect = clients.some(c => c.ws === ws); if (isClientDisconnect) {