1
0
mirror of https://github.com/thib8956/tic-tac-toe-ws.git synced 2024-09-29 06:06:37 +00:00
tic-tac-toe-ws/client.mts
2024-09-25 15:34:59 +02:00

236 lines
7.3 KiB
TypeScript

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");
interface Point {
x: number;
y: number;
}
type Empty = undefined;
type Cell = Empty | Shape;
interface Shape {
kind: "o" | "x";
pos: Point;
hue: number;
time: number | null;
}
let grid: Cell[] = new Array(9);
let pendingEvts: Point[] = [];
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 drawAnimatedCircle(ctx: CanvasRenderingContext2D, dt: number, x: number, y: number, hue: number) {
const radius = SHAPE_SIZE / 2;
const end = dt*2*Math.PI/ANIMATE_DURATION;
ctx.save();
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.min(end, 2*Math.PI));
const percent = Math.trunc(100*Math.min(end, 2*Math.PI)/(2*Math.PI));
ctx.strokeStyle = `hsla(${hue}, ${percent}%, 50%, 1)`;
ctx.lineWidth = 5;
ctx.stroke();
ctx.restore();
}
function drawAnimatedCross(ctx: CanvasRenderingContext2D, dt: number, x: number, y: number, hue: number) {
const startPoint: Point = { x: x-SHAPE_SIZE/2, y: y-SHAPE_SIZE/2 };
const halfAnim = ANIMATE_DURATION/2;
ctx.save();
ctx.beginPath();
ctx.moveTo(startPoint.x, startPoint.y);
const delta = SHAPE_SIZE*dt/halfAnim;
if (delta < SHAPE_SIZE) { // draw \
const d = Math.min(delta, SHAPE_SIZE);
ctx.lineTo(startPoint.x + d, startPoint.y + d);
} else { // draw /
ctx.lineTo(startPoint.x + SHAPE_SIZE, startPoint.y + SHAPE_SIZE); // keep \ drawn
ctx.moveTo(startPoint.x + SHAPE_SIZE, startPoint.y);
const d = Math.min(delta - SHAPE_SIZE, SHAPE_SIZE);
ctx.lineTo(startPoint.x + SHAPE_SIZE - d, startPoint.y + d);
}
ctx.lineWidth = 5;
const percent = Math.trunc(100*Math.min(delta, SHAPE_SIZE)/SHAPE_SIZE);
ctx.strokeStyle = `hsla(${hue}, ${percent}%, 50%, 1)`;
ctx.stroke();
ctx.restore();
}
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, time: number, gridOrigin: Point) {
for (let y = 0; y < 3; ++y) {
for (let x = 0; x < 3; ++x) {
const shape = grid[y*3+x]
if (shape) {
if (shape.time === null) {
shape.time = time;
}
const p = gridIndexToCoords(gridOrigin, shape.pos.x, shape.pos.y);
const dt = time - shape.time;
switch (shape.kind) {
case "o": {
drawAnimatedCircle(ctx, dt, p.x, p.y, shape.hue);
break;
}
case "x": {
drawAnimatedCross(ctx, dt, p.x, p.y, shape.hue);
break;
}
default: 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, time, 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": {
const res = msg.data as Response;
const { x, y } = res.last;
const shape: Shape = {
kind: res.last.symbol,
pos: { x, y },
hue: Math.floor(Math.random() * 255),
time: null,
};
grid[y*3+x] = shape;
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();