Aucun coup possible : le joueur continue lui-même
Le tour du joueur ne passe plus tout seul quand son jet ne permet aucun coup : le domaine le dit (cannotMove) et le joueur laisse passer son tour (skipTurn, bouton « Continuer »). Les dés d'un jet sans coup, du joueur comme de l'ordinateur, restent affichés barrés avec un message jusqu'à l'action suivante. Scénario « Aucun coup possible avec votre jet ». Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
1bb9a95551
commit
39c85172b4
@@ -244,15 +244,32 @@ export class Game {
|
||||
|
||||
// ------------------------------------------------------------- Joueur
|
||||
|
||||
/** Le joueur lance les dés. S'il ne peut pas jouer, son tour passe. */
|
||||
/** Le joueur lance les dés. S'il ne peut pas jouer (cannotMove), son
|
||||
* tour ne passe qu'une fois qu'il l'a constaté (skipTurn). */
|
||||
async roll(): Promise<void> {
|
||||
const state = this.#expect("player", "rolling");
|
||||
const roll = rollDice(this.#dice);
|
||||
this.#append({ type: "DiceRolled", side: "player", roll });
|
||||
const moves = await this.#engine.legalMoves(state.position, roll);
|
||||
if (moves.length === 0) {
|
||||
await this.#playMove("player", [], null);
|
||||
this.#expect("player", "rolling");
|
||||
this.#append({ type: "DiceRolled", side: "player", roll: rollDice(this.#dice) });
|
||||
}
|
||||
|
||||
/** Le joueur a lancé les dés, mais le moteur ne lui donne aucun coup
|
||||
* légal : il ne peut que laisser passer son tour. */
|
||||
async cannotMove(): Promise<boolean> {
|
||||
const state = this.state;
|
||||
if (state.turn !== "player" || state.phase !== "moving") {
|
||||
return false;
|
||||
}
|
||||
const moves = await this.#engine.legalMoves(state.position, rollToPlay(state));
|
||||
return moves.length === 0;
|
||||
}
|
||||
|
||||
/** Le joueur, qui ne peut pas jouer son jet, laisse passer son tour.
|
||||
* Erreur s'il a un coup possible. */
|
||||
async skipTurn(): Promise<void> {
|
||||
this.#expect("player", "moving");
|
||||
if (!(await this.cannotMove())) {
|
||||
throw new Error("vous avez un coup possible : jouez-le");
|
||||
}
|
||||
await this.#playMove("player", [], null);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+60
-14
@@ -52,6 +52,7 @@ export function startApp(options: AppOptions): void {
|
||||
take: byId<HTMLButtonElement>("take"),
|
||||
drop: byId<HTMLButtonElement>("drop"),
|
||||
cancel: byId<HTMLButtonElement>("cancel"),
|
||||
skip: byId<HTMLButtonElement>("skip"),
|
||||
replay: byId<HTMLButtonElement>("replay"),
|
||||
again: byId<HTMLButtonElement>("again"),
|
||||
newGame: byId<HTMLButtonElement>("new-game"),
|
||||
@@ -67,6 +68,7 @@ export function startApp(options: AppOptions): void {
|
||||
ui.take.textContent = TEXTS.take;
|
||||
ui.drop.textContent = TEXTS.drop;
|
||||
ui.cancel.textContent = TEXTS.cancel;
|
||||
ui.skip.textContent = TEXTS.skip;
|
||||
ui.replay.textContent = TEXTS.replay;
|
||||
ui.replay.title = TEXTS.replayTitle;
|
||||
ui.again.textContent = TEXTS.newGame;
|
||||
@@ -80,6 +82,8 @@ export function startApp(options: AppOptions): void {
|
||||
let targets = new Map<Place, Map<Place, HalfMove>>();
|
||||
let selected: Place | null = null;
|
||||
let canDouble = false;
|
||||
// Le joueur a lancé les dés mais ne peut pas jouer : il doit continuer.
|
||||
let stuck = false;
|
||||
let busy = true;
|
||||
let info = "";
|
||||
// Le dernier coup de l'ordinateur, montré (traces, bouton « Revoir »)
|
||||
@@ -98,6 +102,7 @@ export function startApp(options: AppOptions): void {
|
||||
sides = await game.displayedBoard();
|
||||
targets = moveTargets(await game.nextHalfMoves());
|
||||
canDouble = await game.canDouble();
|
||||
stuck = await game.cannotMove();
|
||||
if (selected !== null && !targets.has(selected)) {
|
||||
selected = null;
|
||||
}
|
||||
@@ -117,7 +122,7 @@ export function startApp(options: AppOptions): void {
|
||||
const model = boardModel(sides);
|
||||
if (!animating) {
|
||||
renderBoard(ui.board, model, {
|
||||
dice: shownDice(game),
|
||||
dice: shownDice(game, stuck),
|
||||
cube: state.cube,
|
||||
movable: new Set(targets.keys()),
|
||||
selected,
|
||||
@@ -129,22 +134,41 @@ export function startApp(options: AppOptions): void {
|
||||
});
|
||||
}
|
||||
ui.pips.textContent = TEXTS.pipCounts(model.pipCount.player, model.pipCount.computer);
|
||||
ui.info.textContent = info;
|
||||
ui.prompt.textContent = prompt(game, selected !== null, canDouble);
|
||||
// Un jet sans coup possible reste annoncé jusqu'à l'action suivante,
|
||||
// même après un rechargement de la page.
|
||||
if (stuck && state.roll !== null) {
|
||||
ui.info.textContent = TEXTS.youCannotMove(state.roll);
|
||||
} else if (computerCouldNotMove(game)) {
|
||||
ui.info.textContent = TEXTS.computerCannotMove(rollBefore(game));
|
||||
} else {
|
||||
ui.info.textContent = info;
|
||||
}
|
||||
ui.prompt.textContent = stuck
|
||||
? TEXTS.pressContinue
|
||||
: prompt(game, selected !== null, canDouble);
|
||||
ui.prompt.classList.toggle("thinking", state.turn === "computer" && isPlaying(game));
|
||||
|
||||
const player = state.turn === "player";
|
||||
show(ui.roll, player && state.phase === "rolling");
|
||||
show(ui.double, player && state.phase === "rolling");
|
||||
ui.double.disabled = busy || !canDouble;
|
||||
show(ui.cancel, player && state.phase === "moving");
|
||||
show(ui.cancel, player && state.phase === "moving" && !stuck);
|
||||
show(ui.skip, stuck);
|
||||
ui.cancel.disabled = busy || game.pendingHalfMoves.length === 0;
|
||||
const doubled = state.turn === "computer" && state.phase === "doubled";
|
||||
show(ui.take, doubled);
|
||||
show(ui.drop, doubled);
|
||||
show(ui.again, state.phase === "over");
|
||||
show(ui.replay, shownMove !== null);
|
||||
for (const button of [ui.roll, ui.take, ui.drop, ui.replay, ui.again, ui.newGame]) {
|
||||
for (const button of [
|
||||
ui.roll,
|
||||
ui.take,
|
||||
ui.drop,
|
||||
ui.skip,
|
||||
ui.replay,
|
||||
ui.again,
|
||||
ui.newGame,
|
||||
]) {
|
||||
button.disabled = busy;
|
||||
}
|
||||
// Pour le style et pour les tests dans un navigateur : l'écran
|
||||
@@ -192,7 +216,7 @@ export function startApp(options: AppOptions): void {
|
||||
if (game === null || window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
|
||||
return;
|
||||
}
|
||||
const decor = { dice: shownDice(game), cube: game.state.cube, ...NO_MARKS };
|
||||
const decor = { dice: shownDice(game, false), cube: game.state.cube, ...NO_MARKS };
|
||||
const draw = (i: number) => {
|
||||
const boards = steps.boards[i];
|
||||
if (boards !== undefined) {
|
||||
@@ -267,10 +291,13 @@ export function startApp(options: AppOptions): void {
|
||||
act(async (g) => {
|
||||
info = "";
|
||||
await g.roll();
|
||||
const last = g.events.at(-1);
|
||||
if (last?.type === "MovePlayed" && last.halfMoves.length === 0) {
|
||||
info = TEXTS.youCannotMove(rollBefore(g));
|
||||
}
|
||||
});
|
||||
|
||||
// Aucun coup possible : le joueur laisse passer son tour.
|
||||
ui.skip.onclick = () =>
|
||||
act(async (g) => {
|
||||
info = "";
|
||||
await g.skipTurn();
|
||||
});
|
||||
|
||||
ui.double.onclick = () =>
|
||||
@@ -359,7 +386,16 @@ export function startApp(options: AppOptions): void {
|
||||
// ---------------------------------------------------------- Démarrage
|
||||
|
||||
ui.prompt.textContent = TEXTS.loading;
|
||||
for (const button of [ui.roll, ui.double, ui.take, ui.drop, ui.cancel, ui.replay, ui.again]) {
|
||||
for (const button of [
|
||||
ui.roll,
|
||||
ui.double,
|
||||
ui.take,
|
||||
ui.drop,
|
||||
ui.cancel,
|
||||
ui.skip,
|
||||
ui.replay,
|
||||
ui.again,
|
||||
]) {
|
||||
button.hidden = true;
|
||||
}
|
||||
ui.newGame.disabled = true;
|
||||
@@ -403,15 +439,25 @@ function rollBefore(game: Game): readonly number[] {
|
||||
}
|
||||
|
||||
/** Les dés à montrer : le dernier jet, de la couleur de qui l'a lancé
|
||||
* (au jet d'ouverture, un dé chacun). */
|
||||
function shownDice(game: Game): ShownDie[] {
|
||||
* (au jet d'ouverture, un dé chacun). Ils sont barrés si le jet ne permet
|
||||
* aucun coup : le joueur est bloqué (stuck), ou l'ordinateur vient de passer
|
||||
* son tour sans bouger. */
|
||||
function shownDice(game: Game, stuck: boolean): ShownDie[] {
|
||||
const rolled = game.events.findLast((e) => e.type === "DiceRolled");
|
||||
if (rolled?.type !== "DiceRolled") {
|
||||
return [];
|
||||
}
|
||||
const blocked = stuck || computerCouldNotMove(game);
|
||||
const sides: Side[] =
|
||||
rolled.side === null ? ["player", "computer"] : [rolled.side, rolled.side];
|
||||
return rolled.roll.map((value, i) => ({ value, side: sides[i] ?? "player" }));
|
||||
return rolled.roll.map((value, i) => ({ value, side: sides[i] ?? "player", blocked }));
|
||||
}
|
||||
|
||||
/** La dernière chose jouée (fin de partie mise à part) est un tour de
|
||||
* l'ordinateur qui n'a pas pu jouer son jet. */
|
||||
function computerCouldNotMove(game: Game): boolean {
|
||||
const last = game.events.findLast((e) => e.type !== "GameEnded");
|
||||
return last?.type === "MovePlayed" && last.side === "computer" && last.halfMoves.length === 0;
|
||||
}
|
||||
|
||||
/** Le dernier coup de l'ordinateur, demi-coup par demi-coup, s'il est la
|
||||
|
||||
@@ -17,6 +17,8 @@ import { type BoardModel, type MoveTrace, type Place, pointSlot } from "./board-
|
||||
export interface ShownDie {
|
||||
readonly value: number;
|
||||
readonly side: Side;
|
||||
/** Le jet ne permet aucun coup : le dé est estompé et barré. */
|
||||
readonly blocked: boolean;
|
||||
}
|
||||
|
||||
/** Ce qui s'ajoute aux pions : dés, videau, mises en évidence. */
|
||||
@@ -470,7 +472,10 @@ function drawDice(svg: SVGSVGElement, dice: readonly ShownDie[]): void {
|
||||
dice.forEach((die, i) => {
|
||||
const x = center - total / 2 + i * (DIE + gap);
|
||||
const y = MIDDLE - DIE / 2;
|
||||
const group = add(svg, "g", { class: "die", "data-value": die.value });
|
||||
const group = add(svg, "g", {
|
||||
class: die.blocked ? "die die-blocked" : "die",
|
||||
"data-value": die.value,
|
||||
});
|
||||
add(group, "rect", { class: `die-${die.side}`, x, y, width: DIE, height: DIE, rx: 6 });
|
||||
for (const [px, py] of PIPS[die.value] ?? []) {
|
||||
add(group, "circle", {
|
||||
@@ -480,6 +485,9 @@ function drawDice(svg: SVGSVGElement, dice: readonly ShownDie[]): void {
|
||||
r: 3.5,
|
||||
});
|
||||
}
|
||||
if (die.blocked) {
|
||||
add(group, "line", { class: "die-strike", x1: x, y1: y + DIE, x2: x + DIE, y2: y });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+3
-2
@@ -37,6 +37,7 @@ export const TEXTS = {
|
||||
take: "Prendre",
|
||||
drop: "Passer",
|
||||
cancel: "Annuler",
|
||||
skip: "Continuer",
|
||||
replay: "Revoir",
|
||||
replayTitle: "Revoir le dernier coup de l'ordinateur",
|
||||
newGame: "Nouvelle partie",
|
||||
@@ -59,8 +60,8 @@ export const TEXTS = {
|
||||
yourMove: (dice: readonly number[]) =>
|
||||
`Jouez ${roll(dice)} : touchez un pion, puis sa case d'arrivée.`,
|
||||
pickTarget: "Touchez une case d'arrivée en surbrillance, ou un autre pion.",
|
||||
youCannotMove: (dice: readonly number[]) =>
|
||||
`Vous ne pouvez pas jouer ${roll(dice)} : votre tour passe.`,
|
||||
youCannotMove: (dice: readonly number[]) => `Aucun coup possible avec ${roll(dice)}.`,
|
||||
pressContinue: "Touchez « Continuer » : le tour passe à l'ordinateur.",
|
||||
thinking: "L'ordinateur réfléchit…",
|
||||
computerMoved: (dice: readonly number[], move: string, ms: number) =>
|
||||
`L'ordinateur a joué ${roll(dice)} : ${move} (${seconds(ms)}).`,
|
||||
|
||||
Reference in New Issue
Block a user