diff --git a/client.mts b/client.mts index ab20fce..a414b1f 100644 --- a/client.mts +++ b/client.mts @@ -1,71 +1,80 @@ import { Request, Response, Message, Hello, EndGame } from "common.mjs"; +const CELL_SIZE = 150; +const GRID_SIZE = CELL_SIZE * 3; +const SHAPE_SIZE = 100; +const ANIMATE_DURATION = 500; // ms + + const ws = new WebSocket("ws://localhost:1234"); -const grid = [0, 0, 0, 0, 0, 0, 0, 0, 0] const canvas = document.getElementById("game") as HTMLCanvasElement | null; const ctx = canvas?.getContext("2d") as CanvasRenderingContext2D | null; +interface Point { + x: number; + y: number; +} + +interface Shape { + kind: "circle" | "cross"; + center: Point; + hue: number; + time: number; +} + +let grid = [0, 0, 0, 0, 0, 0, 0, 0, 0] +let circle = true; +let pendingEvts: Point[] = []; +let shapes: Shape[] = []; let myId: number | null = null; -ws.onopen = (e) => { - console.log("connected to server"); +function drawGridBackground(ctx: CanvasRenderingContext2D, origin: Point) { + ctx.strokeStyle = "white"; + ctx.lineWidth = 5; - if (canvas) { - if (ctx) { - drawGrid(ctx, grid); - canvas.addEventListener("click", (evt: any) => { - const {clientX, clientY} = evt; - const x = Math.floor(clientX / 90); - const y = Math.floor(clientY / 90); - if (x < 3 && y < 3) { - const msg: Request = { x, y }; - ws.send(JSON.stringify(msg)); - } - }); + for (let x = 1; x < 3; ++x) { + ctx.moveTo(origin.x + x * CELL_SIZE, 0 + origin.y); + ctx.lineTo(origin.x + x * CELL_SIZE, origin.y + GRID_SIZE); + } + + for (let y = 1; y < 3; ++y) { + ctx.moveTo(origin.x + 0, origin.y + y * CELL_SIZE); + ctx.lineTo(origin.x + GRID_SIZE, origin.y + y * CELL_SIZE); + } + + ctx.stroke(); +} + + +function resizeCanvas(ctx: CanvasRenderingContext2D) { + ctx.canvas.width = window.innerWidth; + ctx.canvas.height = window.innerHeight; +} + + +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)]; + if (gridIndex[0] >= 0 && gridIndex[0] <= 2 && gridIndex[1] >= 0 && gridIndex[1] <= 2) { + return gridIndex; + } + return undefined; +} + +function handlePendingEvts(gridOrigin: Point, ws: WebSocket) { + for (const evt of pendingEvts) { + const gridIndex = coordToGridIndex(gridOrigin, evt); + if (gridIndex) { + const [x, y] = gridIndex; + const msg: Request = { x, y }; + ws.send(JSON.stringify(msg)); } } -}; + pendingEvts = []; +} -ws.onmessage = (evt) => { - const msg: Message = JSON.parse(evt.data); - console.log(msg); - switch (msg.kind) { - case "hello": { - myId = (msg.data as Hello).id; - const h1 = document.getElementById("title") as HTMLHeadingElement | null; - if (h1) { - h1.innerText = `connected to server with id ${myId}` - } - break; - } - case "update": { - if (ctx) { - drawGrid(ctx, (msg.data as Response).grid); - } - break; - } - case "endgame": { - const h1 = document.getElementById("title") as HTMLHeadingElement | null; - if (h1) { - const issue = (msg.data as EndGame).issue; - switch (issue){ - case "win": h1.innerText = "you won"; break; - case "lose": h1.innerText = "you lose"; break; - case "draw": h1.innerText = "it's a draw!"; break; - default: throw new Error(`unexpected ${issue}`); - } - } - break; - } - default: { - console.log(msg); - break; - } - } -}; - - -function drawGrid(ctx: CanvasRenderingContext2D, grid: number[]){ +/*function drawGrid(ctx: CanvasRenderingContext2D, grid: number[]){ for (let y = 0; y < 3; ++y) { for (let x = 0; x < 3; ++x) { switch (grid[y*3+x]) { @@ -79,5 +88,109 @@ function drawGrid(ctx: CanvasRenderingContext2D, grid: number[]){ ctx.fillRect(rx, ry, 90, 90); } } +}*/ + +function drawCircle(ctx: CanvasRenderingContext2D, center: Point) { + const radius = SHAPE_SIZE/2; + ctx.beginPath(); + ctx.arc(center.x, center.y, radius, 0, Math.PI * 2); + ctx.stroke(); } +function drawCross(ctx: CanvasRenderingContext2D, center: Point) { + const startPoint = { x: center.x-SHAPE_SIZE/2, y: center.y-SHAPE_SIZE/2 }; + ctx.beginPath(); + ctx.moveTo(startPoint.x, startPoint.y); + ctx.lineTo(startPoint.x + 100, startPoint.y + 100); + ctx.moveTo(startPoint.x + 100, startPoint.y); + ctx.lineTo(startPoint.x, startPoint.y + 100); + ctx.stroke(); +} + +function gridIndexToCoords(gridOrigin: Point, x: number, y: number): Point { + const center = { + x: gridOrigin.x + x * CELL_SIZE + CELL_SIZE/2, + y: gridOrigin.y + y * CELL_SIZE + CELL_SIZE/2 + }; + return center; +} + +function updateGridState(ctx: CanvasRenderingContext2D, gridOrigin: Point) { + for (let y = 0; y < 3; ++y) { + for (let x = 0; x < 3; ++x) { + switch (grid[y*3+x]) { + case 0: break; + case 1: { + const p = gridIndexToCoords(gridOrigin, x, y); + drawCircle(ctx, p); + break; + + } + case 2: { + const p = gridIndexToCoords(gridOrigin, x, y); + drawCross(ctx, p); + break; + } + default: throw new Error(`unhandled grid state ${grid[y*3+x]}`); + } + } + } +} + +function update(ctx: CanvasRenderingContext2D, time: number, ws: WebSocket) { + const gridOrigin: Point = { + x: ctx.canvas.width / 2 - GRID_SIZE / 2, + y: ctx.canvas.height / 2 - GRID_SIZE / 2 + }; + + drawGridBackground(ctx, gridOrigin); + handlePendingEvts(gridOrigin, ws); + updateGridState(ctx, gridOrigin); + + window.requestAnimationFrame(t => update(ctx, t, ws)); +} + +function init() { + // canvas stuff + const canvas = document.getElementById("game") as HTMLCanvasElement | null; + if (!canvas) throw new Error("unable to get canvas HTML element"); + const ctx = canvas.getContext("2d") as CanvasRenderingContext2D | null; + if (!ctx) throw new Error("unable to get canvas 2D context"); + resizeCanvas(ctx); // Init canvas + + canvas.addEventListener("click", (evt) => { + const {clientX, clientY} = evt; + pendingEvts.push({x: clientX, y: clientY}); + }); + + // websocket stuff + ws.onopen = (e) => { + console.log("connected to websocket"); + }; + + ws.onmessage = (evt) => { + const msg: Message = JSON.parse(evt.data); + console.log(msg); + switch (msg.kind) { + case "hello": { + myId = (msg.data as Hello).id; + console.log(`connected to server with id ${myId}`); + break; + } + case "update": { + grid = (msg.data as Response).grid; + console.log(grid); + break; + } + default: { + console.warn("unhandled message kind:", msg.kind); + break; + } + } + }; + + //window.addEventListener('resize', () => resizeCanvas(ctx)); + window.requestAnimationFrame(t => update(ctx, t, ws)); +} + +init(); diff --git a/index.html b/index.html index 5414f2e..227d59e 100644 --- a/index.html +++ b/index.html @@ -3,9 +3,8 @@ Hello websockets - - -

