From b8c2997ec0cd67543dc04ad25dac6930616ea1cf Mon Sep 17 00:00:00 2001 From: Pierre MARTIN Date: Tue, 3 Nov 2026 15:24:03 +0100 Subject: [PATCH] =?UTF-8?q?Recette=20design=20:=20consigne=20en=20texte=20?= =?UTF-8?q?nu,=20cases=20d'arriv=C3=A9e=20en=20triangle=20et=20anneau,=20f?= =?UTF-8?q?l=C3=A8ches=20sous=20les=20pions,=20panneau=20de=20fin=20struct?= =?UTF-8?q?ur=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- app/src/ui/app.ts | 48 ++++++++++++++++++------ app/src/ui/board-view.ts | 80 ++++++++++++++++++++++++++++++---------- app/src/ui/geometry.ts | 3 ++ app/src/ui/texts.ts | 20 +++++++--- app/tests/ui.test.ts | 10 ++--- tests/browser/play.mjs | 65 +++++++++++++++++++++++++++++++- web/app.css | 56 ++++++++++++++++++++++------ web/index.html | 4 +- 8 files changed, 232 insertions(+), 54 deletions(-) diff --git a/app/src/ui/app.ts b/app/src/ui/app.ts index 9c74002..475c3ab 100644 --- a/app/src/ui/app.ts +++ b/app/src/ui/app.ts @@ -345,6 +345,11 @@ export function startApp(options: AppOptions): void { const model = boardModel(sides); const action = busy ? null : diceAction(game, stuck); const text = promptFor(game, selected !== null, canDouble, stuck); + // Un panneau (fin de partie, double de l'ordinateur en paysage) + // remplace la consigne : elle ne la répète pas dessous. + const doubledByComputer = state.turn === "computer" && state.phase === "doubled"; + const panelShown = + (state.phase === "over" && !panelDismissed) || (landscape && doubledByComputer); if (!animating) { renderBoard(ui.board, g, model, { dice: shownDice(game, stuck), @@ -358,7 +363,8 @@ export function startApp(options: AppOptions): void { : moveTrace(shownMove.side, shownMove.halfMoves, shownMove.hits), diceAction: action, canDouble: !busy && canDouble, - band: landscape ? (text === null ? TEXTS.gameOver : text.short) : null, + band: + landscape && !panelShown ? (text === null ? TEXTS.gameOver : text.short) : null, controls: landscape ? controls() : null, }); } @@ -373,7 +379,11 @@ export function startApp(options: AppOptions): void { } else { ui.info.textContent = info; } - ui.prompt.textContent = text === null ? resultText(game) : promptText(text, g); + ui.prompt.textContent = panelShown + ? "" + : text === null + ? resultText(game) + : promptText(text, g); const thinking = state.turn === "computer" && isPlaying(game); ui.prompt.classList.toggle("thinking", thinking); ui.prompt.classList.toggle("neutral", thinking || state.phase === "over"); @@ -492,13 +502,20 @@ export function startApp(options: AppOptions): void { const over = g.state.phase === "over" && !panelDismissed; const ask = landscape && doubled; ui.panelVeil.hidden = !(over || ask); + const overline = byId("panel-overline"); + const detail = byId("panel-detail"); if (over) { - ui.panelText.textContent = resultText(g); + const end = endParts(g); + overline.textContent = TEXTS.endOverline; + ui.panelText.textContent = end.title; + detail.textContent = end.detail; ui.panelSecondary.textContent = TEXTS.reviewGame; ui.panelPrimary.textContent = TEXTS.newGame; ui.panelVeil.dataset.kind = "over"; } else if (ask) { + overline.textContent = ""; ui.panelText.textContent = TEXTS.prompts.computerDoubles.long; + detail.textContent = ""; ui.panelSecondary.textContent = TEXTS.drop; ui.panelPrimary.textContent = TEXTS.take; ui.panelVeil.dataset.kind = "doubled"; @@ -1263,18 +1280,27 @@ function describeComputerTurn(game: Game): string { return TEXTS.computerMoved(dice, formatHalfMoves(played.halfMoves)); } -/** Le résultat d'une partie finie. */ -function resultText(game: Game): string { +/** Le résultat d'une partie finie, en deux parties : le titre du panneau + * (« L'ordinateur gagne un gammon ») et son détail (« 8 points pour lui, + * videau à 4. »). */ +function endParts(game: Game): { title: string; detail: string } { const result = game.state.result; if (result === null) { - return TEXTS.gameOver; + return { title: TEXTS.gameOver, detail: "" }; } const { winner, kind, points } = result; - const dropped = game.events.find((e) => e.type === "CubeDropped"); - if (dropped !== undefined) { - return winner === "player" ? TEXTS.computerDropped(points) : TEXTS.youDropped(points); - } - return winner === "player" ? TEXTS.youWin(kind, points) : TEXTS.youLose(kind, points); + const dropped = game.events.some((e) => e.type === "CubeDropped"); + return { + title: TEXTS.endTitle(winner, kind, dropped), + detail: TEXTS.endDetail(winner, points, game.state.cube.value), + }; +} + +/** Le résultat d'une partie finie, sur une ligne (consigne, une fois le + * panneau fermé). */ +function resultText(game: Game): string { + const { title, detail } = endParts(game); + return `${title} : ${detail}`; } /** Ce que le joueur doit faire maintenant (null : partie finie). */ diff --git a/app/src/ui/board-view.ts b/app/src/ui/board-view.ts index 688743d..ca3b0a2 100644 --- a/app/src/ui/board-view.ts +++ b/app/src/ui/board-view.ts @@ -163,18 +163,26 @@ export function renderBoard( svg.dataset.layout = g.layout; drawFrame(svg, g); const hits = add(svg, "g", {}); + // Les pions, dessinés à part puis posés par-dessus les flèches du coup + // de l'ordinateur : une flèche ne cache jamais un pion. + const pieces = document.createElementNS(SVG_NS, "g"); for (const point of board.points) { - drawPoint(svg, g, hits, board, point.number, decor); + drawPoint(svg, pieces, g, hits, board, point.number, decor); } - drawBar(svg, g, hits, board, decor); - drawTray(svg, g, hits, board, decor); + drawBar(svg, pieces, g, hits, board, decor); + drawTray(svg, pieces, g, hits, board, decor); + if (decor.trace !== null) { + drawTraceArrows(svg, g, board, decor.trace); + } + svg.append(pieces); + drawLandings(svg, g, board, decor.targets); if (decor.band !== null) { drawBand(svg, g, decor.band); } drawDice(svg, g, decor.dice); const cube = drawCube(svg, g, decor.cube); if (decor.trace !== null) { - drawTrace(svg, g, board, decor.trace); + drawTraceRings(svg, g, board, decor.trace); } if (decor.diceAction !== null) { if (decor.diceAction === "roll" && g.layout === "portrait") { @@ -240,6 +248,7 @@ function drawFrame(svg: SVGSVGElement, g: Geometry): void { function drawPoint( svg: SVGSVGElement, + pieces: Element, g: Geometry, hits: Element, board: BoardModel, @@ -283,18 +292,16 @@ function drawPoint( }, String(number), ); + // Case d'arrivée possible : la flèche seule est voilée (son triangle) ; + // l'endroit où le pion se posera est marqué plus loin (drawLandings). if (decor.targets.has(point.place)) { - add(svg, "rect", { + add(svg, "polygon", { class: "target-zone", - x: left + 2, - y: top ? g.edge : middle, - width: g.pointWidth - 4, - height: middle - g.edge, - rx: 4, + points: `${left},${base} ${left + g.pointWidth},${base} ${center},${tip}`, }); } if (point.owner !== null) { - drawStack(svg, g, point.owner, point.count, point.place, decor); + drawStack(pieces, g, point.owner, point.count, point.place, decor); } addHit(hits, point.place, decor, { x: left, @@ -308,7 +315,7 @@ function drawPoint( * pour les montrer tous. Le pion du dessus porte son endroit dans * data-checker : c'est lui qu'on fait glisser quand un pion arrive. */ function drawStack( - svg: SVGSVGElement, + svg: Element, g: Geometry, side: Side, count: number, @@ -353,6 +360,7 @@ function drawStack( function drawBar( svg: SVGSVGElement, + pieces: Element, g: Geometry, hits: Element, board: BoardModel, @@ -362,7 +370,7 @@ function drawBar( // Les pions du joueur sur la barre entrent en haut à droite : on les met // dans la moitié haute, en partant du milieu ; ceux de l'ordinateur en bas. for (const side of ["player", "computer"] as const) { - drawStack(svg, g, side, board.bar[side], `bar-${side}`, decor); + drawStack(pieces, g, side, board.bar[side], `bar-${side}`, decor); } if (g.layout === "landscape") { drawChips(svg, g, board); @@ -407,6 +415,7 @@ function drawChips(svg: SVGSVGElement, g: Geometry, board: BoardModel): void { function drawTray( svg: SVGSVGElement, + pieces: Element, g: Geometry, hits: Element, board: BoardModel, @@ -417,7 +426,7 @@ function drawTray( const count = board.off[side]; for (let i = 0; i < count; i++) { const slab = addSlab( - svg, + pieces, g, `checker checker-${side}`, slotCenter(g, `off-${side}`, i), @@ -538,11 +547,16 @@ function drawControls( // ------------------------------------------------------ Coup de l'ordinateur /** - * Les traces d'un coup : un pion fantôme (contour seul) là où chaque pion + * Les traces d'un coup (sauf ses flèches, dessinées sous les pions) : un pion fantôme (contour seul) là où chaque pion * est parti, un cercle autour de chaque pion arrivé et de chaque pion frappé * (sur la barre), et une flèche par demi-coup. */ -function drawTrace(svg: SVGSVGElement, g: Geometry, board: BoardModel, trace: MoveTrace): void { +function drawTraceRings( + svg: SVGSVGElement, + g: Geometry, + board: BoardModel, + trace: MoveTrace, +): void { const side = trace.side; const R = g.radius; for (const [place, n] of trace.left) { @@ -564,14 +578,24 @@ function drawTrace(svg: SVGSVGElement, g: Geometry, board: BoardModel, trace: Mo for (let k = 0; k < trace.hits && top - k >= 0; k++) { addRing(svg, g, "trace-hit", bar, slotCenter(g, bar, top - k), R + 3); } +} + +/** Les flèches d'un coup, une par demi-coup : sous les pions. */ +function drawTraceArrows( + svg: SVGSVGElement, + g: Geometry, + board: BoardModel, + trace: MoveTrace, +): void { + const side = trace.side; const defs = add(svg, "defs", {}); const marker = add(defs, "marker", { id: "trace-head", viewBox: "0 0 10 10", refX: 8, refY: 5, - markerWidth: 3.5, - markerHeight: 3.5, + markerWidth: 3, + markerHeight: 3, orient: "auto", }); add(marker, "path", { class: "trace-head", d: "M0,0 L10,5 L0,10 z" }); @@ -583,7 +607,22 @@ function drawTrace(svg: SVGSVGElement, g: Geometry, board: BoardModel, trace: Mo departures.set(from, k + 1); const start = slotCenter(g, from, countAt(board, side, from) + k); const end = slotCenter(g, to, Math.max(topIndex(g, board, side, to), 0)); - drawArrow(svg, start, end, to.startsWith("off-") ? OFF_SLAB : R + 4); + drawArrow(svg, g, start, end, to.startsWith("off-") ? OFF_SLAB : g.radius + 4); + } +} + +/** Les cases d'arrivée du pion touché : un anneau pointillé là où il se + * posera (sur la pile du joueur, ou au pied de la flèche). */ +function drawLandings( + svg: SVGSVGElement, + g: Geometry, + board: BoardModel, + targets: ReadonlySet, +): void { + for (const place of targets) { + const count = countAt(board, "player", place); + const index = place.startsWith("off-") ? count : Math.min(count, MAX_STACK - 1); + addRing(svg, g, "landing", place, slotCenter(g, place, index), g.radius); } } @@ -594,7 +633,7 @@ function topIndex(g: Geometry, board: BoardModel, side: Side, place: Place): num } /** Une flèche de start vers end, arrêtée à gap du centre d'arrivée. */ -function drawArrow(svg: SVGSVGElement, start: Spot, end: Spot, gap: number): void { +function drawArrow(svg: SVGSVGElement, g: Geometry, start: Spot, end: Spot, gap: number): void { const dx = end.x - start.x; const dy = end.y - start.y; const length = Math.hypot(dx, dy); @@ -605,6 +644,7 @@ function drawArrow(svg: SVGSVGElement, start: Spot, end: Spot, gap: number): voi y1: start.y, x2: start.x + dx * keep, y2: start.y + dy * keep, + "stroke-width": g.arrowWidth, "marker-end": "url(#trace-head)", }); } diff --git a/app/src/ui/geometry.ts b/app/src/ui/geometry.ts index 1fe5799..bcacc61 100644 --- a/app/src/ui/geometry.ts +++ b/app/src/ui/geometry.ts @@ -56,6 +56,8 @@ export interface Geometry { readonly cube: number; readonly cubeFont: number; readonly countFont: number; + /** Épaisseur des flèches du coup de l'ordinateur : 2,5 px à l'écran. */ + readonly arrowWidth: number; readonly die: number; /** Nombre de pions dessinés au plus sur la barre, par camp (au-delà, un * nombre sur le dernier) : en paysage, ils s'arrêtent avant les @@ -90,6 +92,7 @@ export function boardGeometry(layout: Layout, aspect: number, pxPerUnit: number) cubeFont: text, cube: Math.max(36, text + 20), countFont: Math.max(18, text), + arrowWidth: 2.5 / pxPerUnit, }; if (layout === "portrait") { // Les mesures du plateau d'origine : flèches de 44, pions de 20. diff --git a/app/src/ui/texts.ts b/app/src/ui/texts.ts index 85eb0e3..182ca99 100644 --- a/app/src/ui/texts.ts +++ b/app/src/ui/texts.ts @@ -134,12 +134,22 @@ export const TEXTS = { } satisfies Record PromptText)>, // Fin de partie - youWin: (kind: ResultKind, n: number) => `Vous gagnez ${RESULT_KIND[kind]} : ${points(n)}.`, - youLose: (kind: ResultKind, n: number) => - `L'ordinateur gagne ${RESULT_KIND[kind]} : ${points(n)} pour lui.`, - computerDropped: (n: number) => `L'ordinateur passe : vous gagnez ${points(n)}.`, - youDropped: (n: number) => `Vous passez : l'ordinateur gagne ${points(n)}.`, gameOver: "Partie finie", + endOverline: "Fin de partie", + /** Titre du panneau de fin : qui gagne, et comment. */ + endTitle: (winner: "player" | "computer", kind: ResultKind, dropped: boolean) => { + if (dropped) { + return winner === "player" ? "L'ordinateur passe" : "Vous passez"; + } + return winner === "player" + ? `Vous gagnez ${RESULT_KIND[kind]}` + : `L'ordinateur gagne ${RESULT_KIND[kind]}`; + }, + /** « 8 points pour lui, videau à 4. » */ + endDetail: (winner: "player" | "computer", n: number, cube: number) => + `${points(n)} pour ${winner === "player" ? "vous" : "lui"}${ + cube > 1 ? `, videau à ${cube}` : "" + }.`, // Aide helpTitle: "Aide", diff --git a/app/tests/ui.test.ts b/app/tests/ui.test.ts index f33e07e..9afc9aa 100644 --- a/app/tests/ui.test.ts +++ b/app/tests/ui.test.ts @@ -207,11 +207,11 @@ test("la partie enregistrée se relit ; un enregistrement illisible est ignoré" }); test("les textes de fin de partie emploient les mots du glossaire", () => { - assert.equal(TEXTS.youWin("gammon", 2), "Vous gagnez un gammon : 2 points."); - assert.equal( - TEXTS.youLose("single", 1), - "L'ordinateur gagne la partie simple : 1 point pour lui.", - ); + assert.equal(TEXTS.endTitle("computer", "gammon", false), "L'ordinateur gagne un gammon"); + assert.equal(TEXTS.endDetail("computer", 8, 4), "8 points pour lui, videau à 4."); + assert.equal(TEXTS.endTitle("player", "single", false), "Vous gagnez la partie simple"); + assert.equal(TEXTS.endDetail("player", 1, 1), "1 point pour vous."); + assert.equal(TEXTS.endTitle("player", "single", true), "L'ordinateur passe"); assert.equal( TEXTS.computerMoved([6, 4], "24/18 13/9"), "L'ordinateur a joué 6-4 : 24/18 13/9.", diff --git a/tests/browser/play.mjs b/tests/browser/play.mjs index 7a469ab..89a8245 100644 --- a/tests/browser/play.mjs +++ b/tests/browser/play.mjs @@ -308,6 +308,7 @@ async function checkView(width, height, when) { scroll: [scroller.scrollWidth, scroller.scrollHeight], topbar: visible(document.querySelector(".topbar")), overscroll: getComputedStyle(document.documentElement).overscrollBehaviorY, + promptBackground: getComputedStyle(document.getElementById("prompt")).backgroundColor, pointHits: [...document.querySelectorAll('#board [data-place^="p"]')].map( (el) => el.getBoundingClientRect().width, ), @@ -326,6 +327,8 @@ async function checkView(width, height, when) { `${where} : couleurs des flèches ${JSON.stringify(seen.fills)}`); check(seen.primaries.length <= 1, `${where} : plusieurs actions principales : ${seen.primaries}`); check(seen.overscroll === "none", `${where} : overscroll-behavior ${seen.overscroll}`); + check(/^(transparent|rgba\(0, 0, 0, 0\))$/.test(seen.promptBackground), + `${where} : la consigne a un fond (${seen.promptBackground})`); const landscape = width > height && height <= 640; check(seen.landscape === landscape, `${where} : mise en page ${seen.landscape ? "paysage" : "portrait"}`); if (landscape) { @@ -439,6 +442,21 @@ async function checkTraces() { } check(traces.arrows === move.halfMoves.length && traces.landed > 0 && traces.ghosts > 0 && traces.replay, `coup de l'ordinateur mal montré : ${JSON.stringify(traces)}`); + // Flèches de 2,5 px, sous les pions. + const arrows = await page.evaluate(() => { + const firstChecker = document.querySelector("#board .checker"); + return [...document.querySelectorAll("#board .trace-arrow")].map((line) => { + const m = line.getScreenCTM(); + return { + px: parseFloat(getComputedStyle(line).strokeWidth) * Math.hypot(m.a, m.b), + under: Boolean(line.compareDocumentPosition(firstChecker) & + Node.DOCUMENT_POSITION_FOLLOWING), + }; + }); + }); + check(arrows.every((a) => Math.abs(a.px - 2.5) < 0.2 && a.under), + `flèches du coup de l'ordinateur : ${JSON.stringify(arrows)}`); + arrowsChecked++; return traces; } @@ -575,7 +593,9 @@ await page.waitForSelector("#board polygon[data-point]"); checkers: document.querySelectorAll("#board .checker").length, info: document.getElementById("info").textContent, moves: document.querySelectorAll(".hit.can-move, [data-action=roll]").length, + prompt: document.getElementById("prompt").textContent, })); + check(loading.prompt === "", `chargement : consigne affichée « ${loading.prompt} »`); check(loading.busy && loading.points === 24 && loading.checkers === 30, `chargement : plateau incomplet ${JSON.stringify(loading)}`); check(loading.info === "Chargement du moteur…", `chargement : « ${loading.info} »`); @@ -619,6 +639,9 @@ let confirms = 0; let undone = 0; let doubles = 0; let responseMs = 0; +let confirmButtonSeen = 0; +let landingsSeen = 0; +let arrowsChecked = 0; const moments = new Set(); // Vrai dès que le joueur a touché un pion ou « Annuler » pendant son coup : // le coup de l'ordinateur n'est plus montré. @@ -811,6 +834,20 @@ for (let actions = 0; ; actions++) { const dice = await shownDice(); check(dice.used === halfMoves && dice.used > 0, `dés estompés : ${dice.used} pour ${halfMoves} demi-coups`); + // « Jouer ce coup » : l'action principale de la barre, à droite. + const bar = await page.evaluate(() => { + const confirm = document.getElementById("confirm"); + const cancel = document.getElementById("cancel"); + return { + text: confirm.textContent, + primary: !confirm.classList.contains("secondary"), + right: confirm.getBoundingClientRect().x > cancel.getBoundingClientRect().x, + inBar: confirm.closest("#actions .slot.primary") !== null, + }; + }); + check(await page.isVisible("#confirm") && bar.text === "Jouer ce coup" && bar.primary && + bar.right && bar.inBar, `coup complet : « Jouer ce coup » mal placé ${JSON.stringify(bar)}`); + confirmButtonSeen++; if (undone === 0 && halfMoves >= 2) { // Une fois : « Annuler » défait les demi-coups un par un. if (confirms === 0) { @@ -883,6 +920,17 @@ for (let actions = 0; ; actions++) { responseMs = Math.max(responseMs, ms); check(ms < 100, `toucher d'un pion : pas d'effet visible en moins de 100 ms (${ms})`); if ((await page.locator(".hit.can-land").count()) > 0) { + // Cases d'arrivée : le triangle de la flèche voilé, et un anneau + // pointillé là où le pion se posera. + const marks = await page.evaluate(() => ({ + lands: document.querySelectorAll(".hit.can-land").length, + rings: document.querySelectorAll("#board .landing").length, + zones: [...document.querySelectorAll("#board .target-zone")].map((z) => z.tagName), + })); + check(marks.rings === marks.lands && + marks.zones.filter((t) => t === "polygon").length >= marks.lands - 1, + `cases d'arrivée mal marquées : ${JSON.stringify(marks)}`); + landingsSeen++; twoTouches++; if (!selectionShot) { selectionShot = true; @@ -963,7 +1011,13 @@ for (let actions = 0; ; actions++) { // --------------------------------------------------------- Fin de partie const journal = await savedJournal(); -const result = await page.textContent("#panel-text"); +const result = `${await page.textContent("#panel-text")}, ${await page.textContent("#panel-detail")}`; +check((await page.textContent("#panel-overline")) === "Fin de partie" && + /^(Vous gagnez|L'ordinateur gagne|Vous passez|L'ordinateur passe)/.test( + await page.textContent("#panel-text")) && + /^\d+ points? pour (vous|lui)(, videau à \d+)?\.$/.test(await page.textContent("#panel-detail")), + `panneau de fin de partie : ${result}`); +check((await page.textContent("#prompt")) === "", "fin de partie : la consigne répète le panneau"); log( `partie finie : ${result} (${countMoves(journal, "player")} coups du joueur, ` + `${countMoves(journal, "computer")} de l'ordinateur, ${journal.events.length} événements)`, @@ -975,6 +1029,11 @@ check((await page.textContent("#panel-secondary")) === "Revoir la partie" && (await page.textContent("#panel-primary")) === "Nouvelle partie", "fin de partie : panneau sans « Revoir la partie » et « Nouvelle partie »"); await review("6-fin-de-partie"); +await page.setViewportSize({ width: 844, height: 320 }); +await sleep(150); +check((await page.locator("#band-text").count()) === 0, "fin de partie : consigne sous le panneau en paysage"); +await page.setViewportSize(PORTRAIT); +await sleep(150); check(countMoves(journal, "player") >= 3 && countMoves(journal, "computer") >= 3, "trop peu de coups"); check(reloaded && resumed, "rechargement ou reprise de l'ancienne partie non essayés"); check(replays === 2, "le coup de l'ordinateur n'a pas été revu"); @@ -986,6 +1045,10 @@ check(singleTouch > 0 && twoTouches > 0, check(undone >= 2, "Annuler n'a pas été essayé sur un coup complet"); check(confirms >= 3, "trop peu de coups joués pour essayer dés et bouton"); check(moments.size === 2 && selectionShot, `moments de la recette manquants : ${[...moments]}`); +check(confirmButtonSeen >= 3 && landingsSeen > 0 && arrowsChecked > 0, + "« Jouer ce coup », cases d'arrivée ou flèches non vérifiés"); +log(`« Jouer ce coup » vu ${confirmButtonSeen} fois à droite de la barre d'actions ; ` + + `cases d'arrivée marquées ${landingsSeen} fois ; flèches vérifiées sur ${arrowsChecked} coups`); log(`dés lancés ${rolls} fois (${keyboardRolls} au clavier), dont la moitié en les touchant`); log(`demi-coups : ${singleTouch} d'un seul toucher, ${twoTouches} en deux touchers ; ` + `${confirms} coups joués ; ${doubles} doubles montrés en quatre dés`); diff --git a/web/app.css b/web/app.css index 870f146..d9e5310 100644 --- a/web/app.css +++ b/web/app.css @@ -135,7 +135,7 @@ --topbar-height: 60px; --strip-height: 44px; --infoline-height: 44px; - --prompt-height: 74px; + --prompt-height: 50px; --actions-height: 76px; color-scheme: light; @@ -579,23 +579,21 @@ button:disabled { color: var(--accent); } -/* La consigne du moment : ce qui doit ressortir de l'écran. Hauteur fixe - * (deux lignes) ; fond d'accent léger quand c'est au joueur. */ +/* La consigne du moment : ce qui doit ressortir de l'écran. Texte nu, + * 17 px gras, sur une hauteur réservée de deux lignes ; rien quand il n'y a + * rien à dire. La teinte « c'est à vous » est celle du bandeau du joueur. */ .prompt { flex: none; height: var(--prompt-height); - margin: 0; - padding: var(--space-3) var(--space-4); + margin: var(--space-2) 0 0; + padding: 0 12px; overflow: hidden; font-weight: 700; - background: var(--accent-soft); - border-radius: var(--radius-md); } -.prompt.neutral, -body.busy .prompt { +.prompt.neutral { font-weight: 400; - background: var(--surface-sunken); + color: var(--muted); } /* L'ordinateur réfléchit : trois points qui apparaissent l'un après @@ -798,6 +796,32 @@ body.busy .prompt { font-weight: 700; } +.panel p:empty { + display: none; +} + +/* Fin de partie : surtitre, titre de 24 px, puis le détail des points. */ +.panel .overline { + margin: 0 0 var(--space-1); + font-size: 13px; + line-height: 16px; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--muted); +} + +.panel .panel-title { + margin-bottom: var(--space-1); + font-size: 24px; + line-height: 30px; +} + +.panel .panel-detail { + font-size: 17px; + line-height: 25px; + font-weight: 400; +} + .panel-actions { display: flex; gap: var(--space-2); @@ -979,15 +1003,25 @@ body.busy .prompt { /* Le dernier coup de l'ordinateur (couleur laiton, charte) : une flèche par * demi-coup, les pions arrivés et frappés entourés, un pion fantôme là où * un pion est parti. */ +/* Épaisseur des flèches : attribut stroke-width (2,5 px à l'écran, + * geometry.ts) ; elles passent sous les pions. */ .trace-arrow { stroke: var(--last-move); - stroke-width: 5; stroke-linecap: round; opacity: 0.85; } .trace-head { fill: var(--last-move); } +/* Où le pion touché se posera : un anneau pointillé, comme les fantômes du + * coup de l'ordinateur, en brique. */ +.landing { + fill: none; + stroke: var(--highlight); + stroke-width: 3; + stroke-dasharray: 6 5; +} + .trace-landed, .trace-hit { fill: none; diff --git a/web/index.html b/web/index.html index 07b7d42..ada1f9f 100644 --- a/web/index.html +++ b/web/index.html @@ -116,7 +116,9 @@ sur un voile. -->