Double toucher sans coup évident : un toucher simple ; tests navigateur adaptés aux raccourcis

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Pierre MARTIN
2026-11-12 10:26:30 +01:00
co-authored by Claude Opus 5.5
parent 0aec6cb7b1
commit 094f535c84
2 changed files with 66 additions and 34 deletions
+55 -32
View File
@@ -1719,45 +1719,44 @@ export function startApp(options: AppOptions): void {
}
/**
* Double toucher : joue le coup évident (le domaine le cherche, le
* moteur le juge) commencé au premier toucher, si le joueur n'a rien
* fait d'autre entre-temps. Le premier toucher a pu faire partir le
* Double toucher (charte, 1i) : joue le coup évident (le domaine le
* cherche, le moteur le juge) qui commence par le pion touché, depuis
* l'état du premier toucher. Ce premier toucher a pu faire partir le
* pion tout de suite (une seule case d'arrivée) : il ne reste alors que
* la suite du coup. Sinon, rien : le double toucher vaut un toucher.
* la suite du coup. Sans coup évident, le second toucher n'est qu'un
* toucher : touchers rapides sur une même pile (un double, par exemple)
* et pion repris (touché deux fois : plus rien de choisi).
* L'appelant a pris l'écran (busy) : le moteur est seul à travailler.
*/
async function playObvious(tap: ObviousTap): Promise<void> {
await whenIdle();
const g = game;
if (g !== tap.game || busy || g.state.phase !== "moving") {
return;
}
const now = g.pendingHalfMoves;
const now = g?.pendingHalfMoves ?? [];
const same = (a: HalfMove | undefined, b: HalfMove | undefined) =>
a !== undefined && b !== undefined && a.from === b.from && a.to === b.to;
if (now.length < tap.pending.length || !tap.pending.every((h, i) => same(h, now[i]))) {
return;
}
// Demandé seulement maintenant : un toucher simple ne coûte rien au
// moteur.
busy = true;
render();
let obvious: HalfMove[] | null = null;
try {
obvious = await g.obviousMove(pointOf(tap.place), tap.pending);
} catch {
obvious = null;
let rest: HalfMove[] = [];
if (
g === tap.game &&
g.state.phase === "moving" &&
now.length >= tap.pending.length &&
tap.pending.every((h, i) => same(h, now[i]))
) {
// Demandé seulement maintenant : un toucher simple ne coûte rien
// au moteur.
const obvious = await g.obviousMove(pointOf(tap.place), tap.pending).catch(() => null);
const done = now.slice(tap.pending.length);
if (obvious !== null && done.every((h, i) => same(h, obvious[i]))) {
rest = obvious.slice(done.length);
}
}
busy = false;
const done = now.slice(tap.pending.length);
const rest = obvious?.slice(done.length) ?? [];
const unchanged =
game === g &&
g.pendingHalfMoves.length === now.length &&
done.every((h, i) => same(h, obvious?.[i]));
if (!unchanged || rest.length === 0) {
if (game !== g || g === null) {
render();
return;
}
if (rest.length === 0) {
tapPlace(tap.place);
return;
}
void act(async (g) => {
info = "";
const before = (await g.displayedBoard()).computer.bar;
@@ -2673,12 +2672,18 @@ export function startApp(options: AppOptions): void {
: { game: current, place, pending: [...current.pendingHalfMoves] },
);
if (second !== null) {
void playObvious(second);
doubleTapped(second);
return;
}
if (busy) {
return;
if (!busy) {
tapPlace(place);
}
});
/** Un toucher simple sur un endroit du plateau : le pion choisi va sur
* cette case d'arrivée ; ou ce pion part tout de suite, s'il n'a qu'une
* case d'arrivée ; ou il est choisi (touché de nouveau : plus rien). */
function tapPlace(place: Place): void {
keyTarget = null;
const halfMove = selected === null ? undefined : targets.get(selected)?.get(place);
if (halfMove !== undefined) {
@@ -2695,7 +2700,25 @@ export function startApp(options: AppOptions): void {
shownMove = null; // toucher un pion : le coup de l'ordinateur s'efface
}
render();
});
}
/** Le second toucher d'un double toucher : l'écran le prend tout de
* suite (réponse immédiate), ou dès qu'il a fini l'effet du premier. */
function doubleTapped(tap: ObviousTap): void {
const claim = () => {
if (busy) {
return;
}
busy = true;
render();
void playObvious(tap);
};
if (busy) {
void whenIdle().then(claim);
} else {
claim();
}
}
/**
* Le plateau au clavier (interaction.md) : les flèches gauche et droite
+11 -2
View File
@@ -353,7 +353,8 @@ async function checkStable(when) {
}
/** Les boutons visibles d'une page dont le texte passe à la ligne : un
* bouton tient sur une ligne et fait 48 px de haut (charte). Un bouton plus
* bouton tient sur une ligne et fait 48 px de haut (charte), touche de son
* raccourci comprise. Un bouton plus
* haut que 48 px n'est refusé que si son texte occupe plusieurs lignes ;
* un texte coupé par le bord (white-space: nowrap) est refusé aussi. */
async function wrappedButtons(target) {
@@ -368,7 +369,9 @@ async function wrappedButtons(target) {
const tops = new Set();
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
for (let node = walker.nextNode(); node !== null; node = walker.nextNode()) {
if (node.textContent.trim() === "") {
// La touche du raccourci (kbd, quand un clavier est détecté)
// est un badge en coin, pas une ligne du libellé.
if (node.textContent.trim() === "" || node.parentElement?.closest("kbd.shortcut")) {
continue;
}
const range = document.createRange();
@@ -1193,6 +1196,7 @@ let resumed = false;
let landscapeTurn = false;
let rotatedChecked = false;
let vibrationsChecked = false;
let lastTouch = 0;
let tracesSeen = 0;
let replays = 0;
let skips = 0;
@@ -1496,9 +1500,14 @@ for (let actions = 0; ; actions++) {
check(!(await page.isVisible("#menu")), "Échap ne ferme pas le menu");
}
const vibrationsBefore = (await vibrations()).length;
// Deux touchers rapprochés sur la même pile seraient un double
// toucher (coup évident, tests/browser/plateau.mjs) : ici, un
// toucher à la fois, un demi-coup à la fois.
await sleep(Math.max(0, lastTouch + 500 - Date.now()));
// Un pion à destination unique avance dès qu'on le touche ; sinon,
// ses cases d'arrivée s'allument. Le premier toucher est mesuré :
// l'écran répond dans le gestionnaire du toucher.
lastTouch = Date.now();
const ms = await page.evaluate(() => {
const hit = document.querySelector(".hit.can-move");
const t0 = performance.now();