Offline

+ + diff --git a/server.mts b/server.mts index 50821ff..07cd75e 100644 --- a/server.mts +++ b/server.mts @@ -10,12 +10,13 @@ console.log(`waiting for connection on ws://localhost:${port}`); interface Client { id: number, + symbol: "x" | "o", ws: WebSocket } let id = 1; let clients: Client[] = []; -let currentPlayerId: number | undefined = undefined; +let currentPlayer: Client | undefined = undefined; wss.on("connection", (ws) => { id += 1; @@ -25,65 +26,65 @@ wss.on("connection", (ws) => { return; } - if (!currentPlayerId) { - currentPlayerId = id; - console.log(`current player is #${currentPlayerId}`); - } - - clients.push({id, ws}); + const symbol = clients.length == 0 ? "o" : "x"; + clients.push({id, ws, symbol}); ws.send(JSON.stringify({kind: "hello", data: { id } as Hello})); console.log(`player #${id} connected`); ws.addEventListener("message", (event: any) => { const message = JSON.parse(event.data); const {x, y} = message; - const playerId = clients.find(x => x.ws === ws)?.id; - console.assert(playerId); - console.log(message, playerId, currentPlayerId); + const player = clients.find(x => x.ws === ws); + console.assert(player !== undefined); + console.log(message, player!.id, currentPlayer?.id); - if (playerId != currentPlayerId) { - return; - } + if (!currentPlayer) { + currentPlayer = player; + } + + if (clients.length < 2 || player!.id != currentPlayer?.id) { + return; + } if (grid[y*3+x] === 0) { - grid[y*3+x] = playerId as number; + grid[y*3+x] = player!.symbol == "o" ? 1 : 2; for (const c of clients) { const msg: Message = { kind: "update", - data: { grid } as Response, - } - c.ws.send(JSON.stringify(msg)); - } + data: { grid } as Response, + } + c.ws.send(JSON.stringify(msg)); + } - const winnerId = checkWin(grid); - if (winnerId == -1) { - currentPlayerId = clients.find(x => x.id !== currentPlayerId)?.id; // change player - console.assert(currentPlayerId); - console.log(`current player is #${currentPlayerId}`); - } else if (winnerId == 0) { - for (const c of clients) { - const msg: Message = { - kind: "endgame", - data: { issue: "draw" } as EndGame - }; - c.ws.send(JSON.stringify(msg)); - } - } else { - console.log(`player ${winnerId} won !`); + const winnerId = checkWin(grid); + if (winnerId == -1) { + currentPlayer = clients.find(x => x.id !== currentPlayer?.id); // change player + console.assert(currentPlayer); + console.log(`current player is #${currentPlayer?.id}`); + } else if (winnerId == 0) { + for (const c of clients) { + const msg: Message = { + kind: "endgame", + data: { issue: "draw" } as EndGame + }; + c.ws.send(JSON.stringify(msg)); + } + } else { + console.log(`player ${winnerId} won !`); - const winner = clients.find(x => x.id === winnerId); - winner?.ws?.send(JSON.stringify({ - kind: "endgame", - data: { issue: "win" } as EndGame - } as Message)); + const winner = clients.find(x => x.id === winnerId); + winner?.ws?.send(JSON.stringify({ + kind: "endgame", + data: { issue: "win" } as EndGame + } as Message)); - const loser = clients.find(x => x.id !== winnerId); - loser?.ws?.send(JSON.stringify({ - kind: "endgame", - data: { issue: "lose" } as EndGame - } as Message)); - } - } + const loser = clients.find(x => x.id !== winnerId); + loser?.ws?.send(JSON.stringify({ + kind: "endgame", + data: { issue: "lose" } as EndGame + } as Message)); + } + } }); ws.on("close", () => {