180 lines
5.1 KiB
TypeScript
180 lines
5.1 KiB
TypeScript
// 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<SnakeWebSession> {
|
|
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();
|
|
},
|
|
};
|
|
}
|