diff --git a/app/src/ui/app.ts b/app/src/ui/app.ts index 425037f..f060594 100644 --- a/app/src/ui/app.ts +++ b/app/src/ui/app.ts @@ -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 { - 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 diff --git a/tests/browser/play.mjs b/tests/browser/play.mjs index 08ebff2..4589407 100644 --- a/tests/browser/play.mjs +++ b/tests/browser/play.mjs @@ -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();