Bilan de fin de partie et écran d'analyse : courbe des chances, visualiseur, liste filtrable, rejouer d'ici

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Pierre MARTIN
2026-11-10 10:30:17 +01:00
co-authored by Claude Opus 5.5
parent a657d7d7a9
commit 006fd62704
22 changed files with 2420 additions and 106 deletions
+7 -11
View File
@@ -139,9 +139,7 @@ export type AnalysisTask =
* a pas encore (ou à un autre niveau). */
export function reviewOf(journal: GameJournal): GameReview {
const review = journal.review;
return review !== undefined && review.ply === ANALYSIS_PLY
? review
: emptyReview(ANALYSIS_PLY);
return review !== undefined && review.ply === ANALYSIS_PLY ? review : emptyReview(ANALYSIS_PLY);
}
/**
@@ -247,11 +245,7 @@ export async function runAnalysis(
// Le camp qui joue le jet ; au jet d'ouverture, celui qu'il
// désigne. La position de départ est la même pour les deux.
const roller = event.side ?? replay(line).turn ?? "player";
const luck = await engine.luck(
before.position,
engineCube(before, roller),
event.roll,
);
const luck = await engine.luck(before.position, engineCube(before, roller), event.roll);
return withReview(journal, { ...review, luck: { ...review.luck, [event.id]: luck } });
}
case "chance": {
@@ -477,7 +471,10 @@ export function summarizeAnalysis(journal: GameJournal): GameSummary {
if (!(a.kind === "cube" && a.action === "noDouble" && verdict === "good")) {
counts[verdict]++;
}
if (verdict !== "good" && (toReview === null || a.equityLoss < toReview.advice.equityLoss)) {
if (
verdict !== "good" &&
(toReview === null || a.equityLoss < toReview.advice.equityLoss)
) {
toReview = { turn: turnOf.get(a.eventId) ?? 0, advice: a };
}
}
@@ -577,7 +574,6 @@ export function chanceCurve(journal: GameJournal): WinChanceCurve {
export function replayPoint(journal: GameJournal, turn: Turn): string | null {
const main = mainAdvice(journal, turn);
const decisions = decisionsOf(turn.events, "player");
const event =
main === null ? decisions.at(-1) : decisions.find((e) => e.id === main.eventId);
const event = main === null ? decisions.at(-1) : decisions.find((e) => e.id === main.eventId);
return event?.parent ?? null;
}
+46 -1
View File
@@ -15,7 +15,8 @@ import {
type Verdict,
verdictOf,
} from "../game/advice.js";
import type { CubeAction } from "../game/engine.js";
import type { CubeAction, LuckRating } from "../game/engine.js";
import type { GameSummary } from "./game-analysis.js";
/** Les gabarits, par clé de section (« coup.meilleur »…). */
export type MasterPhrases = ReadonlyMap<string, readonly string[]>;
@@ -93,8 +94,52 @@ export const PHRASE_KEYS: readonly string[] = [
"contexte.videau.double",
"contexte.videau.pas-de-double",
"contexte.videau.reponse",
"bilan.jeu.gagne",
"bilan.jeu.perdu",
"bilan.des",
"bilan.des.tres-pour",
"bilan.des.pour",
"bilan.des.neutres",
"bilan.des.contre",
"bilan.des.tres-contre",
"bilan.aucun-coup",
];
const DICE_KEYS: Readonly<Record<LuckRating, string>> = {
veryLucky: "bilan.des.tres-pour",
lucky: "bilan.des.pour",
none: "bilan.des.neutres",
unlucky: "bilan.des.contre",
veryUnlucky: "bilan.des.tres-contre",
};
/** La chance de vos dés sur la partie, en mots : « un peu contre vous »… */
export function diceWords(phrases: MasterPhrases, rating: LuckRating): string {
return pick(phrases, DICE_KEYS[rating], "0");
}
/**
* La lecture du bilan d'une partie finie : ce qui a fait la différence,
* selon le résultat corrigé de la chance (gnubg) ; et vos dés, en mots.
* Partie gagnée : votre jeu, si le résultat corrigé de la chance reste
* positif, sinon les dés ; partie perdue : le jeu de l'ordinateur, si le
* résultat corrigé reste négatif, sinon les dés.
*/
export function summaryReading(phrases: MasterPhrases, summary: GameSummary): string {
const dice = diceWords(phrases, summary.player.luck.rating);
const won = summary.result > 0;
const playDecided = won ? summary.luckAdjustedResult > 0 : summary.luckAdjustedResult < 0;
if (!playDecided) {
return pick(phrases, "bilan.des", "0");
}
return fillIn(pick(phrases, won ? "bilan.jeu.gagne" : "bilan.jeu.perdu", "0"), { dice });
}
/** « Aucun coup à revoir… » : la carte du coup à revoir sans erreur. */
export function nothingToReview(phrases: MasterPhrases): string {
return pick(phrases, "bilan.aucun-coup", "0");
}
/** Ce que le maître dit d'un conseil du moteur. */
export function masterWords(phrases: MasterPhrases, advice: Advice): MasterWords {
return advice.kind === "move" ? moveWords(phrases, advice) : cubeWords(phrases, advice);
+1 -1
View File
@@ -17,7 +17,7 @@ import type { Cube } from "./cube.js";
import type { Level, ResultKind } from "./engine.js";
import type { Match, MatchAfterGame } from "./match.js";
import type { HalfMove, PositionId } from "./position.js";
import { REVIEW_VERSION, type GameReview } from "./review.js";
import { type GameReview, REVIEW_VERSION } from "./review.js";
import type { Roll } from "./roll.js";
import type { Side } from "./side.js";