Domaine : coup à valider, demi-coups défaits un par un, dés joués
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
ce2bd46ec3
commit
2c5278965b
+113
-24
@@ -79,9 +79,10 @@ export class Game {
|
||||
readonly #dice: DiceSource;
|
||||
readonly #ply: Ply;
|
||||
#journal: GameJournal = EMPTY_JOURNAL;
|
||||
// Coup du joueur en cours de saisie : pas encore dans le journal.
|
||||
#pending: HalfMove[] = [];
|
||||
#pendingPosition: PositionId | null = null;
|
||||
// Coup du joueur en cours de saisie : pas encore dans le journal (il n'y
|
||||
// entre qu'à sa validation, confirmMove). Pour chaque demi-coup saisi, la
|
||||
// position après lui, et si le coup est alors complet, selon le moteur.
|
||||
#pending: PendingHalfMove[] = [];
|
||||
|
||||
private constructor(engine: Engine, options: GameOptions) {
|
||||
this.#engine = engine;
|
||||
@@ -141,12 +142,37 @@ export class Game {
|
||||
|
||||
/** Les demi-coups déjà saisis du coup en cours. */
|
||||
get pendingHalfMoves(): readonly HalfMove[] {
|
||||
return this.#pending;
|
||||
return this.#pending.map((p) => p.halfMove);
|
||||
}
|
||||
|
||||
/** Les demi-coups saisis forment un coup complet (le moteur le dit) : il
|
||||
* n'attend plus que d'être validé (confirmMove) ou défait
|
||||
* (undoLastHalfMove). */
|
||||
get moveComplete(): boolean {
|
||||
return this.#pending.at(-1)?.complete === true;
|
||||
}
|
||||
|
||||
/** La position à afficher : après les demi-coups déjà saisis. */
|
||||
get displayedPosition(): PositionId {
|
||||
return this.#pendingPosition ?? this.state.position;
|
||||
return this.#pending.at(-1)?.position ?? this.state.position;
|
||||
}
|
||||
|
||||
/**
|
||||
* Les dés du jet à jouer (quatre pour un double), et pour chacun s'il est
|
||||
* déjà utilisé par un demi-coup saisi ; liste vide hors de la saisie d'un
|
||||
* coup du joueur. Un demi-coup utilise le dé qui l'a produit dans
|
||||
* nextHalfMoves (de from vers from - dé, ou vers la sortie) : aucune
|
||||
* règle n'est jugée ici, la légalité vient du moteur.
|
||||
*/
|
||||
get diceToPlay(): DieToPlay[] {
|
||||
const state = this.state;
|
||||
if (state.turn !== "player" || state.phase !== "moving" || state.roll === null) {
|
||||
return [];
|
||||
}
|
||||
const [a, b] = state.roll;
|
||||
const dice = a === b ? [a, a, a, a] : [a, b];
|
||||
const used = assignDice(dice, this.pendingHalfMoves);
|
||||
return dice.map((value, i) => ({ value, used: used[i] === true }));
|
||||
}
|
||||
|
||||
/** Le plateau affiché (après les demi-coups déjà saisis), décodé par le
|
||||
@@ -205,7 +231,7 @@ export class Game {
|
||||
*/
|
||||
async nextHalfMoves(): Promise<HalfMove[]> {
|
||||
const state = this.state;
|
||||
if (state.turn !== "player" || state.phase !== "moving") {
|
||||
if (state.turn !== "player" || state.phase !== "moving" || this.moveComplete) {
|
||||
return [];
|
||||
}
|
||||
const roll = rollToPlay(state);
|
||||
@@ -220,7 +246,7 @@ export class Game {
|
||||
const moves = [...candidates.values()];
|
||||
const checks = await Promise.all(
|
||||
moves.map((h) =>
|
||||
this.#engine.checkHalfMoves(state.position, roll, [...this.#pending, h]),
|
||||
this.#engine.checkHalfMoves(state.position, roll, [...this.pendingHalfMoves, h]),
|
||||
),
|
||||
);
|
||||
return moves.filter((_, i) => checks[i]?.valid === true);
|
||||
@@ -274,34 +300,51 @@ export class Game {
|
||||
|
||||
/**
|
||||
* Le joueur déplace un pion (un dé). Renvoie false si ce demi-coup ne
|
||||
* mène à aucun coup légal : il est refusé et rien ne change. Quand les
|
||||
* demi-coups forment un coup complet, le coup est joué.
|
||||
* mène à aucun coup légal (ou si le coup est déjà complet) : il est
|
||||
* refusé et rien ne change. Le coup n'est pas joué pour autant : une fois
|
||||
* complet (moveComplete), il attend d'être validé (confirmMove).
|
||||
*/
|
||||
async playHalfMove(halfMove: HalfMove): Promise<boolean> {
|
||||
const state = this.#expect("player", "moving");
|
||||
const halfMoves = [...this.#pending, halfMove];
|
||||
const check = await this.#engine.checkHalfMoves(
|
||||
state.position,
|
||||
rollToPlay(state),
|
||||
halfMoves,
|
||||
);
|
||||
if (this.moveComplete) {
|
||||
return false;
|
||||
}
|
||||
const check = await this.#engine.checkHalfMoves(state.position, rollToPlay(state), [
|
||||
...this.pendingHalfMoves,
|
||||
halfMove,
|
||||
]);
|
||||
if (!check.valid || check.position === null) {
|
||||
return false;
|
||||
}
|
||||
if (check.complete) {
|
||||
this.cancelHalfMoves();
|
||||
await this.#playMove("player", halfMoves, check.position);
|
||||
} else {
|
||||
this.#pending = halfMoves;
|
||||
this.#pendingPosition = check.position;
|
||||
}
|
||||
this.#pending = [
|
||||
...this.#pending,
|
||||
{ halfMove, position: check.position, complete: check.complete },
|
||||
];
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Oublie les demi-coups saisis du coup en cours. */
|
||||
/** Défait le dernier demi-coup saisi du coup en cours (rien s'il n'y en a
|
||||
* pas) : il n'a jamais été dans le journal. */
|
||||
undoLastHalfMove(): void {
|
||||
this.#pending = this.#pending.slice(0, -1);
|
||||
}
|
||||
|
||||
/** Oublie tous les demi-coups saisis du coup en cours. */
|
||||
cancelHalfMoves(): void {
|
||||
this.#pending = [];
|
||||
this.#pendingPosition = null;
|
||||
}
|
||||
|
||||
/** Le joueur valide son coup complet : il entre dans le journal
|
||||
* (MovePlayed) et le tour passe. Erreur si le coup n'est pas complet. */
|
||||
async confirmMove(): Promise<void> {
|
||||
this.#expect("player", "moving");
|
||||
const last = this.#pending.at(-1);
|
||||
if (last?.complete !== true) {
|
||||
throw new Error("le coup n'est pas complet : jouez encore");
|
||||
}
|
||||
const halfMoves = this.pendingHalfMoves;
|
||||
this.cancelHalfMoves();
|
||||
await this.#playMove("player", halfMoves, last.position);
|
||||
}
|
||||
|
||||
/** Le joueur double, avant de lancer les dés ; l'ordinateur prend ou passe
|
||||
@@ -454,6 +497,52 @@ export class Game {
|
||||
}
|
||||
}
|
||||
|
||||
/** Un dé du jet à jouer, et s'il est déjà utilisé par un demi-coup saisi. */
|
||||
export interface DieToPlay {
|
||||
readonly value: number;
|
||||
readonly used: boolean;
|
||||
}
|
||||
|
||||
/** Un demi-coup saisi, pas encore validé. */
|
||||
interface PendingHalfMove {
|
||||
readonly halfMove: HalfMove;
|
||||
/** La position après lui, vue du joueur. */
|
||||
readonly position: PositionId;
|
||||
/** Les demi-coups saisis jusqu'à lui forment un coup complet. */
|
||||
readonly complete: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Quels dés les demi-coups ont utilisés : chaque demi-coup reçoit un dé
|
||||
* différent qui le produit (de from vers from - dé, ou vers la sortie si le
|
||||
* dé dépasse), comme les candidats de nextHalfMoves. Les dés sont essayés du
|
||||
* plus petit au plus grand : un pion sorti l'est avec le plus petit dé
|
||||
* suffisant. Si aucune répartition ne convient, les premiers dés sont
|
||||
* marqués, un par demi-coup.
|
||||
*/
|
||||
function assignDice(dice: readonly number[], halfMoves: readonly HalfMove[]): boolean[] {
|
||||
const order = dice.map((_, i) => i).sort((i, j) => (dice[i] ?? 0) - (dice[j] ?? 0));
|
||||
const used = dice.map(() => false);
|
||||
const fits = (h: HalfMove, die: number) => Math.max(h.from - die, OFF) === h.to;
|
||||
const place = (k: number): boolean => {
|
||||
const h = halfMoves[k];
|
||||
if (h === undefined) {
|
||||
return true;
|
||||
}
|
||||
for (const i of order) {
|
||||
if (!used[i] && fits(h, dice[i] ?? 0)) {
|
||||
used[i] = true;
|
||||
if (place(k + 1)) {
|
||||
return true;
|
||||
}
|
||||
used[i] = false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
return place(0) ? used : dice.map((_, i) => i < halfMoves.length);
|
||||
}
|
||||
|
||||
/** Le jet à jouer, en phase moving. */
|
||||
function rollToPlay(state: GameState): Roll {
|
||||
if (state.roll === null) {
|
||||
|
||||
Reference in New Issue
Block a user