Recette design : consigne en texte nu, cases d'arrivée en triangle et anneau, flèches sous les pions, panneau de fin structuré
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
ebaa3b0020
commit
b8c2997ec0
+37
-11
@@ -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). */
|
||||
|
||||
+60
-20
@@ -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<Place>,
|
||||
): 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)",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
+15
-5
@@ -134,12 +134,22 @@ export const TEXTS = {
|
||||
} satisfies Record<string, PromptText | ((dice: readonly number[]) => 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",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
+64
-1
@@ -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`);
|
||||
|
||||
+45
-11
@@ -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;
|
||||
|
||||
+3
-1
@@ -116,7 +116,9 @@
|
||||
sur un voile. -->
|
||||
<div id="panel-veil" class="veil" hidden>
|
||||
<div id="panel" class="panel" role="dialog" aria-modal="true" aria-labelledby="panel-text">
|
||||
<p id="panel-text"></p>
|
||||
<p id="panel-overline" class="overline"></p>
|
||||
<p id="panel-text" class="panel-title"></p>
|
||||
<p id="panel-detail" class="panel-detail"></p>
|
||||
<div class="panel-actions">
|
||||
<button id="panel-secondary" type="button" class="secondary"></button>
|
||||
<button id="panel-primary" type="button"></button>
|
||||
|
||||
Reference in New Issue
Block a user