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 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 pendingEvts: Point[] = []; let shapes: Shape[] = []; let myId: number | null = null; let mySymbol: "x" | "o" | null = null; let canvasMsg: string = "Offline"; function drawGridBackground(ctx: CanvasRenderingContext2D, origin: Point) { ctx.strokeStyle = "white"; ctx.lineWidth = 5; 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(ws: WebSocket, gridOrigin: Point) { 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 = []; } 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 myId: { const p = gridIndexToCoords(gridOrigin, x, y); mySymbol == "o" ? drawCircle(ctx, p) : drawCross(ctx, p); break; } default: { const p = gridIndexToCoords(gridOrigin, x, y); mySymbol == "o" ? drawCross(ctx, p) : drawCircle(ctx, p); break; } } } } } // update loop, called every frame 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 }; ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); ctx.fillStyle = "white"; ctx.font = "24px sans-serif"; ctx.fillText(canvasMsg, 10, 30); drawGridBackground(ctx, gridOrigin); handlePendingEvts(ws, gridOrigin); 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; mySymbol = (msg.data as Hello).symbol; canvasMsg = `connected to server with id ${myId}, ${mySymbol}`; console.log(canvasMsg); break; } case "update": { grid = (msg.data as Response).grid; console.log(grid); break; } case "endgame": { const issue = (msg.data as EndGame).issue; switch (issue) { case "win": canvasMsg = "you won"; break; case "lose": canvasMsg = "you lose"; break; case "draw": canvasMsg = "it's a draw!"; break; default: throw new Error(`unexpected ${issue}`); } break; } default: { console.warn("unhandled message kind:", msg.kind); break; } } }; //window.addEventListener('resize', () => resizeCanvas(ctx)); window.requestAnimationFrame(t => update(ctx, t, ws)); } init();