// file: Web/game-snake-poc/frontend/ts/game.ts // version: 3 import init, { SnakeWasmGame } from "@snake-wasm"; import { webDebug, webInfo } from "./logging"; import { BrowserRuntimeProvenance, type SnakeInputSource } from "./provenance"; const FRAME_MILLIS = 16; const MAX_FRAME_ELAPSED_MILLIS = 250; export type SnakeDirection = "left" | "right" | "up" | "down"; export interface SnakeWebSession { queueDirection(direction: SnakeDirection, source: SnakeInputSource): void; pause(): void; resume(): void; renderNow(): void; refreshDeviceClass(): void; dispose(): void; } function color(red: number, green: number, blue: number): string { return `rgb(${red} ${green} ${blue})`; } function resizeCanvas(canvas: HTMLCanvasElement): void { const ratio = window.devicePixelRatio || 1; const width = Math.max(1, Math.floor(canvas.clientWidth * ratio)); const height = Math.max(1, Math.floor(canvas.clientHeight * ratio)); if (canvas.width !== width || canvas.height !== height) { canvas.width = width; canvas.height = height; } } function render( game: SnakeWasmGame, canvas: HTMLCanvasElement, context: CanvasRenderingContext2D, score: HTMLOutputElement, length: HTMLOutputElement, ): void { resizeCanvas(canvas); context.fillStyle = color(game.background_red(), game.background_green(), game.background_blue()); context.fillRect(0, 0, canvas.width, canvas.height); const count = game.rectangle_count(); for (let index = 0; index < count; index += 1) { context.fillStyle = color(game.rectangle_red(index), game.rectangle_green(index), game.rectangle_blue(index)); context.fillRect( game.rectangle_x(index) * canvas.width, game.rectangle_y(index) * canvas.height, game.rectangle_width(index) * canvas.width, game.rectangle_height(index) * canvas.height, ); } score.value = `Score : ${game.score()}`; length.value = `Longueur : ${game.length()}`; } function queueDirection(game: SnakeWasmGame, direction: SnakeDirection): void { switch (direction) { case "left": game.left(); return; case "right": game.right(); return; case "up": game.up(); return; case "down": game.down(); return; } } export async function startGame( canvas: HTMLCanvasElement, score: HTMLOutputElement, length: HTMLOutputElement, provenanceOutput: HTMLOutputElement, ): Promise { await init(); webInfo("snake-web-runtime", "wasm_initialized"); const context = canvas.getContext("2d"); if (context === null) { throw new Error("Le contexte Canvas 2D est indisponible."); } const renderingContext: CanvasRenderingContext2D = context; const game = new SnakeWasmGame(); const provenance = new BrowserRuntimeProvenance(game, provenanceOutput); let previous = performance.now(); let accumulator = 0; let animationFrame: number | null = null; let running = false; let disposed = false; function renderNow(): void { if (disposed) { return; } render(game, canvas, renderingContext, score, length); } function schedule(): void { if (!running || disposed || animationFrame !== null) { return; } animationFrame = window.requestAnimationFrame(frame); } function frame(now: number): void { animationFrame = null; if (!running || disposed) { return; } const elapsed = Math.min(Math.max(0, now - previous), MAX_FRAME_ELAPSED_MILLIS); previous = now; accumulator += elapsed; while (accumulator >= FRAME_MILLIS) { game.tick(); accumulator -= FRAME_MILLIS; } renderNow(); schedule(); } function pause(): void { if (!running || disposed) { return; } running = false; accumulator = 0; if (animationFrame !== null) { window.cancelAnimationFrame(animationFrame); animationFrame = null; } webDebug("snake-web-runtime", "runtime_paused"); } function resume(): void { if (running || disposed) { return; } previous = performance.now(); accumulator = 0; running = true; schedule(); webDebug("snake-web-runtime", "runtime_resumed"); } renderNow(); resume(); return { queueDirection(direction: SnakeDirection, source: SnakeInputSource): void { if (disposed) { return; } provenance.recordInput(source); queueDirection(game, direction); }, pause, resume, renderNow, refreshDeviceClass(): void { if (!disposed) { provenance.refreshDeviceClass(); } }, dispose(): void { if (disposed) { return; } pause(); disposed = true; game.free(); }, }; }