Files
Ustagammon/web/engine-wasm.mjs
T

90 lines
3.6 KiB
JavaScript

// engine-wasm.mjs -- le moteur WebAssembly vu depuis JavaScript.
//
// Copyright (C) 2026 les auteurs d'Ustagammon.
// Licence : GPLv3 ou ultérieure (voir LICENSE).
//
// Le WebAssembly (build/wasm/, voir engine-wasm/wasm.mk) contient le moteur
// et le programme engine-cli. On charge le module une fois, on initialise le
// moteur, puis chaque commande est un appel à main avec les mêmes arguments
// qu'engine-cli en ligne de commande : la sortie est donc le même JSON.
//
// Utilisé par le Web Worker de la page (engine-worker.mjs) et, sous Node,
// par engine-wasm/engine-cli.mjs et engine-wasm/bench.mjs. Le moteur calcule
// sur le fil qui l'appelle : dans le navigateur, toujours dans un Worker.
// Dossier des données dans le système de fichiers virtuel (--preload-file).
const DATA_DIR = "/data";
// createEngineModule : la fonction exportée par build/wasm/engine.mjs.
// wasmDir : où trouver engine.wasm et engine.data, terminé par « / » (une
// adresse dans le navigateur, un chemin de fichier sous Node).
// Renvoie le moteur prêt, avec :
// - runText(args) : { text, exitCode }, la sortie brute d'engine-cli ;
// - run(args) : { result, ms }, le JSON décodé et le temps de calcul
// (lève une erreur si le moteur en signale une) ;
// - memoryBytes() : taille actuelle de la mémoire WebAssembly.
export async function loadEngine(createEngineModule, wasmDir) {
let output = [];
const module = await createEngineModule({
locateFile: (name) => wasmDir + name,
print: (line) => output.push(line),
printErr: (line) => output.push(line),
});
const status = module.ccall("engine_init", "number", ["string"], [DATA_DIR]);
if (status !== 0) {
throw new Error("le moteur n'a pas pu charger ses données (code " + status + ")");
}
// callMain copie les arguments sur la pile du WebAssembly sans jamais
// les retirer : on remet la pile en place après chaque commande, sinon
// elle déborde au bout de quelques centaines de commandes (environ 540
// pendant une partie) et le moteur plante ou ne répond plus.
function runText(args) {
output = [];
const stack = module.stackSave();
try {
const exitCode = module.callMain(args.map(String));
return { text: output.join("\n"), exitCode };
} finally {
module.stackRestore(stack);
}
}
function run(args) {
const start = performance.now();
const { text } = runText(args);
const ms = performance.now() - start;
const result = JSON.parse(text);
if (result.error) {
throw new Error(result.error.message);
}
return { result, ms };
}
return {
runText,
run,
memoryBytes: () => module.HEAPU8.length,
};
}
// Arguments d'engine-cli pour chaque fonction de l'interface. Les dés sont
// un tableau [3, 1] ou une chaîne « 31 ».
export const commands = {
bestMove: (positionId, matchId, dice, ply) =>
["--ply", ply, "bestmove", positionId, matchId, diceText(dice)],
cubeDecision: (positionId, matchId, ply) =>
["--ply", ply, "cube", positionId, matchId],
evaluate: (positionId, matchId, ply) =>
["--ply", ply, "eval", positionId, matchId],
analyseMove: (positionId, matchId, dice, playedPositionId, ply) =>
["--ply", ply, "analysemove", positionId, matchId, diceText(dice), playedPositionId],
reference: (positionId, matchId, dice, ply) =>
["--ply", ply, "reference", positionId, matchId, ...(dice ? [diceText(dice)] : [])],
};
function diceText(dice) {
return Array.isArray(dice) ? dice.join("") : String(dice);
}