Files
Ustagammon/app/tests/engine-analysis.test.ts
T

127 lines
5.5 KiB
TypeScript

// 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"],
);
});