1
0
mirror of https://github.com/thib8956/tic-tac-toe-ws.git synced 2025-04-03 20:31:01 +00:00

Add spectator mode

This commit is contained in:
Thibaud Gasser 2025-01-29 23:43:33 +01:00
parent 6da3762502
commit 29a587c115
3 changed files with 87 additions and 31 deletions

View File

@ -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;

View File

@ -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)[]
}

View File

@ -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));
}
}
});
});