Coup de l'ordinateur : animation, traces et bouton Revoir

Le domaine donne le dernier coup demi-coup par demi-coup (plateaux
intermédiaires du moteur, pions frappés lus sur la barre) ; l'interface
fait glisser les pions puis laisse flèches, pions entourés et fantômes
jusqu'à l'action suivante. Scénario « Voir le coup de l'ordinateur »,
tests unitaires et test dans le navigateur.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Pierre MARTIN
2026-11-03 01:53:26 +01:00
co-authored by Claude Opus 5.5
parent 9c241de5c9
commit 7a9b376417
12 changed files with 851 additions and 101 deletions
+91
View File
@@ -130,6 +130,77 @@ async function waitForPlayer() {
throw new Error(`rien à faire pour le joueur depuis 60 s : ${screen}`);
}
/** Le dernier coup de l'ordinateur, s'il est la dernière chose jouée (fin de
* partie mise à part) et qu'il a déplacé des pions ; sinon null. */
function computerMoveToShow(journal) {
const last = journal.events.filter((e) => e.type !== "GameEnded").at(-1);
return last?.type === "MovePlayed" && last.side === "computer" && last.halfMoves.length > 0
? last
: null;
}
/** Ce que montre le plateau du coup de l'ordinateur. */
async function shownTraces() {
return page.evaluate(() => ({
arrows: document.querySelectorAll(".trace-arrow").length,
landed: document.querySelectorAll(".trace-landed").length,
ghosts: document.querySelectorAll(".trace-ghost").length,
hits: document.querySelectorAll(".trace-hit").length,
replay: !document.getElementById("replay").hidden,
}));
}
/** Après un tour de l'ordinateur : ses traces sont là si son coup est la
* dernière chose jouée, et rien sinon. */
async function checkTraces() {
const move = computerMoveToShow(await savedJournal());
const traces = await shownTraces();
if (move === null) {
check(traces.arrows + traces.landed + traces.ghosts === 0 && !traces.replay,
`traces sans coup de l'ordinateur à montrer : ${JSON.stringify(traces)}`);
return null;
}
check(traces.arrows === move.halfMoves.length && traces.landed > 0 && traces.ghosts > 0 &&
traces.replay, `coup de l'ordinateur mal montré : ${JSON.stringify(traces)}`);
return traces;
}
/** Observe la page pendant ms millisecondes : le plus grand nombre
* d'animations en cours à la fois, et le plus petit nombre de flèches. */
async function watchReplay(ms) {
let animations = 0;
let arrows = Infinity;
for (const end = Date.now() + ms; Date.now() < end; ) {
const seen = await page.evaluate(() => ({
animations: document.getAnimations().length,
arrows: document.querySelectorAll(".trace-arrow").length,
}));
animations = Math.max(animations, seen.animations);
arrows = Math.min(arrows, seen.arrows);
await new Promise((resolve) => setTimeout(resolve, 40));
}
return { animations, arrows };
}
/** « Revoir » : rejoue le coup (sans animation en mouvement réduit), puis
* remet les mêmes traces. */
async function checkReplay(traces, reducedMotion) {
await page.emulateMedia({ reducedMotion: reducedMotion ? "reduce" : "no-preference" });
await page.click("#replay");
const seen = await watchReplay(300);
if (reducedMotion) {
check(seen.animations === 0, "Revoir en mouvement réduit : des pions glissent quand même");
} else {
check(seen.animations > 0 && seen.arrows === 0,
`Revoir : pas d'animation sans traces (${JSON.stringify(seen)})`);
}
await waitForPlayer();
const after = await shownTraces();
check(JSON.stringify(after) === JSON.stringify(traces),
`traces différentes après Revoir : ${JSON.stringify(after)}`);
await page.emulateMedia({ reducedMotion: "no-preference" });
}
// Garde-fou : une étape bloquée plus de deux minutes arrête tout, avec une
// capture et l'état de l'écran.
let lastStep = Date.now();
@@ -157,10 +228,28 @@ let doubled = false;
let selectionShot = false;
let reloaded = false;
let computerMs = [];
let tracesSeen = 0;
let replays = 0;
for (let actions = 0; ; actions++) {
check(actions < 800, "la partie ne finit pas");
const todo = await waitForPlayer();
step(`partie, action ${actions}`);
// Le coup de l'ordinateur est montré jusqu'à l'action suivante du
// joueur ; deux fois, on le revoit (la seconde en mouvement réduit).
const traces = await checkTraces();
if (traces !== null) {
tracesSeen++;
if (replays < 2) {
replays++;
await checkReplay(traces, replays === 2);
if (replays === 1) {
await shot("coup-adverse");
}
log(`coup de l'ordinateur revu${replays === 2 ? " en mouvement réduit" : ""} : ` +
`${traces.arrows} flèches, ${traces.landed} pions entourés, ` +
`${traces.ghosts} fantômes, ${traces.hits} pions frappés signalés`);
}
}
if (process.env.TRACE) {
log(`${todo} : ${await page.textContent("#info")} / ${await page.textContent("#prompt")}`);
}
@@ -223,6 +312,8 @@ log(
);
check(countMoves(journal, "player") >= 3 && countMoves(journal, "computer") >= 3, "trop peu de coups");
check(reloaded, "la reprise après rechargement n'a pas été essayée");
check(replays === 2, "le coup de l'ordinateur n'a pas été revu");
log(`coup de l'ordinateur montré ${tracesSeen} fois, et effacé à chaque action suivante`);
if (computerMs.length > 0) {
log(`temps affichés des tours de l'ordinateur : max ${Math.max(...computerMs)} ms, ` +
`${computerMs.length} tours`);