diff --git a/client.ts b/client.ts index 984ae0c..84e8ad4 100644 --- a/client.ts +++ b/client.ts @@ -1,4 +1,4 @@ -import { Click, Update, Message, Hello, EndGame } from "common.js"; +import { Click, Update, Message, Hello, EndGame, Spectate } from "common.js"; const CELL_SIZE = 150; const GRID_SIZE = CELL_SIZE * 3; @@ -29,6 +29,7 @@ interface Shape { let grid: Cell[] = new Array(9); let pendingEvts: Point[] = []; +let spectate = false; // is this client in spectator mode? let myId: number | null = null; let mySymbol: "x" | "o" | null = null; let canvasMsg: string = "Offline..."; @@ -181,6 +182,10 @@ function init() { resizeCanvas(ctx); // Init canvas canvas.addEventListener("click", (evt) => { + if (spectate) { + console.debug("ignoring click in spectator mode"); + return; + } const {clientX, clientY} = evt; pendingEvts.push({x: clientX, y: clientY}); }); @@ -201,6 +206,21 @@ function init() { console.log(canvasMsg); break; } + case "spectate": { + spectate = true; + canvasMsg = "connected as spectator"; + // Initialize grid state + for (const [index, sym] of (msg.data as Spectate).grid.entries()) { + if (sym === undefined) continue; + grid[index] = { + kind: sym, + pos: { x: index % 3, y: Math.floor(index / 3) } as Point, + hue: Math.floor(Math.random() * 255), + time: null, + } as Shape; + } + break; + } case "update": { const res = msg.data as Update; const { x, y } = res.last; diff --git a/common.ts b/common.ts index 057591f..d4055ba 100644 --- a/common.ts +++ b/common.ts @@ -1,8 +1,8 @@ -export type MessageKind = "click" | "hello" | "update" | "endgame" | "reset"; +export type MessageKind = "click" | "hello" | "update" | "endgame" | "reset" | "spectate"; export interface Message { kind: MessageKind, - data: Click | Update | Hello | EndGame | Reset, + data: Click | Update | Hello | EndGame | Reset | Spectate, } export interface Click { @@ -10,8 +10,10 @@ export interface Click { y: number } +export type Symbol = "x" | "o"; + export interface Update { - last: { x: number, y: number, symbol: "x" | "o" } + last: { x: number, y: number, symbol: Symbol } } export interface Hello { @@ -25,3 +27,8 @@ export interface EndGame { type Reset = undefined; + +export interface Spectate { + grid: (Symbol | undefined)[] +} + diff --git a/server.ts b/server.ts index d415b2e..d25a758 100644 --- a/server.ts +++ b/server.ts @@ -1,4 +1,4 @@ -import { Message, Update, Hello, EndGame } from "common.js" +import { Message, Update, Hello, EndGame, Symbol } from "common.js" import { WebSocket, WebSocketServer, MessageEvent } from "ws"; const port = 1234 @@ -16,10 +16,11 @@ interface Client { } let id = 1; +let spectators: WebSocket[] = []; let clients: Client[] = []; let currentPlayer: Client | undefined = undefined; -function getPlayerSymbol(): "o" | "x" { +function getPlayerSymbol(): Symbol { console.assert(clients.length < 2, "there should never be more than 2 clients"); if (clients.length === 0) return "o"; return clients[0].symbol === "o" ? "x" : "o"; @@ -28,8 +29,23 @@ function getPlayerSymbol(): "o" | "x" { wss.on("connection", (ws, req) => { id += 1; if (clients.length === 2) { - console.log("too many players"); - ws.close(); + spectators.push(ws); + const spectateData: (Symbol | undefined)[] = []; + for (const playerId of grid) { + if (playerId === 0) { + spectateData.push(undefined); + } else { + const sym = clients.find(c => c.id === playerId)!.symbol; + spectateData.push(sym); + } + } + const spectateMsg: Message = { + kind: "spectate", + data: { grid: spectateData } + }; + ws.send(JSON.stringify(spectateMsg)); + const addr = req.headers["x-forwarded-for"] || req.socket.remoteAddress; + console.log(`new spectator connected with address ${addr}. total spectators ${spectators.length}`); return; } @@ -73,14 +89,19 @@ wss.on("connection", (ws, req) => { if (grid[y*3+x] === 0) { grid[y*3+x] = player.id; + const msg = JSON.stringify({ + kind: "update", + data: { + last: { x, y, symbol: player.symbol } + } as Update, + } as Message); + for (const c of clients) { - const msg: Message = { - kind: "update", - data: { - last: { x, y, symbol: player.symbol } - } as Update, - } - c.ws.send(JSON.stringify(msg)); + c.ws.send(msg); + } + + for (const s of spectators) { + s.send(msg); } const winnerId = checkWin(grid); @@ -89,13 +110,16 @@ wss.on("connection", (ws, req) => { console.assert(currentPlayer); console.log(`current player is #${currentPlayer?.id}`); } else if (winnerId == 0) { + endGame = true; + const msg = JSON.stringify({ + kind: "endgame", + data: { issue: "draw" } as EndGame + } as Message); for (const c of clients) { - const msg: Message = { - kind: "endgame", - data: { issue: "draw" } as EndGame - }; - c.ws.send(JSON.stringify(msg)); - endGame = true; + c.ws.send(msg); + } + for (const s of spectators) { + s.send(msg); } } else { console.log(`player ${winnerId} won !`); @@ -117,16 +141,21 @@ wss.on("connection", (ws, req) => { }); ws.on("close", (code: number) => { - clients = clients.filter(x => x.ws.readyState !== 3); // 3 == CLOSED - console.log(`player disconnected. Resetting game. Total clients ${clients.length}`); - // reset game state - grid = [0, 0, 0, 0, 0, 0, 0, 0, 0]; - currentPlayer = undefined; - endGame = false; - for (const c of clients) { - c.ws.send(JSON.stringify({ - kind: "reset" - } as Message)); + // readyState 3 == CLOSED + spectators = spectators.filter(s => s.readyState !== 3); + const isClientDisconnect = clients.findIndex(c => c.ws.readyState !== 3) + if (isClientDisconnect) { + clients = clients.filter(x => x.ws.readyState !== 3); + console.log(`player disconnected. Resetting game. Total clients ${clients.length}`); + // reset game state + grid = [0, 0, 0, 0, 0, 0, 0, 0, 0]; + currentPlayer = undefined; + endGame = false; + for (const c of clients) { + c.ws.send(JSON.stringify({ + kind: "reset" + } as Message)); + } } }); });