// L'adaptateur du moteur WebAssembly pour les niveaux, l'analyse et la // chance (étape 1a), sur le vrai moteur. La concordance des nombres avec // gnubg de bureau est vérifiée par tests/test_wasm_reference.sh ; ici, la // traduction des réponses en types du domaine. // // Copyright (C) 2026 les auteurs d'Ustagammon. // Licence : GPLv3 ou ultérieure (voir LICENSE). import assert from "node:assert/strict"; import { test } from "node:test"; import { fileURLToPath } from "node:url"; import { type EngineCube, LUCK_THRESHOLDS, SKILL_THRESHOLDS } from "../src/domain/game/engine.js"; import { START_POSITION } from "../src/domain/game/position.js"; import { loadNodeEngine } from "./support/node-engine.js"; const centered: EngineCube = { value: 1, owner: "centered" }; const middlegame = "mM/gATCw5+ABMA"; test("ouverture 3-1 : 24/23 13/10 est une erreur très mauvaise, 8/5 6/5 le meilleur coup", async () => { const engine = await loadNodeEngine(); const analysis = await engine.analyseMove( START_POSITION, centered, [3, 1], "4HPwATDgc+IBKA", 2, ); assert.ok(analysis !== null); assert.equal(analysis.moveCount, 16); assert.equal(analysis.rank, 2); assert.equal(analysis.played.notation, "24/23 13/10"); assert.equal(analysis.best.notation, "8/5 6/5"); assert.equal(analysis.skill, "veryBad"); assert.ok( Math.abs(analysis.equityLoss - (analysis.played.equity - analysis.best.equity)) < 1e-6, ); assert.ok(analysis.equityLoss < -SKILL_THRESHOLDS.veryBad); }); test("sans coup possible, il n'y a rien à analyser", async () => { const engine = await loadNodeEngine(); const position = "27YBBwAcz8EDQA"; assert.equal(await engine.analyseMove(position, centered, [5, 5], position, 0), null); }); test("double/prend : passer est une erreur de l'adversaire, prendre n'en est pas une", async () => { const engine = await loadNodeEngine(); const position = "6O5uAAC4uzsAAA"; const pass = await engine.analyseCube(position, centered, "pass", 2); assert.equal(pass.analysed, true); assert.equal(pass.skill, "bad"); assert.ok(pass.equities !== null && pass.equities.doubleTake < pass.equities.doublePass); const take = await engine.analyseCube(position, centered, "take", 2); assert.deepEqual([take.skill, take.equityLoss], ["none", 0]); const opponentOwns = await engine.analyseCube( position, { value: 2, owner: "opponent" }, "noDouble", 2, ); assert.deepEqual(opponentOwns, { analysed: false, equities: null, equityLoss: 0, skill: "none", }); }); test("la chance d'un jet et sa catégorie", async () => { const engine = await loadNodeEngine(); const opening = await engine.luck(START_POSITION, centered, [3, 1]); assert.ok(Math.abs(opening.value - 0.2126) < 2e-3); assert.equal(opening.rating, "none"); const doubleSix = await engine.luck(START_POSITION, centered, [6, 6]); assert.ok(doubleSix.value > LUCK_THRESHOLDS.lucky); assert.equal(doubleSix.rating, "lucky"); }); // Régression : sans cache vidé à chaque appel (engine.c, start_evaluation), // une analyse à 2 plis changeait la chance calculée ensuite (évaluations à // 0 pli des réseaux d'élagage restées dans le cache de gnubg). test("un résultat ne dépend pas des calculs faits avant", async () => { const engine = await loadNodeEngine(); const before = await engine.luck(middlegame, centered, [6, 4]); await engine.analyseCube(middlegame, centered, "noDouble", 2); await engine.analyseMove(middlegame, centered, [6, 4], "mM/gATDGzuABMA", 2); assert.deepEqual(await engine.luck(middlegame, centered, [6, 4]), before); }); test("le niveau débutant joue toujours le même coup, différent du meilleur", async () => { const engine = await loadNodeEngine(); for (let i = 0; i < 3; i++) { const move = await engine.levelMove(middlegame, centered, [6, 4], "beginner"); assert.equal(move?.notation, "24/20 24/18"); } assert.equal((await engine.levelMove(middlegame, centered, [6, 4], "2ply"))?.notation, "24/14"); const cube = await engine.levelCubeDecision("6O5uAAC4uzsAAA", centered, "beginner"); assert.equal(cube.double, false); }); test("les seuils du domaine sont ceux du moteur", async () => { const root = new URL("../../../", import.meta.url); const module = await import(new URL("build/wasm/engine.mjs", root).href); const wrapper = await import(new URL("web/engine-wasm.mjs", root).href); const raw = await wrapper.loadEngine( module.default, fileURLToPath(new URL("build/wasm/", root)), ); const { result } = raw.run(["levels"]) as { result: { levels: { name: string }[]; skill_thresholds: { doubtful: number; bad: number; very_bad: number }; luck_thresholds: { lucky: number; very_lucky: number }; }; }; const close = (a: number, b: number) => Math.abs(a - b) < 1e-6; assert.ok(close(result.skill_thresholds.doubtful, SKILL_THRESHOLDS.doubtful)); assert.ok(close(result.skill_thresholds.bad, SKILL_THRESHOLDS.bad)); assert.ok(close(result.skill_thresholds.very_bad, SKILL_THRESHOLDS.veryBad)); assert.ok(close(result.luck_thresholds.lucky, LUCK_THRESHOLDS.lucky)); assert.ok(close(result.luck_thresholds.very_lucky, LUCK_THRESHOLDS.veryLucky)); assert.deepEqual( result.levels.map((l) => l.name), ["beginner", "casual", "intermediate", "advanced", "0ply", "1ply", "2ply", "3ply", "4ply"], ); });