127 lines
5.5 KiB
TypeScript
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"],
|
|
);
|
|
});
|