Recette design du match : Crawford en paysage, « Nouvelle partie » en deux colonnes, « Rejouer »
- Paysage : pendant la partie Crawford, la consigne de la bande centrale commence par « Partie Crawford · » ; la police se resserre si besoin. - Écran « Nouvelle partie » en paysage : deux colonnes au-dessus de la barre d'action (type de partie et longueur à gauche, niveau et conseil du maître à droite), tout tient en 915x412 et en 844x320. - Fin de partie : « Rejouer » relance tout de suite une partie aux mêmes réglages (un match de même longueur après un match), « Partie suivante » dans un match qui continue, lien « Changer les réglages ». - Recette automatique : aucun élément interactif visible sous une barre d'action collée, vérifié sur chaque écran capturé (tests/browser/recette.mjs). - Scénario « Comment rejouer avec les mêmes réglages ? ». Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
67bd207337
commit
74693c4277
+28
-11
@@ -245,6 +245,7 @@ export function startApp(options: AppOptions): void {
|
||||
panelVeil: byId("panel-veil"),
|
||||
panelText: byId("panel-text"),
|
||||
panelPrimary: byId<HTMLButtonElement>("panel-primary"),
|
||||
panelSettings: byId<HTMLAnchorElement>("panel-settings"),
|
||||
panelSecondary: byId<HTMLButtonElement>("panel-secondary"),
|
||||
banner: byId("banner"),
|
||||
bannerText: byId("banner-text"),
|
||||
@@ -318,7 +319,7 @@ export function startApp(options: AppOptions): void {
|
||||
ui.confirm.textContent = TEXTS.confirm;
|
||||
label(ui.cancel, TEXTS.cancel, "undo");
|
||||
ui.skip.textContent = TEXTS.skip;
|
||||
ui.again.textContent = TEXTS.newGame;
|
||||
ui.again.textContent = MATCH_TEXTS.replay;
|
||||
ui.moveHint.textContent = TEXTS.moveHint;
|
||||
ui.doubleConfirm.textContent = TEXTS.double;
|
||||
ui.doubleCancel.textContent = TEXTS.cancel;
|
||||
@@ -536,7 +537,11 @@ export function startApp(options: AppOptions): void {
|
||||
diceAction: action,
|
||||
canDouble: !busy && canDouble,
|
||||
band:
|
||||
landscape && !panelShown ? (text === null ? TEXTS.gameOver : text.short) : null,
|
||||
landscape && !panelShown
|
||||
? text === null
|
||||
? TEXTS.gameOver
|
||||
: bandText(text.short, state.match?.crawford === true)
|
||||
: null,
|
||||
controls: landscape ? controls() : null,
|
||||
});
|
||||
}
|
||||
@@ -730,6 +735,7 @@ export function startApp(options: AppOptions): void {
|
||||
detail.textContent = end.detail;
|
||||
ui.panelSecondary.textContent = TEXTS.reviewGame;
|
||||
ui.panelPrimary.textContent = afterGameLabel(g);
|
||||
ui.panelSettings.hidden = false;
|
||||
ui.panelVeil.dataset.kind = "over";
|
||||
} else if (ask) {
|
||||
overline.textContent = "";
|
||||
@@ -737,6 +743,7 @@ export function startApp(options: AppOptions): void {
|
||||
detail.textContent = "";
|
||||
ui.panelSecondary.textContent = TEXTS.drop;
|
||||
ui.panelPrimary.textContent = TEXTS.take;
|
||||
ui.panelSettings.hidden = true;
|
||||
ui.panelVeil.dataset.kind = "doubled";
|
||||
}
|
||||
ui.panelPrimary.disabled = busy;
|
||||
@@ -1019,14 +1026,11 @@ export function startApp(options: AppOptions): void {
|
||||
}
|
||||
|
||||
/** Après une partie finie : la partie suivante du match s'il continue,
|
||||
* sinon l'écran « Nouvelle partie ». */
|
||||
* sinon « Rejouer », une nouvelle partie aux mêmes réglages (un match de
|
||||
* même longueur après un match), tout de suite. */
|
||||
function afterGame(): void {
|
||||
const next = game?.nextGame ?? null;
|
||||
if (next !== null) {
|
||||
newGame(next);
|
||||
} else {
|
||||
closeOverlays();
|
||||
navigation.go(routeHash({ screen: "newGame" }));
|
||||
if (game !== null) {
|
||||
newGame(game.nextGame ?? game.rematch);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1304,6 +1308,13 @@ export function startApp(options: AppOptions): void {
|
||||
afterGame();
|
||||
}
|
||||
};
|
||||
// « Changer les réglages » : un lien vers l'écran « Nouvelle partie » ;
|
||||
// le panneau se ferme, le résultat reste sur l'écran de jeu.
|
||||
ui.panelSettings.textContent = MATCH_TEXTS.changeSettings;
|
||||
ui.panelSettings.addEventListener("click", () => {
|
||||
panelDismissed = true;
|
||||
render();
|
||||
});
|
||||
ui.panelSecondary.onclick = () => {
|
||||
if (ui.panelVeil.dataset.kind === "doubled") {
|
||||
actions.drop();
|
||||
@@ -2153,10 +2164,16 @@ function matchScore(state: GameState): MatchScore | null {
|
||||
return { length: state.match.length, computer: score.computer, player: score.player };
|
||||
}
|
||||
|
||||
/** Paysage : la consigne de la bande centrale ; pendant la partie
|
||||
* Crawford, elle le dit d'abord (la ligne du match n'y est pas). */
|
||||
function bandText(prompt: string, crawford: boolean): string {
|
||||
return crawford && prompt !== "" ? MATCH_TEXTS.crawfordPrompt(prompt) : prompt;
|
||||
}
|
||||
|
||||
/** Le bouton qui suit une partie finie : « Partie suivante » dans un match
|
||||
* qui continue, sinon « Nouvelle partie » (l'écran des choix). */
|
||||
* qui continue, sinon « Rejouer » (mêmes réglages). */
|
||||
function afterGameLabel(game: Game): string {
|
||||
return game.nextGame === null ? TEXTS.newGame : MATCH_TEXTS.nextGame;
|
||||
return game.nextGame === null ? MATCH_TEXTS.replay : MATCH_TEXTS.nextGame;
|
||||
}
|
||||
|
||||
/** Le résultat d'une partie finie, sur une ligne (consigne, une fois le
|
||||
|
||||
@@ -502,6 +502,11 @@ function drawBand(svg: SVGSVGElement, g: Geometry, text: string): void {
|
||||
const width = 6 * g.pointWidth - 24;
|
||||
const height = Math.max(44, g.bandFont + 20);
|
||||
const x = g.x0 + 12;
|
||||
// Une consigne longue (« Partie Crawford · … ») tient dans la pastille :
|
||||
// la police se resserre, sans descendre sous 13 px (charte). Largeur
|
||||
// moyenne d'un caractère d'Alegreya Sans en gras : un peu moins de
|
||||
// 0,45 em.
|
||||
const fontSize = Math.max(13, Math.min(g.bandFont, (width - 24) / (0.45 * text.length)));
|
||||
add(svg, "rect", {
|
||||
class: "band-pill",
|
||||
x,
|
||||
@@ -517,8 +522,8 @@ function drawBand(svg: SVGSVGElement, g: Geometry, text: string): void {
|
||||
class: "band-text",
|
||||
id: "band-text",
|
||||
x: x + width / 2,
|
||||
y: middle + g.bandFont * 0.35,
|
||||
"font-size": g.bandFont,
|
||||
y: middle + fontSize * 0.35,
|
||||
"font-size": fontSize,
|
||||
},
|
||||
text,
|
||||
);
|
||||
|
||||
@@ -64,6 +64,8 @@ export const MATCH_TEXTS = {
|
||||
/** « Match en 5 points », et « · partie Crawford ». */
|
||||
matchLine: (length: number, crawford: boolean) =>
|
||||
`Match en ${length} points${crawford ? " · partie Crawford" : ""}`,
|
||||
/** Paysage : « Partie Crawford · À vous : lancez les dés. ». */
|
||||
crawfordPrompt: (prompt: string) => `Partie Crawford · ${prompt}`,
|
||||
/** Ce que lit le lecteur d'écran. */
|
||||
scoreSpoken: (length: number, computer: number, player: number) =>
|
||||
`Match en ${length} points : l'ordinateur ${computer}, vous ${player}`,
|
||||
@@ -72,7 +74,11 @@ export const MATCH_TEXTS = {
|
||||
playerScore: (n: number) => `Votre score : ${n}`,
|
||||
computerScore: (n: number) => `Score de l'ordinateur : ${n}`,
|
||||
|
||||
// Fin d'une partie de match
|
||||
// Fin d'une partie
|
||||
/** Une nouvelle partie aux mêmes réglages (un match de même longueur
|
||||
* après un match). */
|
||||
replay: "Rejouer",
|
||||
changeSettings: "Changer les réglages",
|
||||
nextGame: "Partie suivante",
|
||||
nextGameStarted: "Partie suivante du match.",
|
||||
matchWonTitle: (winner: "player" | "computer") =>
|
||||
|
||||
@@ -87,10 +87,14 @@ export function createNewGameView(options: NewGameViewOptions): NewGameView {
|
||||
closeLink.append(icon("close"));
|
||||
element(head, "h2", { id: "new-game-title", text: MATCH_TEXTS.title });
|
||||
|
||||
// Deux colonnes, l'une sous l'autre en portrait et côte à côte en
|
||||
// paysage : le type de partie à gauche, le niveau et le conseil à droite.
|
||||
const body = element(section, "div", { className: "new-game-body" });
|
||||
const kindColumn = element(body, "div", { className: "new-game-column" });
|
||||
const levelColumn = element(body, "div", { className: "new-game-column" });
|
||||
|
||||
// 1. Type de partie.
|
||||
const kinds = element(body, "div", { className: "segmented" });
|
||||
const kinds = element(kindColumn, "div", { className: "segmented" });
|
||||
kinds.setAttribute("role", "radiogroup");
|
||||
kinds.setAttribute("aria-label", MATCH_TEXTS.kindLabel);
|
||||
const free = radio(kinds, "new-game-free", "segment", MATCH_TEXTS.free);
|
||||
@@ -98,7 +102,7 @@ export function createNewGameView(options: NewGameViewOptions): NewGameView {
|
||||
free.addEventListener("click", () => options.change({ gameKind: "free" }));
|
||||
match.addEventListener("click", () => options.change({ gameKind: "match" }));
|
||||
|
||||
const matchOptions = element(body, "div", {
|
||||
const matchOptions = element(kindColumn, "div", {
|
||||
id: "new-game-match-options",
|
||||
className: "new-game-options",
|
||||
});
|
||||
@@ -119,7 +123,7 @@ export function createNewGameView(options: NewGameViewOptions): NewGameView {
|
||||
MATCH_TEXTS.crawfordLink,
|
||||
"regle-de-crawford",
|
||||
);
|
||||
const freeOptions = element(body, "div", {
|
||||
const freeOptions = element(kindColumn, "div", {
|
||||
id: "new-game-free-options",
|
||||
className: "new-game-options",
|
||||
});
|
||||
@@ -132,8 +136,8 @@ export function createNewGameView(options: NewGameViewOptions): NewGameView {
|
||||
);
|
||||
|
||||
// 2. Niveau de l'ordinateur : six barres de hauteur croissante.
|
||||
element(body, "h3", { className: "overline", text: MATCH_TEXTS.levelTitle });
|
||||
const bars = element(body, "div", { className: "level-bars" });
|
||||
element(levelColumn, "h3", { className: "overline", text: MATCH_TEXTS.levelTitle });
|
||||
const bars = element(levelColumn, "div", { className: "level-bars" });
|
||||
bars.setAttribute("role", "radiogroup");
|
||||
bars.setAttribute("aria-label", MATCH_TEXTS.levelTitle);
|
||||
const levelButtons = COMPUTER_LEVELS.map((level, i) => {
|
||||
@@ -144,12 +148,21 @@ export function createNewGameView(options: NewGameViewOptions): NewGameView {
|
||||
button.addEventListener("click", () => options.change({ level }));
|
||||
return button;
|
||||
});
|
||||
const levelName = element(body, "p", { id: "new-game-level-name", className: "level-name" });
|
||||
const levelText = element(body, "p", { id: "new-game-level-text", className: "level-text" });
|
||||
const levelName = element(levelColumn, "p", {
|
||||
id: "new-game-level-name",
|
||||
className: "level-name",
|
||||
});
|
||||
const levelText = element(levelColumn, "p", {
|
||||
id: "new-game-level-text",
|
||||
className: "level-text",
|
||||
});
|
||||
|
||||
// 3. Pendant la partie.
|
||||
element(body, "h3", { className: "overline", text: MATCH_TEXTS.duringGame });
|
||||
const advice = element(body, "button", { id: "new-game-advice", className: "item" });
|
||||
element(levelColumn, "h3", {
|
||||
className: "overline during-game",
|
||||
text: MATCH_TEXTS.duringGame,
|
||||
});
|
||||
const advice = element(levelColumn, "button", { id: "new-game-advice", className: "item" });
|
||||
advice.type = "button";
|
||||
advice.setAttribute("role", "switch");
|
||||
const adviceLabel = element(advice, "span", { className: "item-label" });
|
||||
|
||||
Reference in New Issue
Block a user