Compare commits
35
Commits
@@ -107,8 +107,6 @@ lint: ## Vérifie le style et les erreurs courantes
|
||||
deploy: site ## Sert l'application sur le réseau local (Ctrl+C pour arrêter)
|
||||
@ip="$$(ip -4 route get 1.1.1.1 2>/dev/null | awk '{ for (i = 1; i < NF; i++) if ($$i == "src") print $$(i + 1) }')"; \
|
||||
echo "Application : http://$${ip:-ADRESSE-DU-PC}:$(WEB_PORT)/"; \
|
||||
echo "Test du moteur : http://$${ip:-ADRESSE-DU-PC}:$(WEB_PORT)/moteur/"; \
|
||||
echo "Page de mesure : http://$${ip:-ADRESSE-DU-PC}:$(WEB_PORT)/moteur/bench.html"; \
|
||||
echo "À ouvrir depuis le téléphone, sur le même Wi-Fi. Ctrl+C pour arrêter."
|
||||
@docker run --rm --init -p $(WEB_PORT):8080 -v "$(CURDIR)/$(BUILD_DIR)/site:/site:ro" \
|
||||
$(BUILD_C_IMAGE) python3 -m http.server 8080 --directory /site
|
||||
@@ -123,7 +121,7 @@ SITE_ICONS := icone.svg icone-192.png icone-512.png icone-maskable-192.png icone
|
||||
|
||||
# Le site tel qu'il sera publié, reconstruit entièrement à chaque appel :
|
||||
# - web/ à la racine : la page de l'application, son style, sa police, le
|
||||
# manifeste, le Web Worker du moteur, et les pages de test sous moteur/ ;
|
||||
# manifeste et le Web Worker du moteur ;
|
||||
# - sous icons/, le logo et les icônes de la charte, copiés depuis
|
||||
# docs/identite/logo/ (une seule source, ADR 0010) ;
|
||||
# - l'application compilée (build/web/src/) sous app/ ;
|
||||
@@ -165,8 +163,8 @@ reference-image: ## Construit l'image Docker du gnubg de référence (cache Dock
|
||||
docker build -t $(REFERENCE_IMAGE) docker/gnubg-reference
|
||||
@echo "Image $(REFERENCE_IMAGE) prête."
|
||||
|
||||
reference: reference-image ## Régénère tests/reference/results/ avec le gnubg de référence
|
||||
rm -rf tests/reference/results
|
||||
reference: reference-image ## Régénère tests/reference/results/ et analysis/ avec le gnubg de référence
|
||||
rm -rf tests/reference/results tests/reference/analysis
|
||||
docker run --rm --user "$$(id -u):$$(id -g)" -e HOME=/tmp \
|
||||
-v "$(CURDIR)/tests/reference:/reference" $(REFERENCE_IMAGE) \
|
||||
gnubg --tty --quiet --no-rc --python=/reference/generate.py
|
||||
|
||||
@@ -38,6 +38,141 @@ export interface Engine {
|
||||
|
||||
/** Le plateau d'une position, et le résultat si la partie y est finie. */
|
||||
board(position: PositionId, cube: EngineCube): Promise<Board>;
|
||||
|
||||
/** Le coup que joue l'ordinateur de gnubg à ce niveau (bruit compris,
|
||||
* reproductible), ou null si aucun coup n'est possible. */
|
||||
levelMove(
|
||||
position: PositionId,
|
||||
cube: EngineCube,
|
||||
roll: Roll,
|
||||
level: Level,
|
||||
): Promise<Move | null>;
|
||||
|
||||
/** La décision de videau de l'ordinateur à ce niveau, bruit compris. */
|
||||
levelCubeDecision(position: PositionId, cube: EngineCube, level: Level): Promise<CubeDecision>;
|
||||
|
||||
/** L'analyse du coup joué (played : position après le coup, vue du joueur
|
||||
* qui l'a joué, comme Move.position), au niveau d'analyse ply ; null si
|
||||
* aucun coup n'était possible. */
|
||||
analyseMove(
|
||||
position: PositionId,
|
||||
cube: EngineCube,
|
||||
roll: Roll,
|
||||
played: PositionId,
|
||||
ply: Ply,
|
||||
): Promise<MoveAnalysis | null>;
|
||||
|
||||
/** L'analyse d'une décision de videau, position vue du joueur au trait
|
||||
* (celui qui double ou non ; take et pass sont la réponse de son
|
||||
* adversaire). */
|
||||
analyseCube(
|
||||
position: PositionId,
|
||||
cube: EngineCube,
|
||||
action: CubeAction,
|
||||
ply: Ply,
|
||||
): Promise<CubeAnalysis>;
|
||||
|
||||
/** La chance d'un jet pour le joueur au trait. */
|
||||
luck(position: PositionId, cube: EngineCube, roll: Roll): Promise<Luck>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Niveau de jeu de l'ordinateur : un réglage prédéfini de GNU Backgammon
|
||||
* (aecSettings dans eval.c ; voir engine/include/engine.h). Les quatre
|
||||
* premiers jouent à 0 pli avec un bruit qui leur fait commettre des erreurs ;
|
||||
* les suivants sont sans bruit, de 0 à 4 plis.
|
||||
*
|
||||
* Glossaire : docs/glossaire/niveau-de-jeu.md
|
||||
*/
|
||||
export type Level =
|
||||
| "beginner"
|
||||
| "casual"
|
||||
| "intermediate"
|
||||
| "advanced"
|
||||
| "0ply"
|
||||
| "1ply"
|
||||
| "2ply"
|
||||
| "3ply"
|
||||
| "4ply";
|
||||
|
||||
/**
|
||||
* Appréciation d'un coup ou d'une décision de videau par le moteur, selon
|
||||
* l'équité perdue : rien à signaler, douteux, mauvais, très mauvais.
|
||||
*
|
||||
* Glossaire : docs/glossaire/appreciation.md
|
||||
*/
|
||||
export type Skill = "none" | "doubtful" | "bad" | "veryBad";
|
||||
|
||||
/**
|
||||
* Seuils de GNU Backgammon (arSkillLevel dans gnubg.c) : une perte
|
||||
* d'équité strictement plus grande fait passer à l'appréciation. Le moteur
|
||||
* classe lui-même ; ces valeurs servent à l'expliquer.
|
||||
*/
|
||||
export const SKILL_THRESHOLDS = { doubtful: 0.03, bad: 0.06, veryBad: 0.12 } as const;
|
||||
|
||||
/** Catégorie de chance d'un jet, de très malchanceux à très chanceux. */
|
||||
export type LuckRating = "veryUnlucky" | "unlucky" | "none" | "lucky" | "veryLucky";
|
||||
|
||||
/**
|
||||
* Seuils de GNU Backgammon (arLuckLevel dans gnubg.c) sur la chance d'un
|
||||
* jet, dans un sens ou dans l'autre.
|
||||
*/
|
||||
export const LUCK_THRESHOLDS = { lucky: 0.3, veryLucky: 0.6 } as const;
|
||||
|
||||
/**
|
||||
* La chance d'un jet : l'équité qu'il fait gagner (positive) ou perdre au
|
||||
* joueur par rapport à la moyenne des jets.
|
||||
*
|
||||
* Glossaire : docs/glossaire/chance.md
|
||||
*/
|
||||
export interface Luck {
|
||||
readonly value: number;
|
||||
readonly rating: LuckRating;
|
||||
}
|
||||
|
||||
/** Un coup avec son équité (avec videau), du point de vue de celui qui le joue. */
|
||||
export interface EvaluatedMove extends Move {
|
||||
readonly equity: number;
|
||||
/** Niveau auquel le coup a réellement été évalué. */
|
||||
readonly ply: Ply;
|
||||
}
|
||||
|
||||
/** Ce que le moteur dit d'un coup joué, comme l'analyse de partie de gnubg. */
|
||||
export interface MoveAnalysis {
|
||||
/** Nombre de coups légaux. */
|
||||
readonly moveCount: number;
|
||||
/** Place du coup joué dans le classement, à partir de 1. */
|
||||
readonly rank: number;
|
||||
readonly played: EvaluatedMove;
|
||||
readonly best: EvaluatedMove;
|
||||
/** Équité du coup joué moins celle du meilleur : 0 ou négative. */
|
||||
readonly equityLoss: number;
|
||||
readonly skill: Skill;
|
||||
}
|
||||
|
||||
/** Décision de videau : doubler ou non (joueur au trait), prendre ou passer
|
||||
* (son adversaire). */
|
||||
export type CubeAction = "noDouble" | "double" | "take" | "pass";
|
||||
|
||||
/** Ce que le moteur dit d'une décision de videau. */
|
||||
export interface CubeAnalysis {
|
||||
/** gnubg analyse-t-il cette décision ? Pas quand le joueur au trait ne
|
||||
* peut pas doubler, ni à la position de départ. */
|
||||
readonly analysed: boolean;
|
||||
/** Équités du joueur au trait, normalisées pour un videau de 1 (null si
|
||||
* non analysée). */
|
||||
readonly equities: CubeEquities | null;
|
||||
/** Équité perdue par celui qui décide : 0 ou négative. */
|
||||
readonly equityLoss: number;
|
||||
readonly skill: Skill;
|
||||
}
|
||||
|
||||
export interface CubeEquities {
|
||||
readonly noDouble: number;
|
||||
readonly doubleTake: number;
|
||||
readonly doublePass: number;
|
||||
/** Après les meilleures décisions des deux joueurs. */
|
||||
readonly optimal: number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+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) {
|
||||
|
||||
@@ -12,12 +12,20 @@ import type {
|
||||
Board,
|
||||
BoardResult,
|
||||
BoardSide,
|
||||
CubeAction,
|
||||
CubeAnalysis,
|
||||
CubeDecision,
|
||||
Engine,
|
||||
EngineCube,
|
||||
EvaluatedMove,
|
||||
HalfMovesCheck,
|
||||
Level,
|
||||
Luck,
|
||||
LuckRating,
|
||||
Move,
|
||||
MoveAnalysis,
|
||||
Ply,
|
||||
Skill,
|
||||
} from "../domain/game/engine.js";
|
||||
import { formatHalfMoves, type HalfMove, type PositionId } from "../domain/game/position.js";
|
||||
import type { Roll } from "../domain/game/roll.js";
|
||||
@@ -32,6 +40,16 @@ interface CliMove {
|
||||
position_id: string;
|
||||
half_moves: [number, number][];
|
||||
}
|
||||
interface CliEvaluatedMove extends CliMove {
|
||||
ply: number;
|
||||
equity: number;
|
||||
}
|
||||
type CliSkill = "none" | "doubtful" | "bad" | "very_bad";
|
||||
interface CliCubeDecision {
|
||||
available: boolean;
|
||||
double: boolean;
|
||||
take: boolean;
|
||||
}
|
||||
interface CliSide {
|
||||
points: number[];
|
||||
bar: number;
|
||||
@@ -86,8 +104,108 @@ export function createWasmEngine(run: RunCommand): Engine {
|
||||
ply: Ply,
|
||||
): Promise<CubeDecision> {
|
||||
const args = ["--ply", String(ply), "cube", position, await matchId(cube)];
|
||||
const answer = (await run(args)) as CubeDecision;
|
||||
return { available: answer.available, double: answer.double, take: answer.take };
|
||||
return toCubeDecision((await run(args)) as CliCubeDecision);
|
||||
},
|
||||
|
||||
async levelMove(position: PositionId, cube: EngineCube, roll: Roll, level: Level) {
|
||||
const args = ["--level", level, "bestmove", position, await matchId(cube)];
|
||||
const answer = (await run([...args, diceText(roll)])) as { best_move: CliMove | null };
|
||||
return answer.best_move === null ? null : toMove(answer.best_move);
|
||||
},
|
||||
|
||||
async levelCubeDecision(
|
||||
position: PositionId,
|
||||
cube: EngineCube,
|
||||
level: Level,
|
||||
): Promise<CubeDecision> {
|
||||
const args = ["--level", level, "cube", position, await matchId(cube)];
|
||||
return toCubeDecision((await run(args)) as CliCubeDecision);
|
||||
},
|
||||
|
||||
async analyseMove(
|
||||
position: PositionId,
|
||||
cube: EngineCube,
|
||||
roll: Roll,
|
||||
played: PositionId,
|
||||
ply: Ply,
|
||||
): Promise<MoveAnalysis | null> {
|
||||
const args = ["--ply", String(ply), "analysemove", position, await matchId(cube)];
|
||||
const answer = (await run([...args, diceText(roll), played])) as {
|
||||
move_count: number;
|
||||
rank: number | null;
|
||||
played: CliEvaluatedMove | null;
|
||||
best: CliEvaluatedMove | null;
|
||||
equity_loss: number;
|
||||
skill: CliSkill;
|
||||
};
|
||||
if (answer.rank === null || answer.played === null || answer.best === null) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
moveCount: answer.move_count,
|
||||
rank: answer.rank,
|
||||
played: toEvaluatedMove(answer.played),
|
||||
best: toEvaluatedMove(answer.best),
|
||||
equityLoss: answer.equity_loss,
|
||||
skill: toSkill(answer.skill),
|
||||
};
|
||||
},
|
||||
|
||||
async analyseCube(
|
||||
position: PositionId,
|
||||
cube: EngineCube,
|
||||
action: CubeAction,
|
||||
ply: Ply,
|
||||
): Promise<CubeAnalysis> {
|
||||
const cliAction = {
|
||||
noDouble: "no_double",
|
||||
double: "double",
|
||||
take: "take",
|
||||
pass: "pass",
|
||||
}[action];
|
||||
const args = ["--ply", String(ply), "analysecube", position, await matchId(cube)];
|
||||
const answer = (await run([...args, cliAction])) as {
|
||||
analysed: boolean;
|
||||
cube: {
|
||||
no_double: number;
|
||||
double_take: number;
|
||||
double_pass: number;
|
||||
optimal: number;
|
||||
} | null;
|
||||
equity_loss: number;
|
||||
skill: CliSkill;
|
||||
};
|
||||
return {
|
||||
analysed: answer.analysed,
|
||||
equities:
|
||||
answer.cube === null
|
||||
? null
|
||||
: {
|
||||
noDouble: answer.cube.no_double,
|
||||
doubleTake: answer.cube.double_take,
|
||||
doublePass: answer.cube.double_pass,
|
||||
optimal: answer.cube.optimal,
|
||||
},
|
||||
equityLoss: answer.equity_loss,
|
||||
skill: toSkill(answer.skill),
|
||||
};
|
||||
},
|
||||
|
||||
async luck(position: PositionId, cube: EngineCube, roll: Roll): Promise<Luck> {
|
||||
const args = ["luck", position, await matchId(cube), diceText(roll)];
|
||||
const answer = (await run(args)) as { luck: number; rating: string };
|
||||
const ratings: Record<string, LuckRating> = {
|
||||
very_unlucky: "veryUnlucky",
|
||||
unlucky: "unlucky",
|
||||
none: "none",
|
||||
lucky: "lucky",
|
||||
very_lucky: "veryLucky",
|
||||
};
|
||||
const rating = ratings[answer.rating];
|
||||
if (rating === undefined) {
|
||||
throw new Error(`chance inconnue : ${answer.rating}`);
|
||||
}
|
||||
return { value: answer.luck, rating };
|
||||
},
|
||||
|
||||
async checkHalfMoves(
|
||||
@@ -139,6 +257,18 @@ function toMove(move: CliMove): Move {
|
||||
};
|
||||
}
|
||||
|
||||
function toEvaluatedMove(move: CliEvaluatedMove): EvaluatedMove {
|
||||
return { ...toMove(move), equity: move.equity, ply: move.ply };
|
||||
}
|
||||
|
||||
function toCubeDecision(answer: CliCubeDecision): CubeDecision {
|
||||
return { available: answer.available, double: answer.double, take: answer.take };
|
||||
}
|
||||
|
||||
function toSkill(skill: CliSkill): Skill {
|
||||
return skill === "very_bad" ? "veryBad" : skill;
|
||||
}
|
||||
|
||||
function toSide(side: CliSide): BoardSide {
|
||||
return { points: side.points, bar: side.bar, off: side.off, pipCount: side.pip_count };
|
||||
}
|
||||
|
||||
+115
-23
@@ -13,8 +13,22 @@ import type { GameJournal } from "../domain/game/events.js";
|
||||
import type { Game, MoveSteps, SideBoards } from "../domain/game/game.js";
|
||||
import { formatHalfMoves, type HalfMove } from "../domain/game/position.js";
|
||||
import { otherSide, type Side } from "../domain/game/side.js";
|
||||
import { boardModel, moveTargets, moveTrace, type Place, placeOf } from "./board-model.js";
|
||||
import { checkerSpot, NO_MARKS, renderBoard, type ShownDie, slideChecker } from "./board-view.js";
|
||||
import {
|
||||
boardModel,
|
||||
moveTargets,
|
||||
moveTrace,
|
||||
onlyHalfMove,
|
||||
type Place,
|
||||
placeOf,
|
||||
} from "./board-model.js";
|
||||
import {
|
||||
checkerSpot,
|
||||
type DiceAction,
|
||||
NO_MARKS,
|
||||
renderBoard,
|
||||
type ShownDie,
|
||||
slideChecker,
|
||||
} from "./board-view.js";
|
||||
import { showHelp } from "./help.js";
|
||||
import { TEXTS } from "./texts.js";
|
||||
|
||||
@@ -51,6 +65,7 @@ export function startApp(options: AppOptions): void {
|
||||
double: byId<HTMLButtonElement>("double"),
|
||||
take: byId<HTMLButtonElement>("take"),
|
||||
drop: byId<HTMLButtonElement>("drop"),
|
||||
confirm: byId<HTMLButtonElement>("confirm"),
|
||||
cancel: byId<HTMLButtonElement>("cancel"),
|
||||
skip: byId<HTMLButtonElement>("skip"),
|
||||
replay: byId<HTMLButtonElement>("replay"),
|
||||
@@ -61,12 +76,19 @@ export function startApp(options: AppOptions): void {
|
||||
game: byId("game"),
|
||||
help: byId("help"),
|
||||
helpContent: byId("help-content"),
|
||||
doubleDialog: byId<HTMLDialogElement>("double-dialog"),
|
||||
doubleQuestion: byId("double-question"),
|
||||
doubleConfirm: byId<HTMLButtonElement>("double-confirm"),
|
||||
doubleCancel: byId<HTMLButtonElement>("double-cancel"),
|
||||
};
|
||||
ui.board.setAttribute("aria-label", TEXTS.boardLabel);
|
||||
ui.roll.textContent = TEXTS.roll;
|
||||
ui.double.textContent = TEXTS.double;
|
||||
ui.take.textContent = TEXTS.take;
|
||||
ui.drop.textContent = TEXTS.drop;
|
||||
ui.confirm.textContent = TEXTS.confirm;
|
||||
ui.doubleConfirm.textContent = TEXTS.double;
|
||||
ui.doubleCancel.textContent = TEXTS.cancel;
|
||||
ui.cancel.textContent = TEXTS.cancel;
|
||||
ui.skip.textContent = TEXTS.skip;
|
||||
ui.replay.textContent = TEXTS.replay;
|
||||
@@ -131,9 +153,9 @@ export function startApp(options: AppOptions): void {
|
||||
shownMove === null
|
||||
? null
|
||||
: moveTrace(shownMove.side, shownMove.halfMoves, shownMove.hits),
|
||||
// Mêmes conditions que les boutons « Lancer les dés » et
|
||||
// « Doubler ».
|
||||
canRoll: !busy && state.turn === "player" && state.phase === "rolling",
|
||||
// Mêmes conditions que les boutons « Lancer les dés »,
|
||||
// « Valider » et « Doubler ».
|
||||
diceAction: busy ? null : diceAction(game),
|
||||
canDouble: !busy && canDouble,
|
||||
});
|
||||
}
|
||||
@@ -156,6 +178,7 @@ export function startApp(options: AppOptions): void {
|
||||
show(ui.roll, player && state.phase === "rolling");
|
||||
show(ui.double, player && state.phase === "rolling");
|
||||
ui.double.disabled = busy || !canDouble;
|
||||
show(ui.confirm, player && state.phase === "moving" && game.moveComplete);
|
||||
show(ui.cancel, player && state.phase === "moving" && !stuck);
|
||||
show(ui.skip, stuck);
|
||||
ui.cancel.disabled = busy || game.pendingHalfMoves.length === 0;
|
||||
@@ -168,6 +191,7 @@ export function startApp(options: AppOptions): void {
|
||||
ui.roll,
|
||||
ui.take,
|
||||
ui.drop,
|
||||
ui.confirm,
|
||||
ui.skip,
|
||||
ui.replay,
|
||||
ui.again,
|
||||
@@ -304,11 +328,22 @@ export function startApp(options: AppOptions): void {
|
||||
await g.skipTurn();
|
||||
});
|
||||
|
||||
ui.double.onclick = () =>
|
||||
act(async (g) => {
|
||||
// Doubler demande confirmation : un double proposé ne se reprend pas.
|
||||
ui.double.onclick = () => {
|
||||
if (busy || game === null || !canDouble) {
|
||||
return;
|
||||
}
|
||||
ui.doubleQuestion.textContent = TEXTS.confirmDouble(2 * game.state.cube.value);
|
||||
ui.doubleDialog.showModal();
|
||||
};
|
||||
ui.doubleCancel.onclick = () => ui.doubleDialog.close();
|
||||
ui.doubleConfirm.onclick = () => {
|
||||
ui.doubleDialog.close();
|
||||
void act(async (g) => {
|
||||
await g.double();
|
||||
info = g.state.phase === "over" ? "" : TEXTS.computerTakes(g.state.cube.value);
|
||||
});
|
||||
};
|
||||
|
||||
ui.take.onclick = () =>
|
||||
act((g) => {
|
||||
@@ -322,12 +357,23 @@ export function startApp(options: AppOptions): void {
|
||||
info = "";
|
||||
});
|
||||
|
||||
// Le coup saisi, complet, entre dans la partie : le tour passe.
|
||||
ui.confirm.onclick = () =>
|
||||
act(async (g) => {
|
||||
info = "";
|
||||
await g.confirmMove();
|
||||
});
|
||||
|
||||
// Annuler défait le dernier demi-coup saisi, un par un. L'écran est
|
||||
// occupé le temps que le moteur recalcule les coups possibles.
|
||||
ui.cancel.onclick = () => {
|
||||
if (!busy && game !== null) {
|
||||
game.cancelHalfMoves();
|
||||
game.undoLastHalfMove();
|
||||
selected = null;
|
||||
shownMove = null;
|
||||
void refresh();
|
||||
busy = true;
|
||||
render();
|
||||
void settle(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -344,15 +390,30 @@ export function startApp(options: AppOptions): void {
|
||||
ui.newGame.onclick = newGame;
|
||||
ui.again.onclick = newGame;
|
||||
|
||||
// Toucher le plateau : un pion jouable, puis une de ses cases d'arrivée.
|
||||
/** Joue un demi-coup du joueur ; le coup attend ensuite sa suite ou sa
|
||||
* validation. */
|
||||
function playHalfMove(halfMove: HalfMove): void {
|
||||
void act(async (g) => {
|
||||
info = "";
|
||||
await g.playHalfMove(halfMove);
|
||||
});
|
||||
}
|
||||
|
||||
// Toucher le plateau : un pion jouable, puis une de ses cases d'arrivée
|
||||
// (ou aucune, s'il n'en a qu'une : il y va tout de suite).
|
||||
ui.board.addEventListener("click", (event) => {
|
||||
const target = event.target as Element | null;
|
||||
// Toucher les dés en attente ou le videau : comme leurs boutons.
|
||||
// Toucher les dés (d'un côté ou de l'autre du plateau) ou le videau :
|
||||
// comme leurs boutons.
|
||||
const action = target?.closest("[data-action]")?.getAttribute("data-action");
|
||||
if (action === "roll" && !ui.roll.hidden && !ui.roll.disabled) {
|
||||
ui.roll.click();
|
||||
return;
|
||||
}
|
||||
if (action === "confirm" && !ui.confirm.hidden && !ui.confirm.disabled) {
|
||||
ui.confirm.click();
|
||||
return;
|
||||
}
|
||||
if (action === "double" && !ui.double.hidden && !ui.double.disabled) {
|
||||
ui.double.click();
|
||||
return;
|
||||
@@ -363,10 +424,12 @@ export function startApp(options: AppOptions): void {
|
||||
}
|
||||
const halfMove = selected === null ? undefined : targets.get(selected)?.get(place);
|
||||
if (halfMove !== undefined) {
|
||||
void act(async (g) => {
|
||||
info = "";
|
||||
await g.playHalfMove(halfMove);
|
||||
});
|
||||
playHalfMove(halfMove);
|
||||
return;
|
||||
}
|
||||
const only = place === selected ? null : onlyHalfMove(targets, place);
|
||||
if (only !== null) {
|
||||
playHalfMove(only);
|
||||
return;
|
||||
}
|
||||
selected = targets.has(place) && place !== selected ? place : null;
|
||||
@@ -405,6 +468,7 @@ export function startApp(options: AppOptions): void {
|
||||
ui.double,
|
||||
ui.take,
|
||||
ui.drop,
|
||||
ui.confirm,
|
||||
ui.cancel,
|
||||
ui.skip,
|
||||
ui.replay,
|
||||
@@ -452,19 +516,44 @@ function rollBefore(game: Game): readonly number[] {
|
||||
return rolled?.type === "DiceRolled" ? rolled.roll : [];
|
||||
}
|
||||
|
||||
/** Les dés à montrer : le dernier jet, de la couleur de qui l'a lancé
|
||||
* (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. */
|
||||
/** Ce que fait un toucher sur les dés : lancer quand le joueur peut lancer,
|
||||
* valider quand son coup est complet. */
|
||||
function diceAction(game: Game): DiceAction | null {
|
||||
const state = game.state;
|
||||
if (state.turn !== "player") {
|
||||
return null;
|
||||
}
|
||||
if (state.phase === "rolling") {
|
||||
return "roll";
|
||||
}
|
||||
return state.phase === "moving" && game.moveComplete ? "confirm" : null;
|
||||
}
|
||||
|
||||
/** Les dés à montrer : le dernier jet, de la couleur de qui l'a lancé (au
|
||||
* jet d'ouverture, un dé chacun ; quatre pour un double). Pendant que le
|
||||
* joueur saisit son coup, chaque dé déjà utilisé est estompé. 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 toPlay = game.diceToPlay;
|
||||
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", blocked }));
|
||||
// Au jet d'ouverture, un dé chacun.
|
||||
const sideOf = (i: number): Side => rolled.side ?? (i === 0 ? "player" : "computer");
|
||||
if (toPlay.length > 0) {
|
||||
return toPlay.map((die, i) => ({
|
||||
value: die.value,
|
||||
side: sideOf(i),
|
||||
blocked: stuck,
|
||||
used: die.used,
|
||||
}));
|
||||
}
|
||||
const blocked = computerCouldNotMove(game);
|
||||
const [a, b] = rolled.roll;
|
||||
const values = a === b ? [a, a, a, a] : [a, b];
|
||||
return values.map((value, i) => ({ value, side: sideOf(i), blocked, used: false }));
|
||||
}
|
||||
|
||||
/** La dernière chose jouée (fin de partie mise à part) est un tour de
|
||||
@@ -519,6 +608,9 @@ function prompt(game: Game, selecting: boolean, canDouble: boolean): string {
|
||||
if (state.phase === "rolling") {
|
||||
return canDouble ? TEXTS.yourRoll : TEXTS.yourRollNoDouble;
|
||||
}
|
||||
if (state.phase === "moving" && game.moveComplete) {
|
||||
return TEXTS.confirmOrUndo;
|
||||
}
|
||||
if (state.phase === "moving" && state.roll !== null) {
|
||||
return selecting ? TEXTS.pickTarget : TEXTS.yourMove(state.roll);
|
||||
}
|
||||
|
||||
@@ -82,6 +82,19 @@ export function moveTargets(halfMoves: readonly HalfMove[]): Map<Place, Map<Plac
|
||||
return targets;
|
||||
}
|
||||
|
||||
/** Le seul demi-coup possible depuis place, s'il n'a qu'une case d'arrivée
|
||||
* (le pion y va dès qu'on le touche) ; null sinon. */
|
||||
export function onlyHalfMove(
|
||||
targets: ReadonlyMap<Place, ReadonlyMap<Place, HalfMove>>,
|
||||
place: Place,
|
||||
): HalfMove | null {
|
||||
const tos = targets.get(place);
|
||||
if (tos === undefined || tos.size !== 1) {
|
||||
return null;
|
||||
}
|
||||
return [...tos.values()][0] ?? null;
|
||||
}
|
||||
|
||||
/** Ce qui reste dessiné d'un coup une fois joué. */
|
||||
export interface MoveTrace {
|
||||
/** Le camp qui a joué. */
|
||||
@@ -126,6 +139,14 @@ export function moveTrace(
|
||||
return { side, arrows, landed, left, hits: hits.filter(Boolean).length };
|
||||
}
|
||||
|
||||
/** La couleur d'une flèche (case), d'après son numéro affiché (1 à 24,
|
||||
* pour le joueur) : impaire en point-a, paire en point-b (charte). Jamais
|
||||
* d'après sa colonne : les cases 12 et 13, l'une au-dessus de l'autre,
|
||||
* n'ont pas la même couleur. */
|
||||
export function pointColor(number: number): "point-a" | "point-b" {
|
||||
return number % 2 === 1 ? "point-a" : "point-b";
|
||||
}
|
||||
|
||||
/** Où se trouve une case sur le dessin : rangée du haut ou du bas, et
|
||||
* colonne de 0 (à gauche) à 11 (à droite), sans compter la barre. */
|
||||
export function pointSlot(number: number): { top: boolean; column: number } {
|
||||
|
||||
+36
-15
@@ -11,7 +11,13 @@
|
||||
|
||||
import type { Cube } from "../domain/game/cube.js";
|
||||
import type { Side } from "../domain/game/side.js";
|
||||
import { type BoardModel, type MoveTrace, type Place, pointSlot } from "./board-model.js";
|
||||
import {
|
||||
type BoardModel,
|
||||
type MoveTrace,
|
||||
type Place,
|
||||
pointColor,
|
||||
pointSlot,
|
||||
} from "./board-model.js";
|
||||
import { TEXTS } from "./texts.js";
|
||||
|
||||
/** Un dé affiché, de la couleur du camp qui l'a lancé. */
|
||||
@@ -20,8 +26,13 @@ export interface ShownDie {
|
||||
readonly side: Side;
|
||||
/** Le jet ne permet aucun coup : le dé est estompé et barré. */
|
||||
readonly blocked: boolean;
|
||||
/** Le dé est déjà utilisé par un demi-coup saisi : il est estompé. */
|
||||
readonly used: boolean;
|
||||
}
|
||||
|
||||
/** Ce que fait un toucher sur les dés : lancer, ou valider le coup saisi. */
|
||||
export type DiceAction = "roll" | "confirm";
|
||||
|
||||
/** Ce qui s'ajoute aux pions : dés, videau, mises en évidence. */
|
||||
export interface BoardDecor {
|
||||
readonly dice: readonly ShownDie[];
|
||||
@@ -33,8 +44,10 @@ export interface BoardDecor {
|
||||
readonly targets: ReadonlySet<Place>;
|
||||
/** Les traces du dernier coup de l'ordinateur, tant qu'on les montre. */
|
||||
readonly trace: MoveTrace | null;
|
||||
/** Le joueur peut lancer les dés : ses dés en attente se touchent. */
|
||||
readonly canRoll: boolean;
|
||||
/** Ce que fait un toucher sur la zone des dés, d'un côté ou de l'autre
|
||||
* du plateau (null : rien). Pour « roll », les dés du joueur en attente
|
||||
* sont dessinés. */
|
||||
readonly diceAction: DiceAction | null;
|
||||
/** Le joueur peut doubler : le videau se touche. */
|
||||
readonly canDouble: boolean;
|
||||
}
|
||||
@@ -45,7 +58,7 @@ export const NO_MARKS = {
|
||||
selected: null,
|
||||
targets: new Set<Place>(),
|
||||
trace: null,
|
||||
canRoll: false,
|
||||
diceAction: null,
|
||||
canDouble: false,
|
||||
} as const;
|
||||
|
||||
@@ -158,9 +171,16 @@ export function renderBoard(svg: SVGSVGElement, board: BoardModel, decor: BoardD
|
||||
if (decor.trace !== null) {
|
||||
drawTrace(svg, board, decor.trace);
|
||||
}
|
||||
if (decor.canRoll) {
|
||||
const center = waitingDice(svg);
|
||||
addAction(hits, "roll", TEXTS.roll, center, 2 * DIE + 8 + MIN_TARGET / 2);
|
||||
if (decor.diceAction !== null) {
|
||||
if (decor.diceAction === "roll") {
|
||||
waitingDice(svg);
|
||||
}
|
||||
// Une zone sur chaque moitié du plateau : les dés en attente (à
|
||||
// gauche) comme le dernier jet (à droite) se touchent.
|
||||
const label = decor.diceAction === "roll" ? TEXTS.roll : TEXTS.confirm;
|
||||
for (const x of [FRAME + HALF_WIDTH / 2, RIGHT_HALF + HALF_WIDTH / 2]) {
|
||||
addAction(hits, decor.diceAction, label, { x, y: MIDDLE }, HALF_WIDTH - 2 * FRAME);
|
||||
}
|
||||
}
|
||||
if (decor.canDouble) {
|
||||
cube.classList.add("can-double");
|
||||
@@ -207,7 +227,7 @@ function drawPoint(
|
||||
const base = top ? FRAME : HEIGHT - FRAME;
|
||||
const tip = top ? FRAME + POINT_HEIGHT : HEIGHT - FRAME - POINT_HEIGHT;
|
||||
add(svg, "polygon", {
|
||||
class: column % 2 === 0 ? "point-a" : "point-b",
|
||||
class: pointColor(number),
|
||||
points: `${left},${base} ${left + POINT_WIDTH},${base} ${center},${tip}`,
|
||||
});
|
||||
add(
|
||||
@@ -493,7 +513,9 @@ function drawDice(svg: SVGSVGElement, dice: readonly ShownDie[]): void {
|
||||
const x = center - total / 2 + i * (DIE + gap);
|
||||
const y = MIDDLE - DIE / 2;
|
||||
const group = add(svg, "g", {
|
||||
class: die.blocked ? "die die-blocked" : "die",
|
||||
class: ["die", die.blocked ? "die-blocked" : "", die.used ? "die-used" : ""]
|
||||
.filter(Boolean)
|
||||
.join(" "),
|
||||
"data-value": die.value,
|
||||
});
|
||||
add(group, "rect", { class: `die-${die.side}`, x, y, width: DIE, height: DIE, rx: 6 });
|
||||
@@ -535,9 +557,8 @@ function cubeCenter(cube: Cube): Spot {
|
||||
}
|
||||
|
||||
/** Les dés du joueur en attente d'être lancés, faces vides, au milieu de la
|
||||
* moitié gauche (la droite garde le dernier jet de l'ordinateur). Renvoie
|
||||
* leur centre. */
|
||||
function waitingDice(svg: SVGSVGElement): Spot {
|
||||
* moitié gauche (la droite garde le dernier jet de l'ordinateur). */
|
||||
function waitingDice(svg: SVGSVGElement): void {
|
||||
const center = { x: FRAME + HALF_WIDTH / 2, y: MIDDLE };
|
||||
const gap = 8;
|
||||
const group = add(svg, "g", { class: "die die-waiting" });
|
||||
@@ -551,13 +572,13 @@ function waitingDice(svg: SVGSVGElement): Spot {
|
||||
rx: 6,
|
||||
});
|
||||
}
|
||||
return center;
|
||||
}
|
||||
|
||||
/**
|
||||
* Une zone à toucher qui fait la même chose qu'un bouton (lancer les dés,
|
||||
* doubler) : au moins MIN_TARGET de côté, centrée sur ce qu'on touche, avec
|
||||
* un rôle et un nom accessibles. Les boutons restent pour le clavier.
|
||||
* valider le coup, doubler) : au moins MIN_TARGET de côté, centrée sur ce
|
||||
* qu'on touche, avec un rôle et un nom accessibles. Les boutons restent
|
||||
* pour le clavier.
|
||||
*/
|
||||
function addAction(
|
||||
hits: Element,
|
||||
|
||||
+1
-3
@@ -91,10 +91,8 @@ export async function showHelp(container: HTMLElement): Promise<void> {
|
||||
renderFeature(container, feature);
|
||||
}
|
||||
renderGlossary(container, glossary);
|
||||
const about = element(container, "p", `${TEXTS.about} `);
|
||||
const about = element(container, "p", TEXTS.about);
|
||||
about.className = "about";
|
||||
const link = element(about, "a", TEXTS.engineTestLink);
|
||||
link.href = "moteur/";
|
||||
container.dataset.loaded = "true";
|
||||
} catch {
|
||||
status.textContent = TEXTS.helpFailed;
|
||||
|
||||
+4
-1
@@ -36,6 +36,7 @@ export const TEXTS = {
|
||||
double: "Doubler",
|
||||
take: "Prendre",
|
||||
drop: "Passer",
|
||||
confirm: "Valider",
|
||||
cancel: "Annuler",
|
||||
skip: "Continuer",
|
||||
replay: "Revoir",
|
||||
@@ -60,6 +61,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.",
|
||||
confirmOrUndo:
|
||||
"Touchez vos dés ou « Valider » pour jouer ce coup, ou « Annuler » pour reprendre un pion.",
|
||||
youCannotMove: (dice: readonly number[]) => `Aucun coup possible avec ${roll(dice)}.`,
|
||||
pressContinue: "Touchez « Continuer » : le tour passe à l'ordinateur.",
|
||||
thinking: "L'ordinateur réfléchit…",
|
||||
@@ -67,6 +70,7 @@ export const TEXTS = {
|
||||
`L'ordinateur a joué ${roll(dice)} : ${move} (${seconds(ms)}).`,
|
||||
computerCannotMove: (dice: readonly number[]) =>
|
||||
`L'ordinateur ne peut pas jouer ${roll(dice)}.`,
|
||||
confirmDouble: (value: number) => `Doubler à ${value} ? L'ordinateur pourra prendre ou passer.`,
|
||||
computerDoubles: "L'ordinateur vous propose de doubler : prenez ou passez.",
|
||||
computerTakes: (value: number) =>
|
||||
`L'ordinateur prend : la partie vaut maintenant ${points(value)}.`,
|
||||
@@ -90,5 +94,4 @@ export const TEXTS = {
|
||||
helpLoading: "Chargement de l'aide…",
|
||||
helpFailed: "L'aide n'a pas pu se charger.",
|
||||
about: "Ustagammon est un logiciel libre (GPLv3). L'ordinateur joue avec GNU Backgammon.",
|
||||
engineTestLink: "Test du moteur",
|
||||
} as const;
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
// L'adaptateur du moteur WebAssembly pour les niveaux, l'analyse et la
|
||||
// chance (étape 1a), sur le vrai moteur. La concordance des nombres avec
|
||||
// gnubg de bureau est vérifiée par tests/test_wasm_reference.sh ; ici, la
|
||||
// traduction des réponses en types du domaine.
|
||||
//
|
||||
// Copyright (C) 2026 les auteurs d'Ustagammon.
|
||||
// Licence : GPLv3 ou ultérieure (voir LICENSE).
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { type EngineCube, LUCK_THRESHOLDS, SKILL_THRESHOLDS } from "../src/domain/game/engine.js";
|
||||
import { START_POSITION } from "../src/domain/game/position.js";
|
||||
import { loadNodeEngine } from "./support/node-engine.js";
|
||||
|
||||
const centered: EngineCube = { value: 1, owner: "centered" };
|
||||
const middlegame = "mM/gATCw5+ABMA";
|
||||
|
||||
test("ouverture 3-1 : 24/23 13/10 est une erreur très mauvaise, 8/5 6/5 le meilleur coup", async () => {
|
||||
const engine = await loadNodeEngine();
|
||||
const analysis = await engine.analyseMove(
|
||||
START_POSITION,
|
||||
centered,
|
||||
[3, 1],
|
||||
"4HPwATDgc+IBKA",
|
||||
2,
|
||||
);
|
||||
assert.ok(analysis !== null);
|
||||
assert.equal(analysis.moveCount, 16);
|
||||
assert.equal(analysis.rank, 2);
|
||||
assert.equal(analysis.played.notation, "24/23 13/10");
|
||||
assert.equal(analysis.best.notation, "8/5 6/5");
|
||||
assert.equal(analysis.skill, "veryBad");
|
||||
assert.ok(
|
||||
Math.abs(analysis.equityLoss - (analysis.played.equity - analysis.best.equity)) < 1e-6,
|
||||
);
|
||||
assert.ok(analysis.equityLoss < -SKILL_THRESHOLDS.veryBad);
|
||||
});
|
||||
|
||||
test("sans coup possible, il n'y a rien à analyser", async () => {
|
||||
const engine = await loadNodeEngine();
|
||||
const position = "27YBBwAcz8EDQA";
|
||||
assert.equal(await engine.analyseMove(position, centered, [5, 5], position, 0), null);
|
||||
});
|
||||
|
||||
test("double/prend : passer est une erreur de l'adversaire, prendre n'en est pas une", async () => {
|
||||
const engine = await loadNodeEngine();
|
||||
const position = "6O5uAAC4uzsAAA";
|
||||
const pass = await engine.analyseCube(position, centered, "pass", 2);
|
||||
assert.equal(pass.analysed, true);
|
||||
assert.equal(pass.skill, "bad");
|
||||
assert.ok(pass.equities !== null && pass.equities.doubleTake < pass.equities.doublePass);
|
||||
const take = await engine.analyseCube(position, centered, "take", 2);
|
||||
assert.deepEqual([take.skill, take.equityLoss], ["none", 0]);
|
||||
const opponentOwns = await engine.analyseCube(
|
||||
position,
|
||||
{ value: 2, owner: "opponent" },
|
||||
"noDouble",
|
||||
2,
|
||||
);
|
||||
assert.deepEqual(opponentOwns, {
|
||||
analysed: false,
|
||||
equities: null,
|
||||
equityLoss: 0,
|
||||
skill: "none",
|
||||
});
|
||||
});
|
||||
|
||||
test("la chance d'un jet et sa catégorie", async () => {
|
||||
const engine = await loadNodeEngine();
|
||||
const opening = await engine.luck(START_POSITION, centered, [3, 1]);
|
||||
assert.ok(Math.abs(opening.value - 0.2126) < 2e-3);
|
||||
assert.equal(opening.rating, "none");
|
||||
const doubleSix = await engine.luck(START_POSITION, centered, [6, 6]);
|
||||
assert.ok(doubleSix.value > LUCK_THRESHOLDS.lucky);
|
||||
assert.equal(doubleSix.rating, "lucky");
|
||||
});
|
||||
|
||||
// Régression : sans cache vidé à chaque appel (engine.c, start_evaluation),
|
||||
// une analyse à 2 plis changeait la chance calculée ensuite (évaluations à
|
||||
// 0 pli des réseaux d'élagage restées dans le cache de gnubg).
|
||||
test("un résultat ne dépend pas des calculs faits avant", async () => {
|
||||
const engine = await loadNodeEngine();
|
||||
const before = await engine.luck(middlegame, centered, [6, 4]);
|
||||
await engine.analyseCube(middlegame, centered, "noDouble", 2);
|
||||
await engine.analyseMove(middlegame, centered, [6, 4], "mM/gATDGzuABMA", 2);
|
||||
assert.deepEqual(await engine.luck(middlegame, centered, [6, 4]), before);
|
||||
});
|
||||
|
||||
test("le niveau débutant joue toujours le même coup, différent du meilleur", async () => {
|
||||
const engine = await loadNodeEngine();
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const move = await engine.levelMove(middlegame, centered, [6, 4], "beginner");
|
||||
assert.equal(move?.notation, "24/20 24/18");
|
||||
}
|
||||
assert.equal((await engine.levelMove(middlegame, centered, [6, 4], "2ply"))?.notation, "24/14");
|
||||
const cube = await engine.levelCubeDecision("6O5uAAC4uzsAAA", centered, "beginner");
|
||||
assert.equal(cube.double, false);
|
||||
});
|
||||
|
||||
test("les seuils du domaine sont ceux du moteur", async () => {
|
||||
const root = new URL("../../../", import.meta.url);
|
||||
const module = await import(new URL("build/wasm/engine.mjs", root).href);
|
||||
const wrapper = await import(new URL("web/engine-wasm.mjs", root).href);
|
||||
const raw = await wrapper.loadEngine(
|
||||
module.default,
|
||||
fileURLToPath(new URL("build/wasm/", root)),
|
||||
);
|
||||
const { result } = raw.run(["levels"]) as {
|
||||
result: {
|
||||
levels: { name: string }[];
|
||||
skill_thresholds: { doubtful: number; bad: number; very_bad: number };
|
||||
luck_thresholds: { lucky: number; very_lucky: number };
|
||||
};
|
||||
};
|
||||
const close = (a: number, b: number) => Math.abs(a - b) < 1e-6;
|
||||
assert.ok(close(result.skill_thresholds.doubtful, SKILL_THRESHOLDS.doubtful));
|
||||
assert.ok(close(result.skill_thresholds.bad, SKILL_THRESHOLDS.bad));
|
||||
assert.ok(close(result.skill_thresholds.very_bad, SKILL_THRESHOLDS.veryBad));
|
||||
assert.ok(close(result.luck_thresholds.lucky, LUCK_THRESHOLDS.lucky));
|
||||
assert.ok(close(result.luck_thresholds.very_lucky, LUCK_THRESHOLDS.veryLucky));
|
||||
assert.deepEqual(
|
||||
result.levels.map((l) => l.name),
|
||||
["beginner", "casual", "intermediate", "advanced", "0ply", "1ply", "2ply", "3ply", "4ply"],
|
||||
);
|
||||
});
|
||||
@@ -45,6 +45,8 @@ async function playToTheEnd(game: Game, engine: Awaited<ReturnType<typeof loadNo
|
||||
for (const halfMove of first.halfMoves) {
|
||||
assert.equal(await game.playHalfMove(halfMove), true);
|
||||
}
|
||||
assert.equal(game.moveComplete, true);
|
||||
await game.confirmMove();
|
||||
}
|
||||
}
|
||||
return actions;
|
||||
|
||||
@@ -49,6 +49,16 @@ function fakeEngine(overrides: Partial<Engine> = {}): Engine {
|
||||
return { valid: false, complete: false, position: null };
|
||||
},
|
||||
board: async () => notOver,
|
||||
levelMove: async () => move,
|
||||
levelCubeDecision: async () => noDouble,
|
||||
analyseMove: async () => null,
|
||||
analyseCube: async () => ({
|
||||
analysed: false,
|
||||
equities: null,
|
||||
equityLoss: 0,
|
||||
skill: "none",
|
||||
}),
|
||||
luck: async () => ({ value: 0, rating: "none" }),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
@@ -78,14 +88,111 @@ test("un coup se saisit demi-coup par demi-coup ; un demi-coup refusé ne change
|
||||
assert.deepEqual(game.pendingHalfMoves, [{ from: 8, to: 5 }]);
|
||||
assert.equal(await game.playHalfMove({ from: 6, to: 5 }), true);
|
||||
|
||||
// Coup complet : il attend la validation, hors du journal.
|
||||
assert.equal(game.moveComplete, true);
|
||||
assert.equal(game.displayedPosition, "AFTER");
|
||||
assert.equal(game.events.length, 2);
|
||||
assert.equal(game.state.turn, "player");
|
||||
assert.deepEqual(await game.nextHalfMoves(), []);
|
||||
assert.equal(await game.playHalfMove({ from: 8, to: 7 }), false);
|
||||
|
||||
await game.confirmMove();
|
||||
const last = game.events.at(-1);
|
||||
assert.equal(last?.type, "MovePlayed");
|
||||
assert.deepEqual(game.pendingHalfMoves, []);
|
||||
assert.equal(game.moveComplete, false);
|
||||
assert.equal(game.state.turn, "computer");
|
||||
assert.equal(game.state.phase, "rolling");
|
||||
assert.equal(game.state.position, "SWAPPED");
|
||||
});
|
||||
|
||||
test("un coup incomplet ne se valide pas", async () => {
|
||||
const game = Game.start(fakeEngine(), { dice: fixedDice([3, 1]) });
|
||||
await assert.rejects(game.confirmMove(), /pas complet/);
|
||||
await game.playHalfMove({ from: 8, to: 5 });
|
||||
await assert.rejects(game.confirmMove(), /pas complet/);
|
||||
assert.equal(game.events.length, 2);
|
||||
});
|
||||
|
||||
test("Annuler défait les demi-coups un par un, sans toucher au journal", async () => {
|
||||
const game = Game.start(fakeEngine(), { dice: fixedDice([3, 1]) });
|
||||
await game.playHalfMove({ from: 8, to: 5 });
|
||||
await game.playHalfMove({ from: 6, to: 5 });
|
||||
const journal = game.journal;
|
||||
|
||||
game.undoLastHalfMove();
|
||||
assert.deepEqual(game.pendingHalfMoves, [{ from: 8, to: 5 }]);
|
||||
assert.equal(game.moveComplete, false);
|
||||
assert.equal(game.displayedPosition, "HALF");
|
||||
|
||||
game.undoLastHalfMove();
|
||||
assert.deepEqual(game.pendingHalfMoves, []);
|
||||
assert.equal(game.displayedPosition, START_POSITION);
|
||||
game.undoLastHalfMove(); // rien à défaire : sans effet
|
||||
assert.equal(game.journal, journal);
|
||||
|
||||
// Le coup se ressaisit ensuite normalement.
|
||||
assert.equal(await game.playHalfMove({ from: 8, to: 5 }), true);
|
||||
assert.equal(await game.playHalfMove({ from: 6, to: 5 }), true);
|
||||
assert.equal(game.moveComplete, true);
|
||||
});
|
||||
|
||||
test("les dés utilisés par les demi-coups saisis sont marqués", async () => {
|
||||
const game = Game.start(fakeEngine(), { dice: fixedDice([3, 1]) });
|
||||
assert.deepEqual(game.diceToPlay, [
|
||||
{ value: 3, used: false },
|
||||
{ value: 1, used: false },
|
||||
]);
|
||||
await game.playHalfMove({ from: 8, to: 5 });
|
||||
assert.deepEqual(game.diceToPlay, [
|
||||
{ value: 3, used: true },
|
||||
{ value: 1, used: false },
|
||||
]);
|
||||
await game.playHalfMove({ from: 6, to: 5 });
|
||||
assert.deepEqual(
|
||||
game.diceToPlay.map((d) => d.used),
|
||||
[true, true],
|
||||
);
|
||||
await game.confirmMove();
|
||||
assert.deepEqual(game.diceToPlay, []);
|
||||
});
|
||||
|
||||
test("un double donne quatre dés, marqués un par un ; une sortie prend le plus petit dé suffisant", async () => {
|
||||
const engine = fakeEngine({
|
||||
checkHalfMoves: async () => ({ valid: true, complete: false, position: "P" }),
|
||||
});
|
||||
const double = Game.start(engine, {
|
||||
dice: fixedDice([2, 2]),
|
||||
setup: { position: "X", firstTurn: "player" },
|
||||
});
|
||||
await double.roll();
|
||||
assert.equal(double.diceToPlay.length, 4);
|
||||
await double.playHalfMove({ from: 13, to: 11 });
|
||||
await double.playHalfMove({ from: 11, to: 9 });
|
||||
assert.deepEqual(
|
||||
double.diceToPlay.map((d) => d.used),
|
||||
[true, true, false, false],
|
||||
);
|
||||
|
||||
const bearOff = Game.start(engine, {
|
||||
dice: fixedDice([6, 4]),
|
||||
setup: { position: "X", firstTurn: "player" },
|
||||
});
|
||||
await bearOff.roll();
|
||||
// 3/off : le 4 suffit, le 6 reste.
|
||||
await bearOff.playHalfMove({ from: 3, to: 0 });
|
||||
assert.deepEqual(bearOff.diceToPlay, [
|
||||
{ value: 6, used: false },
|
||||
{ value: 4, used: true },
|
||||
]);
|
||||
// 6/2 demande le 4 : la sortie a donc pris le 6.
|
||||
await bearOff.playHalfMove({ from: 6, to: 2 });
|
||||
assert.deepEqual(
|
||||
bearOff.diceToPlay.map((d) => d.used),
|
||||
[true, true],
|
||||
);
|
||||
});
|
||||
|
||||
test("sans coup possible, le tour passe quand le joueur le laisse passer", async () => {
|
||||
const engine = fakeEngine({ legalMoves: async () => [] });
|
||||
const game = Game.start(engine, {
|
||||
@@ -192,6 +299,7 @@ test("annuler repart du parent ; l'action suivante ouvre une branche", async ()
|
||||
const game = Game.start(fakeEngine(), { dice: fixedDice([3, 1, 6, 5, 4, 2]) });
|
||||
await game.playHalfMove({ from: 8, to: 5 });
|
||||
await game.playHalfMove({ from: 6, to: 5 });
|
||||
await game.confirmMove();
|
||||
const played = game.events.at(-1);
|
||||
assert.equal(played?.type, "MovePlayed");
|
||||
|
||||
@@ -200,6 +308,7 @@ test("annuler repart du parent ; l'action suivante ouvre une branche", async ()
|
||||
assert.equal(game.state.turn, "player");
|
||||
await game.playHalfMove({ from: 8, to: 5 });
|
||||
await game.playHalfMove({ from: 6, to: 5 });
|
||||
await game.confirmMove();
|
||||
|
||||
// Deux coups accrochés au même jet : l'ancien reste dans le journal.
|
||||
const moves = game.journal.events.filter((e) => e.type === "MovePlayed");
|
||||
@@ -244,6 +353,7 @@ test("une partie se reprend depuis son journal ; un journal d'une autre version
|
||||
const game = Game.start(fakeEngine(), { dice: fixedDice([3, 1]) });
|
||||
await game.playHalfMove({ from: 8, to: 5 });
|
||||
await game.playHalfMove({ from: 6, to: 5 });
|
||||
await game.confirmMove();
|
||||
const saved = JSON.parse(JSON.stringify(game.journal));
|
||||
|
||||
const resumed = Game.resume(fakeEngine(), saved);
|
||||
|
||||
@@ -42,6 +42,7 @@ async function playerMove(position: PositionId, dice: [number, number], move: st
|
||||
for (const halfMove of parseHalfMoves(move)) {
|
||||
assert.equal(await game.playHalfMove(halfMove), true, formatHalfMoves([halfMove]));
|
||||
}
|
||||
await game.confirmMove();
|
||||
return game;
|
||||
}
|
||||
|
||||
@@ -59,6 +60,7 @@ test("un coup simple : un plateau par demi-coup, sans pion frappé", async () =>
|
||||
for (const halfMove of parseHalfMoves("8/5 6/5")) {
|
||||
await game.playHalfMove(halfMove);
|
||||
}
|
||||
await game.confirmMove();
|
||||
const steps = await game.lastMoveSteps();
|
||||
assert.ok(steps !== null);
|
||||
assert.equal(steps.side, "player");
|
||||
@@ -121,6 +123,7 @@ test("le coup de l'ordinateur, vu dans sa propre numérotation", async () => {
|
||||
for (const halfMove of parseHalfMoves("8/5 6/5")) {
|
||||
await game.playHalfMove(halfMove);
|
||||
}
|
||||
await game.confirmMove();
|
||||
await game.playComputerTurn();
|
||||
const steps = await game.lastMoveSteps();
|
||||
assert.ok(steps !== null);
|
||||
|
||||
@@ -80,6 +80,15 @@ async function play(text: string): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Joue les demi-coups puis valide le coup, complet. */
|
||||
async function playAndConfirm(text: string): Promise<boolean> {
|
||||
if (!(await play(text))) {
|
||||
return false;
|
||||
}
|
||||
await game.confirmMove();
|
||||
return true;
|
||||
}
|
||||
|
||||
function lastEvent(): GameEvent {
|
||||
const event = game.events.at(-1);
|
||||
assert.ok(event !== undefined, "aucun événement");
|
||||
@@ -160,12 +169,62 @@ Then("la position n'a pas changé", () => {
|
||||
assert.equal(lastEvent().type, "DiceRolled");
|
||||
});
|
||||
|
||||
// ------------------------------------------ Valider ou reprendre son coup
|
||||
|
||||
Then("votre coup attend votre validation", async () => {
|
||||
assert.equal(accepted, true);
|
||||
assert.equal(game.moveComplete, true);
|
||||
// Rien n'est encore dans le journal, et plus aucun pion ne bouge.
|
||||
assert.equal(lastEvent().type, "DiceRolled");
|
||||
assert.equal(game.state.turn, "player");
|
||||
assert.deepEqual(await game.nextHalfMoves(), []);
|
||||
});
|
||||
|
||||
Then("vos dés sont tous marqués comme joués", () => {
|
||||
assert.ok(game.diceToPlay.length > 0);
|
||||
assert.ok(game.diceToPlay.every((die) => die.used));
|
||||
});
|
||||
|
||||
When("vous validez votre coup", async () => {
|
||||
await game.confirmMove();
|
||||
});
|
||||
|
||||
Then("seul votre {int} est marqué comme joué", (value: number) => {
|
||||
const used = game.diceToPlay.filter((die) => die.used);
|
||||
assert.deepEqual(
|
||||
used.map((die) => die.value),
|
||||
[value],
|
||||
);
|
||||
});
|
||||
|
||||
Then("chacun de vos pions jouables n'a plus qu'une case d'arrivée", async () => {
|
||||
const halfMoves = await game.nextHalfMoves();
|
||||
assert.ok(halfMoves.length > 0);
|
||||
const starts = halfMoves.map((h) => h.from);
|
||||
assert.equal(new Set(starts).size, starts.length);
|
||||
});
|
||||
|
||||
When("vous annulez le dernier demi-coup", () => {
|
||||
game.undoLastHalfMove();
|
||||
});
|
||||
|
||||
Then("il ne reste de joué que {word}", (text: string) => {
|
||||
assert.deepEqual(game.pendingHalfMoves, parseHalfMoves(text));
|
||||
assert.equal(game.moveComplete, false);
|
||||
assert.equal(lastEvent().type, "DiceRolled");
|
||||
});
|
||||
|
||||
Then("aucun de vos dés n'est marqué comme joué", () => {
|
||||
assert.ok(game.diceToPlay.length > 0);
|
||||
assert.ok(game.diceToPlay.every((die) => !die.used));
|
||||
});
|
||||
|
||||
// ------------------------------------------------------- L'ordinateur joue
|
||||
|
||||
Given("c'est au tour de l'ordinateur", async () => {
|
||||
// Vous commencez avec 3-1, puis l'ordinateur tirera 6-4.
|
||||
game = Game.start(engine, { dice: fixedDice([3, 1, 6, 4]) });
|
||||
assert.equal(await play("8/5 6/5"), true);
|
||||
assert.equal(await playAndConfirm("8/5 6/5"), true);
|
||||
assert.equal(game.state.turn, "computer");
|
||||
});
|
||||
|
||||
@@ -287,6 +346,52 @@ Then("vous pouvez revoir son coup", async () => {
|
||||
|
||||
// ------------------------------------------------------------------ Videau
|
||||
|
||||
// La confirmation elle-même est une question de l'interface (une fenêtre
|
||||
// « Doubler à 2 ? ») : ces étapes vérifient que rien n'est proposé tant que
|
||||
// vous n'avez pas confirmé, et ce qui suit votre confirmation.
|
||||
|
||||
Given("vous pouvez doubler", async () => {
|
||||
game = Game.start(engine, { setup: { position: START_POSITION, firstTurn: "player" } });
|
||||
assert.equal(game.state.phase, "rolling");
|
||||
});
|
||||
|
||||
When("vous demandez à doubler", async () => {
|
||||
assert.equal(await game.canDouble(), true);
|
||||
});
|
||||
|
||||
When("vous changez d'avis", () => {
|
||||
// Vous refermez la question : la partie n'est pas touchée.
|
||||
});
|
||||
|
||||
Then("rien n'est proposé à l'ordinateur", () => {
|
||||
assert.ok(!game.events.some((e) => e.type === "CubeOffered"));
|
||||
assert.equal(game.state.phase, "rolling");
|
||||
assert.equal(game.state.turn, "player");
|
||||
assert.deepEqual(game.state.cube, { value: 1, owner: null });
|
||||
});
|
||||
|
||||
When("vous confirmez", async () => {
|
||||
await game.double();
|
||||
});
|
||||
|
||||
Then("l'ordinateur prend ou passe, comme le recommande le moteur", async () => {
|
||||
const decision = await engine.cubeDecision(
|
||||
START_POSITION,
|
||||
{ value: 1, owner: "centered" },
|
||||
STANDARD_PLY,
|
||||
);
|
||||
const offered = game.events.findIndex((e) => e.type === "CubeOffered");
|
||||
const answer = game.events[offered + 1];
|
||||
assert.ok(offered >= 0 && answer !== undefined);
|
||||
if (decision.take) {
|
||||
assert.equal(answer.type, "CubeTaken");
|
||||
assert.deepEqual(game.state.cube, { value: 2, owner: "computer" });
|
||||
} else {
|
||||
assert.equal(answer.type, "CubeDropped");
|
||||
assert.deepEqual(game.state.result, { winner: "player", kind: "single", points: 1 });
|
||||
}
|
||||
});
|
||||
|
||||
Given("l'ordinateur vous propose de doubler", async () => {
|
||||
game = Game.start(engine, { setup: { position: COMPUTER_DOUBLES, firstTurn: "computer" } });
|
||||
await game.playComputerTurn();
|
||||
@@ -346,7 +451,7 @@ Given("un joueur a sorti tous ses pions", async () => {
|
||||
dice: fixedDice([6, 5]),
|
||||
});
|
||||
await game.roll();
|
||||
assert.equal(await play("6/off 5/off"), true);
|
||||
assert.equal(await playAndConfirm("6/off 5/off"), true);
|
||||
});
|
||||
|
||||
Then("le résultat indique le gagnant et les points gagnés", () => {
|
||||
|
||||
+26
-1
@@ -12,7 +12,15 @@ import type { BoardSide } from "../src/domain/game/engine.js";
|
||||
import { appendEvent, EMPTY_JOURNAL } from "../src/domain/game/events.js";
|
||||
import { parseHalfMoves, START_POSITION } from "../src/domain/game/position.js";
|
||||
import { createGameStore } from "../src/storage/game-store.js";
|
||||
import { boardModel, moveTargets, moveTrace, placeOf, pointSlot } from "../src/ui/board-model.js";
|
||||
import {
|
||||
boardModel,
|
||||
moveTargets,
|
||||
moveTrace,
|
||||
onlyHalfMove,
|
||||
placeOf,
|
||||
pointColor,
|
||||
pointSlot,
|
||||
} from "../src/ui/board-model.js";
|
||||
import { parseFeature, parseGlossaryEntry, parseGlossaryIndex } from "../src/ui/help-text.js";
|
||||
import { TEXTS } from "../src/ui/texts.js";
|
||||
|
||||
@@ -47,6 +55,18 @@ test("cases, barre et sortie de chaque camp", () => {
|
||||
assert.equal(placeOf("computer", 0), "off-computer");
|
||||
});
|
||||
|
||||
test("couleur des flèches : impaires en point-a, paires en point-b, quelle que soit la colonne", () => {
|
||||
for (const number of [1, 13, 23]) {
|
||||
assert.equal(pointColor(number), "point-a", `flèche ${number}`);
|
||||
}
|
||||
for (const number of [2, 12, 24]) {
|
||||
assert.equal(pointColor(number), "point-b", `flèche ${number}`);
|
||||
}
|
||||
// 12 et 13 partagent la colonne 0, mais pas la couleur.
|
||||
assert.equal(pointSlot(12).column, pointSlot(13).column);
|
||||
assert.notEqual(pointColor(12), pointColor(13));
|
||||
});
|
||||
|
||||
test("disposition : 1 à 12 en bas de droite à gauche, 13 à 24 en haut de gauche à droite", () => {
|
||||
assert.deepEqual(pointSlot(1), { top: false, column: 11 });
|
||||
assert.deepEqual(pointSlot(7), { top: false, column: 5 });
|
||||
@@ -65,6 +85,11 @@ test("les demi-coups possibles donnent les pions jouables et leurs cases d'arriv
|
||||
assert.deepEqual([...targets.keys()], ["p8", "bar-player", "p3"]);
|
||||
assert.deepEqual([...(targets.get("p8")?.keys() ?? [])], ["p5", "p7"]);
|
||||
assert.deepEqual(targets.get("p3")?.get("off-player"), { from: 3, to: 0 });
|
||||
// Un seul toucher suffit pour un pion à destination unique.
|
||||
assert.deepEqual(onlyHalfMove(targets, "p3"), { from: 3, to: 0 });
|
||||
assert.deepEqual(onlyHalfMove(targets, "bar-player"), { from: 25, to: 22 });
|
||||
assert.equal(onlyHalfMove(targets, "p8"), null);
|
||||
assert.equal(onlyHalfMove(targets, "p13"), null);
|
||||
});
|
||||
|
||||
test("traces d'un coup simple : une flèche par demi-coup, départs et arrivées", () => {
|
||||
|
||||
@@ -68,3 +68,24 @@ vérification des dépendances.
|
||||
standard ») est tenu même au niveau fort (2 plis) : c'est le niveau standard
|
||||
de l'ordinateur à l'étape 0d.
|
||||
- Firefox sur le téléphone reste à vérifier, sans bloquer.
|
||||
|
||||
## Amendements
|
||||
|
||||
### 2026-11-03 — Pages de test du moteur retirées du site
|
||||
|
||||
Décidé par Pierre après son premier test sur le téléphone : « supprimer la
|
||||
partie test du moteur totalement ».
|
||||
|
||||
- Les pages de test et de mesure de l'étape 0b (`web/moteur/` : `index.html`,
|
||||
`bench.html` et leurs scripts) sont supprimées. Le site publié n'a plus rien
|
||||
sous `/moteur/`, ni dans le cache du service worker ; `make deploy`
|
||||
n'annonce plus que l'application, et l'aide ne renvoie plus vers le test du
|
||||
moteur.
|
||||
- Les mesures du Fairphone 4 ci-dessus, faites avec `bench.html`, restent la
|
||||
référence. Pour les refaire, la mesure se lance sous Node sur le PC
|
||||
(`make bench`) : son code partagé est déplacé de `web/moteur/benchmark.mjs`
|
||||
vers `engine-wasm/benchmark.mjs`. Une mesure sur le téléphone demanderait de
|
||||
remettre une page de mesure, à décider le moment venu.
|
||||
- Le moteur reste vérifié par la suite de contrat sur WebAssembly
|
||||
(`make test`) et, dans l'application, par la partie jouée dans un navigateur
|
||||
(`make test-browser`).
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
# 0013. Une position du plateau dans l'adresse de la page
|
||||
|
||||
- **Statut** : accepté
|
||||
- **Date** : 2026-11-03
|
||||
- **Décideurs** : Pierre (besoin), Claude Code (format)
|
||||
|
||||
## Contexte
|
||||
|
||||
Pierre a demandé, au premier test sur le téléphone (2026-11-03), qu'avec la
|
||||
seule adresse on arrive à une position du plateau : chaque position devient un
|
||||
lien direct, à partager, à mettre dans une leçon ou dans un prompt pour une IA.
|
||||
GNU Backgammon décrit déjà toute position par deux identifiants publics et
|
||||
documentés : le Position ID (14 caractères, les pions) et le Match ID
|
||||
(12 caractères : videau, score, longueur du match, joueur au trait, dés). Le
|
||||
moteur sait les lire et les écrire (`engine.h`).
|
||||
|
||||
## Décision
|
||||
|
||||
- L'adresse porte la position dans le fragment (après `#`) :
|
||||
`https://ustagammon.sans.pub/#position=<Position ID>&match=<Match ID>`,
|
||||
chaque identifiant encodé avec `encodeURIComponent` (le base64 de gnubg
|
||||
contient `+` et `/`). `match` est facultatif : sans lui, partie libre, videau
|
||||
au centre, joueur du bas au trait, sans dés.
|
||||
- Ouvrir un tel lien affiche la position, non jouable, avec deux actions :
|
||||
« Jouer à partir d'ici » (nouvelle partie contre l'ordinateur depuis cette
|
||||
position, sans toucher à la partie en cours) et « Retour à ma partie ».
|
||||
- Toute position affichée (partie, coup de l'ordinateur, analyse, leçon) peut
|
||||
être copiée en lien (« Copier le lien de la position »).
|
||||
- Un identifiant illisible pour le moteur donne un message clair et le retour
|
||||
à la partie, jamais une page cassée.
|
||||
|
||||
## Alternatives écartées
|
||||
|
||||
- **Paramètres de requête (`?position=`)** : ils changent l'adresse demandée
|
||||
au serveur et au service worker ; hors ligne, la page en cache ne
|
||||
correspondrait plus. Le fragment ne quitte jamais le navigateur.
|
||||
- **XGID (format d'eXtreme Gammon)** : répandu, mais un seul identifiant de
|
||||
plus à décoder ; gnubg l'importe, son import pourra s'ajouter plus tard
|
||||
(outil « import d'un identifiant GNU ou XGID » du cadrage).
|
||||
- **Format maison plus lisible** : il faudrait le décoder sans le moteur, donc
|
||||
réimplémenter une règle de représentation (ADR 0007).
|
||||
|
||||
## Conséquences
|
||||
|
||||
- Le fragment est lu au chargement et à chaque changement (`hashchange`) ; le
|
||||
domaine reçoit une position et un Match ID, le moteur les valide.
|
||||
- Les liens restent valables tant que GNU Backgammon garde ces formats, stables
|
||||
depuis des années.
|
||||
- Les liens contiennent la position, rien d'autre : pas de donnée personnelle.
|
||||
|
||||
## Amendements
|
||||
|
||||
### 2026-11-03 — Une adresse pour chaque écran, des liens plutôt que des états
|
||||
|
||||
Demandé par Pierre : « il faudra vraiment que chaque page (l'aide, etc.) ait
|
||||
une URL. Utilise des liens accessibles, pas des états. » Aucun lien de
|
||||
position n'a encore circulé : le format ci-dessus est remplacé par un schéma
|
||||
unique pour tous les écrans, avant sa première mise en œuvre.
|
||||
|
||||
- Chaque écran a son adresse, dans le fragment (pour les mêmes raisons
|
||||
qu'au-dessus : hors ligne, service worker, aucune configuration serveur) :
|
||||
|
||||
| Écran | Adresse |
|
||||
| --- | --- |
|
||||
| Partie en cours | `#/` |
|
||||
| Accueil | `#/accueil` |
|
||||
| Aide (onglet Jouer) | `#/aide`, une question : `#/aide/<identifiant>` |
|
||||
| Les mots du jeu | `#/glossaire`, un terme : `#/glossaire/<fichier sans .md>` |
|
||||
| À propos | `#/a-propos` |
|
||||
| Tutoriel | `#/tutoriel`, une étape : `#/tutoriel/<n>` |
|
||||
| Mes parties | `#/parties`, une partie : `#/parties/<id>` |
|
||||
| Réglages / menu | `#/menu` |
|
||||
| Une position | `#/position/<Position ID>/<Match ID>` (Match ID facultatif) |
|
||||
|
||||
Les identifiants de gnubg sont encodés avec `encodeURIComponent`. Les
|
||||
écrans à venir (analyse, leçons, progression) suivront le même schéma.
|
||||
- On change d'écran par de vrais liens (`<a href="#/aide">`), pas par des
|
||||
boutons qui modifient un état caché : le lien se partage, s'ouvre dans un
|
||||
nouvel onglet, se lit aux lecteurs d'écran comme un lien, et le bouton
|
||||
« retour » du téléphone ramène à l'écran précédent. Les actions de jeu
|
||||
(lancer, jouer, doubler) restent des boutons : elles ne sont pas des écrans.
|
||||
- Les feuilles (menu, explication) et panneaux qui sont des écrans à part
|
||||
entière ont aussi leur adresse ; une confirmation ou un bandeau éphémère
|
||||
n'en a pas.
|
||||
- Une adresse inconnue mène à la partie en cours avec un message clair.
|
||||
- L'adresse de l'écran courant est la source de vérité de la navigation :
|
||||
l'application l'écoute (`hashchange`) et dessine l'écran correspondant.
|
||||
@@ -19,3 +19,4 @@ l'ancien passe au statut « remplacé par NNNN ». Voir l'ADR 0001.
|
||||
| [0010](0010-identite-visuelle.md) | Identité visuelle : symbole, logotype, palette | accepté |
|
||||
| [0011](0011-lecons-conseil-et-bilan.md) | Leçons numérotées, conseil du maître après le coup, bilan de fin de partie | accepté |
|
||||
| [0012](0012-mode-immersif.md) | Mode immersif : le plateau en paysage, sur demande | accepté |
|
||||
| [0013](0013-position-dans-l-adresse.md) | Une position du plateau dans l'adresse de la page | accepté |
|
||||
|
||||
+3
-4
@@ -2,8 +2,8 @@
|
||||
|
||||
Ustagammon est un site statique (PWA, ADR 0009) : des fichiers HTML,
|
||||
JavaScript, le moteur en WebAssembly et ses données. L'application est à la
|
||||
racine ; les pages de test du moteur de l'étape 0b restent publiées sous
|
||||
`/moteur/` (test du moteur) et `/moteur/bench.html` (mesure). Il est publié sur
|
||||
racine ; les pages de test du moteur de l'étape 0b ont été retirées (ADR
|
||||
0009, amendement du 2026-11-03). Il est publié sur
|
||||
CapRover, machine `sans-pub` (https://captain.cloud.sans.pub), app
|
||||
`ustagammon`, sur https://ustagammon.sans.pub.
|
||||
|
||||
@@ -112,8 +112,7 @@ ssh sans.pub 'sudo docker service ps srv-captain--ustagammon --format "{{.Image}
|
||||
```
|
||||
|
||||
Puis ouvrir le site sur le téléphone : le plateau s'affiche et une consigne
|
||||
remplace « Chargement du moteur… ». La page de test du moteur
|
||||
(https://ustagammon.sans.pub/moteur/) doit afficher « Moteur prêt ».
|
||||
remplace « Chargement du moteur… ».
|
||||
|
||||
## Retour arrière
|
||||
|
||||
|
||||
+4
-3
@@ -17,7 +17,7 @@ Repris du cadrage (modules, écrans clés, IA générative en V1) et des ADR 001
|
||||
|
||||
| Sous-étape | Contenu | Dépend de |
|
||||
| --- | --- | --- |
|
||||
| 1a. Moteur pour l'analyse | Niveaux de jeu (plis et bruit de GNU Backgammon : débutant à expert), classement des coups d'un jet avec équités, analyse d'un coup joué et d'une décision de videau, chance d'un jet ; exposés par `engine.h`, `engine-cli` et le Worker, avec positions de référence comparées à gnubg de bureau | — |
|
||||
| 1a. Moteur pour l'analyse (fait) | Niveaux de jeu (plis et bruit de GNU Backgammon : débutant à expert), classement des coups d'un jet avec équités, analyse d'un coup joué et d'une décision de videau, chance d'un jet ; exposés par `engine.h`, `engine-cli` et le Worker, avec positions de référence comparées à gnubg de bureau | — |
|
||||
| 1b. Parties enregistrées | Stockage IndexedDB derrière l'interface de stockage existante : plusieurs parties, reprise automatique, branches (« Rejouer d'ici » crée une branche, l'originale reste intacte et rechargeable), export SGF avec variantes | — |
|
||||
| 1c. Match et réglages | Partie libre ou match en N points, règle Crawford, règle Jacoby, choix du niveau de l'ordinateur, conseil du maître activable ou non ; écran de nouvelle partie | 1a, 1b |
|
||||
| 1d. Conseil du maître | Après chaque coup du joueur, jamais avant (ADR 0011) : appréciation du moteur (bon coup, imprécision, erreur, gaffe) avec mot, icône et couleur, phrase du maître fondée sur les chiffres du moteur, coup recommandé montré sur demande ; aussi pour les décisions de videau ; calcul en arrière-plan sans retarder le jeu | 1a |
|
||||
@@ -43,8 +43,9 @@ Repris du cadrage (modules, écrans clés, IA générative en V1) et des ADR 001
|
||||
- **Questionnaire de premier lancement** (choix du parcours) : le parcours 1
|
||||
seul existe en V1 ; proposé : un simple choix « Je découvre » / « Je connais
|
||||
les règles » qui oriente vers le tutoriel ou la partie.
|
||||
- **Seuils des appréciations** : proposés d'après ceux de GNU Backgammon
|
||||
(douteux 0,04, mauvais 0,08, très mauvais 0,16 d'équité), à calibrer.
|
||||
- **Seuils des appréciations** : ceux de GNU Backgammon (douteux 0,03,
|
||||
mauvais 0,06, très mauvais 0,12 d'équité, `arSkillLevel`), exposés par le
|
||||
moteur depuis 1a ; à calibrer sur de vraies parties.
|
||||
- **Relecture des leçons** par un joueur fort de l'Atelier du Huit.
|
||||
|
||||
## Ordre de réalisation
|
||||
|
||||
@@ -23,10 +23,9 @@ Demandes de Pierre du 2026-11-03, traitées avant d'ouvrir l'étape 1.
|
||||
|
||||
| Sous-étape | Contenu | État | Plan |
|
||||
| --- | --- | --- | --- |
|
||||
| 0e.1 | Saisie du coup : pion à destination unique joué en un toucher, coup à valider et annulable demi-coup par demi-coup, dés joués marqués, dés des deux côtés touchables, videau confirmé ; pages de test du moteur retirées | 🔄 | `plans/2026-11-03-saisie-du-coup.md` |
|
||||
| 0e.3 | Révision UX arbitrée (session « identité », sur délégation de Pierre) : portrait stable, bandeaux de compte de points, menu en feuille et nouvelle partie annulable, vibrations, écran allumé, paysage « plateau = écran » dès la rotation (ADR 0012 amendé) | ⏳ | `plans/2026-11-03-revision-ux.md` |
|
||||
| 0e.4 | Position dans l'adresse : chaque position du plateau a un lien direct (nouvel ADR pour le format) | ⏳ | |
|
||||
| 0e.5 | Page d'accueil, installation en un toucher (« Ajouter à l'écran d'accueil », proposée par l'app), tutoriel des règles très didactique avec plateaux illustrés, aide réécrite sur un ton plus humain ; textes à valider par Pierre | ⏳ | |
|
||||
| 0e.4 | Une adresse par écran (aide, glossaire, menu, mes parties…) et par position, par de vrais liens (ADR 0013 amendé) | ⏳ | `plans/2026-11-03-position-dans-l-adresse.md` |
|
||||
| 0e.5 | Page d'accueil, installation en un toucher (« Ajouter à l'écran d'accueil », proposée par l'app), tutoriel des règles très didactique avec plateaux illustrés, aide réécrite sur un ton plus humain ; textes à valider par Pierre | ⏳ | `plans/2026-11-03-accueil-tutoriel-aide.md` |
|
||||
|
||||
## Étapes suivantes
|
||||
|
||||
|
||||
@@ -35,10 +35,12 @@ exportée. `make test` vérifie le lien dans les deux sens : chaque fiche avec
|
||||
|
||||
| Terme | Anglais (code) |
|
||||
| --- | --- |
|
||||
| [Appréciation](appreciation.md) | Skill |
|
||||
| [Backgammon](backgammon.md) | Backgammon |
|
||||
| [Barre](barre.md) | Bar |
|
||||
| [Blot](blot.md) | Blot |
|
||||
| [Branche](branche.md) | Branch |
|
||||
| [Chance](chance.md) | Luck |
|
||||
| [Compte de points](compte-de-points.md) | Pip count |
|
||||
| [Coup](coup.md) | Move |
|
||||
| [Demi-coup](demi-coup.md) | Half-move |
|
||||
@@ -52,6 +54,7 @@ exportée. `make test` vérifie le lien dans les deux sens : chaque fiche avec
|
||||
| [Jet](jet.md) | Roll |
|
||||
| [Match](match.md) | Match |
|
||||
| [Moteur](moteur.md) | Engine |
|
||||
| [Niveau de jeu](niveau-de-jeu.md) | Level |
|
||||
| [Partie](partie.md) | Game |
|
||||
| [Passer](passer.md) | Drop |
|
||||
| [Pion](pion.md) | Checker |
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
term: Appréciation
|
||||
english: Skill
|
||||
code: Skill
|
||||
---
|
||||
# Appréciation
|
||||
|
||||
**En bref** : le jugement du moteur sur un coup ou une décision de videau, d'après l'équité perdue par rapport au meilleur choix.
|
||||
|
||||
GNU Backgammon distingue trois degrés, selon l'équité perdue : douteux (plus
|
||||
de 0,03), mauvais (plus de 0,06) et très mauvais (plus de 0,12). En dessous,
|
||||
il n'y a rien à signaler.
|
||||
|
||||
Voir aussi : [Équité](equite.md), [Coup](coup.md), [Videau](videau.md)
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
term: Chance
|
||||
english: Luck
|
||||
code: Luck
|
||||
---
|
||||
# Chance
|
||||
|
||||
**En bref** : ce qu'un jet de dés fait gagner ou perdre, en équité, par rapport à la moyenne de tous les jets possibles.
|
||||
|
||||
Un jet est dit chanceux s'il fait gagner plus de 0,3 d'équité par rapport à la
|
||||
moyenne, très chanceux au-delà de 0,6 ; malchanceux ou très malchanceux dans
|
||||
l'autre sens. Comparer la chance d'une partie à ses erreurs aide à savoir si
|
||||
on a perdu à cause des dés ou de son jeu.
|
||||
|
||||
Voir aussi : [Équité](equite.md), [Jet](jet.md)
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
term: Niveau de jeu
|
||||
english: Level
|
||||
code: Level
|
||||
---
|
||||
# Niveau de jeu
|
||||
|
||||
**En bref** : la force de l'ordinateur, du débutant, qui commet volontairement des erreurs, au plus fort, qui réfléchit plusieurs plis à l'avance.
|
||||
|
||||
Les niveaux sont ceux de GNU Backgammon. Les quatre premiers (débutant,
|
||||
amateur, intermédiaire, avancé) jugent la position sans regarder les jets
|
||||
suivants et ajoutent un « bruit » à leur jugement, de plus en plus faible :
|
||||
c'est ce qui leur fait choisir parfois un moins bon coup. Ce bruit dépend de
|
||||
la position seule : dans la même position, avec le même jet, l'ordinateur
|
||||
joue toujours le même coup. Les niveaux suivants, sans bruit, réfléchissent
|
||||
de 0 à 4 plis.
|
||||
|
||||
Voir aussi : [Pli](pli.md), [Moteur](moteur.md)
|
||||
@@ -14,6 +14,8 @@ Décisions : ADR 0010. Fichiers du logo et des icônes : `../logo/`.
|
||||
| Fichier | Contenu |
|
||||
| --- | --- |
|
||||
| `guide-de-marque.md` | Règles d'usage : ton, couleurs, typographie, formes, mouvement, icônes, composants |
|
||||
| `accueil-tutoriel-aide.md` | Accueil, installation sans intrusion, tutoriel des règles (forme et première version du texte), aide en questions |
|
||||
| `recette-ux.md` | Critères de recette design de l'écran de jeu, à cocher sur les captures et sur le téléphone |
|
||||
| `interaction.md` | Comportement : réponse immédiate, annuler plutôt que confirmer, mouvement, vibrations, clavier, mises en page de l'écran de jeu (portrait, paysage, ordinateur), pictogrammes |
|
||||
| `tokens.json` | Toutes les valeurs, thèmes clair et sombre, avec une note d'usage par couleur |
|
||||
| `bundle.css` | Styles des composants ; mêmes noms de classes que `web/app.css` |
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
# Accueil, installation, tutoriel et aide
|
||||
|
||||
Règles de l'étape 0e.5 (et 1g pour le tutoriel). Maquettes : rangée « Étape
|
||||
0e.5 » du canevas https://claude.ai/artifact/CQw6vbDpi6XQqVWQa4sANe et
|
||||
l'artboard « Accueil ». Les textes sont une **première version, à valider par
|
||||
Pierre**. Comportement général : `interaction.md`.
|
||||
|
||||
## Accueil
|
||||
|
||||
**Premier lancement** (aucune partie ni leçon enregistrée) : le logo, le
|
||||
plateau en position de départ, « Bienvenue à la table », une phrase, puis deux
|
||||
grands choix :
|
||||
|
||||
- **« Je découvre »** (action principale) → le tutoriel, étape 1 ;
|
||||
- **« Je connais les règles »** → une partie tout de suite.
|
||||
|
||||
Le choix ne revient plus. Le tutoriel reste dans le menu.
|
||||
|
||||
**Ensuite**, de haut en bas :
|
||||
|
||||
1. **La partie en cours** : miniature du plateau, comptes de points,
|
||||
« Reprendre la partie » (seule action principale de l'écran) et
|
||||
« Nouvelle partie ». Sans partie en cours : « Nouvelle partie » devient
|
||||
l'action principale.
|
||||
2. **L'invitation à installer**, si elle doit apparaître (voir plus bas).
|
||||
3. **La prochaine leçon** : une seule carte (titre, état, progression,
|
||||
« Continuer » en secondaire), avec le lien « Toutes les leçons ».
|
||||
4. **La progression résumée**, en une ligne `caption` : « 1 leçon terminée
|
||||
sur 8 · 12 parties jouées · 4 gagnées ».
|
||||
5. En pied de page, la phrase de l'Atelier du Huit.
|
||||
|
||||
Pas de chiffres d'erreur ou d'équité sur l'accueil : ils viennent avec
|
||||
l'analyse.
|
||||
|
||||
## Installation
|
||||
|
||||
Le but : proposer l'installation au bon moment, une fois, sans jamais
|
||||
interrompre.
|
||||
|
||||
- **Jamais** au premier lancement, jamais pendant une partie, jamais en
|
||||
fenêtre qui s'impose.
|
||||
- **Quand** : à partir de la fin de la **première partie terminée**, quand le
|
||||
joueur a vu ce que vaut l'app.
|
||||
- **Où** : une carte compacte sur l'accueil (symbole, « Installer
|
||||
Ustagammon », « Sur l'écran d'accueil, en plein écran, sans réseau. »,
|
||||
bouton « Installer » et croix « Plus tard »), et une ligne « Installer
|
||||
l'application » dans le menu, toujours présente tant que l'app n'est pas
|
||||
installée.
|
||||
- **« Plus tard »** masque la carte ; elle revient une seule fois, après
|
||||
trois parties de plus. Ensuite, seul le menu la propose.
|
||||
- **Chrome Android** : le bouton déclenche l'invite du système, gardée depuis
|
||||
l'événement `beforeinstallprompt`. Sans cet événement (navigateur qui ne
|
||||
propose pas l'installation), le même bouton ouvre la marche à suivre.
|
||||
- **Firefox Android** : une feuille en trois étapes numérotées (menu ⋮,
|
||||
« Installer » ou « Ajouter à l'écran d'accueil », confirmer), qui se ferme
|
||||
par « J'ai compris ».
|
||||
- **iPhone (Safari)** : même feuille, avec ces étapes : bouton de partage,
|
||||
« Sur l'écran d'accueil », « Ajouter ».
|
||||
- **Une fois installée** (`display-mode: standalone`, ou événement
|
||||
`appinstalled`) : plus aucune proposition, la ligne du menu disparaît.
|
||||
|
||||
## Tutoriel des règles
|
||||
|
||||
### Forme
|
||||
|
||||
- Huit étapes, chacune en **écrans courts** : un titre, un plateau illustré,
|
||||
deux ou trois phrases. Une étape sur deux finit par un écran **« à vous »**
|
||||
où le joueur fait le geste sur le vrai plateau, contrôlé par le moteur.
|
||||
- En haut : une barre de progression en huit segments (`accent` pour les
|
||||
étapes faites) et « Passer » à droite. En bas : « Retour » et
|
||||
« Continuer ».
|
||||
- Les positions sont données par leurs identifiants gnubg (ADR 0013) ; le
|
||||
plateau est le composant de l'app, non jouable sauf sur les écrans
|
||||
« à vous ». Aucune règle n'est codée hors du moteur : un écran « à vous »
|
||||
demande au moteur si le coup du joueur fait partie des coups permis attendus.
|
||||
- **Les pions bougent** : sur un écran d'explication, le coup illustré se joue
|
||||
tout seul après une seconde, pion par pion (`duration-fast`), puis reste
|
||||
affiché ; « Revoir » le rejoue. Sur les écrans à flèches (sens du jeu), un
|
||||
tracé en pointillés `text` sur un liseré ivoire, jamais en brique (la brique
|
||||
veut dire « à faire »).
|
||||
- Un geste juste : coche `verdict-good`, vibration « pion posé », phrase
|
||||
courte, « Continuer » devient l'action principale. Un geste faux : le pion
|
||||
revient, et une phrase dit pourquoi, sans le mot « erreur ».
|
||||
- Chaque terme nouveau est un lien vers sa fiche du glossaire, à sa première
|
||||
apparition.
|
||||
|
||||
### Texte, première version
|
||||
|
||||
1. **Le but du jeu.** « Vous avez quinze pions ivoire, l'ordinateur quinze
|
||||
pions noyer. Chacun fait faire le tour du plateau à ses pions, puis les
|
||||
sort. Le premier qui a sorti tous les siens gagne la partie. »
|
||||
2. **Dans quel sens avancer.** « Vos pions ivoire font le tour du plateau dans
|
||||
le sens des pointillés : ils partent d'en haut à droite, passent à gauche,
|
||||
puis reviennent en bas jusqu'aux six dernières flèches, en bas à droite.
|
||||
C'est là que vous les rassemblerez avant de les sortir. Les pions noyer de
|
||||
l'ordinateur font le même chemin, en sens inverse. »
|
||||
3. **Les dés et les doubles.** « Chaque dé déplace un pion d'autant de
|
||||
flèches. Avec 5 et 2, vous jouez un pion de 5 et un pion de 2, ou le même
|
||||
pion de 5 puis de 2. Un double se joue quatre fois : avec 4-4, vous avancez
|
||||
quatre fois de 4. » *À vous* : jouer 3-1 en faisant le point 5 (8/5 6/5).
|
||||
4. **Frapper un pion seul.** « Un pion seul sur une flèche s'appelle un blot.
|
||||
Si vous vous posez dessus, vous le frappez : il part sur la barre et devra
|
||||
refaire tout le chemin. Une flèche où l'adversaire a deux pions ou plus est
|
||||
fermée : vous ne pouvez pas vous y poser. » *À vous* : frapper le blot du
|
||||
point 7 avec 6-1.
|
||||
5. **Rentrer depuis la barre.** « Votre pion frappé attend sur la barre, au
|
||||
milieu. Avant de jouer quoi que ce soit d'autre, il doit rentrer dans le
|
||||
camp adverse, en haut à droite. Chaque dé indique une flèche d'entrée :
|
||||
avec 3 et 4, la 22 ou la 21. Si les deux flèches sont fermées, votre tour
|
||||
passe. »
|
||||
6. **Sortir ses pions.** « Quand vos quinze pions sont dans les six dernières
|
||||
flèches, vous pouvez commencer à les sortir : un 6 sort un pion de la
|
||||
flèche 6, un 1 un pion de la flèche 1. Touchez le pion, puis la sortie à
|
||||
droite. » *À vous* : sortir deux pions avec 6-3.
|
||||
7. **Gammon et backgammon.** « Une partie gagnée vaut 1 point. Si
|
||||
l'adversaire n'a sorti aucun pion quand vous finissez, c'est un gammon :
|
||||
2 points. S'il lui reste en plus un pion sur la barre ou dans votre camp,
|
||||
c'est un backgammon : 3 points. »
|
||||
8. **Le videau.** « Le cube aux faces 2, 4, 8, 16, 32 et 64 sert à miser.
|
||||
Avant de lancer vos dés, vous pouvez proposer de doubler la valeur de la
|
||||
partie. L'ordinateur prend, et la partie continue en valant le double, ou il
|
||||
passe, et vous gagnez tout de suite. Celui qui a pris garde le cube : lui
|
||||
seul pourra redoubler. » *À vous* : décider de prendre ou passer, avec une
|
||||
phrase du maître ensuite.
|
||||
|
||||
Fin du tutoriel : « Vous connaissez les règles. Le reste s'apprend en jouant,
|
||||
et le maître vous accompagne. » → « Jouer ma première partie ».
|
||||
|
||||
## Aide
|
||||
|
||||
L'aide n'est plus une copie brute des fichiers : c'est une page organisée en
|
||||
questions, avec un ton. **La source reste unique** (ADR 0008) : les mêmes
|
||||
fichiers servent d'aide et de tests.
|
||||
|
||||
- **Trois onglets** : « Jouer » (les scénarios), « Les mots du jeu » (le
|
||||
glossaire), « À propos ». Le tutoriel est proposé en tête de page.
|
||||
- **« Jouer »** : un scénario = une question repliable (`details`), fermée par
|
||||
défaut sauf celle qui correspond à l'écran d'où l'on vient.
|
||||
- Le **titre du scénario** est la question, formulée comme le joueur se la
|
||||
pose : « Comment jouer un coup ? », « Je me suis trompé de pion », « Je ne
|
||||
peux rien jouer avec mes dés ». C'est un changement de titre dans les
|
||||
`.feature` : les tests n'en dépendent pas.
|
||||
- La **description du scénario** (le paragraphe sous le titre) est la
|
||||
réponse affichée. Elle se lit seule, au présent, au « vous ».
|
||||
- Les **étapes Gherkin** ne s'affichent pas dans l'aide : elles restent la
|
||||
preuve que l'app fait ce que dit la réponse. « Voir sur le plateau » ouvre
|
||||
la position de l'exemple quand le scénario en donne une (lien ADR 0013).
|
||||
- La **description de la fonctionnalité** sert d'introduction à l'onglet.
|
||||
- **« Les mots du jeu »** : une fiche du glossaire = une carte (le mot, son nom
|
||||
anglais en `caption` italique, « En bref », les renvois), triées par ordre
|
||||
alphabétique, avec un champ de recherche en tête.
|
||||
- **Ton** : celui du maître de café, patient et concret. Pas de « Étant
|
||||
donné que », pas de vocabulaire technique (moteur, scénario, événement).
|
||||
@@ -34,7 +34,7 @@ window.UstaBoard = (function () {
|
||||
o += '<rect class="board-tray" x="' + TRAY_X + '" y="' + TB + '" width="' + TRAY_W + '" height="' + (H / 2 - TB - 4) + '" rx="4"/>';
|
||||
o += '<rect class="board-tray" x="' + TRAY_X + '" y="' + (H / 2 + 4) + '" width="' + TRAY_W + '" height="' + (H / 2 - TB - 4) + '" rx="4"/>';
|
||||
for (var point = 1; point <= 24; point++) {
|
||||
var pl = place(point), x = colX(pl.col), cls = (pl.col % 2 === 0) === pl.top ? "point-a" : "point-b";
|
||||
var pl = place(point), x = colX(pl.col), cls = point % 2 === 1 ? "point-a" : "point-b"; // impairs en noyer, pairs en chêne
|
||||
var base = pl.top ? TB : H - TB, apex = pl.top ? TB + PH : H - TB - PH;
|
||||
o += '<polygon class="' + cls + '" points="' + x + ',' + base + ' ' + (x + PW) + ',' + base + ' ' + (x + PW / 2) + ',' + apex + '"/>';
|
||||
if ((state.targets || []).indexOf(point) >= 0)
|
||||
|
||||
@@ -22,7 +22,7 @@ La palette, c'est le bois, les pions et le café, avec **un seul accent : le rou
|
||||
- `accent-soft` : fond léger pour signaler « c'est à vous », avec du texte `text` dessus.
|
||||
- Les appréciations de coups ont leurs propres couleurs, distinctes de la brique : `verdict-good` (vert), `verdict-doubtful` (ocre), `verdict-bad` (lie-de-vin). **Toujours avec le mot et l'icône**, jamais la couleur seule.
|
||||
- `brand-laiton` (via `last-move`) marque le dernier coup joué, comme les charnières en laiton d'un plateau de tavla. Rien d'autre.
|
||||
- Le plateau : cadre `frame` en noyer, fond `bed`, flèches alternées `point-a` (noyer clair) et `point-b` (chêne), barre `bar`, sorties `tray`. Le joueur a les pions ivoire (`checker-player`, l'apprenti), l'ordinateur les pions noyer (`checker-computer`, le maître), comme dans le logo.
|
||||
- Le plateau : cadre `frame` en noyer, fond `bed`, flèches impaires en `point-a` (noyer clair) et paires en `point-b` (chêne), barre `bar`, sorties `tray`. Le joueur a les pions ivoire (`checker-player`, l'apprenti), l'ordinateur les pions noyer (`checker-computer`, le maître), comme dans le logo.
|
||||
- **À éviter** : le vert feutre de casino, le noir et or, les dégradés, les néons.
|
||||
|
||||
Les deux thèmes, clair et sombre, sont complets. Tous les textes atteignent 4,5:1 sur leurs fonds dans les deux thèmes ; les contours et marques porteuses de sens, 3:1.
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
# Recette design de l'écran de jeu
|
||||
|
||||
Critères vérifiables sur les captures de `make test-browser` (412×915,
|
||||
915×412, 844×320, thèmes clair et sombre) et à la main sur le Fairphone 4.
|
||||
Chaque ligne se coche ou se refuse ; une ligne refusée dit pourquoi.
|
||||
Référence : `interaction.md`, maquettes « Révision UX » du canevas.
|
||||
|
||||
## Toutes tailles
|
||||
|
||||
- [ ] Flèches impaires en `point-a`, paires en `point-b` ; 12 et 13 de couleurs différentes.
|
||||
- [ ] Une seule action principale (brique) visible à la fois.
|
||||
- [ ] Aucun `window.confirm` ni `alert` dans le code de l'app (`grep -rn "confirm(\|alert(" app/src` vide).
|
||||
- [ ] Aucune cible tactile sous 44 × 44 px (boutons, pictogrammes, dés, videau).
|
||||
- [ ] Textes à lire : 13 px au moins ; numéros des flèches : 12 px au moins.
|
||||
- [ ] Thème sombre : aucune couleur codée en dur hors `tokens` ; le cadre se distingue du fond.
|
||||
- [ ] Anneau de focus `focus` visible au clavier sur chaque élément interactif.
|
||||
- [ ] `overscroll-behavior: none` : tirer vers le bas ne recharge pas la page.
|
||||
|
||||
## Portrait (412 × 915)
|
||||
|
||||
- [ ] Ordre de haut en bas : barre haute, bandeau ordinateur, plateau, bandeau joueur, ligne du maître ou du dernier coup, consigne, barre d'actions collée en bas.
|
||||
- [ ] Stabilité : sur une partie complète, la position verticale du plateau et de la barre d'actions ne varie pas d'un pixel (comparer les boîtes entre deux captures successives).
|
||||
- [ ] Barre haute : pictogrammes avec libellé de 13 px (Aide, Plein écran, Menu) ; pas de bouton « Nouvelle partie ».
|
||||
- [ ] Comptes de points en 20 px gras, chiffres alignés ; le bandeau du joueur qui a la main est teinté `accent-soft` ; l'écart s'écrit « mène de N ».
|
||||
- [ ] Plus de durée de calcul « (0,5 s) » dans les messages.
|
||||
- [ ] Aucun coup possible : dés barrés et message ; le tour ne passe qu'au toucher des dés.
|
||||
- [ ] Conseil du maître : une ligne, après le coup du joueur seulement ; toucher l'ouvre.
|
||||
|
||||
## Paysage (915 × 412 et 844 × 320)
|
||||
|
||||
- [ ] Le cadre noyer touche les quatre bords ; aucune marge de page, aucune colonne de texte.
|
||||
- [ ] Flèches d'au moins 44 px de large ; pions d'au moins 22 px de diamètre à 844 × 320.
|
||||
- [ ] Colonne de commandes à gauche : menu en haut, videau au milieu, annuler (ou revoir) en bas ; colonne des sorties libre.
|
||||
- [ ] Comptes en pastilles aux deux bouts de la barre, 13 px, couleur du camp ; ils ne chevauchent pas les pions sur la barre.
|
||||
- [ ] Consigne et dés dans la bande centrale, sans chevaucher un pion.
|
||||
- [ ] Rotation portrait ↔ paysage en cours de partie : la position, la sélection et les dés sont conservés.
|
||||
- [ ] Fin de partie : panneau centré sur un voile, « Revoir la partie » et « Nouvelle partie ».
|
||||
|
||||
## Comportement
|
||||
|
||||
- [ ] Un toucher sur un pion produit un effet visible en moins de 100 ms (trace de performance de Playwright).
|
||||
- [ ] Déplacements à 160 ms en `easing-standard`, sans rebond ; coups de l'ordinateur pion par pion ; tout est instantané avec `prefers-reduced-motion`.
|
||||
- [ ] « Nouvelle partie » depuis le menu : effet immédiat, bandeau « Reprendre l'ancienne » 10 s, et la reprise restaure la partie exacte.
|
||||
- [ ] Vibrations aux quatre moments, coupées par l'interrupteur du menu (test unitaire sur `haptic()` ; ressenti à la main).
|
||||
- [ ] Écran maintenu allumé pendant une partie, interrupteur dans le menu.
|
||||
@@ -195,7 +195,7 @@
|
||||
"light": "#6B4430",
|
||||
"dark": "#3A2519"
|
||||
},
|
||||
"usage": "Flèches sombres (noyer clair), une sur deux."
|
||||
"usage": "Flèches impaires (1, 3, 5…), noyer clair."
|
||||
},
|
||||
{
|
||||
"name": "point-b",
|
||||
@@ -203,7 +203,7 @@
|
||||
"light": "#C9AE88",
|
||||
"dark": "#7E6142"
|
||||
},
|
||||
"usage": "Flèches claires (chêne), une sur deux."
|
||||
"usage": "Flèches paires (2, 4, 6…), chêne."
|
||||
},
|
||||
{
|
||||
"name": "point-number",
|
||||
|
||||
@@ -136,3 +136,30 @@ Une entrée datée par session : ce qui a été fait, décidé, et la suite.
|
||||
|
||||
**Reste à faire**
|
||||
- Firefox sur le téléphone reste à mesurer.
|
||||
|
||||
## 2026-11-03 (jour) : coordination design et développement
|
||||
|
||||
**Fait**
|
||||
- Leçons numérotées, conseil du maître après le coup et bilan de fin de partie
|
||||
retenus par Pierre (ADR 0011) ; mode immersif en paysage (ADR 0012), puis
|
||||
vue paysage dès la rotation, avec le plateau qui occupe tout l'écran.
|
||||
- Revue UX du designer de la session de développement (étape 0e.2), arbitrée
|
||||
par la session design sur délégation de Pierre : portrait à zones fixes,
|
||||
compte de points en bandeaux (pastilles en paysage), menu en feuille,
|
||||
« annuler plutôt que confirmer », pas de passage automatique du tour,
|
||||
vibrations, pictogrammes, clavier sur ordinateur. Règles dans
|
||||
`docs/identite/charte/interaction.md` (MR !23), second amendement de
|
||||
l'ADR 0012 (placement des contrôles en paysage).
|
||||
- Maquettes des variantes sur le canevas (paysage clair, sombre, avec barre
|
||||
d'adresse, fin de partie ; portrait révisé, conseil, nouvelle partie
|
||||
annulable ; menu ; ordinateur avec raccourcis).
|
||||
- Couleur des flèches corrigée dans la charte (impaires en noyer clair, paires
|
||||
en chêne ; MR !25). La correction dans l'app est faite par la session de
|
||||
développement, dans la MR de saisie du coup.
|
||||
|
||||
**Reste à faire**
|
||||
- Implémentation par la session de développement (plan
|
||||
`docs/plans/2026-11-03-revision-ux.md`), puis revue design sur ses captures
|
||||
Playwright (412×915, 915×412, 844×320, clair et sombre) avant l'essai de
|
||||
Pierre sur le téléphone.
|
||||
- Réglage en main des vibrations sur le Fairphone 4.
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
# Accueil, installation, tutoriel des règles, aide (0e.5)
|
||||
|
||||
- **Date** : 2026-11-03
|
||||
- **Étape** : 0e.5 (feuille de route), qui couvre aussi le tutoriel de 1g
|
||||
- **Statut** : prévu (après la fusion de 0e.4)
|
||||
- **Exécutant** : sous-agent general-purpose, dans un worktree
|
||||
|
||||
## Objectif
|
||||
|
||||
Traiter les points 2 et 3 du message de Pierre du 2026-11-03 et sa demande d'installation en un toucher :
|
||||
- une page d'accueil ;
|
||||
- l'installation proposée par l'app ;
|
||||
- un tutoriel des règles très didactique, illustré par le plateau ;
|
||||
- une aide plus humaine.
|
||||
|
||||
## Références
|
||||
|
||||
- Règles : `docs/identite/charte/accueil-tutoriel-aide.md`. Le texte du tutoriel y figure en première version, à faire valider par Pierre.
|
||||
- Maquettes : https://claude.ai/artifact/CQw6vbDpi6XQqVWQa4sANe, rangée « Étape 0e.5 » et artboard « Accueil ».
|
||||
- ADR 0008 (source unique), ADR 0013 (positions par identifiants gnubg).
|
||||
|
||||
## Consignes
|
||||
|
||||
1. **Accueil**, selon la charte :
|
||||
- au premier lancement, et une seule fois, le choix entre « Je découvre » et « Je connais les règles » ;
|
||||
- ensuite : la partie en cours, la carte d'installation, la prochaine leçon et la progression résumée. La leçon et la progression restent en attente tant que 1g et 1h n'existent pas : n'afficher que ce qui existe.
|
||||
2. **Installation** :
|
||||
- sous Chrome, garder l'invite reçue avec `beforeinstallprompt` ;
|
||||
- sous Firefox et sur iPhone, une feuille en 3 étapes ;
|
||||
- la carte n'apparaît qu'à partir de la première partie terminée. « Plus tard » la masque, et elle ne revient qu'une fois, après 3 parties. Le menu garde une ligne tant que l'app n'est pas installée ;
|
||||
- plus rien en `display-mode: standalone` ni après l'événement `appinstalled`.
|
||||
3. **Tutoriel**, en 8 étapes :
|
||||
- le texte vit dans un fichier source unique sous `docs/` (par exemple `docs/tutoriel/`), copié dans le site par `make site` comme le glossaire, sous licence CC BY-SA 4.0 ;
|
||||
- positions données par leurs identifiants gnubg ;
|
||||
- sur les écrans « à vous », le moteur vérifie le coup joué en le comparant aux coups attendus de l'étape ;
|
||||
- le coup illustré se joue seul après 1 s, avec « Revoir » ;
|
||||
- un test vérifie que chaque position du tutoriel est valide pour le moteur et que chaque coup attendu est légal ;
|
||||
- ajouter la fiche du glossaire « camp ».
|
||||
4. **Aide**, en trois onglets :
|
||||
- « Jouer » : les scénarios deviennent des questions repliables. Le titre est la question et la description la réponse ; les étapes Gherkin sont cachées. Reformuler les titres des `.feature` sans casser les tests ;
|
||||
- « Les mots du jeu » : les cartes du glossaire, avec une recherche ;
|
||||
- « À propos » : licence, crédits, version ;
|
||||
- le tutoriel est proposé en tête de l'aide.
|
||||
5. **Scénarios Gherkin** : premier lancement, installation, suivre le tutoriel.
|
||||
6. **`make test-browser`** :
|
||||
- premier lancement, tutoriel complet avec ses écrans « à vous », aide ;
|
||||
- `beforeinstallprompt` simulé dans Chromium ;
|
||||
- captures dans `build/browser-shots/accueil-tutoriel/` pour la recette design.
|
||||
|
||||
Chaque écran (accueil, étapes du tutoriel, onglets et questions de l'aide) a son adresse et s'ouvre par un vrai lien, selon l'amendement de l'ADR 0013 ; le routeur vient de 0e.4.
|
||||
|
||||
`make lint`, `make test` et `make test-browser` doivent passer. Aucune nouvelle dépendance.
|
||||
|
||||
## Critères de réussite
|
||||
|
||||
- Un débutant suit le tutoriel jusqu'au bout sur le téléphone, et chaque écran « à vous » accepte le bon coup et refuse les autres.
|
||||
- Les trois commandes passent, et les captures sont livrées pour la recette.
|
||||
|
||||
## Résultat
|
||||
|
||||
À remplir à la clôture.
|
||||
@@ -0,0 +1,60 @@
|
||||
# Conseil du maître (1d)
|
||||
|
||||
- **Date** : 2026-11-03
|
||||
- **Étape** : 1d (`docs/etape-1.md`)
|
||||
- **Statut** : prévu (après 1c)
|
||||
- **Exécutant** : sous-agent general-purpose, dans un worktree
|
||||
|
||||
## Objectif
|
||||
|
||||
Le coach bienveillant, « mis en avant mais discret » (Pierre, 2026-11-03) :
|
||||
après chaque coup et chaque décision de videau du joueur, jamais avant
|
||||
(ADR 0011), une appréciation et une phrase du maître fondées sur les chiffres
|
||||
du moteur.
|
||||
|
||||
## Références
|
||||
|
||||
- ADR 0011 ; maquette « conseil du maître après le coup » du canevas
|
||||
https://claude.ai/artifact/CQw6vbDpi6XQqVWQa4sANe ; règles de la charte
|
||||
(`docs/identite/charte/`, dont interaction.md) ; la ligne réservée de 44 px
|
||||
de la révision UX.
|
||||
- Moteur : `analyseMove`, `analyseCube`, `luck`, seuils `SKILL_THRESHOLDS`
|
||||
(gnubg : 0,03 / 0,06 / 0,12).
|
||||
|
||||
## Consignes
|
||||
|
||||
1. **Analyse en arrière-plan** : après la validation d'un coup (ou une
|
||||
décision de videau), l'analyse à 2 plis part dans le Worker sans retarder le
|
||||
tour de l'ordinateur ; son résultat est rattaché au coup dans le journal
|
||||
(événement d'analyse rétrocompatible, ou cache séparé justifié) pour servir
|
||||
au bilan (1e) sans recalcul.
|
||||
2. **Ligne du maître** : icône d'appréciation, mot en couleur (« Bon coup. »,
|
||||
« Imprécision. », « Erreur. », « Gaffe. » : correspondance avec les seuils
|
||||
de gnubg documentée), phrase courte en italique, chevron. Toucher ouvre la
|
||||
feuille d'explication : coup joué et meilleur coup montrés sur le plateau
|
||||
(pions animés, ADR 0011 « comparaison visuelle »), écart d'équité seulement
|
||||
sur demande, lien vers la fiche du glossaire du concept en jeu si le moteur
|
||||
permet de le déterminer, sinon rien.
|
||||
3. **Phrases du maître** : gabarits versionnés en français dans une source
|
||||
unique sous `docs/` (CC BY-SA 4.0), ton encourageant, jamais culpabilisant ;
|
||||
elles ne citent que des faits du moteur (coup recommandé, rang, pions
|
||||
frappés, écart) : aucune explication inventée. Première version à faire
|
||||
valider par Pierre (ton).
|
||||
4. **Discret** : rien n'apparaît pendant que le joueur réfléchit ; la ligne
|
||||
s'efface à l'action suivante ; désactivable (réglage de 1c et menu).
|
||||
5. **Videau** : même traitement pour doubler, ne pas doubler, prendre, passer.
|
||||
6. Scénarios Gherkin (bon coup, erreur avec meilleur coup montré, conseil
|
||||
désactivé, décision de videau commentée) avec positions et dés imposés ;
|
||||
tests unitaires des gabarits (chaque gabarit rendu sur des analyses réelles
|
||||
du moteur). `make lint`, `make test`, `make test-browser`, captures pour la
|
||||
recette design.
|
||||
|
||||
## Critères de réussite
|
||||
|
||||
Après une erreur volontaire dans une position connue, la ligne du maître dit
|
||||
« Erreur. », nomme le coup recommandé par le moteur et le montre sur demande ;
|
||||
le tour de l'ordinateur n'est pas retardé ; les trois commandes passent.
|
||||
|
||||
## Résultat
|
||||
|
||||
À remplir à la clôture.
|
||||
@@ -0,0 +1,47 @@
|
||||
# Match, niveaux et réglages (1c)
|
||||
|
||||
- **Date** : 2026-11-03
|
||||
- **Étape** : 1c (`docs/etape-1.md`)
|
||||
- **Statut** : prévu (après la révision UX et 1b)
|
||||
- **Exécutant** : sous-agent general-purpose, dans un worktree
|
||||
|
||||
## Objectif
|
||||
|
||||
Choisir sa partie : partie libre ou match en N points, avec les règles de
|
||||
Crawford et de Jacoby, contre l'ordinateur au niveau voulu.
|
||||
|
||||
## Consignes
|
||||
|
||||
ADR 0007, 0008. Le moteur expose depuis 1a les niveaux de gnubg
|
||||
(`levelMove`, `levelCubeDecision`) et accepte les matchs dans son API C.
|
||||
|
||||
1. **Domaine** : longueur de match, score, règle de Crawford (partie Crawford
|
||||
et suivantes, calculées par le moteur via le Match ID), règle de Jacoby en
|
||||
partie libre ; enchaînement des manches d'un match jusqu'à sa fin ; le
|
||||
journal d'événements reste rétrocompatible (les parties existantes se
|
||||
rechargent).
|
||||
2. **Adaptateur** : `EngineCube` étendu au match (score, longueur, Crawford)
|
||||
ou remplacé par le Match ID, sans casser les appels existants.
|
||||
3. **Niveau de l'ordinateur** : ses coups et ses décisions de videau au niveau
|
||||
choisi. Reprendre de `ComputerTurn` (gnubg, `play.c`) les règles de
|
||||
déroulement non reprises en 1a : pré-filtre « fenêtre de marché » et double
|
||||
facultatif à 0 pli, avec citation des sources.
|
||||
4. **Écran de nouvelle partie** (maquettes de la session « identité » si
|
||||
disponibles, sinon sobre et conforme à la charte) : type de partie, niveau
|
||||
(noms montrés au joueur à proposer simplement : Débutant, Amateur,
|
||||
Intermédiaire, Avancé, Expert, Champion — correspondance avec les niveaux
|
||||
de gnubg documentée), conseil du maître activé ou non. Par défaut : partie
|
||||
libre, Intermédiaire, conseil activé. Le réglage est mémorisé.
|
||||
5. Affichage du score de match et de la partie Crawford sur le plateau.
|
||||
6. Scénarios Gherkin (match en 3 points jusqu'au bout avec dés imposés,
|
||||
Crawford, Jacoby) et fiches du glossaire (match, Crawford, Jacoby si
|
||||
absentes). `make lint`, `make test`, `make test-browser` (un match court).
|
||||
|
||||
## Critères de réussite
|
||||
|
||||
Un match en 3 points se joue jusqu'au bout, au niveau choisi, avec Crawford
|
||||
appliqué ; les trois commandes passent.
|
||||
|
||||
## Résultat
|
||||
|
||||
À remplir à la clôture.
|
||||
@@ -1,60 +0,0 @@
|
||||
# Moteur pour l'analyse et les niveaux (1a)
|
||||
|
||||
- **Date** : 2026-11-03
|
||||
- **Étape** : 1a (`docs/etape-1.md`)
|
||||
- **Statut** : en cours
|
||||
- **Exécutant** : sous-agent general-purpose, dans un worktree
|
||||
|
||||
## Objectif
|
||||
|
||||
Donner au domaine tout ce dont l'étape 1 a besoin du moteur : niveaux de jeu de
|
||||
l'ordinateur, analyse d'un coup joué et d'une décision de videau, chance d'un
|
||||
jet. Tout vient de GNU Backgammon, comparé à gnubg de bureau.
|
||||
|
||||
## Consignes
|
||||
|
||||
ADR 0005, 0007 et la méthode de comparaison de `tests/reference/` (README).
|
||||
Ne pas modifier `engine/gnubg/` (copie fidèle). Pas de nouvelle dépendance.
|
||||
|
||||
1. **Niveaux de jeu** : les niveaux de GNU Backgammon (débutant, amateur,
|
||||
intermédiaire, avancé, expert, champion du monde…), avec leurs plis, filtres
|
||||
et bruit, repris des réglages prédéfinis de gnubg (pas inventés). Le bruit
|
||||
doit être déterministe (option « deterministic » de gnubg) pour que les
|
||||
tests soient reproductibles. API C (`engine.h`), `engine-cli`, adaptateur
|
||||
WebAssembly et interface `Engine` du domaine (ajout seulement : ne pas
|
||||
toucher au reste de `app/src/domain/game/`, qu'un autre agent modifie).
|
||||
2. **Analyse d'un coup joué** : pour une position, un jet et un coup joué,
|
||||
l'équité du coup joué, celle du meilleur coup, l'écart, et le rang du coup
|
||||
joué, au niveau d'analyse demandé (comme l'analyse de partie de gnubg).
|
||||
3. **Analyse d'une décision de videau** : équités sans double, double/prend,
|
||||
double/passe, et l'écart de la décision prise (double, pas de double,
|
||||
prendre, passer), comme gnubg.
|
||||
4. **Chance d'un jet** : la chance (luck) d'un jet au sens de gnubg, et son
|
||||
classement (très chanceux… très malchanceux) avec les seuils de gnubg.
|
||||
5. **Seuils d'appréciation** : exposer les seuils de gnubg (douteux, mauvais,
|
||||
très mauvais) comme constantes documentées ; l'interface les traduira.
|
||||
6. **Références** : ajouter des positions de référence évaluées par gnubg de
|
||||
bureau (Docker existant, `make reference`) pour l'analyse d'un coup, d'une
|
||||
décision de videau et la chance, comparées par `make test` (natif et
|
||||
WebAssembly) avec la tolérance existante. Les niveaux avec bruit : vérifier
|
||||
au moins que le coup choisi est reproductible et identique à celui de gnubg
|
||||
de bureau sur quelques positions.
|
||||
7. Glossaire : fiches pour les nouveaux termes (niveau, chance, appréciation,
|
||||
équité si absente…), reliées au code.
|
||||
8. Mesurer sous Node (`make bench` ou équivalent) le temps d'une analyse de
|
||||
coup au niveau prévu pour le conseil du maître (2 plis), pour vérifier
|
||||
qu'elle tient en arrière-plan sur le téléphone (repère : ADR 0009).
|
||||
|
||||
Machine ancienne : `-j2`, ne recompiler le moteur que si nécessaire.
|
||||
`make lint` et `make test` doivent passer. Commits fréquents ; `git add` avec
|
||||
chemins explicites ; ne pas pousser, pas de MR.
|
||||
|
||||
## Critères de réussite
|
||||
|
||||
- `engine-cli` répond aux nouvelles commandes en JSON.
|
||||
- Les nouvelles références concordent avec gnubg de bureau, natif et WebAssembly.
|
||||
- `make lint` et `make test` passent.
|
||||
|
||||
## Résultat
|
||||
|
||||
À remplir à la clôture.
|
||||
@@ -0,0 +1,50 @@
|
||||
# Une adresse pour chaque écran, et pour chaque position (0e.4)
|
||||
|
||||
- **Date** : 2026-11-03
|
||||
- **Étape** : 0e.4 (feuille de route)
|
||||
- **Statut** : prévu (après la fusion de la révision UX et de 1b)
|
||||
- **Exécutant** : sous-agent general-purpose, dans un worktree
|
||||
|
||||
## Objectif
|
||||
|
||||
Appliquer l'ADR 0013 et son amendement du 2026-11-03 : chaque écran (aide,
|
||||
glossaire, menu, mes parties…) a son adresse et s'ouvre par un vrai lien ;
|
||||
chaque position du plateau a un lien direct.
|
||||
|
||||
## Consignes
|
||||
|
||||
ADR 0007, 0008 et 0013. Aucune nouvelle dépendance.
|
||||
|
||||
0. **Routeur** : un petit module pur (sans dépendance) qui traduit le
|
||||
fragment en écran et inversement, testé unitairement ; l'app dessine
|
||||
l'écran demandé par l'adresse, sur `hashchange`. Tous les écrans existants
|
||||
(aide et ses questions, glossaire et ses termes, à propos, menu, mes
|
||||
parties de 1b) passent par lui, avec de vrais liens `<a href>` ; le bouton
|
||||
retour du navigateur ramène à l'écran précédent. Les actions de jeu restent
|
||||
des boutons.
|
||||
1. Lecture de `#/position/<Position ID>/<Match ID>` ; validation par le
|
||||
moteur ; message clair si illisible.
|
||||
2. Vue de la position, non jouable, avec « Jouer à partir d'ici » (nouvelle
|
||||
partie depuis la position, la partie en cours restant reprenable) et
|
||||
« Retour à ma partie ».
|
||||
3. Domaine : démarrer une partie depuis une position et un Match ID donnés
|
||||
(événement GameStarted étendu de façon rétrocompatible, ou équivalent
|
||||
justifié), sans règle réimplémentée.
|
||||
4. « Copier le lien de la position » depuis le menu de la partie et depuis la
|
||||
vue du coup de l'ordinateur ; le lien copié rouvre exactement la même
|
||||
position (test aller-retour).
|
||||
5. Scénarios Gherkin (« Ouvrir un lien vers une position », « Partager la
|
||||
position »), fiches du glossaire (Position ID, Match ID si absentes).
|
||||
6. `make lint`, `make test`, `make test-browser` : chaque adresse du tableau
|
||||
de l'ADR ouverte directement dans un nouvel onglet montre le bon écran ;
|
||||
« retour » du navigateur ; liens reconnus comme liens (rôle `link`) ;
|
||||
position : ouvrir un lien, jouer à partir d'ici, revenir, lien invalide.
|
||||
|
||||
## Critères de réussite
|
||||
|
||||
Un lien copié depuis l'app, ouvert dans un nouvel onglet, montre la même
|
||||
position ; les trois commandes passent.
|
||||
|
||||
## Résultat
|
||||
|
||||
À remplir à la clôture.
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
- **Date** : 2026-11-03
|
||||
- **Étape** : 0e.3 (feuille de route)
|
||||
- **Statut** : prévu (après la fusion de 0e.1, qui touche les mêmes fichiers)
|
||||
- **Statut** : prévu
|
||||
- **Exécutant** : sous-agent general-purpose, dans un worktree
|
||||
|
||||
## Objectif
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
# Saisie du coup, videau confirmé, pages de test du moteur retirées
|
||||
|
||||
- **Date** : 2026-11-03
|
||||
- **Étape** : 0e (retours du premier test de Pierre sur le téléphone)
|
||||
- **Statut** : en cours
|
||||
- **Exécutant** : sous-agent general-purpose, dans un worktree
|
||||
|
||||
## Objectif
|
||||
|
||||
Rendre la saisie du coup aussi confortable que dans CBG, d'après les retours de
|
||||
Pierre du 2026-11-03 (points 1, 6, 7, 8, 9 et 11 de son message), et retirer du
|
||||
site les pages de test du moteur.
|
||||
|
||||
## Consignes
|
||||
|
||||
Règles de code : ADR 0007 (domaine pur, aucune règle de backgammon
|
||||
réimplémentée : tout vient du moteur via le domaine). Glossaire et scénarios :
|
||||
ADR 0008 (chaque comportement visible a son scénario Gherkin dans
|
||||
`docs/scenarios/`, chaque nouveau terme sa fiche). Aucune nouvelle dépendance.
|
||||
|
||||
1. **Pages de test du moteur retirées** (« supprimer la partie test du moteur
|
||||
totalement ») : plus de `/moteur/` sur le site ni dans `make deploy`.
|
||||
`make bench` (mesure sous Node) reste : déplacer ce dont il a besoin hors de
|
||||
`web/`. Mettre à jour `tests/test_site.sh`, le Makefile, `deploy/`, le
|
||||
service worker, `docs/guide-test-etape-0.md` et ajouter un amendement daté à
|
||||
l'ADR 0009.
|
||||
2. **Un seul coup possible, le pion avance** : quand le joueur touche un pion
|
||||
(ou une case) et qu'une seule case d'arrivée est possible pour ce pion, le
|
||||
demi-coup est joué aussitôt, sans second toucher. S'il y en a plusieurs, la
|
||||
sélection actuelle reste.
|
||||
3. **Coup à valider, annulable** : quand tous les demi-coups sont joués (ou
|
||||
qu'aucun autre n'est possible), le coup n'est pas envoyé tout de suite. Le
|
||||
joueur le valide en touchant ses dés (ou un bouton « Valider »). Tant qu'il
|
||||
ne l'a pas validé, « Annuler » défait le dernier demi-coup, un par un. Le
|
||||
journal d'événements ne reçoit `MovePlayed` qu'à la validation.
|
||||
4. **Dés joués marqués** : chaque dé déjà utilisé par un demi-coup est
|
||||
visiblement estompé. Pour un double, quatre dés, marqués un par un.
|
||||
5. **Dés de l'adversaire touchables** : toucher la zone des dés de l'un ou
|
||||
l'autre côté du plateau lance les dés quand le joueur peut lancer (et valide
|
||||
le coup en attente de validation).
|
||||
6. **Confirmation du videau** : toucher le videau ou « Doubler » ouvre une
|
||||
confirmation courte (« Doubler à 2 ? L'ordinateur pourra prendre ou
|
||||
passer. » : Doubler / Annuler) avant d'envoyer le double.
|
||||
|
||||
Tests : scénarios Gherkin mis à jour ou ajoutés (validation du coup, annulation
|
||||
demi-coup par demi-coup, confirmation du videau), tests unitaires, et
|
||||
`tests/browser/play.mjs` adapté (Playwright est validé par Pierre). Lancer
|
||||
`make lint`, `make test` et `make test-browser`.
|
||||
|
||||
Rapporter : ce qui a été fait, les écarts, les résultats des trois commandes,
|
||||
et un brouillon de description de MR (7 sections, voir `docs/organisation.md`).
|
||||
Commits fréquents sur la branche du worktree ; ne pas pousser, ne pas ouvrir de
|
||||
MR : la session principale s'en charge.
|
||||
|
||||
## Critères de réussite
|
||||
|
||||
- `make lint`, `make test` et `make test-browser` passent.
|
||||
- Dans le navigateur : un pion à destination unique avance en un toucher ; un
|
||||
coup complet attend la validation ; « Annuler » défait un demi-coup à la
|
||||
fois ; les dés joués sont estompés ; le videau demande confirmation.
|
||||
- `https://…/moteur/` n'existe plus dans le site construit (`make site`).
|
||||
|
||||
## Résultat
|
||||
|
||||
À remplir à la clôture.
|
||||
@@ -11,11 +11,40 @@ Fonctionnalité: Jouer une partie contre l'ordinateur
|
||||
Et le premier jet de dés désigne qui commence
|
||||
|
||||
Scénario: Jouer un coup permis
|
||||
Touchez un pion, puis sa case d'arrivée ; s'il n'en a qu'une, il y va dès
|
||||
que vous le touchez. Chaque dé joué s'estompe. Une fois tous vos dés
|
||||
joués, votre coup attend que vous le validiez : touchez vos dés, d'un côté
|
||||
ou de l'autre du plateau, ou « Valider ».
|
||||
|
||||
Étant donné que c'est à vous de jouer avec le jet 3-1
|
||||
Quand vous jouez 8/5 6/5
|
||||
Alors votre coup attend votre validation
|
||||
Et vos dés sont tous marqués comme joués
|
||||
Quand vous validez votre coup
|
||||
Alors votre coup est accepté
|
||||
Et c'est ensuite au tour de l'ordinateur
|
||||
|
||||
Scénario: Un pion qui n'a qu'une case d'arrivée
|
||||
Après 8/5, il ne vous reste que le 1 : chaque pion jouable n'a plus
|
||||
qu'une case d'arrivée, et il y va dès que vous le touchez.
|
||||
|
||||
Étant donné que c'est à vous de jouer avec le jet 3-1
|
||||
Quand vous jouez 8/5
|
||||
Alors seul votre 3 est marqué comme joué
|
||||
Et chacun de vos pions jouables n'a plus qu'une case d'arrivée
|
||||
|
||||
Scénario: Reprendre un pion avant de valider
|
||||
Tant que vous n'avez pas validé votre coup, « Annuler » reprend le
|
||||
dernier pion déplacé ; touchez-le encore pour reprendre le précédent.
|
||||
|
||||
Étant donné que c'est à vous de jouer avec le jet 3-1
|
||||
Quand vous jouez 8/5 6/5
|
||||
Et que vous annulez le dernier demi-coup
|
||||
Alors il ne reste de joué que 8/5
|
||||
Quand vous annulez le dernier demi-coup
|
||||
Alors la position n'a pas changé
|
||||
Et aucun de vos dés n'est marqué comme joué
|
||||
|
||||
Scénario: Un coup interdit est refusé
|
||||
La case 12 est tenue par l'ordinateur, qui y a cinq pions : vous ne
|
||||
pouvez pas vous y poser.
|
||||
@@ -54,6 +83,19 @@ Fonctionnalité: Jouer une partie contre l'ordinateur
|
||||
Et votre pion frappé part sur la barre
|
||||
Et vous pouvez revoir son coup
|
||||
|
||||
Scénario: Doubler, après confirmation
|
||||
Avant de lancer les dés, vous pouvez doubler en touchant le videau ou
|
||||
« Doubler ». L'application vous demande de confirmer : une fois proposé,
|
||||
un double ne se reprend pas.
|
||||
|
||||
Étant donné que vous pouvez doubler
|
||||
Quand vous demandez à doubler
|
||||
Mais que vous changez d'avis
|
||||
Alors rien n'est proposé à l'ordinateur
|
||||
Quand vous demandez à doubler
|
||||
Et que vous confirmez
|
||||
Alors l'ordinateur prend ou passe, comme le recommande le moteur
|
||||
|
||||
Scénario: Prendre le double de l'ordinateur
|
||||
Étant donné que l'ordinateur vous propose de doubler
|
||||
Quand vous prenez
|
||||
|
||||
+199
-10
@@ -5,7 +5,7 @@
|
||||
* Licence : GPLv3 ou ultérieure (voir LICENSE).
|
||||
*
|
||||
* Usage :
|
||||
* engine-cli [--ply N] [--data DOSSIER] COMMANDE ARGUMENTS
|
||||
* engine-cli [--ply N | --level NIVEAU] [--data DOSSIER] COMMANDE ARGUMENTS
|
||||
*
|
||||
* Commandes :
|
||||
* bestmove POSITION_ID MATCH_ID DÉS meilleur coup (DÉS : ex. 31)
|
||||
@@ -27,8 +27,23 @@
|
||||
* référence (tests/reference/README.md) :
|
||||
* évaluation, puis classement des coups
|
||||
* avec DÉS, décision de videau sans
|
||||
* levels niveaux de jeu, seuils d'appréciation
|
||||
* et de chance de gnubg
|
||||
* analysemove POSITION_ID MATCH_ID DÉS POSITION_JOUÉE
|
||||
* analyse du coup joué (POSITION_JOUÉE :
|
||||
* position après le coup, vue du joueur
|
||||
* qui l'a joué, comme position_id dans
|
||||
* moves et legal)
|
||||
* analysecube POSITION_ID MATCH_ID DÉCISION
|
||||
* analyse d'une décision de videau :
|
||||
* no_double ou double (joueur au trait),
|
||||
* take ou pass (son adversaire)
|
||||
* luck POSITION_ID MATCH_ID DÉS chance du jet pour le joueur au trait
|
||||
*
|
||||
* --ply : niveau de gnubg, de 0 (défaut) à 4.
|
||||
* --ply : niveau de gnubg, de 0 (défaut) à 4 (meilleur coup comme « hint »,
|
||||
* analyses comme l'analyse de partie de gnubg).
|
||||
* --level : niveau de jeu de l'ordinateur (voir levels), pour bestmove, moves
|
||||
* et cube : coups classés comme l'ordinateur de gnubg les joue, bruit compris.
|
||||
* --data : dossier des données ; par défaut « data » à côté du programme.
|
||||
*
|
||||
* En WebAssembly (engine-wasm/), le même programme sert d'interface : le
|
||||
@@ -55,20 +70,32 @@
|
||||
#define NUMBER "%.9g"
|
||||
|
||||
/* Liste des commandes, pour les messages d'erreur. */
|
||||
#define COMMANDS "bestmove, moves, legal, cube, eval, position, halfmoves, matchid ou reference"
|
||||
#define COMMANDS "bestmove, moves, legal, cube, eval, position, halfmoves, matchid, reference, " \
|
||||
"levels, analysemove, analysecube ou luck"
|
||||
|
||||
/* Coups gardés dans le classement de la commande reference (comme hint). */
|
||||
#define REFERENCE_MAX_MOVES 10
|
||||
|
||||
/* Arguments de la ligne de commande, une fois lus. */
|
||||
#define MAX_ARGS 4
|
||||
|
||||
typedef struct {
|
||||
int ply;
|
||||
int level; /* -1 : pas de --level */
|
||||
const char *data_dir;
|
||||
const char *command;
|
||||
const char *args[3];
|
||||
const char *args[MAX_ARGS];
|
||||
int arg_count;
|
||||
} options;
|
||||
|
||||
/* Noms JSON des appréciations (engine_skill), des chances
|
||||
* (engine_luck_rating) et des décisions de videau (engine_cube_action). */
|
||||
static const char *const skill_names[] = { "none", "doubtful", "bad", "very_bad" };
|
||||
static const char *const luck_names[] = {
|
||||
"very_unlucky", "unlucky", "none", "lucky", "very_lucky"
|
||||
};
|
||||
static const char *const cube_action_names[] = { "no_double", "double", "take", "pass" };
|
||||
|
||||
/* Code d'erreur JSON pour chaque engine_status. */
|
||||
static const char *
|
||||
status_code(engine_status status)
|
||||
@@ -92,6 +119,12 @@ status_code(engine_status status)
|
||||
return "too_many_moves";
|
||||
case ENGINE_ERROR_INTERNAL:
|
||||
return "internal_error";
|
||||
case ENGINE_ERROR_LEVEL:
|
||||
return "invalid_level";
|
||||
case ENGINE_ERROR_MOVE:
|
||||
return "invalid_move";
|
||||
case ENGINE_ERROR_CUBE_ACTION:
|
||||
return "invalid_cube_action";
|
||||
}
|
||||
return "unknown_error";
|
||||
}
|
||||
@@ -178,6 +211,18 @@ print_reference_evaluation(const engine_evaluation *e)
|
||||
printf(", \"equity\": " NUMBER "}", e->cubeless_equity);
|
||||
}
|
||||
|
||||
/* « , "ply": N » ou « , "level": "NOM" », selon les options. */
|
||||
static void
|
||||
print_level_or_ply(const options *opt)
|
||||
{
|
||||
engine_level_settings settings;
|
||||
|
||||
if (opt->level >= 0 && engine_level_settings_of(opt->level, &settings) == ENGINE_OK)
|
||||
printf(", \"level\": \"%s\"", settings.name);
|
||||
else
|
||||
printf(", \"ply\": %d", opt->ply);
|
||||
}
|
||||
|
||||
/* Lit des dés au format « 31 » ; renvoie 0 si le format est faux. */
|
||||
static int
|
||||
parse_dice(const char *text, int *die1, int *die2)
|
||||
@@ -208,7 +253,10 @@ run_moves_command(const options *opt)
|
||||
moves = malloc(sizeof *moves * ENGINE_MAX_MOVES);
|
||||
if (!moves)
|
||||
return print_error("out_of_memory", "mémoire insuffisante", EXIT_ENGINE_ERROR);
|
||||
if (evaluate)
|
||||
if (evaluate && opt->level >= 0)
|
||||
status = engine_level_best_moves(position_id, match_id, die1, die2, opt->level,
|
||||
moves, ENGINE_MAX_MOVES, &count);
|
||||
else if (evaluate)
|
||||
status = engine_best_moves(position_id, match_id, die1, die2, opt->ply,
|
||||
moves, ENGINE_MAX_MOVES, &count);
|
||||
else
|
||||
@@ -222,7 +270,7 @@ run_moves_command(const options *opt)
|
||||
printf("{\"position_id\": \"%s\", \"match_id\": \"%s\", \"dice\": [%d, %d]",
|
||||
position_id, match_id, die1, die2);
|
||||
if (evaluate)
|
||||
printf(", \"ply\": %d", opt->ply);
|
||||
print_level_or_ply(opt);
|
||||
if (best_only) {
|
||||
printf(", \"best_move\": ");
|
||||
if (count > 0)
|
||||
@@ -270,11 +318,15 @@ run_position_command(const options *opt)
|
||||
e.cubeless_equity, e.cubeful_equity);
|
||||
} else {
|
||||
engine_cube c;
|
||||
status = engine_cube_decision(position_id, match_id, opt->ply, &c);
|
||||
if (opt->level >= 0)
|
||||
status = engine_level_cube_decision(position_id, match_id, opt->level, &c);
|
||||
else
|
||||
status = engine_cube_decision(position_id, match_id, opt->ply, &c);
|
||||
if (status != ENGINE_OK)
|
||||
return print_engine_error(status);
|
||||
printf("{\"position_id\": \"%s\", \"match_id\": \"%s\", \"ply\": %d, \"match\": ",
|
||||
position_id, match_id, opt->ply);
|
||||
printf("{\"position_id\": \"%s\", \"match_id\": \"%s\"", position_id, match_id);
|
||||
print_level_or_ply(opt);
|
||||
printf(", \"match\": ");
|
||||
print_match(&match);
|
||||
printf(", \"probabilities\": ");
|
||||
print_probabilities(c.probabilities);
|
||||
@@ -507,6 +559,122 @@ run_reference_command(const options *opt)
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
/* levels : niveaux de jeu, seuils d'appréciation et de chance. */
|
||||
static int
|
||||
run_levels_command(const options *opt)
|
||||
{
|
||||
engine_level_settings s;
|
||||
int level;
|
||||
|
||||
if (opt->arg_count != 0)
|
||||
return usage_error("levels n'attend pas d'argument");
|
||||
printf("{\"levels\": [");
|
||||
for (level = 0; level < ENGINE_LEVEL_COUNT; level++) {
|
||||
engine_level_settings_of(level, &s);
|
||||
printf("%s{\"name\": \"%s\", \"plies\": %d, \"noise\": " NUMBER
|
||||
", \"deterministic\": %s, \"prune\": %s}", level ? ", " : "", s.name, s.plies,
|
||||
s.noise, s.deterministic ? "true" : "false", s.prune ? "true" : "false");
|
||||
}
|
||||
printf("], \"skill_thresholds\": {\"doubtful\": " NUMBER ", \"bad\": " NUMBER
|
||||
", \"very_bad\": " NUMBER "}, \"luck_thresholds\": {\"lucky\": " NUMBER
|
||||
", \"very_lucky\": " NUMBER "}}\n",
|
||||
ENGINE_SKILL_DOUBTFUL_THRESHOLD, ENGINE_SKILL_BAD_THRESHOLD,
|
||||
ENGINE_SKILL_VERY_BAD_THRESHOLD, ENGINE_LUCK_THRESHOLD, ENGINE_LUCK_VERY_THRESHOLD);
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
/* analysemove : analyse du coup joué. */
|
||||
static int
|
||||
run_analyse_move_command(const options *opt)
|
||||
{
|
||||
const char *position_id = opt->args[0], *match_id = opt->args[1];
|
||||
const char *played_id = opt->args[3];
|
||||
engine_move_analysis a;
|
||||
engine_status status;
|
||||
int die1, die2;
|
||||
|
||||
if (opt->arg_count != 4)
|
||||
return usage_error("arguments attendus : POSITION_ID MATCH_ID DÉS POSITION_JOUÉE");
|
||||
if (!parse_dice(opt->args[2], &die1, &die2))
|
||||
return usage_error("dés attendus sous la forme 31");
|
||||
status = engine_analyse_move(position_id, match_id, die1, die2, played_id, opt->ply, &a);
|
||||
if (status != ENGINE_OK)
|
||||
return print_engine_error(status);
|
||||
|
||||
printf("{\"position_id\": \"%s\", \"match_id\": \"%s\", \"dice\": [%d, %d], \"ply\": %d, "
|
||||
"\"played_position_id\": \"%s\", \"move_count\": %d",
|
||||
position_id, match_id, die1, die2, opt->ply, played_id, a.move_count);
|
||||
if (a.move_count == 0) {
|
||||
printf(", \"rank\": null, \"played\": null, \"best\": null");
|
||||
} else {
|
||||
printf(", \"rank\": %d, \"played\": ", a.rank);
|
||||
print_move(&a.played, 1);
|
||||
printf(", \"best\": ");
|
||||
print_move(&a.best, 1);
|
||||
}
|
||||
printf(", \"equity_loss\": " NUMBER ", \"skill\": \"%s\"}\n", a.equity_loss,
|
||||
skill_names[a.skill]);
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
/* analysecube : analyse d'une décision de videau. */
|
||||
static int
|
||||
run_analyse_cube_command(const options *opt)
|
||||
{
|
||||
const char *position_id = opt->args[0], *match_id = opt->args[1];
|
||||
engine_cube_analysis a;
|
||||
engine_status status;
|
||||
int action;
|
||||
|
||||
if (opt->arg_count != 3)
|
||||
return usage_error("arguments attendus : POSITION_ID MATCH_ID DÉCISION");
|
||||
for (action = 0; action < 4 && strcmp(opt->args[2], cube_action_names[action]) != 0; action++)
|
||||
;
|
||||
if (action == 4)
|
||||
return usage_error("DÉCISION attend no_double, double, take ou pass");
|
||||
status = engine_analyse_cube(position_id, match_id, opt->ply, (engine_cube_action) action, &a);
|
||||
if (status != ENGINE_OK)
|
||||
return print_engine_error(status);
|
||||
|
||||
printf("{\"position_id\": \"%s\", \"match_id\": \"%s\", \"ply\": %d, \"action\": \"%s\", "
|
||||
"\"analysed\": %s, \"cube\": ", position_id, match_id, opt->ply,
|
||||
cube_action_names[action], a.analysed ? "true" : "false");
|
||||
if (a.analysed) {
|
||||
printf("{\"probabilities\": ");
|
||||
print_probabilities(a.cube.probabilities);
|
||||
printf(", ");
|
||||
print_cube_fields(&a.cube);
|
||||
printf("}");
|
||||
} else {
|
||||
printf("null");
|
||||
}
|
||||
printf(", \"equity_loss\": " NUMBER ", \"skill\": \"%s\"}\n", a.equity_loss,
|
||||
skill_names[a.skill]);
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
/* luck : chance du jet pour le joueur au trait. */
|
||||
static int
|
||||
run_luck_command(const options *opt)
|
||||
{
|
||||
const char *position_id = opt->args[0], *match_id = opt->args[1];
|
||||
engine_luck luck;
|
||||
engine_status status;
|
||||
int die1, die2;
|
||||
|
||||
if (opt->arg_count != 3)
|
||||
return usage_error("arguments attendus : POSITION_ID MATCH_ID DÉS");
|
||||
if (!parse_dice(opt->args[2], &die1, &die2))
|
||||
return usage_error("dés attendus sous la forme 31");
|
||||
status = engine_luck_of(position_id, match_id, die1, die2, &luck);
|
||||
if (status != ENGINE_OK)
|
||||
return print_engine_error(status);
|
||||
printf("{\"position_id\": \"%s\", \"match_id\": \"%s\", \"dice\": [%d, %d], \"luck\": " NUMBER
|
||||
", \"rating\": \"%s\"}\n", position_id, match_id, die1, die2, luck.luck,
|
||||
luck_names[luck.rating]);
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
/* Dossier « data » à côté du programme. */
|
||||
static char *
|
||||
default_data_dir(const char *program)
|
||||
@@ -531,20 +699,28 @@ parse_options(int argc, char **argv, options *opt)
|
||||
{
|
||||
int i;
|
||||
|
||||
int has_ply = 0;
|
||||
|
||||
memset(opt, 0, sizeof *opt);
|
||||
opt->level = -1;
|
||||
for (i = 1; i < argc; i++) {
|
||||
if (strcmp(argv[i], "--ply") == 0 && i + 1 < argc) {
|
||||
char *end;
|
||||
opt->ply = (int) strtol(argv[++i], &end, 10);
|
||||
if (*end || end == argv[i])
|
||||
return "--ply attend un nombre";
|
||||
has_ply = 1;
|
||||
} else if (strcmp(argv[i], "--level") == 0 && i + 1 < argc) {
|
||||
if (engine_level_from_name(argv[++i], &opt->level) != ENGINE_OK)
|
||||
return "--level attend un niveau : beginner, casual, intermediate, advanced, "
|
||||
"0ply, 1ply, 2ply, 3ply ou 4ply";
|
||||
} else if (strcmp(argv[i], "--data") == 0 && i + 1 < argc) {
|
||||
opt->data_dir = argv[++i];
|
||||
} else if (argv[i][0] == '-' && argv[i][1] == '-') {
|
||||
return "option inconnue ou sans valeur";
|
||||
} else if (!opt->command) {
|
||||
opt->command = argv[i];
|
||||
} else if (opt->arg_count < 3) {
|
||||
} else if (opt->arg_count < MAX_ARGS) {
|
||||
opt->args[opt->arg_count++] = argv[i];
|
||||
} else {
|
||||
return "trop d'arguments";
|
||||
@@ -552,6 +728,11 @@ parse_options(int argc, char **argv, options *opt)
|
||||
}
|
||||
if (!opt->command)
|
||||
return "commande attendue : " COMMANDS;
|
||||
if (opt->level >= 0 && has_ply)
|
||||
return "--ply et --level ne vont pas ensemble";
|
||||
if (opt->level >= 0 && strcmp(opt->command, "bestmove") != 0
|
||||
&& strcmp(opt->command, "moves") != 0 && strcmp(opt->command, "cube") != 0)
|
||||
return "--level ne s'applique qu'à bestmove, moves et cube";
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -591,6 +772,14 @@ main(int argc, char **argv)
|
||||
result = run_half_moves_command(&opt);
|
||||
else if (strcmp(opt.command, "matchid") == 0)
|
||||
result = run_match_id_command(&opt);
|
||||
else if (strcmp(opt.command, "levels") == 0)
|
||||
result = run_levels_command(&opt);
|
||||
else if (strcmp(opt.command, "analysemove") == 0)
|
||||
result = run_analyse_move_command(&opt);
|
||||
else if (strcmp(opt.command, "analysecube") == 0)
|
||||
result = run_analyse_cube_command(&opt);
|
||||
else if (strcmp(opt.command, "luck") == 0)
|
||||
result = run_luck_command(&opt);
|
||||
else
|
||||
result = usage_error("commande inconnue : " COMMANDS);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// bench.mjs -- la mesure de la page bench.html, sous Node, sur le PC
|
||||
// bench.mjs -- mesure du moteur WebAssembly sous Node, sur le PC
|
||||
// (`make bench`). Premier ordre de grandeur seulement : le chiffre qui compte
|
||||
// est celui du téléphone.
|
||||
//
|
||||
@@ -10,7 +10,7 @@ import os from "node:os";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { gzipSync } from "node:zlib";
|
||||
import createEngineModule from "../build/wasm/engine.mjs";
|
||||
import { DOWNLOADED_FILES, formatReport, measureTimes } from "../web/moteur/benchmark.mjs";
|
||||
import { DOWNLOADED_FILES, formatReport, measureTimes } from "./benchmark.mjs";
|
||||
import { commands, loadEngine } from "../web/engine-wasm.mjs";
|
||||
|
||||
const root = new URL("../", import.meta.url);
|
||||
@@ -35,6 +35,7 @@ const { positions } = JSON.parse(readFileSync(sitePath("wasm/positions.json"), "
|
||||
const times = await measureTimes({
|
||||
bestMove: (...args) => engine.run(commands.bestMove(...args)),
|
||||
cubeDecision: (...args) => engine.run(commands.cubeDecision(...args)),
|
||||
analyseMove: (...args) => engine.run(commands.analyseMove(...args)),
|
||||
}, positions, (done, total) => process.stderr.write("\r " + done + "/" + total + " calculs"));
|
||||
process.stderr.write("\n");
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// benchmark.mjs -- mesure du moteur WebAssembly, partagée par la page de
|
||||
// mesure (bench.html, dans le navigateur) et `make bench` (sous Node).
|
||||
// benchmark.mjs -- mesure du moteur WebAssembly sous Node, pour `make bench`
|
||||
// (engine-wasm/bench.mjs). Elle servait aussi à la page de mesure du site,
|
||||
// retirée avec les pages de test du moteur (ADR 0009, amendement du
|
||||
// 2026-11-03).
|
||||
//
|
||||
// Copyright (C) 2026 les auteurs d'Ustagammon.
|
||||
// Licence : GPLv3 ou ultérieure (voir LICENSE).
|
||||
@@ -15,18 +17,20 @@ export const TIME_THRESHOLDS = [
|
||||
{ key: "move0", label: "Meilleur coup, 0 pli", limitMs: 300 },
|
||||
{ key: "move2", label: "Meilleur coup, 2 plis", limitMs: 3000 },
|
||||
{ key: "cube2", label: "Décision de videau, 2 plis", limitMs: 3000 },
|
||||
// Étape 1a : l'analyse du coup joué, pour le conseil du maître, calculée
|
||||
// en arrière-plan ; même repère que le meilleur coup (ADR 0009).
|
||||
{ key: "analysis2", label: "Analyse du coup joué, 2 plis", limitMs: 3000 },
|
||||
];
|
||||
|
||||
// Taille totale à télécharger, compressée : moins de 40 Mo.
|
||||
export const DOWNLOAD_LIMIT_BYTES = 40e6;
|
||||
|
||||
// Fichiers téléchargés pour faire tourner le moteur dans la page de test,
|
||||
// chemins relatifs à la racine du site (web/ ; wasm/ est build/wasm/).
|
||||
// Fichiers téléchargés pour faire tourner le moteur dans l'application
|
||||
// (hors code de l'application elle-même, quelques dizaines de Ko), chemins
|
||||
// relatifs à la racine du site (web/ ; wasm/ est build/wasm/).
|
||||
export const DOWNLOADED_FILES = [
|
||||
"moteur/index.html",
|
||||
"moteur/style.css",
|
||||
"moteur/page.mjs",
|
||||
"moteur/engine-client.mjs",
|
||||
"index.html",
|
||||
"app.css",
|
||||
"engine-worker.mjs",
|
||||
"engine-wasm.mjs",
|
||||
"wasm/engine.mjs",
|
||||
@@ -34,19 +38,29 @@ export const DOWNLOADED_FILES = [
|
||||
"wasm/engine.data",
|
||||
];
|
||||
|
||||
// engine : objet avec bestMove(positionId, matchId, dice, ply) et
|
||||
// cubeDecision(positionId, matchId, ply), qui renvoient (ou promettent)
|
||||
// { result, ms }.
|
||||
// engine : objet avec bestMove(positionId, matchId, dice, ply),
|
||||
// cubeDecision(positionId, matchId, ply) et analyseMove(positionId, matchId,
|
||||
// dice, playedPositionId, ply), qui renvoient (ou promettent) { result, ms }.
|
||||
// Le coup analysé est le meilleur coup à 0 pli : celui qu'un joueur aurait
|
||||
// pu jouer, analysé à 2 plis comme pour le conseil du maître.
|
||||
// positions : le tableau « positions » de positions.json.
|
||||
// onProgress(done, total) : appelé après chaque calcul.
|
||||
// Renvoie, pour chaque clé de TIME_THRESHOLDS, la liste des
|
||||
// { id, ms } mesurés.
|
||||
export async function measureTimes(engine, positions, onProgress = () => {}) {
|
||||
const tasks = [];
|
||||
const played = new Map();
|
||||
for (const p of positions) {
|
||||
if (p.type === "move") {
|
||||
tasks.push(["move0", p, () => engine.bestMove(p.position_id, p.match_id, p.dice, 0)]);
|
||||
tasks.push(["move0", p, async () => {
|
||||
const answer = await engine.bestMove(p.position_id, p.match_id, p.dice, 0);
|
||||
// Sans coup possible, n'importe quelle position fait l'affaire.
|
||||
played.set(p.id, answer.result.best_move?.position_id ?? p.position_id);
|
||||
return answer;
|
||||
}]);
|
||||
tasks.push(["move2", p, () => engine.bestMove(p.position_id, p.match_id, p.dice, 2)]);
|
||||
tasks.push(["analysis2", p, () => engine.analyseMove(p.position_id, p.match_id, p.dice,
|
||||
played.get(p.id), 2)]);
|
||||
} else {
|
||||
tasks.push(["cube2", p, () => engine.cubeDecision(p.position_id, p.match_id, 2)]);
|
||||
}
|
||||
+482
-26
@@ -26,8 +26,12 @@
|
||||
#define DEFAULT_BEAVERS 3
|
||||
|
||||
/* Seuil d'erreur « douteuse » de gnubg (arSkillLevel[SKILL_DOUBTFUL] dans
|
||||
* gnubg.c), que la commande « hint » passe à FindnSaveBestMoves. */
|
||||
#define DOUBTFUL_THRESHOLD 0.03f
|
||||
* gnubg.c), que la commande « hint » et l'analyse passent à
|
||||
* FindnSaveBestMoves. */
|
||||
#define DOUBTFUL_THRESHOLD ENGINE_SKILL_DOUBTFUL_THRESHOLD
|
||||
|
||||
/* Position de départ : la même vue des deux joueurs. */
|
||||
#define START_POSITION_ID "4HPwATDgc/ABMA"
|
||||
|
||||
/* Fichier de table d'équité de match : celui de gnubg de bureau par défaut. */
|
||||
#define MET_FILE "Kazaross-XG2.xml"
|
||||
@@ -146,6 +150,12 @@ engine_status_message(engine_status status)
|
||||
return "tableau de coups trop petit";
|
||||
case ENGINE_ERROR_INTERNAL:
|
||||
return "erreur interne de gnubg";
|
||||
case ENGINE_ERROR_LEVEL:
|
||||
return "niveau de jeu inconnu";
|
||||
case ENGINE_ERROR_MOVE:
|
||||
return "coup joué absent des coups légaux";
|
||||
case ENGINE_ERROR_CUBE_ACTION:
|
||||
return "décision de videau inconnue";
|
||||
}
|
||||
return "erreur inconnue";
|
||||
}
|
||||
@@ -242,16 +252,127 @@ check_dice(int die1, int die2)
|
||||
return ENGINE_OK;
|
||||
}
|
||||
|
||||
/* Réglages prédéfinis de gnubg pour un niveau, sans bruit. */
|
||||
/*
|
||||
* Vide le cache d'évaluation de gnubg avant chaque calcul : un résultat ne
|
||||
* dépend ainsi que de ses arguments, comme les références (gnubg de bureau,
|
||||
* « clear cache » avant chaque position). Sans cela, une recherche à 2 plis
|
||||
* avec élagage laisse dans le cache des évaluations à 0 pli faites avec les
|
||||
* réseaux d'élagage, rangées sous la même clé que les évaluations à 0 pli
|
||||
* ordinaires (EvalKey, eval.c l. 2302 : l'élagage n'y compte qu'à partir
|
||||
* d'1 pli) ; une chance de jet ou un coup à 0 pli calculé ensuite les
|
||||
* reprendrait (écarts mesurés jusqu'à 2,5e-2, tests/reference/README.md).
|
||||
* Coût mesuré : environ 3 ms par appel sous Node sur le PC.
|
||||
*/
|
||||
static void
|
||||
start_evaluation(void)
|
||||
{
|
||||
EvalCacheFlush();
|
||||
}
|
||||
|
||||
/* Réglages prédéfinis de gnubg pour un niveau, sans bruit ; prépare le
|
||||
* calcul. */
|
||||
static engine_status
|
||||
eval_context_for_ply(int ply, evalcontext *context)
|
||||
{
|
||||
if (ply < 0 || ply > ENGINE_MAX_PLY)
|
||||
return ENGINE_ERROR_PLY;
|
||||
start_evaluation();
|
||||
*context = aecSettings[SETTINGS_0PLY + ply];
|
||||
return ENGINE_OK;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------- Niveaux de jeu */
|
||||
|
||||
/*
|
||||
* Les niveaux d'engine_level sont, dans le même ordre, les réglages
|
||||
* prédéfinis de gnubg (aecSettings, eval.c l. 318-328, et noms de
|
||||
* aszSettings, l. 305-315, où « casual play » devient « casual »).
|
||||
*/
|
||||
static const char *const level_names[ENGINE_LEVEL_COUNT] = {
|
||||
"beginner", "casual", "intermediate", "advanced", "0ply", "1ply", "2ply", "3ply", "4ply"
|
||||
};
|
||||
|
||||
/* Réglages d'évaluation d'un niveau (bruit compris) ; prépare le calcul. */
|
||||
static engine_status
|
||||
eval_context_for_level(int level, evalcontext *context)
|
||||
{
|
||||
if (level < 0 || level >= ENGINE_LEVEL_COUNT)
|
||||
return ENGINE_ERROR_LEVEL;
|
||||
start_evaluation();
|
||||
*context = aecSettings[level];
|
||||
return ENGINE_OK;
|
||||
}
|
||||
|
||||
/* Filtre de coups d'un niveau : aiSettingsMoveFilter (eval.c l. 331) vaut
|
||||
* « Normal » à partir d'1 pli, et « sans objet » à 0 pli, où aucun filtre ne
|
||||
* sert ; le joueur de gnubg garde alors son filtre par défaut, « Normal »
|
||||
* aussi (ap dans gnubg.c l. 461). */
|
||||
static movefilter (*move_filter_for_level(int level))[MAX_FILTER_PLIES]
|
||||
{
|
||||
int index = aiSettingsMoveFilter[level];
|
||||
|
||||
return aaamfMoveFilterSettings[index < 0 ? 0 : index];
|
||||
}
|
||||
|
||||
engine_status
|
||||
engine_level_settings_of(int level, engine_level_settings *settings)
|
||||
{
|
||||
evalcontext context;
|
||||
engine_status status = eval_context_for_level(level, &context);
|
||||
|
||||
if (status != ENGINE_OK)
|
||||
return status;
|
||||
settings->name = level_names[level];
|
||||
settings->plies = (int) context.nPlies;
|
||||
settings->noise = context.rNoise;
|
||||
settings->deterministic = (int) context.fDeterministic;
|
||||
settings->prune = (int) context.fUsePrune;
|
||||
return ENGINE_OK;
|
||||
}
|
||||
|
||||
engine_status
|
||||
engine_level_from_name(const char *name, int *level)
|
||||
{
|
||||
int i;
|
||||
|
||||
for (i = 0; name && i < ENGINE_LEVEL_COUNT; i++)
|
||||
if (strcmp(name, level_names[i]) == 0) {
|
||||
*level = i;
|
||||
return ENGINE_OK;
|
||||
}
|
||||
return ENGINE_ERROR_LEVEL;
|
||||
}
|
||||
|
||||
/* ----------------------------------------------- Appréciation et chance */
|
||||
|
||||
/* Skill de gnubg (analysis.c l. 288), avec les seuils de arSkillLevel. */
|
||||
engine_skill
|
||||
engine_skill_of(float equity_loss)
|
||||
{
|
||||
if (equity_loss < -ENGINE_SKILL_VERY_BAD_THRESHOLD)
|
||||
return ENGINE_SKILL_VERY_BAD;
|
||||
if (equity_loss < -ENGINE_SKILL_BAD_THRESHOLD)
|
||||
return ENGINE_SKILL_BAD;
|
||||
if (equity_loss < -ENGINE_SKILL_DOUBTFUL_THRESHOLD)
|
||||
return ENGINE_SKILL_DOUBTFUL;
|
||||
return ENGINE_SKILL_NONE;
|
||||
}
|
||||
|
||||
/* Luck de gnubg (analysis.c l. 272), avec les seuils de arLuckLevel. */
|
||||
engine_luck_rating
|
||||
engine_luck_rating_of(float luck)
|
||||
{
|
||||
if (luck > ENGINE_LUCK_VERY_THRESHOLD)
|
||||
return ENGINE_LUCK_VERY_LUCKY;
|
||||
if (luck > ENGINE_LUCK_THRESHOLD)
|
||||
return ENGINE_LUCK_LUCKY;
|
||||
if (luck < -ENGINE_LUCK_VERY_THRESHOLD)
|
||||
return ENGINE_LUCK_VERY_UNLUCKY;
|
||||
if (luck < -ENGINE_LUCK_THRESHOLD)
|
||||
return ENGINE_LUCK_UNLUCKY;
|
||||
return ENGINE_LUCK_NONE;
|
||||
}
|
||||
|
||||
/* En match, gnubg calcule en chances de gain du match : on convertit. */
|
||||
static float
|
||||
normalized_equity(float score, const cubeinfo *cube)
|
||||
@@ -307,6 +428,25 @@ engine_legal_moves(const char *position_id, const char *match_id, int die1, int
|
||||
return ENGINE_OK;
|
||||
}
|
||||
|
||||
/* Copie une liste classée par gnubg, puis la libère. */
|
||||
static engine_status
|
||||
copy_ranked_moves(movelist *list, const position *pos, engine_move *moves, int max_moves,
|
||||
int *count)
|
||||
{
|
||||
engine_status status = ENGINE_OK;
|
||||
unsigned int i;
|
||||
|
||||
if (list->cMoves > (unsigned int) max_moves)
|
||||
status = ENGINE_ERROR_TOO_MANY_MOVES;
|
||||
else {
|
||||
for (i = 0; i < list->cMoves; i++)
|
||||
fill_move(&moves[i], &list->amMoves[i], pos, 1);
|
||||
*count = (int) list->cMoves;
|
||||
}
|
||||
g_free(list->amMoves);
|
||||
return status;
|
||||
}
|
||||
|
||||
engine_status
|
||||
engine_best_moves(const char *position_id, const char *match_id, int die1, int die2, int ply,
|
||||
engine_move *moves, int max_moves, int *count)
|
||||
@@ -315,7 +455,6 @@ engine_best_moves(const char *position_id, const char *match_id, int die1, int d
|
||||
movelist list;
|
||||
evalcontext context;
|
||||
engine_status status;
|
||||
unsigned int i;
|
||||
|
||||
status = decode_position(position_id, match_id, &pos);
|
||||
if (status == ENGINE_OK)
|
||||
@@ -336,15 +475,94 @@ engine_best_moves(const char *position_id, const char *match_id, int die1, int d
|
||||
aaamfMoveFilterSettings[0]) < 0)
|
||||
return ENGINE_ERROR_INTERNAL;
|
||||
RefreshMoveList(&list, NULL);
|
||||
return copy_ranked_moves(&list, &pos, moves, max_moves, count);
|
||||
}
|
||||
|
||||
status = ENGINE_OK;
|
||||
if (list.cMoves > (unsigned int) max_moves)
|
||||
status = ENGINE_ERROR_TOO_MANY_MOVES;
|
||||
else {
|
||||
for (i = 0; i < list.cMoves; i++)
|
||||
fill_move(&moves[i], &list.amMoves[i], &pos, 1);
|
||||
*count = (int) list.cMoves;
|
||||
}
|
||||
engine_status
|
||||
engine_level_best_moves(const char *position_id, const char *match_id, int die1, int die2,
|
||||
int level, engine_move *moves, int max_moves, int *count)
|
||||
{
|
||||
position pos;
|
||||
movelist list;
|
||||
evalcontext context;
|
||||
engine_status status;
|
||||
|
||||
status = decode_position(position_id, match_id, &pos);
|
||||
if (status == ENGINE_OK)
|
||||
status = check_dice(die1, die2);
|
||||
if (status == ENGINE_OK)
|
||||
status = eval_context_for_level(level, &context);
|
||||
if (status != ENGINE_OK)
|
||||
return status;
|
||||
|
||||
/*
|
||||
* Comme l'ordinateur de gnubg quand c'est son tour (ComputerTurn dans
|
||||
* play.c, l. 1436-1453) : FindnSaveBestMoves sans coup à garder, sans
|
||||
* analyse et sans seuil, avec les réglages et le filtre du joueur, puis
|
||||
* RefreshMoveList ; il joue le premier coup de la liste.
|
||||
*/
|
||||
if (FindnSaveBestMoves(&list, die1, die2, (ConstTanBoard) pos.board, NULL, FALSE, 0.0f,
|
||||
&pos.cube, &context, move_filter_for_level(level)) < 0)
|
||||
return ENGINE_ERROR_INTERNAL;
|
||||
RefreshMoveList(&list, NULL);
|
||||
return copy_ranked_moves(&list, &pos, moves, max_moves, count);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------- Analyse d'un coup joué */
|
||||
|
||||
engine_status
|
||||
engine_analyse_move(const char *position_id, const char *match_id, int die1, int die2,
|
||||
const char *played_position_id, int ply, engine_move_analysis *analysis)
|
||||
{
|
||||
position pos;
|
||||
TanBoard played_board;
|
||||
positionkey played_key;
|
||||
movelist list;
|
||||
evalcontext context;
|
||||
engine_status status;
|
||||
unsigned int i;
|
||||
|
||||
status = decode_position(position_id, match_id, &pos);
|
||||
if (status == ENGINE_OK)
|
||||
status = check_dice(die1, die2);
|
||||
if (status == ENGINE_OK)
|
||||
status = eval_context_for_ply(ply, &context);
|
||||
if (status != ENGINE_OK)
|
||||
return status;
|
||||
if (!is_base64_id(played_position_id, L_POSITIONID)
|
||||
|| !PositionFromID(played_board, played_position_id))
|
||||
return ENGINE_ERROR_MOVE;
|
||||
PositionKey((ConstTanBoard) played_board, &played_key);
|
||||
|
||||
/*
|
||||
* Comme l'analyse de partie de gnubg (AnalyzeMove dans analysis.c,
|
||||
* l. 685-735) : FindnSaveBestMoves avec la clé du coup joué (évalué au
|
||||
* niveau le plus profond), fAnalyse vrai, seuil « douteux » et filtre
|
||||
* d'analyse (aamfAnalysis, « Normal » par défaut, gnubg.c l. 406) ; pas
|
||||
* de RefreshMoveList. Le rang est la place du coup joué dans cette
|
||||
* liste, et la perte rChequerSkill (l. 726) son équité moins celle du
|
||||
* premier.
|
||||
*/
|
||||
memset(analysis, 0, sizeof *analysis);
|
||||
if (FindnSaveBestMoves(&list, die1, die2, (ConstTanBoard) pos.board, &played_key, TRUE,
|
||||
DOUBTFUL_THRESHOLD, &pos.cube, &context,
|
||||
aaamfMoveFilterSettings[0]) < 0)
|
||||
return ENGINE_ERROR_INTERNAL;
|
||||
if (list.cMoves == 0)
|
||||
return ENGINE_OK; /* aucun coup possible : rien à analyser */
|
||||
|
||||
status = ENGINE_ERROR_MOVE;
|
||||
for (i = 0; i < list.cMoves; i++)
|
||||
if (EqualKeys(played_key, list.amMoves[i].key)) {
|
||||
analysis->move_count = (int) list.cMoves;
|
||||
analysis->rank = (int) i + 1;
|
||||
fill_move(&analysis->played, &list.amMoves[i], &pos, 1);
|
||||
fill_move(&analysis->best, &list.amMoves[0], &pos, 1);
|
||||
analysis->equity_loss = list.amMoves[i].rScore - list.amMoves[0].rScore;
|
||||
analysis->skill = engine_skill_of(analysis->equity_loss);
|
||||
status = ENGINE_OK;
|
||||
break;
|
||||
}
|
||||
g_free(list.amMoves);
|
||||
return status;
|
||||
}
|
||||
@@ -387,27 +605,19 @@ static const char *const cube_decision_names[] = {
|
||||
"OPTIONAL_DOUBLE_BEAVER", "OPTIONAL_DOUBLE_PASS", "OPTIONAL_REDOUBLE_PASS"
|
||||
};
|
||||
|
||||
engine_status
|
||||
engine_cube_decision(const char *position_id, const char *match_id, int ply, engine_cube *cube)
|
||||
/* Décision de videau de la position avec ces réglages d'évaluation. */
|
||||
static engine_status
|
||||
cube_decision(position *pos, const evalcontext *context, engine_cube *cube)
|
||||
{
|
||||
position pos;
|
||||
evalcontext context;
|
||||
float output[2][NUM_ROLLOUT_OUTPUTS];
|
||||
float equities[NUM_CUBEFUL_OUTPUTS];
|
||||
cubedecision decision;
|
||||
engine_status status;
|
||||
int i;
|
||||
|
||||
status = decode_position(position_id, match_id, &pos);
|
||||
if (status == ENGINE_OK)
|
||||
status = eval_context_for_ply(ply, &context);
|
||||
if (status != ENGINE_OK)
|
||||
return status;
|
||||
|
||||
if (GeneralCubeDecisionE(output, (ConstTanBoard) pos.board, &pos.cube, &context, NULL) < 0)
|
||||
if (GeneralCubeDecisionE(output, (ConstTanBoard) pos->board, &pos->cube, context, NULL) < 0)
|
||||
return ENGINE_ERROR_INTERNAL;
|
||||
/* FindCubeDecision renvoie des équités déjà normalisées. */
|
||||
decision = FindCubeDecision(equities, output, &pos.cube);
|
||||
decision = FindCubeDecision(equities, output, &pos->cube);
|
||||
if ((size_t) decision >= sizeof cube_decision_names / sizeof cube_decision_names[0])
|
||||
return ENGINE_ERROR_INTERNAL;
|
||||
|
||||
@@ -419,13 +629,259 @@ engine_cube_decision(const char *position_id, const char *match_id, int ply, eng
|
||||
cube->optimal = equities[OUTPUT_OPTIMAL];
|
||||
cube->decision = cube_decision_names[decision];
|
||||
cube->decision_text = GetCubeRecommendation(decision);
|
||||
GetDPEq(&cube->available, NULL, &pos.cube);
|
||||
GetDPEq(&cube->available, NULL, &pos->cube);
|
||||
cube->should_double = decision == DOUBLE_TAKE || decision == DOUBLE_PASS
|
||||
|| decision == DOUBLE_BEAVER || decision == REDOUBLE_TAKE || decision == REDOUBLE_PASS;
|
||||
cube->should_take = cube->double_take <= cube->double_pass;
|
||||
return ENGINE_OK;
|
||||
}
|
||||
|
||||
engine_status
|
||||
engine_cube_decision(const char *position_id, const char *match_id, int ply, engine_cube *cube)
|
||||
{
|
||||
position pos;
|
||||
evalcontext context;
|
||||
engine_status status;
|
||||
|
||||
status = decode_position(position_id, match_id, &pos);
|
||||
if (status == ENGINE_OK)
|
||||
status = eval_context_for_ply(ply, &context);
|
||||
if (status != ENGINE_OK)
|
||||
return status;
|
||||
return cube_decision(&pos, &context, cube);
|
||||
}
|
||||
|
||||
engine_status
|
||||
engine_level_cube_decision(const char *position_id, const char *match_id, int level,
|
||||
engine_cube *cube)
|
||||
{
|
||||
position pos;
|
||||
evalcontext context;
|
||||
engine_status status;
|
||||
|
||||
status = decode_position(position_id, match_id, &pos);
|
||||
if (status == ENGINE_OK)
|
||||
status = eval_context_for_level(level, &context);
|
||||
if (status != ENGINE_OK)
|
||||
return status;
|
||||
return cube_decision(&pos, &context, cube);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- Analyse d'une décision de videau */
|
||||
|
||||
/* Le plus petit de deux nombres. */
|
||||
static float
|
||||
min_float(float a, float b)
|
||||
{
|
||||
return a < b ? a : b;
|
||||
}
|
||||
|
||||
engine_status
|
||||
engine_analyse_cube(const char *position_id, const char *match_id, int ply,
|
||||
engine_cube_action action, engine_cube_analysis *analysis)
|
||||
{
|
||||
position pos;
|
||||
evalcontext context;
|
||||
engine_status status;
|
||||
int can_double;
|
||||
float take, pass, optimal;
|
||||
|
||||
status = decode_position(position_id, match_id, &pos);
|
||||
if (status == ENGINE_OK)
|
||||
status = eval_context_for_ply(ply, &context);
|
||||
if (status == ENGINE_OK && (action < ENGINE_CUBE_ACTION_NO_DOUBLE || action > ENGINE_CUBE_ACTION_PASS))
|
||||
status = ENGINE_ERROR_CUBE_ACTION;
|
||||
if (status != ENGINE_OK)
|
||||
return status;
|
||||
|
||||
/*
|
||||
* Quand gnubg analyse-t-il la décision (AnalyzeMove dans analysis.c) ?
|
||||
* - ne pas doubler (cas MOVE_NORMAL, l. 655) : hors de la position de
|
||||
* départ, et si le joueur au trait a accès au videau (GetDPEq) ;
|
||||
* - doubler, prendre, passer (cas MOVE_DOUBLE, l. 760) : si GetDPEq le
|
||||
* permet, ou si le videau est au centre ou au joueur au trait.
|
||||
*/
|
||||
memset(analysis, 0, sizeof *analysis);
|
||||
can_double = GetDPEq(NULL, NULL, &pos.cube);
|
||||
if (action == ENGINE_CUBE_ACTION_NO_DOUBLE)
|
||||
analysis->analysed = can_double && strcmp(position_id, START_POSITION_ID) != 0;
|
||||
else
|
||||
analysis->analysed = can_double || pos.cube.fCubeOwner < 0
|
||||
|| pos.cube.fCubeOwner == pos.cube.fMove;
|
||||
if (!analysis->analysed)
|
||||
return ENGINE_OK;
|
||||
|
||||
status = cube_decision(&pos, &context, &analysis->cube);
|
||||
if (status != ENGINE_OK)
|
||||
return status;
|
||||
|
||||
/*
|
||||
* Pertes de gnubg (AnalyzeMove), à partir des équités de FindCubeDecision
|
||||
* vues du joueur au trait :
|
||||
* - ne pas doubler : sans double moins optimale (l. 672) ;
|
||||
* - doubler : la moins bonne des réponses moins optimale (l. 777) ;
|
||||
* - prendre : Skill(-doubleError) avec doubleError = prend moins passe
|
||||
* (l. 772 et 803), soit passe moins prend pour l'adversaire ;
|
||||
* - passer : Skill(doubleError) (l. 823), soit prend moins passe.
|
||||
* Les deux dernières ne sont des erreurs que négatives : un écart positif
|
||||
* est un bon choix, ramené à 0.
|
||||
*/
|
||||
take = analysis->cube.double_take;
|
||||
pass = analysis->cube.double_pass;
|
||||
optimal = analysis->cube.optimal;
|
||||
switch (action) {
|
||||
case ENGINE_CUBE_ACTION_NO_DOUBLE:
|
||||
analysis->equity_loss = analysis->cube.no_double - optimal;
|
||||
break;
|
||||
case ENGINE_CUBE_ACTION_DOUBLE:
|
||||
analysis->equity_loss = (take < pass ? take : pass) - optimal;
|
||||
break;
|
||||
case ENGINE_CUBE_ACTION_TAKE:
|
||||
analysis->equity_loss = min_float(0.0f, pass - take);
|
||||
break;
|
||||
case ENGINE_CUBE_ACTION_PASS:
|
||||
analysis->equity_loss = min_float(0.0f, take - pass);
|
||||
break;
|
||||
}
|
||||
analysis->skill = engine_skill_of(analysis->equity_loss);
|
||||
return ENGINE_OK;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------- Chance d'un jet */
|
||||
|
||||
/*
|
||||
* Réglages de l'analyse de la chance : ecLuck de gnubg (analysis.c l. 89),
|
||||
* 0 pli avec videau, sans bruit.
|
||||
*/
|
||||
static const evalcontext luck_context = {
|
||||
.fCubeful = TRUE, .nPlies = 0, .fUsePrune = FALSE, .fDeterministic = TRUE, .rNoise = 0.0f
|
||||
};
|
||||
|
||||
/*
|
||||
* Équité, pour le joueur au trait (cube), après le meilleur coup du jet
|
||||
* die1, die2 à 0 pli ; s'il ne peut pas jouer, équité de la position vue
|
||||
* de l'adversaire (opponent), changée de signe. Repris de la boucle de
|
||||
* LuckNormal (analysis.c l. 213-243). Renvoie ERR_VAL en cas d'échec.
|
||||
*/
|
||||
static float
|
||||
equity_after_best_move(ConstTanBoard board, int die1, int die2, const cubeinfo *cube,
|
||||
cubeinfo *opponent)
|
||||
{
|
||||
TanBoard swapped;
|
||||
float output[NUM_ROLLOUT_OUTPUTS];
|
||||
movelist list;
|
||||
float equity;
|
||||
|
||||
if (FindnSaveBestMoves(&list, die1, die2, board, NULL, FALSE, 0.0f, cube, &luck_context,
|
||||
defaultFilters) < 0) {
|
||||
g_free(list.amMoves);
|
||||
return ERR_VAL;
|
||||
}
|
||||
if (list.cMoves) {
|
||||
equity = list.amMoves[0].rScore;
|
||||
g_free(list.amMoves);
|
||||
return equity;
|
||||
}
|
||||
memcpy(swapped, board, sizeof swapped);
|
||||
SwapSides(swapped);
|
||||
if (GeneralEvaluationE(output, (ConstTanBoard) swapped, opponent, &luck_context) < 0)
|
||||
return ERR_VAL;
|
||||
return -normalized_equity(output[OUTPUT_CUBEFUL_EQUITY], opponent);
|
||||
}
|
||||
|
||||
/*
|
||||
* LuckNormal (analysis.c l. 200-247) : équité du jet moins la moyenne des
|
||||
* 36 jets. i et j sont les dés moins 1, i >= j.
|
||||
*/
|
||||
static float
|
||||
luck_normal(ConstTanBoard board, int i0, int j0, cubeinfo *cube)
|
||||
{
|
||||
cubeinfo opponent = *cube;
|
||||
float equities[6][6], mean = 0.0f;
|
||||
int i, j;
|
||||
|
||||
opponent.fMove = !cube->fMove;
|
||||
for (i = 0; i < 6; i++)
|
||||
for (j = 0; j <= i; j++) {
|
||||
equities[i][j] = equity_after_best_move(board, i + 1, j + 1, cube, &opponent);
|
||||
if (equities[i][j] == ERR_VAL)
|
||||
return ERR_VAL;
|
||||
mean += i == j ? equities[i][j] : equities[i][j] * 2.0f;
|
||||
}
|
||||
return equities[i0][j0] - mean / 36.0f;
|
||||
}
|
||||
|
||||
/*
|
||||
* LuckFirst (analysis.c l. 104-197) : au premier jet de la partie, les
|
||||
* doubles sont impossibles et le jet peut échoir à l'un ou l'autre joueur.
|
||||
* Équité du jet moins la moyenne des 30 cas : les 15 jets pour le joueur au
|
||||
* trait, et les 15 pour l'adversaire (meilleur coup de l'adversaire, changé
|
||||
* de signe). i et j sont les dés moins 1, i > j.
|
||||
*/
|
||||
static float
|
||||
luck_first(ConstTanBoard board, int i0, int j0, cubeinfo *cube)
|
||||
{
|
||||
cubeinfo opponent = *cube;
|
||||
TanBoard swapped;
|
||||
float equities[6][6], mean = 0.0f, equity;
|
||||
int i, j;
|
||||
|
||||
opponent.fMove = !cube->fMove;
|
||||
for (i = 0; i < 6; i++)
|
||||
for (j = 0; j < i; j++) {
|
||||
equities[i][j] = equity_after_best_move(board, i + 1, j + 1, cube, &opponent);
|
||||
if (equities[i][j] == ERR_VAL)
|
||||
return ERR_VAL;
|
||||
mean += equities[i][j];
|
||||
}
|
||||
|
||||
/* L'adversaire au trait : son meilleur coup, vu de lui, changé de signe.
|
||||
* gnubg, s'il ne peut pas jouer (impossible au départ), reprend
|
||||
* l'évaluation du joueur au trait (l. 168-181). */
|
||||
memcpy(swapped, board, sizeof swapped);
|
||||
SwapSides(swapped);
|
||||
for (i = 0; i < 6; i++)
|
||||
for (j = i + 1; j < 6; j++) {
|
||||
equity = equity_after_best_move((ConstTanBoard) swapped, i + 1, j + 1, &opponent, cube);
|
||||
if (equity == ERR_VAL)
|
||||
return ERR_VAL;
|
||||
equities[i][j] = -equity;
|
||||
mean += equities[i][j];
|
||||
}
|
||||
return equities[i0][j0] - mean / 30.0f;
|
||||
}
|
||||
|
||||
engine_status
|
||||
engine_luck_of(const char *position_id, const char *match_id, int die1, int die2,
|
||||
engine_luck *luck)
|
||||
{
|
||||
position pos;
|
||||
engine_status status;
|
||||
int high, low;
|
||||
float value;
|
||||
|
||||
status = decode_position(position_id, match_id, &pos);
|
||||
if (status == ENGINE_OK)
|
||||
status = check_dice(die1, die2);
|
||||
if (status != ENGINE_OK)
|
||||
return status;
|
||||
start_evaluation();
|
||||
|
||||
/* LuckAnalysis (analysis.c l. 252-268) : dés rangés du plus fort au plus
|
||||
* faible ; LuckFirst à la position de départ hors doubles. */
|
||||
high = (die1 > die2 ? die1 : die2) - 1;
|
||||
low = (die1 > die2 ? die2 : die1) - 1;
|
||||
if (strcmp(position_id, START_POSITION_ID) == 0 && high != low)
|
||||
value = luck_first((ConstTanBoard) pos.board, high, low, &pos.cube);
|
||||
else
|
||||
value = luck_normal((ConstTanBoard) pos.board, high, low, &pos.cube);
|
||||
if (value == ERR_VAL)
|
||||
return ENGINE_ERROR_INTERNAL;
|
||||
luck->luck = value;
|
||||
luck->rating = engine_luck_rating_of(value);
|
||||
return ENGINE_OK;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------- Plateau et fin de partie */
|
||||
|
||||
static void
|
||||
|
||||
+177
-1
@@ -48,11 +48,93 @@ typedef enum {
|
||||
ENGINE_ERROR_DICE, /* dés hors de 1..6 */
|
||||
ENGINE_ERROR_PLY, /* niveau hors de 0..ENGINE_MAX_PLY */
|
||||
ENGINE_ERROR_TOO_MANY_MOVES, /* tableau de coups trop petit */
|
||||
ENGINE_ERROR_INTERNAL /* échec signalé par gnubg */
|
||||
ENGINE_ERROR_INTERNAL, /* échec signalé par gnubg */
|
||||
ENGINE_ERROR_LEVEL, /* niveau de jeu inconnu */
|
||||
ENGINE_ERROR_MOVE, /* coup joué absent des coups légaux */
|
||||
ENGINE_ERROR_CUBE_ACTION /* décision de videau inconnue */
|
||||
} engine_status;
|
||||
|
||||
#define ENGINE_MAX_PLY 4
|
||||
|
||||
/*
|
||||
* Niveaux de jeu de l'ordinateur : les réglages prédéfinis de gnubg, dans
|
||||
* l'ordre de son tableau aecSettings (engine/gnubg/eval.c, l. 305-328, noms
|
||||
* de aszSettings). Les quatre premiers évaluent à 0 pli et ajoutent un
|
||||
* bruit (rNoise, écart type en équité) qui fait commettre des erreurs ; ce
|
||||
* bruit est « déterministe » (fDeterministic) : il est tiré d'un hachage MD5
|
||||
* de la position (Noise, eval.c l. 2241), donc la même position donne
|
||||
* toujours le même coup. Les suivants sont sans bruit, de 0 à 4 plis, avec
|
||||
* le filtre de coups « Normal » (aiSettingsMoveFilter, eval.c l. 331).
|
||||
*
|
||||
* Les anciennes versions de gnubg nommaient « expert » le niveau 0 pli et
|
||||
* « world class » (champion du monde) le niveau 2 plis.
|
||||
*/
|
||||
typedef enum {
|
||||
ENGINE_LEVEL_BEGINNER = 0, /* « beginner » : 0 pli, bruit 0,060 */
|
||||
ENGINE_LEVEL_CASUAL, /* « casual play » : 0 pli, bruit 0,050 */
|
||||
ENGINE_LEVEL_INTERMEDIATE, /* « intermediate » : 0 pli, bruit 0,040 */
|
||||
ENGINE_LEVEL_ADVANCED, /* « advanced » : 0 pli, bruit 0,015 */
|
||||
ENGINE_LEVEL_0PLY, /* « 0ply » (ancien « expert ») */
|
||||
ENGINE_LEVEL_1PLY, /* « 1ply » */
|
||||
ENGINE_LEVEL_2PLY, /* « 2ply » (ancien « world class ») */
|
||||
ENGINE_LEVEL_3PLY, /* « 3ply » */
|
||||
ENGINE_LEVEL_4PLY, /* « 4ply » */
|
||||
ENGINE_LEVEL_COUNT
|
||||
} engine_level;
|
||||
|
||||
/* Réglages d'un niveau, tels que gnubg les définit. */
|
||||
typedef struct {
|
||||
const char *name; /* identifiant : « beginner », « casual »,
|
||||
* « intermediate », « advanced », « 0ply »… « 4ply » */
|
||||
int plies; /* profondeur, en plis de gnubg */
|
||||
float noise; /* écart type du bruit (0 : aucun) */
|
||||
int deterministic; /* bruit tiré de la position, reproductible */
|
||||
int prune; /* élagage des réponses (gnubg : à partir d'1 pli) */
|
||||
} engine_level_settings;
|
||||
|
||||
/*
|
||||
* Appréciation d'un coup ou d'une décision de videau : seuils de gnubg sur
|
||||
* l'équité perdue, normalisée pour un videau de 1 (arSkillLevel dans
|
||||
* gnubg.c l. 280-285 et Skill dans analysis.c l. 288). Une perte
|
||||
* strictement plus grande que le seuil fait passer à la catégorie.
|
||||
*/
|
||||
#define ENGINE_SKILL_DOUBTFUL_THRESHOLD 0.03f /* douteux */
|
||||
#define ENGINE_SKILL_BAD_THRESHOLD 0.06f /* mauvais */
|
||||
#define ENGINE_SKILL_VERY_BAD_THRESHOLD 0.12f /* très mauvais */
|
||||
|
||||
typedef enum {
|
||||
ENGINE_SKILL_NONE = 0, /* rien à signaler */
|
||||
ENGINE_SKILL_DOUBTFUL, /* douteux */
|
||||
ENGINE_SKILL_BAD, /* mauvais */
|
||||
ENGINE_SKILL_VERY_BAD /* très mauvais */
|
||||
} engine_skill;
|
||||
|
||||
/*
|
||||
* Chance d'un jet : seuils de gnubg sur l'équité gagnée ou perdue par
|
||||
* rapport à la moyenne des jets (arLuckLevel dans gnubg.c l. 274-279 et Luck
|
||||
* dans analysis.c l. 272). Un écart strictement plus grand que le seuil
|
||||
* fait passer à la catégorie.
|
||||
*/
|
||||
#define ENGINE_LUCK_THRESHOLD 0.3f /* chanceux, malchanceux */
|
||||
#define ENGINE_LUCK_VERY_THRESHOLD 0.6f /* très chanceux, très malchanceux */
|
||||
|
||||
typedef enum {
|
||||
ENGINE_LUCK_VERY_UNLUCKY = 0, /* très malchanceux */
|
||||
ENGINE_LUCK_UNLUCKY, /* malchanceux */
|
||||
ENGINE_LUCK_NONE, /* ordinaire */
|
||||
ENGINE_LUCK_LUCKY, /* chanceux */
|
||||
ENGINE_LUCK_VERY_LUCKY /* très chanceux */
|
||||
} engine_luck_rating;
|
||||
|
||||
/* Décision de videau prise par un joueur. Les deux premières sont celles
|
||||
* du joueur au trait, les deux dernières sa réponse par l'adversaire. */
|
||||
typedef enum {
|
||||
ENGINE_CUBE_ACTION_NO_DOUBLE = 0,
|
||||
ENGINE_CUBE_ACTION_DOUBLE,
|
||||
ENGINE_CUBE_ACTION_TAKE,
|
||||
ENGINE_CUBE_ACTION_PASS
|
||||
} engine_cube_action;
|
||||
|
||||
/* Nombre maximal de coups légaux pour un jet (MAX_MOVES de gnubg). */
|
||||
#define ENGINE_MAX_MOVES 3060
|
||||
|
||||
@@ -124,6 +206,39 @@ typedef struct {
|
||||
* (double_take <= double_pass) */
|
||||
} engine_cube;
|
||||
|
||||
/* Analyse d'un coup joué, comme l'analyse de partie de gnubg. */
|
||||
typedef struct {
|
||||
int move_count; /* nombre de coups légaux (0 : aucun coup
|
||||
* possible, rien d'autre n'est rempli) */
|
||||
int rank; /* rang du coup joué dans le classement, à
|
||||
* partir de 1 */
|
||||
engine_move played; /* le coup joué, évalué */
|
||||
engine_move best; /* le meilleur coup, évalué */
|
||||
float equity_loss; /* équité du coup joué moins celle du meilleur :
|
||||
* 0 ou négative */
|
||||
engine_skill skill; /* appréciation de gnubg pour cette perte */
|
||||
} engine_move_analysis;
|
||||
|
||||
/* Analyse d'une décision de videau, comme l'analyse de partie de gnubg. */
|
||||
typedef struct {
|
||||
int analysed; /* gnubg analyse cette décision ; sinon (videau
|
||||
* inaccessible, position de départ…), perte
|
||||
* nulle et cube non rempli */
|
||||
engine_cube cube; /* la décision de videau du joueur au trait */
|
||||
float equity_loss; /* équité perdue par celui qui décide, du point
|
||||
* de vue de celui-ci, normalisée pour un videau
|
||||
* de 1 : 0 ou négative */
|
||||
engine_skill skill; /* appréciation de gnubg pour cette perte */
|
||||
} engine_cube_analysis;
|
||||
|
||||
/* Chance d'un jet, comme l'analyse de partie de gnubg. */
|
||||
typedef struct {
|
||||
float luck; /* équité gagnée (positive) ou perdue par ce jet
|
||||
* par rapport à la moyenne des jets, du point
|
||||
* de vue du joueur au trait */
|
||||
engine_luck_rating rating;
|
||||
} engine_luck;
|
||||
|
||||
/* Un camp sur le plateau, vu de lui-même (sa case 1 est la plus proche de
|
||||
* la sortie). */
|
||||
typedef struct {
|
||||
@@ -237,6 +352,67 @@ engine_status engine_evaluate(const char *position_id, const char *match_id,
|
||||
engine_status engine_cube_decision(const char *position_id, const char *match_id,
|
||||
int ply, engine_cube *cube);
|
||||
|
||||
/* ------------------------------------------------------ Niveaux de jeu */
|
||||
|
||||
/* Réglages d'un niveau (engine_level). */
|
||||
engine_status engine_level_settings_of(int level, engine_level_settings *settings);
|
||||
|
||||
/* Le niveau qui porte ce nom, ou ENGINE_ERROR_LEVEL. */
|
||||
engine_status engine_level_from_name(const char *name, int *level);
|
||||
|
||||
/*
|
||||
* Coups légaux classés comme les classe l'ordinateur de gnubg quand il joue
|
||||
* à ce niveau (ComputerTurn dans play.c : FindnSaveBestMoves sans analyse
|
||||
* ni seuil, filtre « Normal », puis RefreshMoveList) ; il joue le premier.
|
||||
* Les équités incluent le bruit du niveau.
|
||||
*/
|
||||
engine_status engine_level_best_moves(const char *position_id, const char *match_id,
|
||||
int die1, int die2, int level,
|
||||
engine_move *moves, int max_moves, int *count);
|
||||
|
||||
/* Décision de videau évaluée avec les réglages de ce niveau, bruit compris. */
|
||||
engine_status engine_level_cube_decision(const char *position_id, const char *match_id,
|
||||
int level, engine_cube *cube);
|
||||
|
||||
/* ------------------------------------------------------------- Analyse */
|
||||
|
||||
/* Appréciation de gnubg pour une perte d'équité (0 ou négative). */
|
||||
engine_skill engine_skill_of(float equity_loss);
|
||||
|
||||
/* Catégorie de chance de gnubg pour une chance de jet. */
|
||||
engine_luck_rating engine_luck_rating_of(float luck);
|
||||
|
||||
/*
|
||||
* Analyse le coup joué par le joueur au trait avec le jet die1, die2, au
|
||||
* niveau ply, comme l'analyse de partie de gnubg (AnalyzeMove dans
|
||||
* analysis.c : FindnSaveBestMoves avec le coup joué, seuil « douteux »,
|
||||
* filtre « Normal », sans RefreshMoveList). played_position_id est la
|
||||
* position après le coup, vue du joueur qui vient de le jouer (champ
|
||||
* position_id d'engine_move). ENGINE_ERROR_MOVE si ce n'est pas un coup
|
||||
* légal ; si aucun coup n'est possible, move_count vaut 0.
|
||||
*/
|
||||
engine_status engine_analyse_move(const char *position_id, const char *match_id,
|
||||
int die1, int die2, const char *played_position_id, int ply,
|
||||
engine_move_analysis *analysis);
|
||||
|
||||
/*
|
||||
* Analyse une décision de videau au niveau ply, comme l'analyse de partie de
|
||||
* gnubg (AnalyzeMove, cas MOVE_NORMAL, MOVE_DOUBLE, MOVE_TAKE et
|
||||
* MOVE_DROP). La position et le Match ID sont ceux du joueur au trait, qui
|
||||
* double ou non ; pour TAKE et PASS, la perte est celle de son adversaire.
|
||||
*/
|
||||
engine_status engine_analyse_cube(const char *position_id, const char *match_id, int ply,
|
||||
engine_cube_action action, engine_cube_analysis *analysis);
|
||||
|
||||
/*
|
||||
* Chance du jet die1, die2 pour le joueur au trait, comme l'analyse de
|
||||
* partie de gnubg (LuckAnalysis dans analysis.c : meilleur coup de chaque
|
||||
* jet à 0 pli, avec videau ; à la position de départ, le premier jet ne
|
||||
* peut pas être un double).
|
||||
*/
|
||||
engine_status engine_luck_of(const char *position_id, const char *match_id,
|
||||
int die1, int die2, engine_luck *luck);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
+155
-32
@@ -1,6 +1,7 @@
|
||||
// play.mjs -- joue une partie complète par l'interface, dans un vrai
|
||||
// navigateur sans écran (Playwright), sur le site servi par l'image de
|
||||
// production : dés, coups au toucher, coups de l'ordinateur, videau, reprise
|
||||
// production : dés, coups au toucher (validés, annulés), coups de
|
||||
// l'ordinateur, videau (avec sa confirmation), reprise
|
||||
// après rechargement, aide, puis hors ligne (service worker).
|
||||
//
|
||||
// Copyright (C) 2026 les auteurs d'Ustagammon.
|
||||
@@ -101,7 +102,7 @@ async function waitForPlayer() {
|
||||
const b = document.getElementById(id);
|
||||
return !b.hidden && !b.disabled;
|
||||
};
|
||||
for (const id of ["again", "take", "skip", "roll"]) {
|
||||
for (const id of ["again", "take", "skip", "confirm", "roll"]) {
|
||||
if (usable(id)) {
|
||||
return id;
|
||||
}
|
||||
@@ -161,7 +162,8 @@ async function checkTraces() {
|
||||
// L'ordinateur n'a pas pu jouer : son jet reste barré, avec un message.
|
||||
const info = await page.textContent("#info");
|
||||
const blocked = await page.locator(".die-blocked").count();
|
||||
check(/^L'ordinateur ne peut pas jouer \d-\d\.$/.test(info ?? "") && blocked === 2,
|
||||
check(/^L'ordinateur ne peut pas jouer \d-\d\.$/.test(info ?? "") && blocked >= 2 &&
|
||||
blocked === (await shownDice()).dice,
|
||||
`jet de l'ordinateur sans coup mal montré : « ${info} », ${blocked} dés barrés`);
|
||||
log(`l'ordinateur ne peut pas jouer : « ${info} »`);
|
||||
}
|
||||
@@ -175,18 +177,35 @@ async function checkTraces() {
|
||||
return traces;
|
||||
}
|
||||
|
||||
/** La zone à toucher d'une action du plateau (dés ou videau) : un bouton
|
||||
* accessible, d'au moins 44 px de côté. Renvoie son sélecteur. */
|
||||
async function touchTarget(action, label) {
|
||||
const selector = `[data-action="${action}"]`;
|
||||
const zone = page.locator(selector);
|
||||
check((await zone.count()) === 1, `zone ${action} absente du plateau`);
|
||||
check((await zone.getAttribute("role")) === "button" &&
|
||||
(await zone.getAttribute("aria-label")) === label, `zone ${action} sans rôle accessible`);
|
||||
const box = await zone.boundingBox();
|
||||
check(box.width >= 44 && box.height >= 44,
|
||||
`zone ${action} trop petite : ${Math.round(box.width)} x ${Math.round(box.height)} px`);
|
||||
return selector;
|
||||
/** Une zone à toucher d'une action du plateau (dés ou videau) : count zones
|
||||
* (les dés en ont une de chaque côté du plateau), chacune un bouton
|
||||
* accessible d'au moins 44 px de côté. Renvoie le locator de la zone index. */
|
||||
async function touchTarget(action, label, count = 1, index = 0) {
|
||||
const zones = page.locator(`[data-action="${action}"]`);
|
||||
check((await zones.count()) === count,
|
||||
`zone ${action} : ${await zones.count()} sur le plateau, ${count} attendues`);
|
||||
for (const zone of await zones.all()) {
|
||||
check((await zone.getAttribute("role")) === "button" &&
|
||||
(await zone.getAttribute("aria-label")) === label, `zone ${action} sans rôle accessible`);
|
||||
const box = await zone.boundingBox();
|
||||
check(box.width >= 44 && box.height >= 44,
|
||||
`zone ${action} trop petite : ${Math.round(box.width)} x ${Math.round(box.height)} px`);
|
||||
}
|
||||
return zones.nth(index);
|
||||
}
|
||||
|
||||
/** Le nombre de dés montrés, et de dés estompés (déjà joués). */
|
||||
async function shownDice() {
|
||||
return {
|
||||
dice: await page.locator("g.die[data-value]").count(),
|
||||
used: await page.locator("g.die-used").count(),
|
||||
};
|
||||
}
|
||||
|
||||
/** Le dernier jet du joueur, d'après le journal. */
|
||||
function lastPlayerRoll(journal) {
|
||||
const rolled = journal.events.filter((e) => e.type === "DiceRolled").at(-1);
|
||||
return rolled?.roll ?? [];
|
||||
}
|
||||
|
||||
/** Observe la page pendant ms millisecondes : le plus grand nombre
|
||||
@@ -256,13 +275,24 @@ let tracesSeen = 0;
|
||||
let replays = 0;
|
||||
let skips = 0;
|
||||
let rolls = 0;
|
||||
// Saisie du coup : demi-coups du coup en cours, pions avancés d'un seul
|
||||
// toucher ou en deux (pion, puis case), coups validés et annulations.
|
||||
let halfMoves = 0;
|
||||
let singleTouch = 0;
|
||||
let twoTouches = 0;
|
||||
let confirms = 0;
|
||||
let undone = 0;
|
||||
let doubles = 0;
|
||||
// Vrai dès que le joueur a touché un pion ou « Annuler » pendant son coup :
|
||||
// le coup de l'ordinateur n'est plus montré.
|
||||
let touched = false;
|
||||
for (let actions = 0; ; actions++) {
|
||||
check(actions < 800, "la partie ne finit pas");
|
||||
check(actions < 1500, "la partie ne finit pas");
|
||||
const todo = await waitForPlayer();
|
||||
step(`partie, action ${actions}`);
|
||||
// Le coup de l'ordinateur est montré jusqu'à l'action suivante du
|
||||
// joueur ; deux fois, on le revoit (la seconde en mouvement réduit).
|
||||
const traces = await checkTraces();
|
||||
const traces = touched ? null : await checkTraces();
|
||||
if (traces !== null) {
|
||||
tracesSeen++;
|
||||
if (replays < 2) {
|
||||
@@ -292,7 +322,8 @@ for (let actions = 0; ; actions++) {
|
||||
// message, et le tour ne passe qu'en touchant « Continuer ».
|
||||
const info = await page.textContent("#info");
|
||||
const blocked = await page.locator(".die-blocked").count();
|
||||
check(/^Aucun coup possible avec \d-\d\.$/.test(info ?? "") && blocked === 2,
|
||||
check(/^Aucun coup possible avec \d-\d\.$/.test(info ?? "") && blocked >= 2 &&
|
||||
blocked === (await shownDice()).dice,
|
||||
`aucun coup possible mal montré : « ${info} », ${blocked} dés barrés`);
|
||||
skips++;
|
||||
log(`aucun coup possible : « ${info} », vous continuez`);
|
||||
@@ -304,30 +335,115 @@ for (let actions = 0; ; actions++) {
|
||||
} else if (todo === "roll") {
|
||||
if (!doubled && (await page.isEnabled("#double"))) {
|
||||
doubled = true;
|
||||
log("vous doublez en touchant le videau");
|
||||
await page.click(await touchTarget("double", "Doubler"));
|
||||
// Toucher le videau demande confirmation ; « Annuler » ne
|
||||
// propose rien, puis « Doubler » (le bouton) et confirmer.
|
||||
const dialog = page.locator("#double-dialog");
|
||||
const question = "Doubler à 2 ? L'ordinateur pourra prendre ou passer.";
|
||||
await (await touchTarget("double", "Doubler")).click();
|
||||
check(await dialog.isVisible(), "videau touché : pas de confirmation");
|
||||
check((await page.textContent("#double-question")) === question,
|
||||
`confirmation du videau : « ${await page.textContent("#double-question")} »`);
|
||||
await shot("videau-confirmation");
|
||||
await page.click("#double-cancel");
|
||||
check(!(await dialog.isVisible()), "Annuler ne ferme pas la confirmation");
|
||||
const offered = (await savedJournal()).events.some((e) => e.type === "CubeOffered");
|
||||
check(!offered && (await page.isVisible("#roll")), "double proposé malgré Annuler");
|
||||
await page.click("#double");
|
||||
check(await dialog.isVisible(), "« Doubler » : pas de confirmation");
|
||||
await page.click("#double-confirm");
|
||||
await waitForPlayer();
|
||||
const answer = (await page.textContent("#info")) || (await page.textContent("#prompt"));
|
||||
log(`réponse : ${answer}`);
|
||||
log(`videau touché : confirmation demandée, annulée, puis double confirmé ; réponse : ${answer}`);
|
||||
continue;
|
||||
}
|
||||
// Une fois sur deux, en touchant les dés en attente plutôt que le
|
||||
// bouton.
|
||||
// Une fois sur deux, en touchant les dés plutôt que le bouton, d'un
|
||||
// côté puis de l'autre du plateau.
|
||||
rolls++;
|
||||
await page.click(rolls % 2 === 0 ? await touchTarget("roll", "Lancer les dés") : "#roll");
|
||||
if (rolls % 2 === 0) {
|
||||
await (await touchTarget("roll", "Lancer les dés", 2, (rolls / 2) % 2)).click();
|
||||
} else {
|
||||
await page.click("#roll");
|
||||
}
|
||||
} else if (todo === "confirm") {
|
||||
// Coup complet : rien n'est joué tant qu'il n'est pas validé.
|
||||
const journal = await savedJournal();
|
||||
const last = journal.events.at(-1);
|
||||
check(last?.type === "DiceRolled" && (await page.isEnabled("#cancel")),
|
||||
`coup complet déjà joué ou non annulable (dernier événement ${last?.type})`);
|
||||
check(/Valider/.test((await page.textContent("#prompt")) ?? ""),
|
||||
`consigne de validation absente : « ${await page.textContent("#prompt")} »`);
|
||||
const dice = await shownDice();
|
||||
check(dice.used === halfMoves && dice.used > 0,
|
||||
`dés estompés : ${dice.used} pour ${halfMoves} demi-coups`);
|
||||
if (undone === 0 && halfMoves >= 2) {
|
||||
// Une fois : « Annuler » défait les demi-coups un par un.
|
||||
if (confirms === 0) {
|
||||
await shot("2b-coup-a-valider");
|
||||
}
|
||||
for (let left = halfMoves - 1; left >= 0; left--) {
|
||||
await page.click("#cancel");
|
||||
await waitForPlayer();
|
||||
const after = await shownDice();
|
||||
check(after.used === left && (await page.isHidden("#confirm")),
|
||||
`Annuler : ${after.used} dés estompés, ${left} attendus`);
|
||||
undone++;
|
||||
}
|
||||
check(await page.isDisabled("#cancel"), "Annuler reste actif sans demi-coup à défaire");
|
||||
check((await savedJournal()).head === journal.head, "Annuler a touché au journal");
|
||||
log(`Annuler : ${undone} demi-coups défaits un par un, journal intact`);
|
||||
halfMoves = 0;
|
||||
continue;
|
||||
}
|
||||
confirms++;
|
||||
// Une fois sur trois avec le bouton, sinon en touchant les dés
|
||||
// (d'un côté ou de l'autre).
|
||||
if (confirms % 3 === 0) {
|
||||
await page.click("#confirm");
|
||||
} else {
|
||||
await (await touchTarget("confirm", "Valider", 2, confirms % 2)).click();
|
||||
}
|
||||
await waitForPlayer();
|
||||
const played = await savedJournal();
|
||||
check(countMoves(played, "player") === countMoves(journal, "player") + 1,
|
||||
"le coup validé n'est pas dans le journal");
|
||||
halfMoves = 0;
|
||||
touched = false;
|
||||
} else {
|
||||
check((await page.locator("[data-action]").count()) === 0,
|
||||
"dés ou videau touchables pendant que vous jouez votre coup");
|
||||
await page.click(".hit.can-move");
|
||||
await page.waitForSelector(".hit.can-land");
|
||||
if (!selectionShot) {
|
||||
selectionShot = true;
|
||||
await shot("2-pion-touche");
|
||||
if (halfMoves === 0) {
|
||||
// Quatre dés pour un double, deux sinon.
|
||||
const [a, b] = lastPlayerRoll(await savedJournal());
|
||||
const dice = await shownDice();
|
||||
check(dice.dice === (a === b ? 4 : 2) && dice.used === 0,
|
||||
`jet ${a}-${b} : ${dice.dice} dés montrés, ${dice.used} estompés`);
|
||||
if (a === b) {
|
||||
doubles++;
|
||||
}
|
||||
}
|
||||
await page.click(".hit.can-land");
|
||||
touched = true;
|
||||
// Un pion à destination unique avance dès qu'on le touche ; sinon,
|
||||
// ses cases d'arrivée s'allument.
|
||||
await page.click(".hit.can-move");
|
||||
if ((await page.locator(".hit.can-land").count()) > 0) {
|
||||
twoTouches++;
|
||||
if (!selectionShot) {
|
||||
selectionShot = true;
|
||||
await shot("2-pion-touche");
|
||||
}
|
||||
await page.click(".hit.can-land");
|
||||
} else {
|
||||
singleTouch++;
|
||||
}
|
||||
halfMoves++;
|
||||
await waitForPlayer();
|
||||
const dice = await shownDice();
|
||||
check(dice.used === halfMoves, `${dice.used} dés estompés après ${halfMoves} demi-coups`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Une fois, au milieu de la partie : fermer et rouvrir la page.
|
||||
// Une fois, au milieu de la partie, entre deux coups : fermer et rouvrir
|
||||
// la page.
|
||||
const journal = await savedJournal();
|
||||
if (!reloaded && countMoves(journal, "computer") >= 3 && countMoves(journal, "player") >= 3) {
|
||||
reloaded = true;
|
||||
@@ -355,7 +471,14 @@ check(countMoves(journal, "player") >= 3 && countMoves(journal, "computer") >= 3
|
||||
check(reloaded, "la reprise après rechargement n'a pas été essayée");
|
||||
check(replays === 2, "le coup de l'ordinateur n'a pas été revu");
|
||||
check(doubled && rolls >= 2, "dés ou videau jamais touchés sur le plateau");
|
||||
log(`dés lancés ${Math.floor(rolls / 2)} fois en les touchant, videau touché pour doubler`);
|
||||
check(singleTouch > 0 && twoTouches > 0,
|
||||
`saisie : ${singleTouch} pions avancés d'un toucher, ${twoTouches} en deux touchers`);
|
||||
check(undone >= 2, "Annuler n'a pas été essayé sur un coup complet");
|
||||
check(confirms >= 3, "trop peu de coups validés pour essayer dés et bouton");
|
||||
log(`dés lancés ${Math.floor(rolls / 2)} fois en les touchant, des deux côtés du plateau`);
|
||||
log(`demi-coups : ${singleTouch} d'un seul toucher (destination unique), ${twoTouches} en deux ` +
|
||||
`touchers ; ${confirms} coups validés (${Math.floor(confirms / 3)} par le bouton, les autres ` +
|
||||
`en touchant les dés) ; ${doubles} doubles montrés en quatre dés`);
|
||||
log(`coup de l'ordinateur montré ${tracesSeen} fois, et effacé à chaque action suivante`);
|
||||
log(skips > 0 ? `« Continuer » demandé ${skips} fois` : "aucun jet sans coup possible dans cette partie");
|
||||
if (computerMs.length > 0) {
|
||||
|
||||
@@ -11,14 +11,16 @@ d'un fichier de résultat.
|
||||
| --- | --- |
|
||||
| `positions.json` | Positions d'entrée, versionnées, écrites à la main |
|
||||
| `results/<id>.json` | Résultats de gnubg, un fichier par position, générés |
|
||||
| `generate.py` | Script exécuté dans gnubg qui produit `results/` |
|
||||
| `compare.py` | Compare le moteur embarqué à `results/` (dernière section) |
|
||||
| `analyses.json` | Analyses d'entrée : coups joués, décisions de videau, niveaux de jeu |
|
||||
| `analysis/<id>.json` | Résultats de l'analyse de partie de gnubg, générés (section « Analyses ») |
|
||||
| `generate.py` | Script exécuté dans gnubg qui produit `results/` et `analysis/` |
|
||||
| `compare.py` | Compare le moteur embarqué à `results/` et `analysis/` (dernière section) |
|
||||
|
||||
Commandes :
|
||||
|
||||
```
|
||||
make reference-image # construit l'image Docker du gnubg de référence
|
||||
make reference # régénère results/ (une vingtaine de secondes)
|
||||
make reference # régénère results/ et analysis/ (une quarantaine de secondes)
|
||||
make test # vérifie la cohérence (tests/test_reference.sh) et
|
||||
# compare le moteur embarqué (tests/test_engine_reference.sh)
|
||||
```
|
||||
@@ -222,6 +224,69 @@ Sortie de `gnubg.cfevaluate` (`GeneralCubeDecisionE` puis
|
||||
`TOOGOOD_PASS`, `REDOUBLE_TAKE`, `NODOUBLE_DEADCUBE`…) ;
|
||||
- `decision_text` : le libellé anglais correspondant de gnubg.
|
||||
|
||||
## Analyses : `analyses.json` et `analysis/<id>.json`
|
||||
|
||||
Ce que gnubg dit d'un coup joué, d'une décision de videau et d'un jet de dés
|
||||
(sa commande « analyse game »), et le coup que joue son ordinateur à chaque
|
||||
niveau. Pour chaque cas, `generate.py` rejoue dans gnubg une courte partie
|
||||
entre deux joueurs humains : la position (`set gnubgid`), puis le coup ou la
|
||||
décision, puis l'analyse. Rien n'est calculé dans le script : les valeurs
|
||||
viennent des enregistrements de la partie analysée et des statistiques que
|
||||
gnubg en tire.
|
||||
|
||||
`analyses.json` a trois listes :
|
||||
|
||||
- `moves` : `position_id`, `match_id`, `dice` et `played`, le coup joué en
|
||||
notation de gnubg (`null` : le meilleur coup selon gnubg, pour les cas qui
|
||||
ne servent qu'à la chance du jet) ;
|
||||
- `cubes` : `position_id`, `match_id` (sans dés) et `action`, la décision :
|
||||
`no_double` ou `double` pour le joueur au trait, `take` ou `pass` pour son
|
||||
adversaire ;
|
||||
- `levels` : `position_id`, `match_id` et `dice` (`null` : décision de
|
||||
videau).
|
||||
|
||||
Un fichier de résultat reprend le cas, avec `type` (`move`, `cube` ou
|
||||
`level`), `gnubg_commit` et `met`, puis, aux niveaux d'analyse 0 et 2 plis
|
||||
(clés de `plies`, réglages prédéfinis de gnubg sans bruit, filtre
|
||||
« Normal ») :
|
||||
|
||||
- **coup joué** : `played_move` et `played_position_id` (position après le
|
||||
coup, vue de celui qui l'a joué), `move_count` (coups légaux), `rank` (place
|
||||
du coup joué dans le classement de l'analyse, à partir de 1 ; `imove` de
|
||||
gnubg plus 1), `played_equity`, `best_move`, `best_equity`, `equity_loss`
|
||||
(perte du coup joué, 0 ou négative : statistique « error-skill » de gnubg
|
||||
changée de signe), `skill` (`none`, `doubtful`, `bad`, `very_bad`) et
|
||||
`ranking` (les dix premiers coups, et jusqu'au coup joué) ; plus, une fois
|
||||
pour toutes, `luck` : `value` (chance du jet) et `rating`
|
||||
(`very_unlucky`, `unlucky`, `none`, `lucky`, `very_lucky`) ;
|
||||
- **videau** : `analysed` (gnubg analyse-t-il cette décision ? pas à la
|
||||
partie Crawford ni quand l'adversaire possède le videau), `cube` (la
|
||||
décision de videau de gnubg au même niveau, format de la section
|
||||
précédente, `null` si non analysée), `equity_loss` (perte de celui qui
|
||||
décide, statistique « error-skill » changée de signe) et `skill` ;
|
||||
- **niveaux** (`levels`, à la place de `plies`) : pour les niveaux
|
||||
`beginner`, `casual`, `intermediate`, `advanced`, `0ply`, `1ply` et `2ply`
|
||||
(3 et 4 plis sont trop lents pour la suite de tests), `plies` et `noise`
|
||||
(réglages de gnubg), puis `move`, le coup que joue l'ordinateur de gnubg
|
||||
réglé à ce niveau (commande « play »), ou la décision de videau de gnubg
|
||||
avec ces réglages, bruit compris.
|
||||
|
||||
Deux précautions, faute de quoi gnubg donne des valeurs qui dépendent de ce
|
||||
qu'il a calculé avant :
|
||||
|
||||
- **le cache d'évaluation est vidé avant chaque analyse**, et l'analyse du
|
||||
videau est désactivée pour les coups joués. Une évaluation à 2 plis avec
|
||||
élagage range dans le cache des évaluations à 0 pli faites avec les réseaux
|
||||
d'élagage, sous la même clé que les évaluations à 0 pli ordinaires (la clé
|
||||
d'`EvalKey`, dans `eval.c`, n'inclut l'élagage qu'à partir d'1 pli). Une
|
||||
analyse à 0 pli, ou l'analyse de la chance qui suit l'analyse du videau à
|
||||
2 plis, les reprend : écarts observés de 5e-3 à 2,5e-2. Le moteur embarqué
|
||||
a le même cache ; `compare.py` l'interroge par un processus neuf à chaque
|
||||
commande ;
|
||||
- **le coup choisi pour un cas sans `played` vient de `findbestmove`** et non
|
||||
de « hint », dont gnubg garde l'analyse dans l'enregistrement du coup :
|
||||
« analyse game » la reprendrait au lieu de calculer au niveau demandé.
|
||||
|
||||
## Comparaison avec le moteur embarqué
|
||||
|
||||
`make test` compare le moteur embarqué à ces résultats
|
||||
@@ -264,6 +329,22 @@ Pour chaque position et chaque niveau :
|
||||
- même décision de videau (`decision` et `decision_text`), et les quatre
|
||||
équités du videau à la tolérance près.
|
||||
|
||||
Pour chaque analyse (`engine-cli analysemove`, `analysecube`, `luck`, et
|
||||
`--level NIVEAU bestmove` ou `cube`) :
|
||||
|
||||
- coup joué : même nombre de coups légaux, même notation du coup joué, même
|
||||
rang sauf égalité (les coups entre les deux rangs ont pour gnubg la même
|
||||
équité que le coup joué, à la tolérance près : remarque), même meilleur coup
|
||||
sauf égalité, et `played_equity`, `best_equity`, `equity_loss` à la
|
||||
tolérance près ;
|
||||
- chance : `value` à la tolérance près ;
|
||||
- videau : même `analysed`, même décision et équités à la tolérance près,
|
||||
`equity_loss` à la tolérance près ;
|
||||
- appréciation (`skill`) et chance (`rating`) identiques, sauf si la valeur
|
||||
de gnubg est à moins de la tolérance d'un seuil (remarque) ;
|
||||
- niveaux : mêmes réglages que gnubg (`engine-cli levels`), même coup choisi,
|
||||
ou même décision de videau et équités à la tolérance près.
|
||||
|
||||
Le rapport donne, pour chaque écart hors critères, la position et le niveau,
|
||||
le champ, la valeur attendue (gnubg), la valeur obtenue (moteur) et l'écart ;
|
||||
il donne toujours l'écart maximal observé.
|
||||
@@ -309,6 +390,11 @@ qu'affiche gnubg (trois décimales). Les vrais défauts (mauvais niveau,
|
||||
mauvais contexte de videau, mauvaise conversion d'équité) donnent des écarts
|
||||
d'un ou plusieurs centièmes.
|
||||
|
||||
Mesure du 2026-11-03, avec les 37 analyses (2 890 nombres comparés, natif et
|
||||
WebAssembly) : écart maximal 1,64e-3, sur `move-jacoby` à 2 plis (équité du
|
||||
coup joué `8/2 5/1*`, et donc sa perte), même phénomène de bascule à 2 plis ; tous les autres
|
||||
nombres des analyses restent sous 1e-3. La tolérance n'a pas été changée.
|
||||
|
||||
Sur téléphone (NEON) les écarts seront à mesurer de la même façon : avec
|
||||
`-ffast-math`, gnubg y remplace aussi la division par une approximation
|
||||
(`vrecpeq_f32` et une itération de Newton), différente de celle de SSE2.
|
||||
|
||||
@@ -0,0 +1,283 @@
|
||||
{
|
||||
"moves": [
|
||||
{
|
||||
"id": "move-opening-31-best",
|
||||
"description": "Ouverture 3-1 jouée au mieux (8/5 6/5) ; chance du premier jet.",
|
||||
"position_id": "4HPwATDgc/ABMA",
|
||||
"match_id": "cIkFAAAAAAAA",
|
||||
"dice": [3, 1],
|
||||
"played": "8/5 6/5"
|
||||
},
|
||||
{
|
||||
"id": "move-opening-31-error",
|
||||
"description": "Ouverture 3-1 jouée 24/23 13/10 au lieu de faire le point 5.",
|
||||
"position_id": "4HPwATDgc/ABMA",
|
||||
"match_id": "cIkFAAAAAAAA",
|
||||
"dice": [3, 1],
|
||||
"played": "24/23 13/10"
|
||||
},
|
||||
{
|
||||
"id": "move-opening-65-error",
|
||||
"description": "Ouverture 6-5 jouée 13/8 13/7 au lieu de courir 24/13.",
|
||||
"position_id": "4HPwATDgc/ABMA",
|
||||
"match_id": "cAkXAAAAAAAA",
|
||||
"dice": [6, 5],
|
||||
"played": "13/8 13/7"
|
||||
},
|
||||
{
|
||||
"id": "move-middlegame-close",
|
||||
"description": "Milieu de partie, 6-4 : un coup proche du meilleur.",
|
||||
"position_id": "mM/gATCw5+ABMA",
|
||||
"match_id": "cAkTAAAAAAAA",
|
||||
"dice": [6, 4],
|
||||
"played": "8/2 6/2"
|
||||
},
|
||||
{
|
||||
"id": "move-middlegame-blunder",
|
||||
"description": "Milieu de partie, 6-4 : un des pires coups.",
|
||||
"position_id": "mM/gATCw5+ABMA",
|
||||
"match_id": "cAkTAAAAAAAA",
|
||||
"dice": [6, 4],
|
||||
"played": "8/4 8/2"
|
||||
},
|
||||
{
|
||||
"id": "move-match",
|
||||
"description": "Match en 7 points à 3-3, 5-3 : frappe au lieu du meilleur coup.",
|
||||
"position_id": "xGfhATDQ2+BBIA",
|
||||
"match_id": "cImuACAAEAAE",
|
||||
"dice": [5, 3],
|
||||
"played": "24/16"
|
||||
},
|
||||
{
|
||||
"id": "move-race",
|
||||
"description": "Course, 5-4 : sortie maladroite.",
|
||||
"position_id": "cLezAQBwN2cGAA",
|
||||
"match_id": "cIkSAAAAAAAA",
|
||||
"dice": [5, 4],
|
||||
"played": "6/1 5/1"
|
||||
},
|
||||
{
|
||||
"id": "move-blitz-doubles",
|
||||
"description": "Attaque, double 3 : un coup parmi 110.",
|
||||
"position_id": "4HPwAVC0XYMHAA",
|
||||
"match_id": "cIkNAAAAAAAA",
|
||||
"dice": [3, 3],
|
||||
"played": "13/10 13/10 5/2 4/1"
|
||||
},
|
||||
{
|
||||
"id": "move-crawford",
|
||||
"description": "Match en 5 points, partie Crawford, 4-3.",
|
||||
"position_id": "4Dn4AGiynYMHAA",
|
||||
"match_id": "8AmuAEAAEAAE",
|
||||
"dice": [4, 3],
|
||||
"played": "5/2 5/1"
|
||||
},
|
||||
{
|
||||
"id": "move-jacoby",
|
||||
"description": "Partie libre avec la règle Jacoby, 6-4.",
|
||||
"position_id": "4HPwAVC0XYMHAA",
|
||||
"match_id": "cAkTAAAAAAAA",
|
||||
"dice": [6, 4],
|
||||
"played": "8/2 5/1"
|
||||
},
|
||||
{
|
||||
"id": "move-bearoff-doubles",
|
||||
"description": "Sortie, double 6 : coup unique ; chance d'un double.",
|
||||
"position_id": "xgAA4MgAAAAAAA",
|
||||
"match_id": "cAkbAAAAAAAA",
|
||||
"dice": [6, 6],
|
||||
"played": "6/off 6/off 4/off 1/off"
|
||||
},
|
||||
{
|
||||
"id": "move-forced",
|
||||
"description": "Rentrée forcée depuis la barre, 3-1.",
|
||||
"position_id": "tm3ABwBw5+ABYA",
|
||||
"match_id": "cIkFAAAAAAAA",
|
||||
"dice": [3, 1],
|
||||
"played": "bar/24"
|
||||
},
|
||||
{
|
||||
"id": "move-luck-unlucky",
|
||||
"description": "Milieu de partie, 5-4 : jet malchanceux, joué au mieux.",
|
||||
"position_id": "xGfhATDQ2+BBIA",
|
||||
"match_id": "cIkOAAAAAAAA",
|
||||
"dice": [5, 4],
|
||||
"played": null
|
||||
},
|
||||
{
|
||||
"id": "move-luck-unlucky-bearoff",
|
||||
"description": "Fin de sortie, 2-1 : jet malchanceux, joué au mieux.",
|
||||
"position_id": "TQAAYAsAAAAAAA",
|
||||
"match_id": "cAkFAAAAAAAA",
|
||||
"dice": [2, 1],
|
||||
"played": null
|
||||
},
|
||||
{
|
||||
"id": "move-luck-lucky",
|
||||
"description": "Course, double 4 : jet chanceux, joué au mieux.",
|
||||
"position_id": "cLezAQBwN2cGAA",
|
||||
"match_id": "cIkSAAAAAAAA",
|
||||
"dice": [4, 4],
|
||||
"played": null
|
||||
},
|
||||
{
|
||||
"id": "move-opening-double",
|
||||
"description": "Position de départ et double 6 : pas un premier jet, chance comptée sur les 36 jets.",
|
||||
"position_id": "4HPwATDgc/ABMA",
|
||||
"match_id": "cIkFAAAAAAAA",
|
||||
"dice": [6, 6],
|
||||
"played": null
|
||||
}
|
||||
],
|
||||
"cubes": [
|
||||
{
|
||||
"id": "cube-double-take-no-double",
|
||||
"description": "Double/prend : le joueur au trait ne double pas.",
|
||||
"position_id": "6O5uAAC4uzsAAA",
|
||||
"match_id": "cAkAAAAAAAAA",
|
||||
"action": "no_double"
|
||||
},
|
||||
{
|
||||
"id": "cube-double-take-double",
|
||||
"description": "Double/prend : le joueur au trait double.",
|
||||
"position_id": "6O5uAAC4uzsAAA",
|
||||
"match_id": "cAkAAAAAAAAA",
|
||||
"action": "double"
|
||||
},
|
||||
{
|
||||
"id": "cube-double-take-take",
|
||||
"description": "Double/prend : l'adversaire prend.",
|
||||
"position_id": "6O5uAAC4uzsAAA",
|
||||
"match_id": "cAkAAAAAAAAA",
|
||||
"action": "take"
|
||||
},
|
||||
{
|
||||
"id": "cube-double-take-pass",
|
||||
"description": "Double/prend : l'adversaire passe à tort.",
|
||||
"position_id": "6O5uAAC4uzsAAA",
|
||||
"match_id": "cAkAAAAAAAAA",
|
||||
"action": "pass"
|
||||
},
|
||||
{
|
||||
"id": "cube-double-pass-take",
|
||||
"description": "Double/passe : l'adversaire prend à tort.",
|
||||
"position_id": "cHd3AADvvQMAAA",
|
||||
"match_id": "cAkAAAAAAAAA",
|
||||
"action": "take"
|
||||
},
|
||||
{
|
||||
"id": "cube-double-pass-pass",
|
||||
"description": "Double/passe : l'adversaire passe.",
|
||||
"position_id": "cHd3AADvvQMAAA",
|
||||
"match_id": "cAkAAAAAAAAA",
|
||||
"action": "pass"
|
||||
},
|
||||
{
|
||||
"id": "cube-no-double-double",
|
||||
"description": "Pas de double : le joueur au trait double trop tôt.",
|
||||
"position_id": "2J0dBgDYnRsGAA",
|
||||
"match_id": "cAkAAAAAAAAA",
|
||||
"action": "double"
|
||||
},
|
||||
{
|
||||
"id": "cube-no-double-no-double",
|
||||
"description": "Pas de double : le joueur au trait ne double pas.",
|
||||
"position_id": "2J0dBgDYnRsGAA",
|
||||
"match_id": "cAkAAAAAAAAA",
|
||||
"action": "no_double"
|
||||
},
|
||||
{
|
||||
"id": "cube-redouble-take",
|
||||
"description": "Redouble : videau à 2 possédé par le joueur au trait, l'adversaire prend.",
|
||||
"position_id": "6O5uAAC4uzsAAA",
|
||||
"match_id": "UQkAAAAAAAAA",
|
||||
"action": "take"
|
||||
},
|
||||
{
|
||||
"id": "cube-match-double",
|
||||
"description": "Match en 7 points, 3-5 : le joueur au trait double.",
|
||||
"position_id": "cHd3AAC4uzsAAA",
|
||||
"match_id": "cAngAFAAGAAE",
|
||||
"action": "double"
|
||||
},
|
||||
{
|
||||
"id": "cube-match-pass",
|
||||
"description": "Match en 7 points, 3-5 : l'adversaire passe.",
|
||||
"position_id": "cHd3AAC4uzsAAA",
|
||||
"match_id": "cAngAFAAGAAE",
|
||||
"action": "pass"
|
||||
},
|
||||
{
|
||||
"id": "cube-post-crawford-double",
|
||||
"description": "Match en 5 points, après Crawford, mené 2-4 : le joueur au trait double.",
|
||||
"position_id": "2J0dBgDYnRsGAA",
|
||||
"match_id": "cAmgAEAAEAAE",
|
||||
"action": "double"
|
||||
},
|
||||
{
|
||||
"id": "cube-crawford-not-analysed",
|
||||
"description": "Partie Crawford : pas de double possible, gnubg n'analyse pas le videau.",
|
||||
"position_id": "cHd3AAC4uzsAAA",
|
||||
"match_id": "8AmgAEAAEAAE",
|
||||
"action": "no_double"
|
||||
},
|
||||
{
|
||||
"id": "cube-opponent-owns-not-analysed",
|
||||
"description": "Videau à 2 possédé par l'adversaire : pas de double possible, pas d'analyse.",
|
||||
"position_id": "6O5uAAC4uzsAAA",
|
||||
"match_id": "EQEAAAAAAAAA",
|
||||
"action": "no_double"
|
||||
}
|
||||
],
|
||||
"levels": [
|
||||
{
|
||||
"id": "level-opening-31",
|
||||
"description": "Ouverture 3-1.",
|
||||
"position_id": "4HPwATDgc/ABMA",
|
||||
"match_id": "cIkFAAAAAAAA",
|
||||
"dice": [3, 1]
|
||||
},
|
||||
{
|
||||
"id": "level-middlegame-1",
|
||||
"description": "Milieu de partie, 6-4 : le bruit change le coup choisi.",
|
||||
"position_id": "mM/gATCw5+ABMA",
|
||||
"match_id": "cAkTAAAAAAAA",
|
||||
"dice": [6, 4]
|
||||
},
|
||||
{
|
||||
"id": "level-middlegame-2",
|
||||
"description": "Milieu de partie avec contact, 5-3.",
|
||||
"position_id": "xGfhATDQ2+BBIA",
|
||||
"match_id": "cIkOAAAAAAAA",
|
||||
"dice": [5, 3]
|
||||
},
|
||||
{
|
||||
"id": "level-prime-vs-prime",
|
||||
"description": "Prime contre prime, 4-2.",
|
||||
"position_id": "4LZtAAbAbZsBAw",
|
||||
"match_id": "cAkKAAAAAAAA",
|
||||
"dice": [4, 2]
|
||||
},
|
||||
{
|
||||
"id": "level-blitz-doubles",
|
||||
"description": "Attaque, double 3.",
|
||||
"position_id": "4HPwAVC0XYMHAA",
|
||||
"match_id": "cIkNAAAAAAAA",
|
||||
"dice": [3, 3]
|
||||
},
|
||||
{
|
||||
"id": "level-cube-double-take",
|
||||
"description": "Décision de videau double/prend, à chaque niveau.",
|
||||
"position_id": "6O5uAAC4uzsAAA",
|
||||
"match_id": "cAkAAAAAAAAA",
|
||||
"dice": null
|
||||
},
|
||||
{
|
||||
"id": "level-cube-no-double",
|
||||
"description": "Décision de videau sans double, à chaque niveau.",
|
||||
"position_id": "2J0dBgDYnRsGAA",
|
||||
"match_id": "cAkAAAAAAAAA",
|
||||
"dice": null
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"id": "cube-crawford-not-analysed",
|
||||
"description": "Partie Crawford : pas de double possible, gnubg n'analyse pas le videau.",
|
||||
"position_id": "cHd3AAC4uzsAAA",
|
||||
"match_id": "8AmgAEAAEAAE",
|
||||
"action": "no_double",
|
||||
"type": "cube",
|
||||
"gnubg_commit": "14b67984737ded0f6de43d63dcb789f729d9b06b",
|
||||
"met": "Kazaross-XG2",
|
||||
"plies": {
|
||||
"0": {
|
||||
"analysed": false,
|
||||
"cube": null,
|
||||
"equity_loss": -0.0,
|
||||
"skill": "none"
|
||||
},
|
||||
"2": {
|
||||
"analysed": false,
|
||||
"cube": null,
|
||||
"equity_loss": -0.0,
|
||||
"skill": "none"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"id": "cube-double-pass-pass",
|
||||
"description": "Double/passe : l'adversaire passe.",
|
||||
"position_id": "cHd3AADvvQMAAA",
|
||||
"match_id": "cAkAAAAAAAAA",
|
||||
"action": "pass",
|
||||
"type": "cube",
|
||||
"gnubg_commit": "14b67984737ded0f6de43d63dcb789f729d9b06b",
|
||||
"met": "Kazaross-XG2",
|
||||
"plies": {
|
||||
"0": {
|
||||
"analysed": true,
|
||||
"cube": {
|
||||
"no_double": 0.9999689,
|
||||
"double_take": 2.0565755,
|
||||
"double_pass": 1.0,
|
||||
"optimal": 1.0,
|
||||
"decision": "DOUBLE_PASS",
|
||||
"decision_text": "Double, pass"
|
||||
},
|
||||
"equity_loss": -0.0,
|
||||
"skill": "none"
|
||||
},
|
||||
"2": {
|
||||
"analysed": true,
|
||||
"cube": {
|
||||
"no_double": 1.0,
|
||||
"double_take": 2.0675642,
|
||||
"double_pass": 1.0,
|
||||
"optimal": 1.0,
|
||||
"decision": "DOUBLE_PASS",
|
||||
"decision_text": "Double, pass"
|
||||
},
|
||||
"equity_loss": -0.0,
|
||||
"skill": "none"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"id": "cube-double-pass-take",
|
||||
"description": "Double/passe : l'adversaire prend à tort.",
|
||||
"position_id": "cHd3AADvvQMAAA",
|
||||
"match_id": "cAkAAAAAAAAA",
|
||||
"action": "take",
|
||||
"type": "cube",
|
||||
"gnubg_commit": "14b67984737ded0f6de43d63dcb789f729d9b06b",
|
||||
"met": "Kazaross-XG2",
|
||||
"plies": {
|
||||
"0": {
|
||||
"analysed": true,
|
||||
"cube": {
|
||||
"no_double": 0.9999689,
|
||||
"double_take": 2.0565755,
|
||||
"double_pass": 1.0,
|
||||
"optimal": 1.0,
|
||||
"decision": "DOUBLE_PASS",
|
||||
"decision_text": "Double, pass"
|
||||
},
|
||||
"equity_loss": -1.0565755,
|
||||
"skill": "very_bad"
|
||||
},
|
||||
"2": {
|
||||
"analysed": true,
|
||||
"cube": {
|
||||
"no_double": 1.0,
|
||||
"double_take": 2.0675642,
|
||||
"double_pass": 1.0,
|
||||
"optimal": 1.0,
|
||||
"decision": "DOUBLE_PASS",
|
||||
"decision_text": "Double, pass"
|
||||
},
|
||||
"equity_loss": -1.0675642,
|
||||
"skill": "very_bad"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"id": "cube-double-take-double",
|
||||
"description": "Double/prend : le joueur au trait double.",
|
||||
"position_id": "6O5uAAC4uzsAAA",
|
||||
"match_id": "cAkAAAAAAAAA",
|
||||
"action": "double",
|
||||
"type": "cube",
|
||||
"gnubg_commit": "14b67984737ded0f6de43d63dcb789f729d9b06b",
|
||||
"met": "Kazaross-XG2",
|
||||
"plies": {
|
||||
"0": {
|
||||
"analysed": true,
|
||||
"cube": {
|
||||
"no_double": 0.8189599,
|
||||
"double_take": 0.88185024,
|
||||
"double_pass": 1.0,
|
||||
"optimal": 0.88185024,
|
||||
"decision": "DOUBLE_TAKE",
|
||||
"decision_text": "Double, take"
|
||||
},
|
||||
"equity_loss": -0.0,
|
||||
"skill": "none"
|
||||
},
|
||||
"2": {
|
||||
"analysed": true,
|
||||
"cube": {
|
||||
"no_double": 0.83056116,
|
||||
"double_take": 0.9218164,
|
||||
"double_pass": 1.0,
|
||||
"optimal": 0.9218164,
|
||||
"decision": "DOUBLE_TAKE",
|
||||
"decision_text": "Double, take"
|
||||
},
|
||||
"equity_loss": -0.0,
|
||||
"skill": "none"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"id": "cube-double-take-no-double",
|
||||
"description": "Double/prend : le joueur au trait ne double pas.",
|
||||
"position_id": "6O5uAAC4uzsAAA",
|
||||
"match_id": "cAkAAAAAAAAA",
|
||||
"action": "no_double",
|
||||
"type": "cube",
|
||||
"gnubg_commit": "14b67984737ded0f6de43d63dcb789f729d9b06b",
|
||||
"met": "Kazaross-XG2",
|
||||
"plies": {
|
||||
"0": {
|
||||
"analysed": true,
|
||||
"cube": {
|
||||
"no_double": 0.8189599,
|
||||
"double_take": 0.88185024,
|
||||
"double_pass": 1.0,
|
||||
"optimal": 0.88185024,
|
||||
"decision": "DOUBLE_TAKE",
|
||||
"decision_text": "Double, take"
|
||||
},
|
||||
"equity_loss": -0.06289035,
|
||||
"skill": "bad"
|
||||
},
|
||||
"2": {
|
||||
"analysed": true,
|
||||
"cube": {
|
||||
"no_double": 0.83056116,
|
||||
"double_take": 0.9218164,
|
||||
"double_pass": 1.0,
|
||||
"optimal": 0.9218164,
|
||||
"decision": "DOUBLE_TAKE",
|
||||
"decision_text": "Double, take"
|
||||
},
|
||||
"equity_loss": -0.09125525,
|
||||
"skill": "bad"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"id": "cube-double-take-pass",
|
||||
"description": "Double/prend : l'adversaire passe à tort.",
|
||||
"position_id": "6O5uAAC4uzsAAA",
|
||||
"match_id": "cAkAAAAAAAAA",
|
||||
"action": "pass",
|
||||
"type": "cube",
|
||||
"gnubg_commit": "14b67984737ded0f6de43d63dcb789f729d9b06b",
|
||||
"met": "Kazaross-XG2",
|
||||
"plies": {
|
||||
"0": {
|
||||
"analysed": true,
|
||||
"cube": {
|
||||
"no_double": 0.8189599,
|
||||
"double_take": 0.88185024,
|
||||
"double_pass": 1.0,
|
||||
"optimal": 0.88185024,
|
||||
"decision": "DOUBLE_TAKE",
|
||||
"decision_text": "Double, take"
|
||||
},
|
||||
"equity_loss": -0.11814976,
|
||||
"skill": "bad"
|
||||
},
|
||||
"2": {
|
||||
"analysed": true,
|
||||
"cube": {
|
||||
"no_double": 0.83056116,
|
||||
"double_take": 0.9218164,
|
||||
"double_pass": 1.0,
|
||||
"optimal": 0.9218164,
|
||||
"decision": "DOUBLE_TAKE",
|
||||
"decision_text": "Double, take"
|
||||
},
|
||||
"equity_loss": -0.07818359,
|
||||
"skill": "bad"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"id": "cube-double-take-take",
|
||||
"description": "Double/prend : l'adversaire prend.",
|
||||
"position_id": "6O5uAAC4uzsAAA",
|
||||
"match_id": "cAkAAAAAAAAA",
|
||||
"action": "take",
|
||||
"type": "cube",
|
||||
"gnubg_commit": "14b67984737ded0f6de43d63dcb789f729d9b06b",
|
||||
"met": "Kazaross-XG2",
|
||||
"plies": {
|
||||
"0": {
|
||||
"analysed": true,
|
||||
"cube": {
|
||||
"no_double": 0.8189599,
|
||||
"double_take": 0.88185024,
|
||||
"double_pass": 1.0,
|
||||
"optimal": 0.88185024,
|
||||
"decision": "DOUBLE_TAKE",
|
||||
"decision_text": "Double, take"
|
||||
},
|
||||
"equity_loss": -0.0,
|
||||
"skill": "none"
|
||||
},
|
||||
"2": {
|
||||
"analysed": true,
|
||||
"cube": {
|
||||
"no_double": 0.83056116,
|
||||
"double_take": 0.9218164,
|
||||
"double_pass": 1.0,
|
||||
"optimal": 0.9218164,
|
||||
"decision": "DOUBLE_TAKE",
|
||||
"decision_text": "Double, take"
|
||||
},
|
||||
"equity_loss": -0.0,
|
||||
"skill": "none"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"id": "cube-match-double",
|
||||
"description": "Match en 7 points, 3-5 : le joueur au trait double.",
|
||||
"position_id": "cHd3AAC4uzsAAA",
|
||||
"match_id": "cAngAFAAGAAE",
|
||||
"action": "double",
|
||||
"type": "cube",
|
||||
"gnubg_commit": "14b67984737ded0f6de43d63dcb789f729d9b06b",
|
||||
"met": "Kazaross-XG2",
|
||||
"plies": {
|
||||
"0": {
|
||||
"analysed": true,
|
||||
"cube": {
|
||||
"no_double": 0.93459773,
|
||||
"double_take": 1.1651278,
|
||||
"double_pass": 1.0,
|
||||
"optimal": 1.0,
|
||||
"decision": "DOUBLE_PASS",
|
||||
"decision_text": "Double, pass"
|
||||
},
|
||||
"equity_loss": -0.0,
|
||||
"skill": "none"
|
||||
},
|
||||
"2": {
|
||||
"analysed": true,
|
||||
"cube": {
|
||||
"no_double": 0.9405141,
|
||||
"double_take": 1.1657741,
|
||||
"double_pass": 1.0,
|
||||
"optimal": 1.0,
|
||||
"decision": "DOUBLE_PASS",
|
||||
"decision_text": "Double, pass"
|
||||
},
|
||||
"equity_loss": -0.0,
|
||||
"skill": "none"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"id": "cube-match-pass",
|
||||
"description": "Match en 7 points, 3-5 : l'adversaire passe.",
|
||||
"position_id": "cHd3AAC4uzsAAA",
|
||||
"match_id": "cAngAFAAGAAE",
|
||||
"action": "pass",
|
||||
"type": "cube",
|
||||
"gnubg_commit": "14b67984737ded0f6de43d63dcb789f729d9b06b",
|
||||
"met": "Kazaross-XG2",
|
||||
"plies": {
|
||||
"0": {
|
||||
"analysed": true,
|
||||
"cube": {
|
||||
"no_double": 0.93459773,
|
||||
"double_take": 1.1651278,
|
||||
"double_pass": 1.0,
|
||||
"optimal": 1.0,
|
||||
"decision": "DOUBLE_PASS",
|
||||
"decision_text": "Double, pass"
|
||||
},
|
||||
"equity_loss": -0.0,
|
||||
"skill": "none"
|
||||
},
|
||||
"2": {
|
||||
"analysed": true,
|
||||
"cube": {
|
||||
"no_double": 0.9405141,
|
||||
"double_take": 1.1657741,
|
||||
"double_pass": 1.0,
|
||||
"optimal": 1.0,
|
||||
"decision": "DOUBLE_PASS",
|
||||
"decision_text": "Double, pass"
|
||||
},
|
||||
"equity_loss": -0.0,
|
||||
"skill": "none"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"id": "cube-no-double-double",
|
||||
"description": "Pas de double : le joueur au trait double trop tôt.",
|
||||
"position_id": "2J0dBgDYnRsGAA",
|
||||
"match_id": "cAkAAAAAAAAA",
|
||||
"action": "double",
|
||||
"type": "cube",
|
||||
"gnubg_commit": "14b67984737ded0f6de43d63dcb789f729d9b06b",
|
||||
"met": "Kazaross-XG2",
|
||||
"plies": {
|
||||
"0": {
|
||||
"analysed": true,
|
||||
"cube": {
|
||||
"no_double": 0.35839266,
|
||||
"double_take": 0.18619786,
|
||||
"double_pass": 1.0,
|
||||
"optimal": 0.35839266,
|
||||
"decision": "NODOUBLE_TAKE",
|
||||
"decision_text": "No double, take"
|
||||
},
|
||||
"equity_loss": -0.1721948,
|
||||
"skill": "very_bad"
|
||||
},
|
||||
"2": {
|
||||
"analysed": true,
|
||||
"cube": {
|
||||
"no_double": 0.28532878,
|
||||
"double_take": 0.11171284,
|
||||
"double_pass": 1.0,
|
||||
"optimal": 0.28532878,
|
||||
"decision": "NODOUBLE_TAKE",
|
||||
"decision_text": "No double, take"
|
||||
},
|
||||
"equity_loss": -0.17361593,
|
||||
"skill": "very_bad"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"id": "cube-no-double-no-double",
|
||||
"description": "Pas de double : le joueur au trait ne double pas.",
|
||||
"position_id": "2J0dBgDYnRsGAA",
|
||||
"match_id": "cAkAAAAAAAAA",
|
||||
"action": "no_double",
|
||||
"type": "cube",
|
||||
"gnubg_commit": "14b67984737ded0f6de43d63dcb789f729d9b06b",
|
||||
"met": "Kazaross-XG2",
|
||||
"plies": {
|
||||
"0": {
|
||||
"analysed": true,
|
||||
"cube": {
|
||||
"no_double": 0.35839266,
|
||||
"double_take": 0.18619786,
|
||||
"double_pass": 1.0,
|
||||
"optimal": 0.35839266,
|
||||
"decision": "NODOUBLE_TAKE",
|
||||
"decision_text": "No double, take"
|
||||
},
|
||||
"equity_loss": -0.0,
|
||||
"skill": "none"
|
||||
},
|
||||
"2": {
|
||||
"analysed": true,
|
||||
"cube": {
|
||||
"no_double": 0.28532878,
|
||||
"double_take": 0.11171284,
|
||||
"double_pass": 1.0,
|
||||
"optimal": 0.28532878,
|
||||
"decision": "NODOUBLE_TAKE",
|
||||
"decision_text": "No double, take"
|
||||
},
|
||||
"equity_loss": -0.0,
|
||||
"skill": "none"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"id": "cube-opponent-owns-not-analysed",
|
||||
"description": "Videau à 2 possédé par l'adversaire : pas de double possible, pas d'analyse.",
|
||||
"position_id": "6O5uAAC4uzsAAA",
|
||||
"match_id": "EQEAAAAAAAAA",
|
||||
"action": "no_double",
|
||||
"type": "cube",
|
||||
"gnubg_commit": "14b67984737ded0f6de43d63dcb789f729d9b06b",
|
||||
"met": "Kazaross-XG2",
|
||||
"plies": {
|
||||
"0": {
|
||||
"analysed": false,
|
||||
"cube": null,
|
||||
"equity_loss": -0.0,
|
||||
"skill": "none"
|
||||
},
|
||||
"2": {
|
||||
"analysed": false,
|
||||
"cube": null,
|
||||
"equity_loss": -0.0,
|
||||
"skill": "none"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"id": "cube-post-crawford-double",
|
||||
"description": "Match en 5 points, après Crawford, mené 2-4 : le joueur au trait double.",
|
||||
"position_id": "2J0dBgDYnRsGAA",
|
||||
"match_id": "cAmgAEAAEAAE",
|
||||
"action": "double",
|
||||
"type": "cube",
|
||||
"gnubg_commit": "14b67984737ded0f6de43d63dcb789f729d9b06b",
|
||||
"met": "Kazaross-XG2",
|
||||
"plies": {
|
||||
"0": {
|
||||
"analysed": true,
|
||||
"cube": {
|
||||
"no_double": 0.26475775,
|
||||
"double_take": 0.2732438,
|
||||
"double_pass": 1.0,
|
||||
"optimal": 0.2732438,
|
||||
"decision": "DOUBLE_TAKE",
|
||||
"decision_text": "Double, take"
|
||||
},
|
||||
"equity_loss": -0.0,
|
||||
"skill": "none"
|
||||
},
|
||||
"2": {
|
||||
"analysed": true,
|
||||
"cube": {
|
||||
"no_double": 0.22408937,
|
||||
"double_take": 0.22408937,
|
||||
"double_pass": 1.0,
|
||||
"optimal": 0.22408937,
|
||||
"decision": "OPTIONAL_DOUBLE_TAKE",
|
||||
"decision_text": "Optional double, take"
|
||||
},
|
||||
"equity_loss": -0.0,
|
||||
"skill": "none"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"id": "cube-redouble-take",
|
||||
"description": "Redouble : videau à 2 possédé par le joueur au trait, l'adversaire prend.",
|
||||
"position_id": "6O5uAAC4uzsAAA",
|
||||
"match_id": "UQkAAAAAAAAA",
|
||||
"action": "take",
|
||||
"type": "cube",
|
||||
"gnubg_commit": "14b67984737ded0f6de43d63dcb789f729d9b06b",
|
||||
"met": "Kazaross-XG2",
|
||||
"plies": {
|
||||
"0": {
|
||||
"analysed": true,
|
||||
"cube": {
|
||||
"no_double": 0.840925,
|
||||
"double_take": 0.88185024,
|
||||
"double_pass": 1.0,
|
||||
"optimal": 0.88185024,
|
||||
"decision": "REDOUBLE_TAKE",
|
||||
"decision_text": "Redouble, take"
|
||||
},
|
||||
"equity_loss": -0.0,
|
||||
"skill": "none"
|
||||
},
|
||||
"2": {
|
||||
"analysed": true,
|
||||
"cube": {
|
||||
"no_double": 0.84627455,
|
||||
"double_take": 0.9218164,
|
||||
"double_pass": 1.0,
|
||||
"optimal": 0.9218164,
|
||||
"decision": "REDOUBLE_TAKE",
|
||||
"decision_text": "Redouble, take"
|
||||
},
|
||||
"equity_loss": -0.0,
|
||||
"skill": "none"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"id": "level-blitz-doubles",
|
||||
"description": "Attaque, double 3.",
|
||||
"position_id": "4HPwAVC0XYMHAA",
|
||||
"match_id": "cIkNAAAAAAAA",
|
||||
"dice": [
|
||||
3,
|
||||
3
|
||||
],
|
||||
"type": "level",
|
||||
"gnubg_commit": "14b67984737ded0f6de43d63dcb789f729d9b06b",
|
||||
"met": "Kazaross-XG2",
|
||||
"levels": {
|
||||
"beginner": {
|
||||
"plies": 0,
|
||||
"noise": 0.06,
|
||||
"move": "5/2(2) 4/1*(2)"
|
||||
},
|
||||
"casual": {
|
||||
"plies": 0,
|
||||
"noise": 0.05,
|
||||
"move": "5/2(2) 4/1*(2)"
|
||||
},
|
||||
"intermediate": {
|
||||
"plies": 0,
|
||||
"noise": 0.04,
|
||||
"move": "5/2(2) 4/1*(2)"
|
||||
},
|
||||
"advanced": {
|
||||
"plies": 0,
|
||||
"noise": 0.015,
|
||||
"move": "13/10 6/3 4/1*(2)"
|
||||
},
|
||||
"0ply": {
|
||||
"plies": 0,
|
||||
"noise": 0.0,
|
||||
"move": "13/10 6/3 4/1*(2)"
|
||||
},
|
||||
"1ply": {
|
||||
"plies": 1,
|
||||
"noise": 0.0,
|
||||
"move": "13/10(2) 8/5 6/3"
|
||||
},
|
||||
"2ply": {
|
||||
"plies": 2,
|
||||
"noise": 0.0,
|
||||
"move": "13/7 13/10 6/3"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"id": "level-cube-double-take",
|
||||
"description": "Décision de videau double/prend, à chaque niveau.",
|
||||
"position_id": "6O5uAAC4uzsAAA",
|
||||
"match_id": "cAkAAAAAAAAA",
|
||||
"dice": null,
|
||||
"type": "level",
|
||||
"gnubg_commit": "14b67984737ded0f6de43d63dcb789f729d9b06b",
|
||||
"met": "Kazaross-XG2",
|
||||
"levels": {
|
||||
"beginner": {
|
||||
"plies": 0,
|
||||
"noise": 0.06,
|
||||
"no_double": 0.42431837,
|
||||
"double_take": 0.26415053,
|
||||
"double_pass": 1.0,
|
||||
"optimal": 0.42431837,
|
||||
"decision": "NODOUBLE_TAKE",
|
||||
"decision_text": "No double, take"
|
||||
},
|
||||
"casual": {
|
||||
"plies": 0,
|
||||
"noise": 0.05,
|
||||
"no_double": 0.49009216,
|
||||
"double_take": 0.36710066,
|
||||
"double_pass": 1.0,
|
||||
"optimal": 0.49009216,
|
||||
"decision": "NODOUBLE_TAKE",
|
||||
"decision_text": "No double, take"
|
||||
},
|
||||
"intermediate": {
|
||||
"plies": 0,
|
||||
"noise": 0.04,
|
||||
"no_double": 0.55586565,
|
||||
"double_take": 0.47005057,
|
||||
"double_pass": 1.0,
|
||||
"optimal": 0.55586565,
|
||||
"decision": "NODOUBLE_TAKE",
|
||||
"decision_text": "No double, take"
|
||||
},
|
||||
"advanced": {
|
||||
"plies": 0,
|
||||
"noise": 0.015,
|
||||
"no_double": 0.7202995,
|
||||
"double_take": 0.7274252,
|
||||
"double_pass": 1.0,
|
||||
"optimal": 0.7274252,
|
||||
"decision": "DOUBLE_TAKE",
|
||||
"decision_text": "Double, take"
|
||||
},
|
||||
"0ply": {
|
||||
"plies": 0,
|
||||
"noise": 0.0,
|
||||
"no_double": 0.8189599,
|
||||
"double_take": 0.88185024,
|
||||
"double_pass": 1.0,
|
||||
"optimal": 0.88185024,
|
||||
"decision": "DOUBLE_TAKE",
|
||||
"decision_text": "Double, take"
|
||||
},
|
||||
"1ply": {
|
||||
"plies": 1,
|
||||
"noise": 0.0,
|
||||
"no_double": 0.79611415,
|
||||
"double_take": 0.8986737,
|
||||
"double_pass": 1.0,
|
||||
"optimal": 0.8986737,
|
||||
"decision": "DOUBLE_TAKE",
|
||||
"decision_text": "Double, take"
|
||||
},
|
||||
"2ply": {
|
||||
"plies": 2,
|
||||
"noise": 0.0,
|
||||
"no_double": 0.83056116,
|
||||
"double_take": 0.9218164,
|
||||
"double_pass": 1.0,
|
||||
"optimal": 0.9218164,
|
||||
"decision": "DOUBLE_TAKE",
|
||||
"decision_text": "Double, take"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"id": "level-cube-no-double",
|
||||
"description": "Décision de videau sans double, à chaque niveau.",
|
||||
"position_id": "2J0dBgDYnRsGAA",
|
||||
"match_id": "cAkAAAAAAAAA",
|
||||
"dice": null,
|
||||
"type": "level",
|
||||
"gnubg_commit": "14b67984737ded0f6de43d63dcb789f729d9b06b",
|
||||
"met": "Kazaross-XG2",
|
||||
"levels": {
|
||||
"beginner": {
|
||||
"plies": 0,
|
||||
"noise": 0.06,
|
||||
"no_double": -0.16227412,
|
||||
"double_take": -0.6363357,
|
||||
"double_pass": 1.0,
|
||||
"optimal": -0.16227412,
|
||||
"decision": "NODOUBLE_BEAVER",
|
||||
"decision_text": "No double, beaver"
|
||||
},
|
||||
"casual": {
|
||||
"plies": 0,
|
||||
"noise": 0.05,
|
||||
"no_double": -0.075523466,
|
||||
"double_take": -0.4993003,
|
||||
"double_pass": 1.0,
|
||||
"optimal": -0.075523466,
|
||||
"decision": "NODOUBLE_BEAVER",
|
||||
"decision_text": "No double, beaver"
|
||||
},
|
||||
"intermediate": {
|
||||
"plies": 0,
|
||||
"noise": 0.04,
|
||||
"no_double": 0.011226978,
|
||||
"double_take": -0.36226535,
|
||||
"double_pass": 1.0,
|
||||
"optimal": 0.011226978,
|
||||
"decision": "NODOUBLE_BEAVER",
|
||||
"decision_text": "No double, beaver"
|
||||
},
|
||||
"advanced": {
|
||||
"plies": 0,
|
||||
"noise": 0.015,
|
||||
"no_double": 0.22810358,
|
||||
"double_take": -0.019677103,
|
||||
"double_pass": 1.0,
|
||||
"optimal": 0.22810358,
|
||||
"decision": "NODOUBLE_BEAVER",
|
||||
"decision_text": "No double, beaver"
|
||||
},
|
||||
"0ply": {
|
||||
"plies": 0,
|
||||
"noise": 0.0,
|
||||
"no_double": 0.35839266,
|
||||
"double_take": 0.18619786,
|
||||
"double_pass": 1.0,
|
||||
"optimal": 0.35839266,
|
||||
"decision": "NODOUBLE_TAKE",
|
||||
"decision_text": "No double, take"
|
||||
},
|
||||
"1ply": {
|
||||
"plies": 1,
|
||||
"noise": 0.0,
|
||||
"no_double": 0.33552775,
|
||||
"double_take": 0.15480106,
|
||||
"double_pass": 1.0,
|
||||
"optimal": 0.33552775,
|
||||
"decision": "NODOUBLE_TAKE",
|
||||
"decision_text": "No double, take"
|
||||
},
|
||||
"2ply": {
|
||||
"plies": 2,
|
||||
"noise": 0.0,
|
||||
"no_double": 0.28532878,
|
||||
"double_take": 0.11171284,
|
||||
"double_pass": 1.0,
|
||||
"optimal": 0.28532878,
|
||||
"decision": "NODOUBLE_TAKE",
|
||||
"decision_text": "No double, take"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"id": "level-middlegame-1",
|
||||
"description": "Milieu de partie, 6-4 : le bruit change le coup choisi.",
|
||||
"position_id": "mM/gATCw5+ABMA",
|
||||
"match_id": "cAkTAAAAAAAA",
|
||||
"dice": [
|
||||
6,
|
||||
4
|
||||
],
|
||||
"type": "level",
|
||||
"gnubg_commit": "14b67984737ded0f6de43d63dcb789f729d9b06b",
|
||||
"met": "Kazaross-XG2",
|
||||
"levels": {
|
||||
"beginner": {
|
||||
"plies": 0,
|
||||
"noise": 0.06,
|
||||
"move": "24/20 24/18"
|
||||
},
|
||||
"casual": {
|
||||
"plies": 0,
|
||||
"noise": 0.05,
|
||||
"move": "24/20 24/18"
|
||||
},
|
||||
"intermediate": {
|
||||
"plies": 0,
|
||||
"noise": 0.04,
|
||||
"move": "24/20 24/18"
|
||||
},
|
||||
"advanced": {
|
||||
"plies": 0,
|
||||
"noise": 0.015,
|
||||
"move": "24/14"
|
||||
},
|
||||
"0ply": {
|
||||
"plies": 0,
|
||||
"noise": 0.0,
|
||||
"move": "24/14"
|
||||
},
|
||||
"1ply": {
|
||||
"plies": 1,
|
||||
"noise": 0.0,
|
||||
"move": "24/14"
|
||||
},
|
||||
"2ply": {
|
||||
"plies": 2,
|
||||
"noise": 0.0,
|
||||
"move": "24/14"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"id": "level-middlegame-2",
|
||||
"description": "Milieu de partie avec contact, 5-3.",
|
||||
"position_id": "xGfhATDQ2+BBIA",
|
||||
"match_id": "cIkOAAAAAAAA",
|
||||
"dice": [
|
||||
5,
|
||||
3
|
||||
],
|
||||
"type": "level",
|
||||
"gnubg_commit": "14b67984737ded0f6de43d63dcb789f729d9b06b",
|
||||
"met": "Kazaross-XG2",
|
||||
"levels": {
|
||||
"beginner": {
|
||||
"plies": 0,
|
||||
"noise": 0.06,
|
||||
"move": "13/5"
|
||||
},
|
||||
"casual": {
|
||||
"plies": 0,
|
||||
"noise": 0.05,
|
||||
"move": "13/5"
|
||||
},
|
||||
"intermediate": {
|
||||
"plies": 0,
|
||||
"noise": 0.04,
|
||||
"move": "13/5"
|
||||
},
|
||||
"advanced": {
|
||||
"plies": 0,
|
||||
"noise": 0.015,
|
||||
"move": "13/5"
|
||||
},
|
||||
"0ply": {
|
||||
"plies": 0,
|
||||
"noise": 0.0,
|
||||
"move": "13/5"
|
||||
},
|
||||
"1ply": {
|
||||
"plies": 1,
|
||||
"noise": 0.0,
|
||||
"move": "13/5"
|
||||
},
|
||||
"2ply": {
|
||||
"plies": 2,
|
||||
"noise": 0.0,
|
||||
"move": "13/5"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"id": "level-opening-31",
|
||||
"description": "Ouverture 3-1.",
|
||||
"position_id": "4HPwATDgc/ABMA",
|
||||
"match_id": "cIkFAAAAAAAA",
|
||||
"dice": [
|
||||
3,
|
||||
1
|
||||
],
|
||||
"type": "level",
|
||||
"gnubg_commit": "14b67984737ded0f6de43d63dcb789f729d9b06b",
|
||||
"met": "Kazaross-XG2",
|
||||
"levels": {
|
||||
"beginner": {
|
||||
"plies": 0,
|
||||
"noise": 0.06,
|
||||
"move": "8/5 6/5"
|
||||
},
|
||||
"casual": {
|
||||
"plies": 0,
|
||||
"noise": 0.05,
|
||||
"move": "8/5 6/5"
|
||||
},
|
||||
"intermediate": {
|
||||
"plies": 0,
|
||||
"noise": 0.04,
|
||||
"move": "8/5 6/5"
|
||||
},
|
||||
"advanced": {
|
||||
"plies": 0,
|
||||
"noise": 0.015,
|
||||
"move": "8/5 6/5"
|
||||
},
|
||||
"0ply": {
|
||||
"plies": 0,
|
||||
"noise": 0.0,
|
||||
"move": "8/5 6/5"
|
||||
},
|
||||
"1ply": {
|
||||
"plies": 1,
|
||||
"noise": 0.0,
|
||||
"move": "8/5 6/5"
|
||||
},
|
||||
"2ply": {
|
||||
"plies": 2,
|
||||
"noise": 0.0,
|
||||
"move": "8/5 6/5"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"id": "level-prime-vs-prime",
|
||||
"description": "Prime contre prime, 4-2.",
|
||||
"position_id": "4LZtAAbAbZsBAw",
|
||||
"match_id": "cAkKAAAAAAAA",
|
||||
"dice": [
|
||||
4,
|
||||
2
|
||||
],
|
||||
"type": "level",
|
||||
"gnubg_commit": "14b67984737ded0f6de43d63dcb789f729d9b06b",
|
||||
"met": "Kazaross-XG2",
|
||||
"levels": {
|
||||
"beginner": {
|
||||
"plies": 0,
|
||||
"noise": 0.06,
|
||||
"move": "13/11 7/3"
|
||||
},
|
||||
"casual": {
|
||||
"plies": 0,
|
||||
"noise": 0.05,
|
||||
"move": "13/11 7/3"
|
||||
},
|
||||
"intermediate": {
|
||||
"plies": 0,
|
||||
"noise": 0.04,
|
||||
"move": "13/11 7/3"
|
||||
},
|
||||
"advanced": {
|
||||
"plies": 0,
|
||||
"noise": 0.015,
|
||||
"move": "13/11 7/3"
|
||||
},
|
||||
"0ply": {
|
||||
"plies": 0,
|
||||
"noise": 0.0,
|
||||
"move": "13/9 7/5"
|
||||
},
|
||||
"1ply": {
|
||||
"plies": 1,
|
||||
"noise": 0.0,
|
||||
"move": "13/9 7/5"
|
||||
},
|
||||
"2ply": {
|
||||
"plies": 2,
|
||||
"noise": 0.0,
|
||||
"move": "13/11 13/9"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"id": "move-bearoff-doubles",
|
||||
"description": "Sortie, double 6 : coup unique ; chance d'un double.",
|
||||
"position_id": "xgAA4MgAAAAAAA",
|
||||
"match_id": "cAkbAAAAAAAA",
|
||||
"dice": [
|
||||
6,
|
||||
6
|
||||
],
|
||||
"played": "6/off 6/off 4/off 1/off",
|
||||
"type": "move",
|
||||
"gnubg_commit": "14b67984737ded0f6de43d63dcb789f729d9b06b",
|
||||
"met": "Kazaross-XG2",
|
||||
"plies": {
|
||||
"0": {
|
||||
"played_move": "6/off(2) 4/off 1/off",
|
||||
"played_position_id": "xgAAYAAAAAAAAA",
|
||||
"move_count": 1,
|
||||
"rank": 1,
|
||||
"played_equity": 0.8888838,
|
||||
"best_move": "6/off(2) 4/off 1/off",
|
||||
"best_equity": 0.8888838,
|
||||
"equity_loss": -0.0,
|
||||
"skill": "none",
|
||||
"ranking": [
|
||||
{
|
||||
"move": "6/off(2) 4/off 1/off",
|
||||
"equity": 0.8888838
|
||||
}
|
||||
]
|
||||
},
|
||||
"2": {
|
||||
"played_move": "6/off(2) 4/off 1/off",
|
||||
"played_position_id": "xgAAYAAAAAAAAA",
|
||||
"move_count": 1,
|
||||
"rank": 1,
|
||||
"played_equity": 0.8888838,
|
||||
"best_move": "6/off(2) 4/off 1/off",
|
||||
"best_equity": 0.8888838,
|
||||
"equity_loss": -0.0,
|
||||
"skill": "none",
|
||||
"ranking": [
|
||||
{
|
||||
"move": "6/off(2) 4/off 1/off",
|
||||
"equity": 0.8888838
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"luck": {
|
||||
"value": 1.6398247,
|
||||
"rating": "very_lucky"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,522 @@
|
||||
{
|
||||
"id": "move-blitz-doubles",
|
||||
"description": "Attaque, double 3 : un coup parmi 110.",
|
||||
"position_id": "4HPwAVC0XYMHAA",
|
||||
"match_id": "cIkNAAAAAAAA",
|
||||
"dice": [
|
||||
3,
|
||||
3
|
||||
],
|
||||
"played": "13/10 13/10 5/2 4/1",
|
||||
"type": "move",
|
||||
"gnubg_commit": "14b67984737ded0f6de43d63dcb789f729d9b06b",
|
||||
"met": "Kazaross-XG2",
|
||||
"plies": {
|
||||
"0": {
|
||||
"played_move": "13/10(2) 5/2 4/1*",
|
||||
"played_position_id": "4HPwAWBVXTMGAA",
|
||||
"move_count": 110,
|
||||
"rank": 109,
|
||||
"played_equity": 0.084023334,
|
||||
"best_move": "13/10 6/3 4/1*(2)",
|
||||
"best_equity": 0.9465638,
|
||||
"equity_loss": -0.8625404,
|
||||
"skill": "very_bad",
|
||||
"ranking": [
|
||||
{
|
||||
"move": "13/10 6/3 4/1*(2)",
|
||||
"equity": 0.9465638
|
||||
},
|
||||
{
|
||||
"move": "8/5 6/3 4/1*(2)",
|
||||
"equity": 0.9422797
|
||||
},
|
||||
{
|
||||
"move": "13/7 13/10 6/3",
|
||||
"equity": 0.94128984
|
||||
},
|
||||
{
|
||||
"move": "13/7 8/5 6/3",
|
||||
"equity": 0.9370698
|
||||
},
|
||||
{
|
||||
"move": "13/10(2) 7/4 6/3",
|
||||
"equity": 0.93615985
|
||||
},
|
||||
{
|
||||
"move": "13/10(2) 8/5 6/3",
|
||||
"equity": 0.9330082
|
||||
},
|
||||
{
|
||||
"move": "13/10 8/5 7/4 6/3",
|
||||
"equity": 0.932737
|
||||
},
|
||||
{
|
||||
"move": "13/10 7/1* 6/3",
|
||||
"equity": 0.9313148
|
||||
},
|
||||
{
|
||||
"move": "8/5 7/1* 6/3",
|
||||
"equity": 0.93129885
|
||||
},
|
||||
{
|
||||
"move": "13/4 6/3",
|
||||
"equity": 0.92980635
|
||||
},
|
||||
{
|
||||
"move": "13/10 8/5(2) 6/3",
|
||||
"equity": 0.9264537
|
||||
},
|
||||
{
|
||||
"move": "13/10(3) 6/3",
|
||||
"equity": 0.9262361
|
||||
},
|
||||
{
|
||||
"move": "13/10(2) 4/1*(2)",
|
||||
"equity": 0.92529553
|
||||
},
|
||||
{
|
||||
"move": "8/5(2) 7/4 6/3",
|
||||
"equity": 0.9252107
|
||||
},
|
||||
{
|
||||
"move": "7/1* 6/3 4/1",
|
||||
"equity": 0.92395884
|
||||
},
|
||||
{
|
||||
"move": "13/10 6/3 5/2(2)",
|
||||
"equity": 0.9204093
|
||||
},
|
||||
{
|
||||
"move": "13/7 4/1*(2)",
|
||||
"equity": 0.9190106
|
||||
},
|
||||
{
|
||||
"move": "13/7(2)",
|
||||
"equity": 0.9184398
|
||||
},
|
||||
{
|
||||
"move": "13/10 8/5 4/1*(2)",
|
||||
"equity": 0.9182539
|
||||
},
|
||||
{
|
||||
"move": "7/4 6/3 5/2(2)",
|
||||
"equity": 0.91752934
|
||||
},
|
||||
{
|
||||
"move": "13/7 13/10(2)",
|
||||
"equity": 0.91483337
|
||||
},
|
||||
{
|
||||
"move": "13/10 8/2 6/3",
|
||||
"equity": 0.912639
|
||||
},
|
||||
{
|
||||
"move": "13/7 13/10 8/5",
|
||||
"equity": 0.91152656
|
||||
},
|
||||
{
|
||||
"move": "13/10(2) 7/1*",
|
||||
"equity": 0.9115156
|
||||
},
|
||||
{
|
||||
"move": "13/1*",
|
||||
"equity": 0.9111869
|
||||
},
|
||||
{
|
||||
"move": "13/10(2) 6/3 4/1*",
|
||||
"equity": 0.91055506
|
||||
},
|
||||
{
|
||||
"move": "8/2 8/5 6/3",
|
||||
"equity": 0.9091699
|
||||
},
|
||||
{
|
||||
"move": "8/2 7/4 6/3",
|
||||
"equity": 0.90897554
|
||||
},
|
||||
{
|
||||
"move": "13/7 8/5(2)",
|
||||
"equity": 0.90817225
|
||||
},
|
||||
{
|
||||
"move": "13/7 6/3 4/1*",
|
||||
"equity": 0.90714073
|
||||
},
|
||||
{
|
||||
"move": "13/10(2) 8/5 7/4",
|
||||
"equity": 0.9070756
|
||||
},
|
||||
{
|
||||
"move": "8/2(2)",
|
||||
"equity": 0.9065211
|
||||
},
|
||||
{
|
||||
"move": "13/10 8/5 7/1*",
|
||||
"equity": 0.90571
|
||||
},
|
||||
{
|
||||
"move": "13/10(3) 7/4",
|
||||
"equity": 0.9040942
|
||||
},
|
||||
{
|
||||
"move": "13/10 8/5 6/3 4/1*",
|
||||
"equity": 0.90343916
|
||||
},
|
||||
{
|
||||
"move": "8/5(2) 4/1*(2)",
|
||||
"equity": 0.90325713
|
||||
},
|
||||
{
|
||||
"move": "5/2(2) 4/1*(2)",
|
||||
"equity": 0.90183336
|
||||
},
|
||||
{
|
||||
"move": "13/10 7/1* 4/1",
|
||||
"equity": 0.9013542
|
||||
},
|
||||
{
|
||||
"move": "13/4 7/4",
|
||||
"equity": 0.9010268
|
||||
},
|
||||
{
|
||||
"move": "13/7 5/2(2)",
|
||||
"equity": 0.90037674
|
||||
},
|
||||
{
|
||||
"move": "8/5(2) 7/1*",
|
||||
"equity": 0.89956194
|
||||
},
|
||||
{
|
||||
"move": "13/10 8/5(2) 7/4",
|
||||
"equity": 0.8988826
|
||||
},
|
||||
{
|
||||
"move": "8/2 6/3 5/2",
|
||||
"equity": 0.8984738
|
||||
},
|
||||
{
|
||||
"move": "13/4 13/10",
|
||||
"equity": 0.89749956
|
||||
},
|
||||
{
|
||||
"move": "6/3(2) 4/1*(2)",
|
||||
"equity": 0.8951012
|
||||
},
|
||||
{
|
||||
"move": "13/10(2) 8/5(2)",
|
||||
"equity": 0.89493525
|
||||
},
|
||||
{
|
||||
"move": "13/7 8/2",
|
||||
"equity": 0.8946096
|
||||
},
|
||||
{
|
||||
"move": "13/7 6/3(2)",
|
||||
"equity": 0.89403474
|
||||
},
|
||||
{
|
||||
"move": "7/1* 5/2(2)",
|
||||
"equity": 0.8924208
|
||||
},
|
||||
{
|
||||
"move": "8/5 7/1* 4/1",
|
||||
"equity": 0.891961
|
||||
},
|
||||
{
|
||||
"move": "13/10(2) 6/3(2)",
|
||||
"equity": 0.89077055
|
||||
},
|
||||
{
|
||||
"move": "13/4 8/5",
|
||||
"equity": 0.8907301
|
||||
},
|
||||
{
|
||||
"move": "8/5(2) 6/3 4/1*",
|
||||
"equity": 0.8900244
|
||||
},
|
||||
{
|
||||
"move": "13/10(2) 5/2(2)",
|
||||
"equity": 0.8890666
|
||||
},
|
||||
{
|
||||
"move": "7/4 6/3(3)",
|
||||
"equity": 0.8890004
|
||||
},
|
||||
{
|
||||
"move": "13/10 7/4 5/2(2)",
|
||||
"equity": 0.8888954
|
||||
},
|
||||
{
|
||||
"move": "8/2 8/5 7/4",
|
||||
"equity": 0.8876569
|
||||
},
|
||||
{
|
||||
"move": "13/10(4)",
|
||||
"equity": 0.88716996
|
||||
},
|
||||
{
|
||||
"move": "7/1* 6/3(2)",
|
||||
"equity": 0.8856077
|
||||
},
|
||||
{
|
||||
"move": "8/2 4/1*(2)",
|
||||
"equity": 0.8853629
|
||||
},
|
||||
{
|
||||
"move": "13/10 6/3(3)",
|
||||
"equity": 0.8853096
|
||||
},
|
||||
{
|
||||
"move": "13/7 6/3 5/2",
|
||||
"equity": 0.8831558
|
||||
},
|
||||
{
|
||||
"move": "13/10 8/2 7/4",
|
||||
"equity": 0.8819176
|
||||
},
|
||||
{
|
||||
"move": "7/1* 4/1(2)",
|
||||
"equity": 0.88113916
|
||||
},
|
||||
{
|
||||
"move": "13/10 7/4 6/3(2)",
|
||||
"equity": 0.88064635
|
||||
},
|
||||
{
|
||||
"move": "13/10(3) 8/5",
|
||||
"equity": 0.88049895
|
||||
},
|
||||
{
|
||||
"move": "6/3 5/2(2) 4/1*",
|
||||
"equity": 0.8790827
|
||||
},
|
||||
{
|
||||
"move": "13/10(2) 8/2",
|
||||
"equity": 0.8782023
|
||||
},
|
||||
{
|
||||
"move": "6/3 5/2 4/1*(2)",
|
||||
"equity": 0.8774586
|
||||
},
|
||||
{
|
||||
"move": "8/5 6/3(3)",
|
||||
"equity": 0.8774268
|
||||
},
|
||||
{
|
||||
"move": "8/2 7/1*",
|
||||
"equity": 0.8772669
|
||||
},
|
||||
{
|
||||
"move": "13/7 13/10 4/1*",
|
||||
"equity": 0.87700987
|
||||
},
|
||||
{
|
||||
"move": "13/10 8/5 6/3(2)",
|
||||
"equity": 0.8757389
|
||||
},
|
||||
{
|
||||
"move": "13/10 8/2 8/5",
|
||||
"equity": 0.87530077
|
||||
},
|
||||
{
|
||||
"move": "13/10(2) 6/3 5/2",
|
||||
"equity": 0.87215745
|
||||
},
|
||||
{
|
||||
"move": "13/7 8/5 4/1*",
|
||||
"equity": 0.87202764
|
||||
},
|
||||
{
|
||||
"move": "8/5 7/4 6/3(2)",
|
||||
"equity": 0.86896765
|
||||
},
|
||||
{
|
||||
"move": "7/1* 6/3 5/2",
|
||||
"equity": 0.8687051
|
||||
},
|
||||
{
|
||||
"move": "13/10 7/4 6/3 5/2",
|
||||
"equity": 0.8679464
|
||||
},
|
||||
{
|
||||
"move": "8/2 6/3 4/1*",
|
||||
"equity": 0.86750233
|
||||
},
|
||||
{
|
||||
"move": "13/10(2) 8/5 4/1*",
|
||||
"equity": 0.86701244
|
||||
},
|
||||
{
|
||||
"move": "8/5(2) 6/3(2)",
|
||||
"equity": 0.86167765
|
||||
},
|
||||
{
|
||||
"move": "13/10(3) 4/1*",
|
||||
"equity": 0.8568015
|
||||
},
|
||||
{
|
||||
"move": "6/3(2) 5/2(2)",
|
||||
"equity": 0.85501754
|
||||
},
|
||||
{
|
||||
"move": "8/2 7/4 5/2",
|
||||
"equity": 0.8537997
|
||||
},
|
||||
{
|
||||
"move": "8/2 5/2(2)",
|
||||
"equity": 0.8524791
|
||||
},
|
||||
{
|
||||
"move": "13/10 8/5(2) 4/1*",
|
||||
"equity": 0.84969926
|
||||
},
|
||||
{
|
||||
"move": "13/10 8/2 5/2",
|
||||
"equity": 0.8466199
|
||||
},
|
||||
{
|
||||
"move": "8/2 6/3(2)",
|
||||
"equity": 0.80814815
|
||||
},
|
||||
{
|
||||
"move": "13/10 5/2(2) 4/1*",
|
||||
"equity": 0.8037353
|
||||
},
|
||||
{
|
||||
"move": "13/10 5/2 4/1*(2)",
|
||||
"equity": 0.77350885
|
||||
},
|
||||
{
|
||||
"move": "13/10 6/3(2) 4/1*",
|
||||
"equity": 0.74067366
|
||||
},
|
||||
{
|
||||
"move": "13/7 13/10 5/2",
|
||||
"equity": 0.72703636
|
||||
},
|
||||
{
|
||||
"move": "13/10(2) 7/4 5/2",
|
||||
"equity": 0.7053956
|
||||
},
|
||||
{
|
||||
"move": "6/3(3) 4/1*",
|
||||
"equity": 0.66575944
|
||||
},
|
||||
{
|
||||
"move": "13/10 7/1* 5/2",
|
||||
"equity": 0.6462204
|
||||
},
|
||||
{
|
||||
"move": "8/5 6/3(2) 4/1*",
|
||||
"equity": 0.6374039
|
||||
},
|
||||
{
|
||||
"move": "8/2 8/5 4/1*",
|
||||
"equity": 0.62353325
|
||||
},
|
||||
{
|
||||
"move": "13/10 8/2 4/1*",
|
||||
"equity": 0.61257124
|
||||
},
|
||||
{
|
||||
"move": "13/10 6/3 5/2 4/1*",
|
||||
"equity": 0.5349428
|
||||
},
|
||||
{
|
||||
"move": "7/1* 5/2 4/1",
|
||||
"equity": 0.45721298
|
||||
},
|
||||
{
|
||||
"move": "8/2 5/2 4/1*",
|
||||
"equity": 0.44362232
|
||||
},
|
||||
{
|
||||
"move": "13/4 5/2",
|
||||
"equity": 0.43828273
|
||||
},
|
||||
{
|
||||
"move": "13/10(3) 5/2",
|
||||
"equity": 0.2798175
|
||||
},
|
||||
{
|
||||
"move": "13/10 6/3(2) 5/2",
|
||||
"equity": 0.24207813
|
||||
},
|
||||
{
|
||||
"move": "6/3(3) 5/2",
|
||||
"equity": 0.23385207
|
||||
},
|
||||
{
|
||||
"move": "7/4 6/3(2) 5/2",
|
||||
"equity": 0.22096781
|
||||
},
|
||||
{
|
||||
"move": "13/7 5/2 4/1*",
|
||||
"equity": 0.18849733
|
||||
},
|
||||
{
|
||||
"move": "13/10(2) 5/2 4/1*",
|
||||
"equity": 0.084023334
|
||||
}
|
||||
]
|
||||
},
|
||||
"2": {
|
||||
"played_move": "13/10(2) 5/2 4/1*",
|
||||
"played_position_id": "4HPwAWBVXTMGAA",
|
||||
"move_count": 110,
|
||||
"rank": 7,
|
||||
"played_equity": -0.022027964,
|
||||
"best_move": "13/10 7/1* 6/3",
|
||||
"best_equity": 1.0,
|
||||
"equity_loss": -1.022028,
|
||||
"skill": "very_bad",
|
||||
"ranking": [
|
||||
{
|
||||
"move": "13/10 7/1* 6/3",
|
||||
"equity": 1.0
|
||||
},
|
||||
{
|
||||
"move": "13/7 13/10 6/3",
|
||||
"equity": 1.0
|
||||
},
|
||||
{
|
||||
"move": "13/10(2) 8/5 6/3",
|
||||
"equity": 1.0
|
||||
},
|
||||
{
|
||||
"move": "13/7 8/5 6/3",
|
||||
"equity": 1.0
|
||||
},
|
||||
{
|
||||
"move": "13/10(2) 7/4 6/3",
|
||||
"equity": 1.0
|
||||
},
|
||||
{
|
||||
"move": "13/10 8/5 7/4 6/3",
|
||||
"equity": 1.0
|
||||
},
|
||||
{
|
||||
"move": "13/10(2) 5/2 4/1*",
|
||||
"equity": -0.022027964
|
||||
},
|
||||
{
|
||||
"move": "13/10 8/5(2) 6/3",
|
||||
"equity": 1.0
|
||||
},
|
||||
{
|
||||
"move": "13/4 6/3",
|
||||
"equity": 1.0
|
||||
},
|
||||
{
|
||||
"move": "13/10 6/3 4/1*(2)",
|
||||
"equity": 0.99692726
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"luck": {
|
||||
"value": 0.011263669,
|
||||
"rating": "none"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
{
|
||||
"id": "move-crawford",
|
||||
"description": "Match en 5 points, partie Crawford, 4-3.",
|
||||
"position_id": "4Dn4AGiynYMHAA",
|
||||
"match_id": "8AmuAEAAEAAE",
|
||||
"dice": [
|
||||
4,
|
||||
3
|
||||
],
|
||||
"played": "5/2 5/1",
|
||||
"type": "move",
|
||||
"gnubg_commit": "14b67984737ded0f6de43d63dcb789f729d9b06b",
|
||||
"met": "Kazaross-XG2",
|
||||
"plies": {
|
||||
"0": {
|
||||
"played_move": "5/2 5/1*",
|
||||
"played_position_id": "4Dn4AHDNnIMHAA",
|
||||
"move_count": 20,
|
||||
"rank": 2,
|
||||
"played_equity": 0.7229391,
|
||||
"best_move": "8/5 6/2",
|
||||
"best_equity": 0.7345603,
|
||||
"equity_loss": -0.011621237,
|
||||
"skill": "none",
|
||||
"ranking": [
|
||||
{
|
||||
"move": "8/5 6/2",
|
||||
"equity": 0.7345603
|
||||
},
|
||||
{
|
||||
"move": "5/2 5/1*",
|
||||
"equity": 0.7229391
|
||||
},
|
||||
{
|
||||
"move": "8/1*",
|
||||
"equity": 0.71705425
|
||||
},
|
||||
{
|
||||
"move": "13/10 6/2",
|
||||
"equity": 0.7142835
|
||||
},
|
||||
{
|
||||
"move": "13/9 8/5",
|
||||
"equity": 0.6963019
|
||||
},
|
||||
{
|
||||
"move": "8/5 8/4",
|
||||
"equity": 0.68610644
|
||||
},
|
||||
{
|
||||
"move": "13/10 8/4",
|
||||
"equity": 0.6855368
|
||||
},
|
||||
{
|
||||
"move": "13/10 13/9",
|
||||
"equity": 0.6818689
|
||||
},
|
||||
{
|
||||
"move": "13/6",
|
||||
"equity": 0.6633271
|
||||
},
|
||||
{
|
||||
"move": "13/9 5/2",
|
||||
"equity": 0.6538005
|
||||
}
|
||||
]
|
||||
},
|
||||
"2": {
|
||||
"played_move": "5/2 5/1*",
|
||||
"played_position_id": "4Dn4AHDNnIMHAA",
|
||||
"move_count": 20,
|
||||
"rank": 4,
|
||||
"played_equity": 0.6911188,
|
||||
"best_move": "8/5 6/2",
|
||||
"best_equity": 0.7697691,
|
||||
"equity_loss": -0.078650296,
|
||||
"skill": "bad",
|
||||
"ranking": [
|
||||
{
|
||||
"move": "8/5 6/2",
|
||||
"equity": 0.7697691
|
||||
},
|
||||
{
|
||||
"move": "13/10 6/2",
|
||||
"equity": 0.7615671
|
||||
},
|
||||
{
|
||||
"move": "13/9 8/5",
|
||||
"equity": 0.69608325
|
||||
},
|
||||
{
|
||||
"move": "5/2 5/1*",
|
||||
"equity": 0.6911188
|
||||
},
|
||||
{
|
||||
"move": "8/1*",
|
||||
"equity": 0.6836242
|
||||
},
|
||||
{
|
||||
"move": "13/10 13/9",
|
||||
"equity": 0.6807338
|
||||
},
|
||||
{
|
||||
"move": "13/10 8/4",
|
||||
"equity": 0.6733323
|
||||
},
|
||||
{
|
||||
"move": "13/6",
|
||||
"equity": 0.66055346
|
||||
},
|
||||
{
|
||||
"move": "8/5 8/4",
|
||||
"equity": 0.6556259
|
||||
},
|
||||
{
|
||||
"move": "13/9 5/2",
|
||||
"equity": 0.645951
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"luck": {
|
||||
"value": -0.050364614,
|
||||
"rating": "none"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"id": "move-forced",
|
||||
"description": "Rentrée forcée depuis la barre, 3-1.",
|
||||
"position_id": "tm3ABwBw5+ABYA",
|
||||
"match_id": "cIkFAAAAAAAA",
|
||||
"dice": [
|
||||
3,
|
||||
1
|
||||
],
|
||||
"played": "bar/24",
|
||||
"type": "move",
|
||||
"gnubg_commit": "14b67984737ded0f6de43d63dcb789f729d9b06b",
|
||||
"met": "Kazaross-XG2",
|
||||
"plies": {
|
||||
"0": {
|
||||
"played_move": "bar/24",
|
||||
"played_position_id": "tm3ABwBw5+ABUA",
|
||||
"move_count": 1,
|
||||
"rank": 1,
|
||||
"played_equity": -1.0,
|
||||
"best_move": "bar/24",
|
||||
"best_equity": -1.0,
|
||||
"equity_loss": -0.0,
|
||||
"skill": "none",
|
||||
"ranking": [
|
||||
{
|
||||
"move": "bar/24",
|
||||
"equity": -1.0
|
||||
}
|
||||
]
|
||||
},
|
||||
"2": {
|
||||
"played_move": "bar/24",
|
||||
"played_position_id": "tm3ABwBw5+ABUA",
|
||||
"move_count": 1,
|
||||
"rank": 1,
|
||||
"played_equity": -1.0,
|
||||
"best_move": "bar/24",
|
||||
"best_equity": -1.0,
|
||||
"equity_loss": -0.0,
|
||||
"skill": "none",
|
||||
"ranking": [
|
||||
{
|
||||
"move": "bar/24",
|
||||
"equity": -1.0
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"luck": {
|
||||
"value": 0.0,
|
||||
"rating": "none"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
{
|
||||
"id": "move-jacoby",
|
||||
"description": "Partie libre avec la règle Jacoby, 6-4.",
|
||||
"position_id": "4HPwAVC0XYMHAA",
|
||||
"match_id": "cAkTAAAAAAAA",
|
||||
"dice": [
|
||||
6,
|
||||
4
|
||||
],
|
||||
"played": "8/2 5/1",
|
||||
"type": "move",
|
||||
"gnubg_commit": "14b67984737ded0f6de43d63dcb789f729d9b06b",
|
||||
"met": "Kazaross-XG2",
|
||||
"plies": {
|
||||
"0": {
|
||||
"played_move": "8/2 5/1*",
|
||||
"played_position_id": "4HPwAWDVuoIHAA",
|
||||
"move_count": 14,
|
||||
"rank": 14,
|
||||
"played_equity": 0.50889754,
|
||||
"best_move": "13/3",
|
||||
"best_equity": 0.9328213,
|
||||
"equity_loss": -0.42392373,
|
||||
"skill": "very_bad",
|
||||
"ranking": [
|
||||
{
|
||||
"move": "13/3",
|
||||
"equity": 0.9328213
|
||||
},
|
||||
{
|
||||
"move": "13/9 13/7",
|
||||
"equity": 0.9175232
|
||||
},
|
||||
{
|
||||
"move": "8/2 7/3",
|
||||
"equity": 0.91263336
|
||||
},
|
||||
{
|
||||
"move": "13/7 8/4",
|
||||
"equity": 0.90736973
|
||||
},
|
||||
{
|
||||
"move": "13/9 7/1*",
|
||||
"equity": 0.9053596
|
||||
},
|
||||
{
|
||||
"move": "8/2 6/2",
|
||||
"equity": 0.9017469
|
||||
},
|
||||
{
|
||||
"move": "8/4 7/1*",
|
||||
"equity": 0.89993495
|
||||
},
|
||||
{
|
||||
"move": "13/7 6/2",
|
||||
"equity": 0.8962312
|
||||
},
|
||||
{
|
||||
"move": "7/1* 5/1",
|
||||
"equity": 0.8907822
|
||||
},
|
||||
{
|
||||
"move": "7/1* 6/2",
|
||||
"equity": 0.87481093
|
||||
},
|
||||
{
|
||||
"move": "13/9 8/2",
|
||||
"equity": 0.87251335
|
||||
},
|
||||
{
|
||||
"move": "8/4 8/2",
|
||||
"equity": 0.8716274
|
||||
},
|
||||
{
|
||||
"move": "13/7 5/1*",
|
||||
"equity": 0.87005275
|
||||
},
|
||||
{
|
||||
"move": "8/2 5/1*",
|
||||
"equity": 0.50889754
|
||||
}
|
||||
]
|
||||
},
|
||||
"2": {
|
||||
"played_move": "8/2 5/1*",
|
||||
"played_position_id": "4HPwAWDVuoIHAA",
|
||||
"move_count": 14,
|
||||
"rank": 7,
|
||||
"played_equity": 0.3304975,
|
||||
"best_move": "13/3",
|
||||
"best_equity": 1.0,
|
||||
"equity_loss": -0.6695025,
|
||||
"skill": "very_bad",
|
||||
"ranking": [
|
||||
{
|
||||
"move": "13/3",
|
||||
"equity": 1.0
|
||||
},
|
||||
{
|
||||
"move": "13/9 13/7",
|
||||
"equity": 0.99894416
|
||||
},
|
||||
{
|
||||
"move": "8/2 7/3",
|
||||
"equity": 0.99641544
|
||||
},
|
||||
{
|
||||
"move": "13/7 8/4",
|
||||
"equity": 0.98718643
|
||||
},
|
||||
{
|
||||
"move": "13/7 6/2",
|
||||
"equity": 0.9808178
|
||||
},
|
||||
{
|
||||
"move": "13/9 7/1*",
|
||||
"equity": 0.9452111
|
||||
},
|
||||
{
|
||||
"move": "8/2 5/1*",
|
||||
"equity": 0.3304975
|
||||
},
|
||||
{
|
||||
"move": "8/4 7/1*",
|
||||
"equity": 0.93114305
|
||||
},
|
||||
{
|
||||
"move": "7/1* 5/1",
|
||||
"equity": 0.91711015
|
||||
},
|
||||
{
|
||||
"move": "8/2 6/2",
|
||||
"equity": 0.9160911
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"luck": {
|
||||
"value": -0.002478838,
|
||||
"rating": "none"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
{
|
||||
"id": "move-luck-lucky",
|
||||
"description": "Course, double 4 : jet chanceux, joué au mieux.",
|
||||
"position_id": "cLezAQBwN2cGAA",
|
||||
"match_id": "cIkSAAAAAAAA",
|
||||
"dice": [
|
||||
4,
|
||||
4
|
||||
],
|
||||
"played": null,
|
||||
"type": "move",
|
||||
"gnubg_commit": "14b67984737ded0f6de43d63dcb789f729d9b06b",
|
||||
"met": "Kazaross-XG2",
|
||||
"plies": {
|
||||
"0": {
|
||||
"played_move": "13/5 11/3",
|
||||
"played_position_id": "cLezAQDk3ZwEAA",
|
||||
"move_count": 106,
|
||||
"rank": 1,
|
||||
"played_equity": 0.16933289,
|
||||
"best_move": "13/5 11/3",
|
||||
"best_equity": 0.16933289,
|
||||
"equity_loss": -0.0,
|
||||
"skill": "none",
|
||||
"ranking": [
|
||||
{
|
||||
"move": "13/5 11/3",
|
||||
"equity": 0.16933289
|
||||
},
|
||||
{
|
||||
"move": "13/9(2) 11/3",
|
||||
"equity": 0.16701066
|
||||
},
|
||||
{
|
||||
"move": "13/5 9/5 7/3",
|
||||
"equity": 0.16552198
|
||||
},
|
||||
{
|
||||
"move": "13/5(2)",
|
||||
"equity": 0.16473158
|
||||
},
|
||||
{
|
||||
"move": "13/5 13/9 11/7",
|
||||
"equity": 0.16365689
|
||||
},
|
||||
{
|
||||
"move": "13/5 13/9 7/3",
|
||||
"equity": 0.16245355
|
||||
},
|
||||
{
|
||||
"move": "13/5 11/7 9/5",
|
||||
"equity": 0.16167049
|
||||
},
|
||||
{
|
||||
"move": "11/3 9/5(2)",
|
||||
"equity": 0.16043647
|
||||
},
|
||||
{
|
||||
"move": "13/9 11/3 11/7",
|
||||
"equity": 0.1603657
|
||||
},
|
||||
{
|
||||
"move": "13/5 11/7(2)",
|
||||
"equity": 0.1602792
|
||||
}
|
||||
]
|
||||
},
|
||||
"2": {
|
||||
"played_move": "13/5 11/3",
|
||||
"played_position_id": "cLezAQDk3ZwEAA",
|
||||
"move_count": 106,
|
||||
"rank": 1,
|
||||
"played_equity": 0.15129912,
|
||||
"best_move": "13/5 11/3",
|
||||
"best_equity": 0.15129912,
|
||||
"equity_loss": -0.0,
|
||||
"skill": "none",
|
||||
"ranking": [
|
||||
{
|
||||
"move": "13/5 11/3",
|
||||
"equity": 0.15129912
|
||||
},
|
||||
{
|
||||
"move": "13/5 13/9 11/7",
|
||||
"equity": 0.15059008
|
||||
},
|
||||
{
|
||||
"move": "13/9(2) 11/3",
|
||||
"equity": 0.14950007
|
||||
},
|
||||
{
|
||||
"move": "13/5 11/7 9/5",
|
||||
"equity": 0.14858595
|
||||
},
|
||||
{
|
||||
"move": "11/3 11/7 9/5",
|
||||
"equity": 0.14829463
|
||||
},
|
||||
{
|
||||
"move": "13/9 11/3 11/7",
|
||||
"equity": 0.1463402
|
||||
},
|
||||
{
|
||||
"move": "13/5 9/5 7/3",
|
||||
"equity": 0.13938732
|
||||
},
|
||||
{
|
||||
"move": "13/5 11/7(2)",
|
||||
"equity": 0.13921705
|
||||
},
|
||||
{
|
||||
"move": "13/9(2) 11/7(2)",
|
||||
"equity": 0.13873637
|
||||
},
|
||||
{
|
||||
"move": "13/5 13/9 6/2",
|
||||
"equity": 0.13852078
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"luck": {
|
||||
"value": 0.43559298,
|
||||
"rating": "lucky"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"id": "move-luck-unlucky-bearoff",
|
||||
"description": "Fin de sortie, 2-1 : jet malchanceux, joué au mieux.",
|
||||
"position_id": "TQAAYAsAAAAAAA",
|
||||
"match_id": "cAkFAAAAAAAA",
|
||||
"dice": [
|
||||
2,
|
||||
1
|
||||
],
|
||||
"played": null,
|
||||
"type": "move",
|
||||
"gnubg_commit": "14b67984737ded0f6de43d63dcb789f729d9b06b",
|
||||
"met": "Kazaross-XG2",
|
||||
"plies": {
|
||||
"0": {
|
||||
"played_move": "2/off 1/off",
|
||||
"played_position_id": "TQAAoAIAAAAAAA",
|
||||
"move_count": 4,
|
||||
"rank": 2,
|
||||
"played_equity": -0.8607614,
|
||||
"best_move": "3/off",
|
||||
"best_equity": -0.8607614,
|
||||
"equity_loss": -0.0,
|
||||
"skill": "none",
|
||||
"ranking": [
|
||||
{
|
||||
"move": "3/off",
|
||||
"equity": -0.8607614
|
||||
},
|
||||
{
|
||||
"move": "2/off 1/off",
|
||||
"equity": -0.8607614
|
||||
},
|
||||
{
|
||||
"move": "2/1 2/off",
|
||||
"equity": -0.95561147
|
||||
},
|
||||
{
|
||||
"move": "3/1 2/1",
|
||||
"equity": -1.0
|
||||
}
|
||||
]
|
||||
},
|
||||
"2": {
|
||||
"played_move": "2/off 1/off",
|
||||
"played_position_id": "TQAAoAIAAAAAAA",
|
||||
"move_count": 4,
|
||||
"rank": 2,
|
||||
"played_equity": -0.8607614,
|
||||
"best_move": "3/off",
|
||||
"best_equity": -0.8607614,
|
||||
"equity_loss": -0.0,
|
||||
"skill": "none",
|
||||
"ranking": [
|
||||
{
|
||||
"move": "3/off",
|
||||
"equity": -0.8607614
|
||||
},
|
||||
{
|
||||
"move": "2/off 1/off",
|
||||
"equity": -0.8607614
|
||||
},
|
||||
{
|
||||
"move": "2/1 2/off",
|
||||
"equity": -0.95561147
|
||||
},
|
||||
{
|
||||
"move": "3/1 2/1",
|
||||
"equity": -1.0
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"luck": {
|
||||
"value": -0.32329792,
|
||||
"rating": "unlucky"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
{
|
||||
"id": "move-luck-unlucky",
|
||||
"description": "Milieu de partie, 5-4 : jet malchanceux, joué au mieux.",
|
||||
"position_id": "xGfhATDQ2+BBIA",
|
||||
"match_id": "cIkOAAAAAAAA",
|
||||
"dice": [
|
||||
5,
|
||||
4
|
||||
],
|
||||
"played": null,
|
||||
"type": "move",
|
||||
"gnubg_commit": "14b67984737ded0f6de43d63dcb789f729d9b06b",
|
||||
"met": "Kazaross-XG2",
|
||||
"plies": {
|
||||
"0": {
|
||||
"played_move": "18/9",
|
||||
"played_position_id": "xGfhATDQ28IDIA",
|
||||
"move_count": 24,
|
||||
"rank": 1,
|
||||
"played_equity": 0.2544027,
|
||||
"best_move": "18/9",
|
||||
"best_equity": 0.2544027,
|
||||
"equity_loss": -0.0,
|
||||
"skill": "none",
|
||||
"ranking": [
|
||||
{
|
||||
"move": "18/9",
|
||||
"equity": 0.2544027
|
||||
},
|
||||
{
|
||||
"move": "18/14 13/8",
|
||||
"equity": 0.22496489
|
||||
},
|
||||
{
|
||||
"move": "24/20 18/13",
|
||||
"equity": 0.20747662
|
||||
},
|
||||
{
|
||||
"move": "24/20 13/8",
|
||||
"equity": 0.1158272
|
||||
},
|
||||
{
|
||||
"move": "13/9 13/8",
|
||||
"equity": 0.09201278
|
||||
},
|
||||
{
|
||||
"move": "24/15",
|
||||
"equity": 0.061017293
|
||||
},
|
||||
{
|
||||
"move": "18/13 6/2",
|
||||
"equity": 0.03581623
|
||||
},
|
||||
{
|
||||
"move": "13/4",
|
||||
"equity": -0.028887823
|
||||
},
|
||||
{
|
||||
"move": "13/8 6/2",
|
||||
"equity": -0.033572298
|
||||
},
|
||||
{
|
||||
"move": "18/13 8/4",
|
||||
"equity": -0.09662818
|
||||
}
|
||||
]
|
||||
},
|
||||
"2": {
|
||||
"played_move": "18/9",
|
||||
"played_position_id": "xGfhATDQ28IDIA",
|
||||
"move_count": 24,
|
||||
"rank": 1,
|
||||
"played_equity": 0.27687487,
|
||||
"best_move": "18/9",
|
||||
"best_equity": 0.27687487,
|
||||
"equity_loss": -0.0,
|
||||
"skill": "none",
|
||||
"ranking": [
|
||||
{
|
||||
"move": "18/9",
|
||||
"equity": 0.27687487
|
||||
},
|
||||
{
|
||||
"move": "18/14 13/8",
|
||||
"equity": 0.21801135
|
||||
},
|
||||
{
|
||||
"move": "24/20 18/13",
|
||||
"equity": 0.21000355
|
||||
},
|
||||
{
|
||||
"move": "24/20 13/8",
|
||||
"equity": 0.1144446
|
||||
},
|
||||
{
|
||||
"move": "13/9 13/8",
|
||||
"equity": 0.09412725
|
||||
},
|
||||
{
|
||||
"move": "24/15",
|
||||
"equity": 0.07892294
|
||||
},
|
||||
{
|
||||
"move": "18/13 6/2",
|
||||
"equity": -0.020656409
|
||||
},
|
||||
{
|
||||
"move": "13/4",
|
||||
"equity": -0.028887823
|
||||
},
|
||||
{
|
||||
"move": "13/8 6/2",
|
||||
"equity": -0.033572298
|
||||
},
|
||||
{
|
||||
"move": "18/13 8/4",
|
||||
"equity": -0.09662818
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"luck": {
|
||||
"value": -0.3615805,
|
||||
"rating": "unlucky"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
{
|
||||
"id": "move-match",
|
||||
"description": "Match en 7 points à 3-3, 5-3 : frappe au lieu du meilleur coup.",
|
||||
"position_id": "xGfhATDQ2+BBIA",
|
||||
"match_id": "cImuACAAEAAE",
|
||||
"dice": [
|
||||
5,
|
||||
3
|
||||
],
|
||||
"played": "24/16",
|
||||
"type": "move",
|
||||
"gnubg_commit": "14b67984737ded0f6de43d63dcb789f729d9b06b",
|
||||
"met": "Kazaross-XG2",
|
||||
"plies": {
|
||||
"0": {
|
||||
"played_move": "24/16*",
|
||||
"played_position_id": "xGfwAFjQ2+CRAA",
|
||||
"move_count": 28,
|
||||
"rank": 3,
|
||||
"played_equity": 0.4581785,
|
||||
"best_move": "13/5",
|
||||
"best_equity": 0.7553502,
|
||||
"equity_loss": -0.29717168,
|
||||
"skill": "very_bad",
|
||||
"ranking": [
|
||||
{
|
||||
"move": "13/5",
|
||||
"equity": 0.7553502
|
||||
},
|
||||
{
|
||||
"move": "18/13 8/5",
|
||||
"equity": 0.5814649
|
||||
},
|
||||
{
|
||||
"move": "24/16*",
|
||||
"equity": 0.4581785
|
||||
},
|
||||
{
|
||||
"move": "18/10",
|
||||
"equity": 0.31719947
|
||||
},
|
||||
{
|
||||
"move": "8/5 8/3",
|
||||
"equity": 0.31371918
|
||||
},
|
||||
{
|
||||
"move": "18/15 13/8",
|
||||
"equity": 0.26037523
|
||||
},
|
||||
{
|
||||
"move": "24/21 18/13",
|
||||
"equity": 0.23547156
|
||||
},
|
||||
{
|
||||
"move": "8/3 6/3",
|
||||
"equity": 0.15432969
|
||||
},
|
||||
{
|
||||
"move": "13/10 13/8",
|
||||
"equity": 0.13837479
|
||||
},
|
||||
{
|
||||
"move": "18/13 5/2",
|
||||
"equity": 0.12815686
|
||||
}
|
||||
]
|
||||
},
|
||||
"2": {
|
||||
"played_move": "24/16*",
|
||||
"played_position_id": "xGfwAFjQ2+CRAA",
|
||||
"move_count": 28,
|
||||
"rank": 2,
|
||||
"played_equity": 0.46805125,
|
||||
"best_move": "13/5",
|
||||
"best_equity": 0.85497326,
|
||||
"equity_loss": -0.386922,
|
||||
"skill": "very_bad",
|
||||
"ranking": [
|
||||
{
|
||||
"move": "13/5",
|
||||
"equity": 0.85497326
|
||||
},
|
||||
{
|
||||
"move": "24/16*",
|
||||
"equity": 0.46805125
|
||||
},
|
||||
{
|
||||
"move": "18/13 8/5",
|
||||
"equity": 0.6511238
|
||||
},
|
||||
{
|
||||
"move": "18/10",
|
||||
"equity": 0.31719947
|
||||
},
|
||||
{
|
||||
"move": "8/5 8/3",
|
||||
"equity": 0.31371918
|
||||
},
|
||||
{
|
||||
"move": "18/15 13/8",
|
||||
"equity": 0.26037523
|
||||
},
|
||||
{
|
||||
"move": "24/21 18/13",
|
||||
"equity": 0.23547156
|
||||
},
|
||||
{
|
||||
"move": "8/3 6/3",
|
||||
"equity": 0.15432969
|
||||
},
|
||||
{
|
||||
"move": "13/10 13/8",
|
||||
"equity": 0.13837479
|
||||
},
|
||||
{
|
||||
"move": "18/13 5/2",
|
||||
"equity": 0.12815686
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"luck": {
|
||||
"value": 0.026781201,
|
||||
"rating": "none"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
{
|
||||
"id": "move-middlegame-blunder",
|
||||
"description": "Milieu de partie, 6-4 : un des pires coups.",
|
||||
"position_id": "mM/gATCw5+ABMA",
|
||||
"match_id": "cAkTAAAAAAAA",
|
||||
"dice": [
|
||||
6,
|
||||
4
|
||||
],
|
||||
"played": "8/4 8/2",
|
||||
"type": "move",
|
||||
"gnubg_commit": "14b67984737ded0f6de43d63dcb789f729d9b06b",
|
||||
"met": "Kazaross-XG2",
|
||||
"plies": {
|
||||
"0": {
|
||||
"played_move": "8/4 8/2",
|
||||
"played_position_id": "mM/gATDSnuABMA",
|
||||
"move_count": 14,
|
||||
"rank": 14,
|
||||
"played_equity": -0.35321844,
|
||||
"best_move": "24/14",
|
||||
"best_equity": 0.15439394,
|
||||
"equity_loss": -0.50761235,
|
||||
"skill": "very_bad",
|
||||
"ranking": [
|
||||
{
|
||||
"move": "24/14",
|
||||
"equity": 0.15439394
|
||||
},
|
||||
{
|
||||
"move": "8/2 6/2",
|
||||
"equity": 0.10821653
|
||||
},
|
||||
{
|
||||
"move": "24/18 13/9",
|
||||
"equity": 0.10337136
|
||||
},
|
||||
{
|
||||
"move": "24/20 24/18",
|
||||
"equity": 0.072762795
|
||||
},
|
||||
{
|
||||
"move": "13/9 13/7",
|
||||
"equity": 0.0029048775
|
||||
},
|
||||
{
|
||||
"move": "24/20 13/7",
|
||||
"equity": 9.1438415e-05
|
||||
},
|
||||
{
|
||||
"move": "13/3",
|
||||
"equity": -0.012697897
|
||||
},
|
||||
{
|
||||
"move": "24/18 6/2",
|
||||
"equity": -0.075399
|
||||
},
|
||||
{
|
||||
"move": "24/18 8/4",
|
||||
"equity": -0.088734664
|
||||
},
|
||||
{
|
||||
"move": "24/20 8/2",
|
||||
"equity": -0.095189385
|
||||
},
|
||||
{
|
||||
"move": "13/9 8/2",
|
||||
"equity": -0.09528468
|
||||
},
|
||||
{
|
||||
"move": "13/7 8/4",
|
||||
"equity": -0.12819159
|
||||
},
|
||||
{
|
||||
"move": "13/7 6/2",
|
||||
"equity": -0.14467186
|
||||
},
|
||||
{
|
||||
"move": "8/4 8/2",
|
||||
"equity": -0.35321844
|
||||
}
|
||||
]
|
||||
},
|
||||
"2": {
|
||||
"played_move": "8/4 8/2",
|
||||
"played_position_id": "mM/gATDSnuABMA",
|
||||
"move_count": 14,
|
||||
"rank": 6,
|
||||
"played_equity": -0.36824828,
|
||||
"best_move": "24/14",
|
||||
"best_equity": 0.13111727,
|
||||
"equity_loss": -0.49936557,
|
||||
"skill": "very_bad",
|
||||
"ranking": [
|
||||
{
|
||||
"move": "24/14",
|
||||
"equity": 0.13111727
|
||||
},
|
||||
{
|
||||
"move": "8/2 6/2",
|
||||
"equity": 0.11303549
|
||||
},
|
||||
{
|
||||
"move": "24/18 13/9",
|
||||
"equity": 0.105096616
|
||||
},
|
||||
{
|
||||
"move": "24/20 24/18",
|
||||
"equity": 0.049068402
|
||||
},
|
||||
{
|
||||
"move": "13/3",
|
||||
"equity": -0.0040049786
|
||||
},
|
||||
{
|
||||
"move": "8/4 8/2",
|
||||
"equity": -0.36824828
|
||||
},
|
||||
{
|
||||
"move": "13/9 13/7",
|
||||
"equity": -0.014309813
|
||||
},
|
||||
{
|
||||
"move": "24/20 13/7",
|
||||
"equity": -0.029367238
|
||||
},
|
||||
{
|
||||
"move": "24/18 8/4",
|
||||
"equity": -0.081171095
|
||||
},
|
||||
{
|
||||
"move": "24/18 6/2",
|
||||
"equity": -0.09313507
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"luck": {
|
||||
"value": -0.07414439,
|
||||
"rating": "none"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
{
|
||||
"id": "move-middlegame-close",
|
||||
"description": "Milieu de partie, 6-4 : un coup proche du meilleur.",
|
||||
"position_id": "mM/gATCw5+ABMA",
|
||||
"match_id": "cAkTAAAAAAAA",
|
||||
"dice": [
|
||||
6,
|
||||
4
|
||||
],
|
||||
"played": "8/2 6/2",
|
||||
"type": "move",
|
||||
"gnubg_commit": "14b67984737ded0f6de43d63dcb789f729d9b06b",
|
||||
"met": "Kazaross-XG2",
|
||||
"plies": {
|
||||
"0": {
|
||||
"played_move": "8/2 6/2",
|
||||
"played_position_id": "mM/gATDGzuABMA",
|
||||
"move_count": 14,
|
||||
"rank": 2,
|
||||
"played_equity": 0.10821653,
|
||||
"best_move": "24/14",
|
||||
"best_equity": 0.15439394,
|
||||
"equity_loss": -0.04617741,
|
||||
"skill": "doubtful",
|
||||
"ranking": [
|
||||
{
|
||||
"move": "24/14",
|
||||
"equity": 0.15439394
|
||||
},
|
||||
{
|
||||
"move": "8/2 6/2",
|
||||
"equity": 0.10821653
|
||||
},
|
||||
{
|
||||
"move": "24/18 13/9",
|
||||
"equity": 0.10337136
|
||||
},
|
||||
{
|
||||
"move": "24/20 24/18",
|
||||
"equity": 0.072762795
|
||||
},
|
||||
{
|
||||
"move": "13/9 13/7",
|
||||
"equity": 0.0029048775
|
||||
},
|
||||
{
|
||||
"move": "24/20 13/7",
|
||||
"equity": 9.1438415e-05
|
||||
},
|
||||
{
|
||||
"move": "13/3",
|
||||
"equity": -0.012697897
|
||||
},
|
||||
{
|
||||
"move": "24/18 6/2",
|
||||
"equity": -0.075399
|
||||
},
|
||||
{
|
||||
"move": "24/18 8/4",
|
||||
"equity": -0.088734664
|
||||
},
|
||||
{
|
||||
"move": "24/20 8/2",
|
||||
"equity": -0.095189385
|
||||
}
|
||||
]
|
||||
},
|
||||
"2": {
|
||||
"played_move": "8/2 6/2",
|
||||
"played_position_id": "mM/gATDGzuABMA",
|
||||
"move_count": 14,
|
||||
"rank": 2,
|
||||
"played_equity": 0.11303549,
|
||||
"best_move": "24/14",
|
||||
"best_equity": 0.13111727,
|
||||
"equity_loss": -0.018081777,
|
||||
"skill": "none",
|
||||
"ranking": [
|
||||
{
|
||||
"move": "24/14",
|
||||
"equity": 0.13111727
|
||||
},
|
||||
{
|
||||
"move": "8/2 6/2",
|
||||
"equity": 0.11303549
|
||||
},
|
||||
{
|
||||
"move": "24/18 13/9",
|
||||
"equity": 0.105096616
|
||||
},
|
||||
{
|
||||
"move": "24/20 24/18",
|
||||
"equity": 0.049068402
|
||||
},
|
||||
{
|
||||
"move": "13/3",
|
||||
"equity": -0.0040049786
|
||||
},
|
||||
{
|
||||
"move": "13/9 13/7",
|
||||
"equity": -0.014309813
|
||||
},
|
||||
{
|
||||
"move": "24/20 13/7",
|
||||
"equity": -0.029367238
|
||||
},
|
||||
{
|
||||
"move": "24/18 8/4",
|
||||
"equity": -0.081171095
|
||||
},
|
||||
{
|
||||
"move": "24/18 6/2",
|
||||
"equity": -0.09313507
|
||||
},
|
||||
{
|
||||
"move": "24/20 8/2",
|
||||
"equity": -0.095189385
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"luck": {
|
||||
"value": -0.07414439,
|
||||
"rating": "none"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
{
|
||||
"id": "move-opening-31-best",
|
||||
"description": "Ouverture 3-1 jouée au mieux (8/5 6/5) ; chance du premier jet.",
|
||||
"position_id": "4HPwATDgc/ABMA",
|
||||
"match_id": "cIkFAAAAAAAA",
|
||||
"dice": [
|
||||
3,
|
||||
1
|
||||
],
|
||||
"played": "8/5 6/5",
|
||||
"type": "move",
|
||||
"gnubg_commit": "14b67984737ded0f6de43d63dcb789f729d9b06b",
|
||||
"met": "Kazaross-XG2",
|
||||
"plies": {
|
||||
"0": {
|
||||
"played_move": "8/5 6/5",
|
||||
"played_position_id": "4HPwATCwZ/ABMA",
|
||||
"move_count": 16,
|
||||
"rank": 1,
|
||||
"played_equity": 0.21258597,
|
||||
"best_move": "8/5 6/5",
|
||||
"best_equity": 0.21258597,
|
||||
"equity_loss": -0.0,
|
||||
"skill": "none",
|
||||
"ranking": [
|
||||
{
|
||||
"move": "8/5 6/5",
|
||||
"equity": 0.21258597
|
||||
},
|
||||
{
|
||||
"move": "24/23 13/10",
|
||||
"equity": -0.010789523
|
||||
},
|
||||
{
|
||||
"move": "24/20",
|
||||
"equity": -0.017392516
|
||||
},
|
||||
{
|
||||
"move": "24/23 24/21",
|
||||
"equity": -0.023609947
|
||||
},
|
||||
{
|
||||
"move": "13/9",
|
||||
"equity": -0.027675275
|
||||
},
|
||||
{
|
||||
"move": "24/21 6/5",
|
||||
"equity": -0.04369401
|
||||
},
|
||||
{
|
||||
"move": "13/10 6/5",
|
||||
"equity": -0.04984435
|
||||
},
|
||||
{
|
||||
"move": "24/23 8/5",
|
||||
"equity": -0.104100645
|
||||
},
|
||||
{
|
||||
"move": "24/23 6/3",
|
||||
"equity": -0.10542354
|
||||
},
|
||||
{
|
||||
"move": "24/21 8/7",
|
||||
"equity": -0.107585564
|
||||
}
|
||||
]
|
||||
},
|
||||
"2": {
|
||||
"played_move": "8/5 6/5",
|
||||
"played_position_id": "4HPwATCwZ/ABMA",
|
||||
"move_count": 16,
|
||||
"rank": 1,
|
||||
"played_equity": 0.20373425,
|
||||
"best_move": "8/5 6/5",
|
||||
"best_equity": 0.20373425,
|
||||
"equity_loss": -0.0,
|
||||
"skill": "none",
|
||||
"ranking": [
|
||||
{
|
||||
"move": "8/5 6/5",
|
||||
"equity": 0.20373425
|
||||
},
|
||||
{
|
||||
"move": "24/23 13/10",
|
||||
"equity": -0.022293653
|
||||
},
|
||||
{
|
||||
"move": "24/20",
|
||||
"equity": -0.031027086
|
||||
},
|
||||
{
|
||||
"move": "24/23 24/21",
|
||||
"equity": -0.03213631
|
||||
},
|
||||
{
|
||||
"move": "13/9",
|
||||
"equity": -0.040205758
|
||||
},
|
||||
{
|
||||
"move": "24/21 6/5",
|
||||
"equity": -0.04369401
|
||||
},
|
||||
{
|
||||
"move": "13/10 6/5",
|
||||
"equity": -0.04984435
|
||||
},
|
||||
{
|
||||
"move": "24/23 8/5",
|
||||
"equity": -0.104100645
|
||||
},
|
||||
{
|
||||
"move": "24/23 6/3",
|
||||
"equity": -0.10542354
|
||||
},
|
||||
{
|
||||
"move": "24/21 8/7",
|
||||
"equity": -0.107585564
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"luck": {
|
||||
"value": 0.21258597,
|
||||
"rating": "none"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
{
|
||||
"id": "move-opening-31-error",
|
||||
"description": "Ouverture 3-1 jouée 24/23 13/10 au lieu de faire le point 5.",
|
||||
"position_id": "4HPwATDgc/ABMA",
|
||||
"match_id": "cIkFAAAAAAAA",
|
||||
"dice": [
|
||||
3,
|
||||
1
|
||||
],
|
||||
"played": "24/23 13/10",
|
||||
"type": "move",
|
||||
"gnubg_commit": "14b67984737ded0f6de43d63dcb789f729d9b06b",
|
||||
"met": "Kazaross-XG2",
|
||||
"plies": {
|
||||
"0": {
|
||||
"played_move": "24/23 13/10",
|
||||
"played_position_id": "4HPwATDgc+IBKA",
|
||||
"move_count": 16,
|
||||
"rank": 2,
|
||||
"played_equity": -0.010789523,
|
||||
"best_move": "8/5 6/5",
|
||||
"best_equity": 0.21258597,
|
||||
"equity_loss": -0.2233755,
|
||||
"skill": "very_bad",
|
||||
"ranking": [
|
||||
{
|
||||
"move": "8/5 6/5",
|
||||
"equity": 0.21258597
|
||||
},
|
||||
{
|
||||
"move": "24/23 13/10",
|
||||
"equity": -0.010789523
|
||||
},
|
||||
{
|
||||
"move": "24/20",
|
||||
"equity": -0.017392516
|
||||
},
|
||||
{
|
||||
"move": "24/23 24/21",
|
||||
"equity": -0.023609947
|
||||
},
|
||||
{
|
||||
"move": "13/9",
|
||||
"equity": -0.027675275
|
||||
},
|
||||
{
|
||||
"move": "24/21 6/5",
|
||||
"equity": -0.04369401
|
||||
},
|
||||
{
|
||||
"move": "13/10 6/5",
|
||||
"equity": -0.04984435
|
||||
},
|
||||
{
|
||||
"move": "24/23 8/5",
|
||||
"equity": -0.104100645
|
||||
},
|
||||
{
|
||||
"move": "24/23 6/3",
|
||||
"equity": -0.10542354
|
||||
},
|
||||
{
|
||||
"move": "24/21 8/7",
|
||||
"equity": -0.107585564
|
||||
}
|
||||
]
|
||||
},
|
||||
"2": {
|
||||
"played_move": "24/23 13/10",
|
||||
"played_position_id": "4HPwATDgc+IBKA",
|
||||
"move_count": 16,
|
||||
"rank": 2,
|
||||
"played_equity": -0.0118339015,
|
||||
"best_move": "8/5 6/5",
|
||||
"best_equity": 0.1998143,
|
||||
"equity_loss": -0.21164821,
|
||||
"skill": "very_bad",
|
||||
"ranking": [
|
||||
{
|
||||
"move": "8/5 6/5",
|
||||
"equity": 0.1998143
|
||||
},
|
||||
{
|
||||
"move": "24/23 13/10",
|
||||
"equity": -0.0118339015
|
||||
},
|
||||
{
|
||||
"move": "24/20",
|
||||
"equity": -0.031027086
|
||||
},
|
||||
{
|
||||
"move": "24/23 24/21",
|
||||
"equity": -0.03213631
|
||||
},
|
||||
{
|
||||
"move": "13/9",
|
||||
"equity": -0.040205758
|
||||
},
|
||||
{
|
||||
"move": "24/21 6/5",
|
||||
"equity": -0.04369401
|
||||
},
|
||||
{
|
||||
"move": "13/10 6/5",
|
||||
"equity": -0.04984435
|
||||
},
|
||||
{
|
||||
"move": "24/23 8/5",
|
||||
"equity": -0.104100645
|
||||
},
|
||||
{
|
||||
"move": "24/23 6/3",
|
||||
"equity": -0.10542354
|
||||
},
|
||||
{
|
||||
"move": "24/21 8/7",
|
||||
"equity": -0.107585564
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"luck": {
|
||||
"value": 0.21258597,
|
||||
"rating": "none"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"id": "move-opening-65-error",
|
||||
"description": "Ouverture 6-5 jouée 13/8 13/7 au lieu de courir 24/13.",
|
||||
"position_id": "4HPwATDgc/ABMA",
|
||||
"match_id": "cAkXAAAAAAAA",
|
||||
"dice": [
|
||||
6,
|
||||
5
|
||||
],
|
||||
"played": "13/8 13/7",
|
||||
"type": "move",
|
||||
"gnubg_commit": "14b67984737ded0f6de43d63dcb789f729d9b06b",
|
||||
"met": "Kazaross-XG2",
|
||||
"plies": {
|
||||
"0": {
|
||||
"played_move": "13/8 13/7",
|
||||
"played_position_id": "4HPwATDg68EBMA",
|
||||
"move_count": 7,
|
||||
"rank": 3,
|
||||
"played_equity": -0.05142035,
|
||||
"best_move": "24/13",
|
||||
"best_equity": 0.082985915,
|
||||
"equity_loss": -0.13440627,
|
||||
"skill": "very_bad",
|
||||
"ranking": [
|
||||
{
|
||||
"move": "24/13",
|
||||
"equity": 0.082985915
|
||||
},
|
||||
{
|
||||
"move": "24/18 13/8",
|
||||
"equity": 0.03631018
|
||||
},
|
||||
{
|
||||
"move": "13/8 13/7",
|
||||
"equity": -0.05142035
|
||||
},
|
||||
{
|
||||
"move": "13/2",
|
||||
"equity": -0.13522762
|
||||
},
|
||||
{
|
||||
"move": "24/18 8/3",
|
||||
"equity": -0.14635906
|
||||
},
|
||||
{
|
||||
"move": "13/7 8/3",
|
||||
"equity": -0.17384717
|
||||
},
|
||||
{
|
||||
"move": "8/3 8/2",
|
||||
"equity": -0.37779096
|
||||
}
|
||||
]
|
||||
},
|
||||
"2": {
|
||||
"played_move": "13/8 13/7",
|
||||
"played_position_id": "4HPwATDg68EBMA",
|
||||
"move_count": 7,
|
||||
"rank": 3,
|
||||
"played_equity": -0.061850794,
|
||||
"best_move": "24/13",
|
||||
"best_equity": 0.07996891,
|
||||
"equity_loss": -0.1418197,
|
||||
"skill": "very_bad",
|
||||
"ranking": [
|
||||
{
|
||||
"move": "24/13",
|
||||
"equity": 0.07996891
|
||||
},
|
||||
{
|
||||
"move": "24/18 13/8",
|
||||
"equity": 0.03882884
|
||||
},
|
||||
{
|
||||
"move": "13/8 13/7",
|
||||
"equity": -0.061850794
|
||||
},
|
||||
{
|
||||
"move": "13/2",
|
||||
"equity": -0.12083713
|
||||
},
|
||||
{
|
||||
"move": "24/18 8/3",
|
||||
"equity": -0.13999933
|
||||
},
|
||||
{
|
||||
"move": "13/7 8/3",
|
||||
"equity": -0.17384717
|
||||
},
|
||||
{
|
||||
"move": "8/3 8/2",
|
||||
"equity": -0.37779096
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"luck": {
|
||||
"value": 0.082985915,
|
||||
"rating": "none"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
{
|
||||
"id": "move-opening-double",
|
||||
"description": "Position de départ et double 6 : pas un premier jet, chance comptée sur les 36 jets.",
|
||||
"position_id": "4HPwATDgc/ABMA",
|
||||
"match_id": "cIkFAAAAAAAA",
|
||||
"dice": [
|
||||
6,
|
||||
6
|
||||
],
|
||||
"played": null,
|
||||
"type": "move",
|
||||
"gnubg_commit": "14b67984737ded0f6de43d63dcb789f729d9b06b",
|
||||
"met": "Kazaross-XG2",
|
||||
"plies": {
|
||||
"0": {
|
||||
"played_move": "24/18(2) 13/7(2)",
|
||||
"played_position_id": "4HPwATDg28HBAA",
|
||||
"move_count": 11,
|
||||
"rank": 1,
|
||||
"played_equity": 0.539388,
|
||||
"best_move": "24/18(2) 13/7(2)",
|
||||
"best_equity": 0.539388,
|
||||
"equity_loss": -0.0,
|
||||
"skill": "none",
|
||||
"ranking": [
|
||||
{
|
||||
"move": "24/18(2) 13/7(2)",
|
||||
"equity": 0.539388
|
||||
},
|
||||
{
|
||||
"move": "24/18 13/7(3)",
|
||||
"equity": 0.33099693
|
||||
},
|
||||
{
|
||||
"move": "24/18(2) 8/2(2)",
|
||||
"equity": 0.27119082
|
||||
},
|
||||
{
|
||||
"move": "13/7(2) 8/2(2)",
|
||||
"equity": 0.1563462
|
||||
},
|
||||
{
|
||||
"move": "24/18 13/7(2) 8/2",
|
||||
"equity": 0.14750561
|
||||
},
|
||||
{
|
||||
"move": "13/7(4)",
|
||||
"equity": 0.14310652
|
||||
},
|
||||
{
|
||||
"move": "24/18(2) 13/7 8/2",
|
||||
"equity": 0.12353878
|
||||
},
|
||||
{
|
||||
"move": "13/7(3) 8/2",
|
||||
"equity": 0.101389736
|
||||
},
|
||||
{
|
||||
"move": "24/18 13/7 8/2(2)",
|
||||
"equity": -0.05435923
|
||||
},
|
||||
{
|
||||
"move": "24/18 8/2(3)",
|
||||
"equity": -0.12482335
|
||||
}
|
||||
]
|
||||
},
|
||||
"2": {
|
||||
"played_move": "24/18(2) 13/7(2)",
|
||||
"played_position_id": "4HPwATDg28HBAA",
|
||||
"move_count": 11,
|
||||
"rank": 1,
|
||||
"played_equity": 0.5886327,
|
||||
"best_move": "24/18(2) 13/7(2)",
|
||||
"best_equity": 0.5886327,
|
||||
"equity_loss": -0.0,
|
||||
"skill": "none",
|
||||
"ranking": [
|
||||
{
|
||||
"move": "24/18(2) 13/7(2)",
|
||||
"equity": 0.5886327
|
||||
},
|
||||
{
|
||||
"move": "24/18 13/7(3)",
|
||||
"equity": 0.33312234
|
||||
},
|
||||
{
|
||||
"move": "24/18(2) 8/2(2)",
|
||||
"equity": 0.27119082
|
||||
},
|
||||
{
|
||||
"move": "13/7(2) 8/2(2)",
|
||||
"equity": 0.1563462
|
||||
},
|
||||
{
|
||||
"move": "24/18 13/7(2) 8/2",
|
||||
"equity": 0.14750561
|
||||
},
|
||||
{
|
||||
"move": "13/7(4)",
|
||||
"equity": 0.14310652
|
||||
},
|
||||
{
|
||||
"move": "24/18(2) 13/7 8/2",
|
||||
"equity": 0.12353878
|
||||
},
|
||||
{
|
||||
"move": "13/7(3) 8/2",
|
||||
"equity": 0.101389736
|
||||
},
|
||||
{
|
||||
"move": "24/18 13/7 8/2(2)",
|
||||
"equity": -0.05435923
|
||||
},
|
||||
{
|
||||
"move": "24/18 8/2(3)",
|
||||
"equity": -0.12482335
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"luck": {
|
||||
"value": 0.43498537,
|
||||
"rating": "lucky"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
{
|
||||
"id": "move-race",
|
||||
"description": "Course, 5-4 : sortie maladroite.",
|
||||
"position_id": "cLezAQBwN2cGAA",
|
||||
"match_id": "cIkSAAAAAAAA",
|
||||
"dice": [
|
||||
5,
|
||||
4
|
||||
],
|
||||
"played": "6/1 5/1",
|
||||
"type": "move",
|
||||
"gnubg_commit": "14b67984737ded0f6de43d63dcb789f729d9b06b",
|
||||
"met": "Kazaross-XG2",
|
||||
"plies": {
|
||||
"0": {
|
||||
"played_move": "6/1 5/1",
|
||||
"played_position_id": "cLezAQDDNmcGAA",
|
||||
"move_count": 29,
|
||||
"rank": 29,
|
||||
"played_equity": -0.31466243,
|
||||
"best_move": "13/9 11/6",
|
||||
"best_equity": -0.22230339,
|
||||
"equity_loss": -0.092359036,
|
||||
"skill": "bad",
|
||||
"ranking": [
|
||||
{
|
||||
"move": "13/9 11/6",
|
||||
"equity": -0.22230339
|
||||
},
|
||||
{
|
||||
"move": "11/6 9/5",
|
||||
"equity": -0.22993731
|
||||
},
|
||||
{
|
||||
"move": "13/8 9/5",
|
||||
"equity": -0.23005769
|
||||
},
|
||||
{
|
||||
"move": "13/9 13/8",
|
||||
"equity": -0.23073325
|
||||
},
|
||||
{
|
||||
"move": "13/8 11/7",
|
||||
"equity": -0.23153114
|
||||
},
|
||||
{
|
||||
"move": "11/7 11/6",
|
||||
"equity": -0.2335976
|
||||
},
|
||||
{
|
||||
"move": "13/8 7/3",
|
||||
"equity": -0.23374501
|
||||
},
|
||||
{
|
||||
"move": "11/6 7/3",
|
||||
"equity": -0.23394687
|
||||
},
|
||||
{
|
||||
"move": "13/4",
|
||||
"equity": -0.23507257
|
||||
},
|
||||
{
|
||||
"move": "13/9 7/2",
|
||||
"equity": -0.23529215
|
||||
},
|
||||
{
|
||||
"move": "7/3 7/2",
|
||||
"equity": -0.23544219
|
||||
},
|
||||
{
|
||||
"move": "11/2",
|
||||
"equity": -0.235758
|
||||
},
|
||||
{
|
||||
"move": "11/7 9/4",
|
||||
"equity": -0.23589191
|
||||
},
|
||||
{
|
||||
"move": "13/8 6/2",
|
||||
"equity": -0.24026787
|
||||
},
|
||||
{
|
||||
"move": "9/5 7/2",
|
||||
"equity": -0.24087214
|
||||
},
|
||||
{
|
||||
"move": "13/8 5/1",
|
||||
"equity": -0.24252185
|
||||
},
|
||||
{
|
||||
"move": "9/5 9/4",
|
||||
"equity": -0.2440444
|
||||
},
|
||||
{
|
||||
"move": "9/4 6/2",
|
||||
"equity": -0.24487257
|
||||
},
|
||||
{
|
||||
"move": "9/4 7/3",
|
||||
"equity": -0.24512678
|
||||
},
|
||||
{
|
||||
"move": "11/6 5/1",
|
||||
"equity": -0.25016445
|
||||
},
|
||||
{
|
||||
"move": "9/4 5/1",
|
||||
"equity": -0.25572774
|
||||
},
|
||||
{
|
||||
"move": "7/2 5/1",
|
||||
"equity": -0.258138
|
||||
},
|
||||
{
|
||||
"move": "7/3 6/1",
|
||||
"equity": -0.25878713
|
||||
},
|
||||
{
|
||||
"move": "7/2 6/2",
|
||||
"equity": -0.26088732
|
||||
},
|
||||
{
|
||||
"move": "13/9 6/1",
|
||||
"equity": -0.26104733
|
||||
},
|
||||
{
|
||||
"move": "9/5 6/1",
|
||||
"equity": -0.26680842
|
||||
},
|
||||
{
|
||||
"move": "11/7 6/1",
|
||||
"equity": -0.2670043
|
||||
},
|
||||
{
|
||||
"move": "6/2 6/1",
|
||||
"equity": -0.27360544
|
||||
},
|
||||
{
|
||||
"move": "6/1 5/1",
|
||||
"equity": -0.31466243
|
||||
}
|
||||
]
|
||||
},
|
||||
"2": {
|
||||
"played_move": "6/1 5/1",
|
||||
"played_position_id": "cLezAQDDNmcGAA",
|
||||
"move_count": 29,
|
||||
"rank": 7,
|
||||
"played_equity": -0.29347342,
|
||||
"best_move": "13/9 11/6",
|
||||
"best_equity": -0.21922025,
|
||||
"equity_loss": -0.07425317,
|
||||
"skill": "bad",
|
||||
"ranking": [
|
||||
{
|
||||
"move": "13/9 11/6",
|
||||
"equity": -0.21922025
|
||||
},
|
||||
{
|
||||
"move": "13/9 13/8",
|
||||
"equity": -0.2202243
|
||||
},
|
||||
{
|
||||
"move": "11/6 9/5",
|
||||
"equity": -0.22022447
|
||||
},
|
||||
{
|
||||
"move": "13/8 9/5",
|
||||
"equity": -0.22088149
|
||||
},
|
||||
{
|
||||
"move": "11/7 11/6",
|
||||
"equity": -0.22277355
|
||||
},
|
||||
{
|
||||
"move": "13/4",
|
||||
"equity": -0.2230124
|
||||
},
|
||||
{
|
||||
"move": "6/1 5/1",
|
||||
"equity": -0.29347342
|
||||
},
|
||||
{
|
||||
"move": "11/7 9/4",
|
||||
"equity": -0.22985312
|
||||
},
|
||||
{
|
||||
"move": "13/8 11/7",
|
||||
"equity": -0.23169218
|
||||
},
|
||||
{
|
||||
"move": "13/9 7/2",
|
||||
"equity": -0.23561361
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"luck": {
|
||||
"value": 0.043956697,
|
||||
"rating": "none"
|
||||
}
|
||||
}
|
||||
+122
-3
@@ -138,6 +138,123 @@ def compare_position(report, cli, path):
|
||||
return len(position["plies"])
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Analyses (analysis/<id>.json, voir README.md, section « Analyses »).
|
||||
|
||||
# Seuils de gnubg (arSkillLevel et arLuckLevel) : une appréciation ou une
|
||||
# chance n'est comparée strictement que loin de ces seuils.
|
||||
SKILL_THRESHOLDS = (0.03, 0.06, 0.12)
|
||||
LUCK_THRESHOLDS = (0.3, 0.6)
|
||||
|
||||
|
||||
def run_cli(cli, args):
|
||||
command = shlex.split(cli) + args
|
||||
output = subprocess.run(command, capture_output=True, text=True)
|
||||
if output.returncode != 0:
|
||||
raise SystemExit("%s a échoué : %s" % (" ".join(command), output.stdout + output.stderr))
|
||||
return json.loads(output.stdout)
|
||||
|
||||
|
||||
def near_threshold(value, thresholds, tolerance):
|
||||
return any(abs(abs(value) - t) <= tolerance for t in thresholds)
|
||||
|
||||
|
||||
def same_label(report, where, field, expected, actual, value, thresholds):
|
||||
"""Même appréciation ou chance, sauf si la valeur de gnubg est à moins de
|
||||
la tolérance d'un seuil : remarque sans échec."""
|
||||
if expected == actual:
|
||||
return
|
||||
if near_threshold(value, thresholds, report.tolerance):
|
||||
report.notes.append("%s : %s %s pour gnubg, %s pour le moteur ; valeur %.4f proche "
|
||||
"d'un seuil" % (where, field, expected, actual, value))
|
||||
else:
|
||||
report.same(where, field, expected, actual)
|
||||
|
||||
|
||||
def compare_move_analysis(report, cli, case):
|
||||
dice = "%d%d" % tuple(case["dice"])
|
||||
args = [case["position_id"], case["match_id"], dice]
|
||||
luck = run_cli(cli, ["luck"] + args)
|
||||
where = "%s chance" % case["id"]
|
||||
report.number(where, "luck", case["luck"]["value"], luck["luck"])
|
||||
same_label(report, where, "rating", case["luck"]["rating"], luck["rating"],
|
||||
case["luck"]["value"], LUCK_THRESHOLDS)
|
||||
for ply, expected in sorted(case["plies"].items()):
|
||||
actual = run_cli(cli, ["--ply", ply, "analysemove"] + args
|
||||
+ [expected["played_position_id"]])
|
||||
where = "%s à %s pli(s)" % (case["id"], ply)
|
||||
report.same(where, "move_count", expected["move_count"], actual["move_count"])
|
||||
report.same(where, "coup joué", expected["played_move"], actual["played"]["move"])
|
||||
# Rang : le même, sauf si les coups entre les deux rangs ont, pour
|
||||
# gnubg, la même équité que le coup joué à la tolérance près.
|
||||
if expected["rank"] != actual["rank"]:
|
||||
low, high = sorted((expected["rank"], actual["rank"]))
|
||||
between = expected["ranking"][low - 1:high]
|
||||
tied = len(between) == high - low + 1 and all(
|
||||
abs(m["equity"] - expected["played_equity"]) <= report.tolerance for m in between)
|
||||
if tied:
|
||||
report.notes.append("%s : rang %d pour gnubg, %d pour le moteur ; égalité"
|
||||
% (where, expected["rank"], actual["rank"]))
|
||||
else:
|
||||
report.same(where, "rank", expected["rank"], actual["rank"])
|
||||
if expected["best_move"] != actual["best"]["move"]:
|
||||
ranking = expected["ranking"]
|
||||
tied = len(ranking) > 1 and ranking[0]["equity"] - ranking[1]["equity"] <= report.tolerance
|
||||
if tied:
|
||||
report.notes.append("%s : meilleur coup %s pour gnubg, %s pour le moteur ; égalité"
|
||||
% (where, expected["best_move"], actual["best"]["move"]))
|
||||
else:
|
||||
report.same(where, "meilleur coup", expected["best_move"], actual["best"]["move"])
|
||||
report.number(where, "played_equity", expected["played_equity"], actual["played"]["equity"])
|
||||
report.number(where, "best_equity", expected["best_equity"], actual["best"]["equity"])
|
||||
report.number(where, "equity_loss", expected["equity_loss"], actual["equity_loss"])
|
||||
same_label(report, where, "skill", expected["skill"], actual["skill"],
|
||||
expected["equity_loss"], SKILL_THRESHOLDS)
|
||||
|
||||
|
||||
def compare_cube_analysis(report, cli, case):
|
||||
for ply, expected in sorted(case["plies"].items()):
|
||||
actual = run_cli(cli, ["--ply", ply, "analysecube", case["position_id"],
|
||||
case["match_id"], case["action"]])
|
||||
where = "%s à %s pli(s)" % (case["id"], ply)
|
||||
report.same(where, "analysed", expected["analysed"], actual["analysed"])
|
||||
if expected["analysed"] and actual["analysed"]:
|
||||
compare_cube(report, where, expected["cube"], actual["cube"])
|
||||
report.number(where, "equity_loss", expected["equity_loss"], actual["equity_loss"])
|
||||
same_label(report, where, "skill", expected["skill"], actual["skill"],
|
||||
expected["equity_loss"], SKILL_THRESHOLDS)
|
||||
|
||||
|
||||
def compare_levels(report, cli, case, settings):
|
||||
for name, expected in sorted(case["levels"].items()):
|
||||
where = "%s niveau %s" % (case["id"], name)
|
||||
report.same(where, "réglages", (expected["plies"], expected["noise"]),
|
||||
(settings[name]["plies"], round(settings[name]["noise"], 6)))
|
||||
if case["dice"]:
|
||||
actual = run_cli(cli, ["--level", name, "bestmove", case["position_id"],
|
||||
case["match_id"], "%d%d" % tuple(case["dice"])])
|
||||
report.same(where, "coup choisi", expected["move"], actual["best_move"]["move"])
|
||||
else:
|
||||
actual = run_cli(cli, ["--level", name, "cube", case["position_id"], case["match_id"]])
|
||||
compare_cube(report, where, expected, actual)
|
||||
|
||||
|
||||
def compare_analyses(report, cli, folder):
|
||||
"""Compare toutes les analyses ; renvoie leur nombre."""
|
||||
settings = {level["name"]: level for level in run_cli(cli, ["levels"])["levels"]}
|
||||
paths = sorted(glob.glob(os.path.join(folder, "*.json")))
|
||||
for path in paths:
|
||||
with open(path, encoding="utf-8") as f:
|
||||
case = json.load(f)
|
||||
if case["type"] == "move":
|
||||
compare_move_analysis(report, cli, case)
|
||||
elif case["type"] == "cube":
|
||||
compare_cube_analysis(report, cli, case)
|
||||
else:
|
||||
compare_levels(report, cli, case, settings)
|
||||
return len(paths)
|
||||
|
||||
|
||||
def print_table(lines):
|
||||
header = ("position et niveau", "champ", "attendu", "obtenu", "écart")
|
||||
rows = [header] + lines
|
||||
@@ -152,6 +269,7 @@ def main():
|
||||
parser.add_argument("--cli", default="build/engine-cli",
|
||||
help="programme interrogé, éventuellement avec ses arguments")
|
||||
parser.add_argument("--results", default=os.path.join(here, "results"))
|
||||
parser.add_argument("--analysis", default=os.path.join(here, "analysis"))
|
||||
parser.add_argument("--tolerance", type=float, default=TOLERANCE)
|
||||
parser.add_argument("--details", action="store_true",
|
||||
help="affiche tous les nombres comparés")
|
||||
@@ -160,14 +278,15 @@ def main():
|
||||
report = Report(args.tolerance)
|
||||
paths = sorted(glob.glob(os.path.join(args.results, "*.json")))
|
||||
levels = sum(compare_position(report, args.cli, path) for path in paths)
|
||||
analyses = compare_analyses(report, args.cli, args.analysis)
|
||||
|
||||
if args.details:
|
||||
print_table(report.lines)
|
||||
for note in report.notes:
|
||||
print(" Remarque : " + note)
|
||||
print(" %d positions, %d niveaux, %d nombres comparés ; écart maximal %.2e (%s), "
|
||||
"tolérance %.0e." % (len(paths), levels, len(report.lines), report.max_gap,
|
||||
report.max_gap_where, args.tolerance))
|
||||
print(" %d positions, %d niveaux, %d analyses, %d nombres comparés ; écart maximal %.2e "
|
||||
"(%s), tolérance %.0e." % (len(paths), levels, analyses, len(report.lines),
|
||||
report.max_gap, report.max_gap_where, args.tolerance))
|
||||
if report.failures:
|
||||
print(" %d écart(s) hors critères :" % len(report.failures))
|
||||
print_table(report.failures)
|
||||
|
||||
@@ -161,6 +161,259 @@ def check_settings():
|
||||
raise SystemExit("table d'équité de match inattendue")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Analyses (analyses.json -> analysis/<id>.json) : coup joué, décision de
|
||||
# videau, chance d'un jet et niveaux de jeu. Les valeurs viennent de l'analyse
|
||||
# de partie de gnubg (« analyse game ») sur une courte partie rejouée ici :
|
||||
# la position, puis le coup ou la décision. Voir README.md.
|
||||
|
||||
# Appréciations et chances : noms de gnubg (skillString et luckString de
|
||||
# gnubgmodule.c ; absents du dictionnaire quand rien n'est à signaler) vers
|
||||
# ceux d'engine-cli.
|
||||
SKILLS = {None: "none", "doubtful": "doubtful", "bad": "bad", "very bad": "very_bad"}
|
||||
LUCKS = {None: "none", "verybad": "very_unlucky", "bad": "unlucky", "good": "lucky",
|
||||
"verygood": "very_lucky"}
|
||||
|
||||
# Niveaux de jeu : réglages prédéfinis de gnubg (aecSettings, eval.c
|
||||
# l. 318-328), en (plis, bruit) ; l'élagage est actif à partir d'1 pli, le
|
||||
# bruit est déterministe. Ceux qui sont vérifiés : 3 et 4 plis sont trop
|
||||
# lents pour la suite de tests.
|
||||
LEVEL_SETTINGS = {
|
||||
"beginner": (0, 0.06), "casual": (0, 0.05), "intermediate": (0, 0.04),
|
||||
"advanced": (0, 0.015), "0ply": (0, 0.0), "1ply": (1, 0.0), "2ply": (2, 0.0),
|
||||
"3ply": (3, 0.0), "4ply": (4, 0.0),
|
||||
}
|
||||
CHECKED_LEVELS = ("beginner", "casual", "intermediate", "advanced", "0ply", "1ply", "2ply")
|
||||
RANKING_KEPT = 10 # coups gardés dans le classement d'une analyse (plus le coup joué)
|
||||
|
||||
|
||||
def level_context(name):
|
||||
plies, noise = LEVEL_SETTINGS[name]
|
||||
return {"cubeful": 1, "plies": plies, "deterministic": 1,
|
||||
"prune": 1 if plies > 0 else 0, "noise": noise}
|
||||
|
||||
|
||||
def start_game(case):
|
||||
"""Nouvelle partie entre deux joueurs humains (gnubg ne joue rien de
|
||||
lui-même), placée sur la position du cas, cache vidé."""
|
||||
gnubg.command("set player 0 human")
|
||||
gnubg.command("set player 1 human")
|
||||
gnubg.command("new session")
|
||||
gnubg.command("clear cache")
|
||||
gnubg.command("set gnubgid %s:%s" % (case["position_id"], case["match_id"]))
|
||||
read_back = (gnubg.positionid(), gnubg.matchid())
|
||||
if read_back != (case["position_id"], case["match_id"]):
|
||||
raise SystemExit("%s : gnubg relit %s" % (case["id"], read_back))
|
||||
|
||||
|
||||
def set_analysis(ply):
|
||||
"""Réglages de l'analyse de partie : niveau ply sans bruit, comme le
|
||||
réglage prédéfini de gnubg ; filtre de coups et analyse de la chance par
|
||||
défaut (« Normal », ecLuck)."""
|
||||
for kind in ("chequerplay", "cubedecision"):
|
||||
prefix = "set analysis %s evaluation " % kind
|
||||
gnubg.command("set analysis %s type evaluation" % kind)
|
||||
gnubg.command(prefix + "plies %d" % ply)
|
||||
gnubg.command(prefix + "cubeful on")
|
||||
gnubg.command(prefix + "prune " + ("on" if ply > 0 else "off"))
|
||||
gnubg.command(prefix + "deterministic on")
|
||||
gnubg.command(prefix + "noise 0")
|
||||
|
||||
|
||||
def analyse_game():
|
||||
"""Analyse la partie en cours ; renvoie ses enregistrements et ses
|
||||
statistiques par joueur (« X », « O »). Le cache est vidé avant : les
|
||||
évaluations à 2 plis faites pour choisir un coup y laissent des
|
||||
résultats à 0 pli des réseaux d'élagage, que l'analyse à 0 pli
|
||||
reprendrait (voir analyse_move)."""
|
||||
gnubg.command("clear cache")
|
||||
gnubg.command("analyse game")
|
||||
game = gnubg.match(analysis=1, verbose=1, statistics=1, boards=1)["games"][-1]
|
||||
return game["game"], game["stats"]
|
||||
|
||||
|
||||
def last_record(records, action):
|
||||
found = [r for r in records if r.get("action") == action]
|
||||
if not found:
|
||||
raise SystemExit("aucun enregistrement « %s » dans la partie analysée" % action)
|
||||
return found[-1]
|
||||
|
||||
|
||||
def swapped(board):
|
||||
"""La même position vue de l'autre joueur (gnubg.board() donne le joueur
|
||||
au trait en second)."""
|
||||
return (board[1], board[0])
|
||||
|
||||
|
||||
def set_dice(case):
|
||||
"""Les dés du cas, qui priment sur ceux du Match ID."""
|
||||
gnubg.command("set dice %d %d" % tuple(case["dice"]))
|
||||
|
||||
|
||||
def best_move():
|
||||
"""Le meilleur coup selon gnubg (findbestmove, réglages d'évaluation
|
||||
courants), en notation de gnubg. Pas « hint » : gnubg garderait son
|
||||
analyse dans l'enregistrement du coup, et « analyse game » la
|
||||
reprendrait au lieu de calculer au niveau demandé."""
|
||||
board = gnubg.board()
|
||||
return gnubg.movetupletostring(gnubg.findbestmove(), board)
|
||||
|
||||
|
||||
def play_move(case):
|
||||
"""Joue le coup du cas, ou le meilleur selon gnubg s'il n'est pas donné
|
||||
(cas qui ne servent qu'à la chance du jet)."""
|
||||
gnubg.command("move " + (case.get("played") or best_move()))
|
||||
|
||||
|
||||
def analyse_move(case, ply):
|
||||
start_game(case)
|
||||
set_dice(case)
|
||||
before = gnubg.board()
|
||||
play_move(case)
|
||||
# Sans analyse du videau : gnubg la ferait juste avant la chance du jet,
|
||||
# et à 2 plis elle laisse dans son cache d'évaluation des résultats à
|
||||
# 0 pli calculés avec les réseaux d'élagage, que l'analyse de la chance
|
||||
# (0 pli, sans élagage) reprend ensuite : la chance dépendrait alors de ce
|
||||
# qui a été calculé avant (écart observé : 5e-3 sur move-middlegame-close).
|
||||
gnubg.command("set analysis cube off")
|
||||
# Après le coup, l'adversaire est au trait : la position vue de celui qui
|
||||
# vient de jouer est la position retournée.
|
||||
played_id = gnubg.positionid(swapped(gnubg.board()))
|
||||
set_analysis(ply)
|
||||
records, stats = analyse_game()
|
||||
record = last_record(records, "move")
|
||||
analysis = record["analysis"]
|
||||
moves, index = analysis["moves"], analysis["imove"]
|
||||
for m in moves:
|
||||
if m["type"] != "eval":
|
||||
raise SystemExit("%s : coup évalué autrement que prévu : %r" % (case["id"], m))
|
||||
|
||||
def notation(move):
|
||||
return gnubg.movetupletostring(move, before)
|
||||
|
||||
kept = moves[:max(RANKING_KEPT, index + 1)]
|
||||
level = {
|
||||
"played_move": notation(record["move"]),
|
||||
"played_position_id": played_id,
|
||||
"move_count": len(moves),
|
||||
"rank": index + 1,
|
||||
"played_equity": f32(moves[index]["score"]),
|
||||
"best_move": notation(moves[0]["move"]),
|
||||
"best_equity": f32(moves[0]["score"]),
|
||||
# Perte calculée par gnubg pour ses statistiques (rChequerSkill),
|
||||
# comptée en positif.
|
||||
"equity_loss": f32(-stats[record["player"]]["moves"]["error-skill"]),
|
||||
"skill": SKILLS[analysis.get("skill")],
|
||||
"ranking": [{"move": notation(m["move"]), "equity": f32(m["score"])} for m in kept],
|
||||
}
|
||||
luck = {"value": f32(analysis["luck-value"]), "rating": LUCKS[analysis.get("luck")]}
|
||||
return level, luck
|
||||
|
||||
|
||||
def analyse_cube(case, ply):
|
||||
start_game(case)
|
||||
gnubg.command("set analysis cube on")
|
||||
set_analysis(ply)
|
||||
# Décision de videau de gnubg au même niveau, avant toute action.
|
||||
decision = cube_decision(ply)
|
||||
action = case["action"]
|
||||
if action == "no_double":
|
||||
# Ne pas doubler, c'est jeter les dés : n'importe quel jet et le
|
||||
# meilleur coup, l'analyse du videau portant sur la position avant.
|
||||
gnubg.command("set dice 2 1")
|
||||
gnubg.command("move " + best_move())
|
||||
records, stats = analyse_game()
|
||||
record = last_record(records, "move")
|
||||
analysed = "nd-cubeful-eq" in record["analysis"]
|
||||
skill = record["analysis"].get("cube-skill")
|
||||
else:
|
||||
# gnubg n'analyse un double qu'une fois la réponse donnée : après
|
||||
# « double », l'adversaire prend, ce qui ne change rien à l'analyse
|
||||
# du double.
|
||||
gnubg.command("double")
|
||||
gnubg.command("drop" if action == "pass" else "take")
|
||||
records, stats = analyse_game()
|
||||
analysed = "nd-cubeful-eq" in last_record(records, "double").get("analysis", {})
|
||||
record = last_record(records, {"double": "double", "take": "take", "pass": "drop"}[action])
|
||||
skill = record.get("analysis", {}).get("skill")
|
||||
return {
|
||||
"analysed": analysed,
|
||||
"cube": decision if analysed else None,
|
||||
"equity_loss": f32(-stats[record["player"]]["cube"]["error-skill"]),
|
||||
"skill": SKILLS[skill],
|
||||
}
|
||||
|
||||
|
||||
def level_move(case, name):
|
||||
"""Le coup que joue l'ordinateur de gnubg réglé à ce niveau."""
|
||||
start_game(case)
|
||||
set_dice(case)
|
||||
before = gnubg.board()
|
||||
turn = gnubg.posinfo()["turn"]
|
||||
plies, noise = LEVEL_SETTINGS[name]
|
||||
prefix = "set player %d chequerplay evaluation " % turn
|
||||
gnubg.command("set player %d gnubg" % turn)
|
||||
gnubg.command(prefix + "plies %d" % plies)
|
||||
gnubg.command(prefix + "noise %g" % noise)
|
||||
gnubg.command(prefix + "deterministic on")
|
||||
gnubg.command(prefix + "cubeful on")
|
||||
gnubg.command(prefix + "prune " + ("on" if plies > 0 else "off"))
|
||||
gnubg.command("play")
|
||||
record = last_record(gnubg.match(analysis=0, boards=1)["games"][-1]["game"], "move")
|
||||
gnubg.command("set player %d human" % turn)
|
||||
return {"move": gnubg.movetupletostring(record["move"], before)}
|
||||
|
||||
|
||||
def level_cube(case, name):
|
||||
"""Décision de videau de gnubg avec les réglages de ce niveau."""
|
||||
start_game(case)
|
||||
r = gnubg.cfevaluate(gnubg.board(), cube_info(), level_context(name))
|
||||
return {
|
||||
"no_double": f32(r[1]),
|
||||
"double_take": f32(r[2]),
|
||||
"double_pass": f32(r[3]),
|
||||
"optimal": f32(r[0]),
|
||||
"decision": CUBE_DECISIONS[r[4]],
|
||||
"decision_text": r[5],
|
||||
}
|
||||
|
||||
|
||||
def write_analysis(out_dir, result):
|
||||
with open(os.path.join(out_dir, result["id"] + ".json"), "w", encoding="utf-8") as f:
|
||||
json.dump(result, f, ensure_ascii=False, indent=2)
|
||||
f.write("\n")
|
||||
print("analyse écrite :", result["id"])
|
||||
|
||||
|
||||
def generate_analyses(commit):
|
||||
with open(os.path.join(FOLDER, "analyses.json"), encoding="utf-8") as f:
|
||||
cases = json.load(f)
|
||||
out_dir = os.path.join(FOLDER, "analysis")
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
# Pas d'affichage du plateau ; pas de nouvelle partie ni de jet de dés
|
||||
# automatiques, qui ajouteraient des enregistrements après le dernier.
|
||||
gnubg.command("set display off")
|
||||
gnubg.command("set automatic game off")
|
||||
gnubg.command("set automatic roll off")
|
||||
for kind in ("moves", "cubes", "levels"):
|
||||
for case in cases[kind]:
|
||||
result = dict(case, type=kind[:-1], gnubg_commit=commit, met=MET)
|
||||
if kind == "moves":
|
||||
result["plies"] = {}
|
||||
for ply in PLIES:
|
||||
result["plies"][str(ply)], result["luck"] = analyse_move(case, ply)
|
||||
elif kind == "cubes":
|
||||
result["plies"] = {str(ply): analyse_cube(case, ply) for ply in PLIES}
|
||||
else:
|
||||
result["levels"] = {}
|
||||
for name in CHECKED_LEVELS:
|
||||
plies, noise = LEVEL_SETTINGS[name]
|
||||
level = {"plies": plies, "noise": noise}
|
||||
level.update(level_move(case, name) if case["dice"] else level_cube(case, name))
|
||||
result["levels"][name] = level
|
||||
write_analysis(out_dir, result)
|
||||
|
||||
|
||||
def main():
|
||||
with open(os.path.join(FOLDER, "positions.json"), encoding="utf-8") as f:
|
||||
positions = json.load(f)["positions"]
|
||||
@@ -195,6 +448,7 @@ def main():
|
||||
json.dump(result, f, ensure_ascii=False, indent=2)
|
||||
f.write("\n")
|
||||
print("référence écrite :", pos["id"])
|
||||
generate_analyses(commit)
|
||||
|
||||
|
||||
main()
|
||||
|
||||
+46
-1
@@ -2,7 +2,8 @@
|
||||
# Tests du moteur embarqué, via build/engine-cli (étape 0a.3) :
|
||||
# meilleurs coups d'ouverture connus, nombre de coups légaux, bornes et
|
||||
# symétrie des probabilités, décision de videau évidente, identifiants
|
||||
# invalides. La comparaison systématique au gnubg de bureau est faite par
|
||||
# invalides, niveaux de jeu (bruit reproductible), analyses et chance. La
|
||||
# comparaison systématique au gnubg de bureau est faite par
|
||||
# tests/test_engine_reference.sh.
|
||||
#
|
||||
# Lance `make engine` (sans effet si tout est à jour).
|
||||
@@ -219,6 +220,50 @@ grep -qF '"available": true, "double": true, "take": false' <<<"$($cli cube "$co
|
||||
grep -qF '"available": false, "double": false' <<<"$($cli cube "$course" "$(valeur match_id "$($cli matchid 2 1)")")" \
|
||||
|| echouer "videau chez l'adversaire : double indisponible attendu"
|
||||
|
||||
# Niveaux de jeu : les neuf réglages de gnubg, seuils exposés.
|
||||
sortie="$($cli levels)"
|
||||
[ "$(grep -o '"name"' <<<"$sortie" | wc -l)" = 9 ] || echouer "levels : 9 niveaux attendus"
|
||||
grep -qF '"name": "beginner", "plies": 0, "noise": 0.0599999987' <<<"$sortie" \
|
||||
|| echouer "levels : réglage débutant inattendu"
|
||||
grep -qF '"skill_thresholds": {"doubtful": 0.0299999993, "bad": 0.0599999987, "very_bad": 0.119999997}' \
|
||||
<<<"$sortie" || echouer "levels : seuils d'appréciation inattendus"
|
||||
# Le bruit est déterministe : le même coup à chaque appel, et le bruit change
|
||||
# le coup de cette position (vérifié contre gnubg par analysis/level-*.json).
|
||||
milieu=mM/gATCw5+ABMA
|
||||
milieu_mid=cAkTAAAAAAAA
|
||||
for essai in 1 2 3; do
|
||||
coup="$(valeur move "$($cli --level beginner bestmove "$milieu" "$milieu_mid" 64)")"
|
||||
[ "$coup" = "24/20 24/18" ] || echouer "niveau débutant, essai $essai : « $coup »"
|
||||
done
|
||||
coup="$(valeur move "$($cli --level 0ply bestmove "$milieu" "$milieu_mid" 64)")"
|
||||
[ "$coup" = "24/14" ] || echouer "niveau 0 pli : « $coup » au lieu de 24/14"
|
||||
grep -qF '"level": "advanced"' <<<"$($cli --level advanced cube "$depart" "$libre_j0")" \
|
||||
|| echouer "cube au niveau avancé : niveau absent de la réponse"
|
||||
attendre_erreur usage --level champion bestmove "$depart" "$libre_j0" 31
|
||||
attendre_erreur usage --level beginner --ply 2 bestmove "$depart" "$libre_j0" 31
|
||||
attendre_erreur usage --level beginner eval "$depart" "$libre_j0"
|
||||
|
||||
# Analyse d'un coup : le meilleur coup n'est pas une erreur ; un coup illégal
|
||||
# est refusé ; sans coup possible, rien à analyser.
|
||||
sortie="$($cli analysemove "$depart" "$libre_j0" 31 4HPwATCwZ/ABMA)"
|
||||
[ "$(valeur rank "$sortie") $(valeur skill "$sortie")" = "1 none" ] \
|
||||
|| echouer "analysemove 8/5 6/5 : rang 1 sans erreur attendus ($sortie)"
|
||||
attendre_erreur invalid_move analysemove "$depart" "$libre_j0" 31 "$depart"
|
||||
grep -qF '"move_count": 0, "rank": null' <<<"$($cli analysemove 27YBBwAcz8EDQA cIkWAAAAAAAA 55 27YBBwAcz8EDQA)" \
|
||||
|| echouer "analysemove sans coup possible : move_count 0 attendu"
|
||||
|
||||
# Analyse du videau : décision inconnue refusée ; pas d'analyse quand le
|
||||
# joueur au trait ne peut pas doubler.
|
||||
attendre_erreur usage analysecube "$depart" "$libre_j0" redouble
|
||||
grep -qF '"analysed": false, "cube": null, "equity_loss": 0, "skill": "none"' \
|
||||
<<<"$($cli analysecube "$course" "$(valeur match_id "$($cli matchid 2 1)")" no_double)" \
|
||||
|| echouer "analysecube, videau chez l'adversaire : pas d'analyse attendue"
|
||||
|
||||
# Chance : même jet, même chance ; ordre des dés indifférent.
|
||||
[ "$($cli luck "$milieu" "$milieu_mid" 64 | sed 's/"dice": \[[0-9], [0-9]\]//')" \
|
||||
= "$($cli luck "$milieu" "$milieu_mid" 46 | sed 's/"dice": \[[0-9], [0-9]\]//')" ] \
|
||||
|| echouer "luck : l'ordre des dés change la chance"
|
||||
|
||||
if [ "$echecs" -gt 0 ]; then
|
||||
echo " $echecs vérification(s) du moteur en échec."
|
||||
exit 1
|
||||
|
||||
@@ -117,4 +117,51 @@ for f in results/*.json; do
|
||||
printf '%s\n' "${ids[@]}" | grep -qxF "$id" || signaler "$f : position absente de positions.json"
|
||||
done
|
||||
|
||||
# Analyses (README.md, section « Analyses ») : un résultat par cas, et les
|
||||
# champs du format.
|
||||
jq empty analyses.json || { echo "analyses.json : JSON invalide"; exit 1; }
|
||||
read -r -d '' controle_analyse <<'JQ' || true
|
||||
def nombre: type == "number";
|
||||
def skill: IN("none", "doubtful", "bad", "very_bad");
|
||||
if .type == "move" then
|
||||
(if (.plies | keys) == ["0", "2"] and (.luck.value | nombre)
|
||||
and (.luck.rating | IN("very_unlucky", "unlucky", "none", "lucky", "very_lucky"))
|
||||
and all(.plies[]; (.move_count | nombre) and (.rank | nombre) and (.played_move | type) == "string"
|
||||
and (.played_position_id | test("^[A-Za-z0-9+/]{14}$")) and (.played_equity | nombre)
|
||||
and (.best_move | type) == "string" and (.best_equity | nombre)
|
||||
and (.equity_loss | nombre) and .equity_loss <= 0 and (.skill | skill)
|
||||
and (.ranking | length) >= ([.rank, ([.move_count, 10] | min)] | max))
|
||||
then empty else "analyse de coup incomplète" end)
|
||||
elif .type == "cube" then
|
||||
(if (.plies | keys) == ["0", "2"] and (.action | IN("no_double", "double", "take", "pass"))
|
||||
and all(.plies[]; (.analysed | type) == "boolean" and (.equity_loss | nombre)
|
||||
and .equity_loss <= 0 and (.skill | skill)
|
||||
and (if .analysed then (.cube.decision | type) == "string" else .cube == null end))
|
||||
then empty else "analyse de videau incomplète" end)
|
||||
elif .type == "level" then
|
||||
(if (.levels | length) > 0 and all(.levels[]; (.plies | nombre) and (.noise | nombre)
|
||||
and ((.move | type) == "string" or (.decision | type) == "string"))
|
||||
then empty else "niveaux incomplets" end)
|
||||
else "type inconnu" end
|
||||
JQ
|
||||
|
||||
mapfile -t analyses < <(jq -r '(.moves + .cubes + .levels)[].id' analyses.json)
|
||||
doublons="$(printf '%s\n' "${analyses[@]}" "${ids[@]}" | sort | uniq -d | paste -sd' ' -)"
|
||||
[ -z "$doublons" ] || signaler "analyses.json : identifiants en double : $doublons"
|
||||
for id in "${analyses[@]}"; do
|
||||
f="analysis/$id.json"
|
||||
if [ ! -f "$f" ]; then
|
||||
signaler "$f absent : lancer make reference"
|
||||
continue
|
||||
fi
|
||||
while read -r defaut; do
|
||||
[ -n "$defaut" ] && signaler "$f : $defaut"
|
||||
done < <(jq -r "$controle_analyse" "$f")
|
||||
done
|
||||
for f in analysis/*.json; do
|
||||
[ -e "$f" ] || continue
|
||||
printf '%s\n' "${analyses[@]}" | grep -qxF "$(basename "$f" .json)" \
|
||||
|| signaler "$f : cas absent d'analyses.json"
|
||||
done
|
||||
|
||||
[ "$erreurs" -eq 0 ]
|
||||
|
||||
+14
-10
@@ -1,8 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
# Vérifie l'image de production (deploy/) avec le site assemblé (make package) :
|
||||
# construit l'image en local, la lance, et contrôle ce que reçoit un
|
||||
# navigateur (application, pages de test du moteur, aide, manifeste, service
|
||||
# worker, types MIME, compression, politique de sécurité, version).
|
||||
# navigateur (application, aide, manifeste, service worker, types MIME,
|
||||
# compression, politique de sécurité, version), et l'absence des anciennes
|
||||
# pages de test du moteur.
|
||||
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
@@ -71,10 +72,12 @@ check "wasm/engine.mjs" "text/javascript"
|
||||
check "wasm/engine.wasm" "application/wasm"
|
||||
check "wasm/engine.data" "application/octet-stream"
|
||||
check "version.json" "application/json"
|
||||
# Pages de test du moteur (étape 0b).
|
||||
check "moteur/" "text/html"
|
||||
check "moteur/bench.html" "text/html"
|
||||
check "moteur/engine-client.mjs" "text/javascript"
|
||||
# Les pages de test du moteur (étape 0b) sont retirées (ADR 0009,
|
||||
# amendement du 2026-11-03) : plus rien sous /moteur/.
|
||||
[ ! -e build/site/moteur ] || report "build/site/moteur/ existe encore"
|
||||
for gone in moteur/ moteur/bench.html; do
|
||||
curl -s -o /dev/null -w "%{http_code}" "$base/$gone" | grep -qx 404 || report "/$gone : 404 attendu"
|
||||
done
|
||||
# Aide : copies du glossaire et des scénarios (ADR 0008).
|
||||
check "docs/scenarios/jouer-une-partie.feature" "text/plain"
|
||||
check "docs/glossaire/README.md" "text/markdown"
|
||||
@@ -94,15 +97,16 @@ done < <(grep -o '"src": "[^"]*"' <<<"$manifest" | sed 's/"src": "\(.*\)"/\1/')
|
||||
# Le service worker met en cache chaque fichier du site, avec une version.
|
||||
sw="$(curl -s "$base/sw.js")"
|
||||
grep -qE '^const VERSION = "[0-9a-f]{16}";' <<<"$sw" || report "sw.js : version absente"
|
||||
for file in ./ index.html app/main.js wasm/engine.data wasm/engine.wasm moteur/ \
|
||||
for file in ./ index.html app/main.js wasm/engine.data wasm/engine.wasm \
|
||||
docs/glossaire/videau.md version.json fonts/AlegreyaSans-Bold.woff2 icons/favicon.svg; do
|
||||
grep -qF " \"$file\"," <<<"$sw" || report "sw.js : $file absent de la liste à mettre en cache"
|
||||
done
|
||||
listed="$(grep -c '^ "' <<<"$sw")"
|
||||
present="$(find build/site -type f ! -name sw.js | wc -l)"
|
||||
# Chaque fichier, plus « ./ » et « moteur/ » pour les deux index.html.
|
||||
[ "$listed" -eq "$((present + 2))" ] \
|
||||
|| report "sw.js liste $listed adresses pour $present fichiers (plus 2 dossiers)"
|
||||
# Chaque fichier, plus « ./ » pour index.html.
|
||||
[ "$listed" -eq "$((present + 1))" ] \
|
||||
|| report "sw.js liste $listed adresses pour $present fichiers (plus « ./ »)"
|
||||
grep -q '"moteur/' <<<"$sw" && report "sw.js met encore en cache des pages de test du moteur"
|
||||
grep -o '^ "[^"]*"' <<<"$sw" | tr -d ' "' | while read -r file; do
|
||||
curl -sf -o /dev/null "$base/$file" || echo "sw.js : $file introuvable sur le site"
|
||||
done | grep . && report "sw.js liste des fichiers introuvables"
|
||||
|
||||
+37
-6
@@ -499,6 +499,10 @@ body.busy .prompt {
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
/* Dé déjà utilisé par un demi-coup du coup en cours : estompé, sans être
|
||||
* barré (il n'est pas perdu : « Annuler » le rend). */
|
||||
.die-used { opacity: 0.35; }
|
||||
|
||||
.cube-face { fill: var(--cube); stroke: var(--checker-player-edge); stroke-width: 2; }
|
||||
|
||||
.cube-value {
|
||||
@@ -513,6 +517,39 @@ body.busy .prompt {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------ Confirmation (videau) */
|
||||
|
||||
/* Une carte au milieu de l'écran, comme le glossaire ; le fond de la page
|
||||
* s'assombrit derrière. */
|
||||
.confirm {
|
||||
width: min(22rem, calc(100vw - 2 * var(--space-4)));
|
||||
padding: var(--space-4);
|
||||
color: var(--text);
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-raised);
|
||||
}
|
||||
|
||||
.confirm::backdrop {
|
||||
background: rgba(43, 29, 21, 0.5);
|
||||
}
|
||||
|
||||
.confirm p {
|
||||
margin: 0 0 var(--space-4);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.confirm-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.confirm-actions button {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------- Aide */
|
||||
|
||||
.help {
|
||||
@@ -582,12 +619,6 @@ body.busy .prompt {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
/* Une cible tactile confortable pour le lien. */
|
||||
.about a {
|
||||
display: inline-block;
|
||||
padding-block: 12px;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------- Mode paysage */
|
||||
|
||||
/* Téléphone tourné : le plateau prend toute la hauteur de l'écran, à
|
||||
|
||||
@@ -78,6 +78,8 @@ export const commands = {
|
||||
["--ply", ply, "cube", positionId, matchId],
|
||||
evaluate: (positionId, matchId, ply) =>
|
||||
["--ply", ply, "eval", positionId, matchId],
|
||||
analyseMove: (positionId, matchId, dice, playedPositionId, ply) =>
|
||||
["--ply", ply, "analysemove", positionId, matchId, diceText(dice), playedPositionId],
|
||||
reference: (positionId, matchId, dice, ply) =>
|
||||
["--ply", ply, "reference", positionId, matchId, ...(dice ? [diceText(dice)] : [])],
|
||||
};
|
||||
|
||||
@@ -43,11 +43,20 @@
|
||||
<button id="double" type="button" class="secondary" hidden></button>
|
||||
<button id="take" type="button" hidden></button>
|
||||
<button id="drop" type="button" class="secondary" hidden></button>
|
||||
<button id="confirm" type="button" hidden></button>
|
||||
<button id="cancel" type="button" class="secondary" hidden></button>
|
||||
<button id="skip" type="button" hidden></button>
|
||||
<button id="replay" type="button" class="secondary" hidden></button>
|
||||
<button id="again" type="button" hidden></button>
|
||||
</div>
|
||||
<!-- Confirmation avant de doubler (ouverte par app/ui/app.js). -->
|
||||
<dialog id="double-dialog" class="confirm" aria-labelledby="double-question">
|
||||
<p id="double-question"></p>
|
||||
<div class="confirm-actions">
|
||||
<button id="double-confirm" type="button"></button>
|
||||
<button id="double-cancel" type="button" class="secondary"></button>
|
||||
</div>
|
||||
</dialog>
|
||||
<noscript><p>Ustagammon a besoin de JavaScript.</p></noscript>
|
||||
</main>
|
||||
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
// bench-page.mjs -- page de mesure (bench.html) : chronomètre le moteur
|
||||
// dans un Web Worker et produit le rapport à copier.
|
||||
//
|
||||
// Copyright (C) 2026 les auteurs d'Ustagammon.
|
||||
// Licence : GPLv3 ou ultérieure (voir LICENSE).
|
||||
|
||||
import { DOWNLOADED_FILES, DOWNLOAD_LIMIT_BYTES, formatReport, measureTimes, summarize }
|
||||
from "./benchmark.mjs";
|
||||
import { startEngine } from "./engine-client.mjs";
|
||||
|
||||
const $ = (id) => document.getElementById(id);
|
||||
|
||||
// Taille d'un fichier du site, brute et compressée gzip (comme le ferait un
|
||||
// serveur qui compresse ses réponses).
|
||||
// name est relatif à la racine du site, un dossier au-dessus de cette page.
|
||||
async function fileSize(name) {
|
||||
const blob = await (await fetch("../" + name, { cache: "no-store" })).blob();
|
||||
const gzipped = blob.stream().pipeThrough(new CompressionStream("gzip"));
|
||||
const gzipBytes = (await new Response(gzipped).arrayBuffer()).byteLength;
|
||||
return { name, bytes: blob.size, gzipBytes };
|
||||
}
|
||||
|
||||
function addRow(cells) {
|
||||
const row = $("table").insertRow();
|
||||
for (const text of cells) {
|
||||
row.insertCell().textContent = text;
|
||||
}
|
||||
}
|
||||
|
||||
async function measure() {
|
||||
$("start").disabled = true;
|
||||
$("results").hidden = true;
|
||||
$("table").replaceChildren();
|
||||
|
||||
$("status").textContent = "Chargement du moteur…";
|
||||
const start = performance.now();
|
||||
const engine = startEngine();
|
||||
await engine.ready;
|
||||
const loadMs = performance.now() - start;
|
||||
|
||||
const { positions } = await (await fetch("../wasm/positions.json")).json();
|
||||
let memoryBytes = 0;
|
||||
const times = await measureTimes({
|
||||
bestMove: (...args) => engine.bestMove(...args).then(keepMemory),
|
||||
cubeDecision: (...args) => engine.cubeDecision(...args).then(keepMemory),
|
||||
}, positions, (done, total) => {
|
||||
$("status").textContent = "Calcul " + done + " sur " + total + "…";
|
||||
});
|
||||
function keepMemory(answer) {
|
||||
memoryBytes = answer.memoryBytes;
|
||||
return answer;
|
||||
}
|
||||
|
||||
$("status").textContent = "Mesure de la taille à télécharger…";
|
||||
const files = await Promise.all(DOWNLOADED_FILES.map(fileSize));
|
||||
|
||||
for (const s of summarize(times)) {
|
||||
addRow([s.label, Math.round(s.medianMs) + " ms", Math.round(s.maxMs) + " ms",
|
||||
s.limitMs + " ms", s.ok ? "OK" : "dépassé"]);
|
||||
}
|
||||
const gzipTotal = files.reduce((sum, f) => sum + f.gzipBytes, 0);
|
||||
addRow(["Taille compressée", "", (gzipTotal / 1e6).toFixed(1).replace(".", ",") + " Mo",
|
||||
DOWNLOAD_LIMIT_BYTES / 1e6 + " Mo", gzipTotal < DOWNLOAD_LIMIT_BYTES ? "OK" : "dépassé"]);
|
||||
|
||||
$("report").value = formatReport({
|
||||
environment: navigator.userAgent, files, loadMs, memoryBytes, times,
|
||||
});
|
||||
$("results").hidden = false;
|
||||
$("status").textContent = "Mesure terminée.";
|
||||
}
|
||||
|
||||
// Copie dans le presse-papiers. L'API moderne n'existe que sur les pages
|
||||
// sécurisées (https ou localhost) : sur le réseau local en http, on
|
||||
// sélectionne le texte et on utilise l'ancienne commande de copie.
|
||||
async function copyReport() {
|
||||
try {
|
||||
if (navigator.clipboard) {
|
||||
await navigator.clipboard.writeText($("report").value);
|
||||
} else {
|
||||
$("report").select();
|
||||
if (!document.execCommand("copy")) {
|
||||
throw new Error();
|
||||
}
|
||||
}
|
||||
$("copy-status").textContent = "Rapport copié : collez-le dans la conversation.";
|
||||
} catch {
|
||||
$("report").select();
|
||||
$("copy-status").textContent = "Copie impossible : le texte est sélectionné, "
|
||||
+ "copiez-le à la main (appui long, puis Copier).";
|
||||
}
|
||||
}
|
||||
|
||||
$("start").onclick = () => measure().catch((error) => {
|
||||
$("status").textContent = "La mesure a échoué : " + error.message;
|
||||
$("status").classList.add("error");
|
||||
$("start").disabled = false;
|
||||
});
|
||||
$("copy").onclick = copyReport;
|
||||
@@ -1,35 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Ustagammon : mesure du moteur</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<script type="module" src="bench-page.mjs"></script>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>Mesure du moteur</h1>
|
||||
<p>Cette page chronomètre le moteur sur les positions de référence : meilleur coup
|
||||
aux niveaux rapide et fort, décision de videau au niveau fort. Elle mesure aussi
|
||||
la taille à télécharger et la mémoire utilisée. Comptez une à quelques minutes ;
|
||||
gardez l'écran allumé et la page au premier plan.</p>
|
||||
<button id="start" type="button">Lancer la mesure</button>
|
||||
<p id="status" class="status"></p>
|
||||
|
||||
<section id="results" hidden>
|
||||
<h2>Résultats</h2>
|
||||
<table>
|
||||
<thead><tr><th>Mesure</th><th>Médiane</th><th>Maximum</th><th>Seuil</th><th></th></tr></thead>
|
||||
<tbody id="table"></tbody>
|
||||
</table>
|
||||
<h2>Rapport</h2>
|
||||
<button id="copy" type="button">Copier les résultats</button>
|
||||
<p id="copy-status" class="status"></p>
|
||||
<textarea id="report" readonly></textarea>
|
||||
</section>
|
||||
|
||||
<p><a href="./">Page de test</a></p>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,66 +0,0 @@
|
||||
// engine-client.mjs -- le moteur vu depuis les pages de test : démarre le
|
||||
// Web Worker (../engine-worker.mjs, partagé avec l'application) et lui envoie
|
||||
// les commandes.
|
||||
//
|
||||
// Copyright (C) 2026 les auteurs d'Ustagammon.
|
||||
// Licence : GPLv3 ou ultérieure (voir LICENSE).
|
||||
//
|
||||
// Toutes les fonctions renvoient une promesse de { result, ms, memoryBytes } :
|
||||
// result est l'objet JSON d'engine-cli, ms le temps de calcul mesuré dans le
|
||||
// Worker. La promesse est rejetée si le moteur signale une erreur.
|
||||
|
||||
import { commands } from "../engine-wasm.mjs";
|
||||
|
||||
export function startEngine() {
|
||||
const worker = new Worker(new URL("../engine-worker.mjs", import.meta.url), { type: "module" });
|
||||
const pending = new Map();
|
||||
let nextId = 1;
|
||||
let resolveReady, rejectReady;
|
||||
const ready = new Promise((resolve, reject) => {
|
||||
resolveReady = resolve;
|
||||
rejectReady = reject;
|
||||
});
|
||||
|
||||
worker.onmessage = (event) => {
|
||||
const message = event.data;
|
||||
if (message.type === "ready") {
|
||||
resolveReady(message);
|
||||
} else if (message.type === "failed") {
|
||||
rejectReady(new Error(message.message));
|
||||
} else {
|
||||
const { resolve, reject } = pending.get(message.id);
|
||||
pending.delete(message.id);
|
||||
if (message.error) {
|
||||
reject(new Error(message.error));
|
||||
} else {
|
||||
resolve(message);
|
||||
}
|
||||
}
|
||||
};
|
||||
// Erreur au chargement du Worker lui-même (fichier absent, navigateur
|
||||
// trop ancien pour les Workers en module ES…).
|
||||
worker.onerror = (event) => {
|
||||
rejectReady(new Error(event.message || "le Web Worker du moteur n'a pas démarré"));
|
||||
};
|
||||
|
||||
function run(args) {
|
||||
const id = nextId++;
|
||||
return new Promise((resolve, reject) => {
|
||||
pending.set(id, { resolve, reject });
|
||||
worker.postMessage({ id, args: args.map(String) });
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
// Promesse de { loadMs, memoryBytes } une fois le moteur chargé.
|
||||
ready,
|
||||
bestMove: (positionId, matchId, dice, ply) =>
|
||||
run(commands.bestMove(positionId, matchId, dice, ply)),
|
||||
cubeDecision: (positionId, matchId, ply) =>
|
||||
run(commands.cubeDecision(positionId, matchId, ply)),
|
||||
evaluate: (positionId, matchId, ply) =>
|
||||
run(commands.evaluate(positionId, matchId, ply)),
|
||||
reference: (positionId, matchId, dice, ply) =>
|
||||
run(commands.reference(positionId, matchId, dice, ply)),
|
||||
};
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Ustagammon : test du moteur</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<script type="module" src="page.mjs"></script>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>Test du moteur</h1>
|
||||
<p>Le moteur GNU Backgammon, compilé en WebAssembly, calcule dans le navigateur.</p>
|
||||
<p id="status" class="status">Chargement du moteur…</p>
|
||||
|
||||
<form id="form">
|
||||
<label>Exemple
|
||||
<select id="example"><option value="">(choisir une position de référence)</option></select>
|
||||
</label>
|
||||
<label>Position ID
|
||||
<input id="position-id" required autocomplete="off" spellcheck="false" value="4HPwATDgc/ABMA">
|
||||
</label>
|
||||
<label>Match ID
|
||||
<input id="match-id" required autocomplete="off" spellcheck="false" value="cIkFAAAAAAAA">
|
||||
</label>
|
||||
<div class="row">
|
||||
<label>Dé 1 <input id="die1" type="number" min="1" max="6" required value="3"></label>
|
||||
<label>Dé 2 <input id="die2" type="number" min="1" max="6" required value="1"></label>
|
||||
<label>Niveau
|
||||
<select id="ply">
|
||||
<option value="0">rapide (0 pli)</option>
|
||||
<option value="2">fort (2 plis)</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<button id="best-move" type="submit" disabled>Meilleur coup</button>
|
||||
</form>
|
||||
|
||||
<section id="result" class="result" hidden>
|
||||
<p>Meilleur coup : <strong id="move"></strong></p>
|
||||
<p id="details"></p>
|
||||
<p>Temps de calcul : <strong id="time"></strong></p>
|
||||
</section>
|
||||
|
||||
<p><a href="bench.html">Page de mesure</a> · <a href="../">L'application</a></p>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,71 +0,0 @@
|
||||
// page.mjs -- page de test du moteur (index.html) : meilleur coup d'une
|
||||
// position, avec son temps de calcul.
|
||||
//
|
||||
// Copyright (C) 2026 les auteurs d'Ustagammon.
|
||||
// Licence : GPLv3 ou ultérieure (voir LICENSE).
|
||||
|
||||
import { startEngine } from "./engine-client.mjs";
|
||||
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const engine = startEngine();
|
||||
|
||||
engine.ready.then(
|
||||
({ loadMs }) => {
|
||||
$("status").textContent = "Moteur prêt, chargé en " + Math.round(loadMs) + " ms.";
|
||||
$("best-move").disabled = false;
|
||||
},
|
||||
(error) => {
|
||||
$("status").textContent = "Le moteur n'a pas pu se charger : " + error.message;
|
||||
$("status").classList.add("error");
|
||||
},
|
||||
);
|
||||
|
||||
// Exemples : les positions de référence avec des dés.
|
||||
fetch("../wasm/positions.json")
|
||||
.then((response) => response.json())
|
||||
.then(({ positions }) => {
|
||||
for (const p of positions.filter((p) => p.type === "move")) {
|
||||
const option = new Option(p.id + " : " + p.description, p.id);
|
||||
option.position = p;
|
||||
$("example").add(option);
|
||||
}
|
||||
})
|
||||
.catch(() => {}); // la page reste utilisable sans les exemples
|
||||
|
||||
$("example").onchange = () => {
|
||||
const p = $("example").selectedOptions[0].position;
|
||||
if (p) {
|
||||
$("position-id").value = p.position_id;
|
||||
$("match-id").value = p.match_id;
|
||||
[$("die1").value, $("die2").value] = p.dice;
|
||||
}
|
||||
};
|
||||
|
||||
function showResult(move, details, time) {
|
||||
$("move").textContent = move;
|
||||
$("details").textContent = details;
|
||||
$("time").textContent = time;
|
||||
$("result").hidden = false;
|
||||
}
|
||||
|
||||
$("form").onsubmit = async (event) => {
|
||||
event.preventDefault();
|
||||
$("best-move").disabled = true;
|
||||
$("result").hidden = true;
|
||||
try {
|
||||
const { result, ms } = await engine.bestMove($("position-id").value.trim(),
|
||||
$("match-id").value.trim(), [$("die1").value, $("die2").value], $("ply").value);
|
||||
const best = result.best_move;
|
||||
if (best) {
|
||||
showResult(best.move, "Équité " + best.equity.toFixed(3).replace(".", ",")
|
||||
+ ", chances de gain " + (100 * best.probabilities.win).toFixed(1).replace(".", ",")
|
||||
+ " %, évalué à " + best.ply + " pli(s).", Math.round(ms) + " ms");
|
||||
} else {
|
||||
showResult("aucun coup possible", "", Math.round(ms) + " ms");
|
||||
}
|
||||
} catch (error) {
|
||||
showResult("erreur", error.message, "");
|
||||
} finally {
|
||||
$("best-move").disabled = false;
|
||||
}
|
||||
};
|
||||
@@ -1,113 +0,0 @@
|
||||
/* Pages de test du moteur : mise en forme minimale, lisible sur téléphone. */
|
||||
|
||||
:root {
|
||||
--text: #1d1d1b;
|
||||
--background: #fbfaf7;
|
||||
--muted: #5f5e5a;
|
||||
--accent: #1f5c4a;
|
||||
--error: #a3261b;
|
||||
--border: #cfccc4;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--text: #ecebe6;
|
||||
--background: #1b1c1a;
|
||||
--muted: #a9a8a2;
|
||||
--accent: #7cc4a8;
|
||||
--error: #f08a7e;
|
||||
--border: #4a4a46;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font: 17px/1.5 system-ui, sans-serif;
|
||||
color: var(--text);
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
main {
|
||||
max-width: 40rem;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
input,
|
||||
select,
|
||||
button,
|
||||
textarea {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-top: 0.25rem;
|
||||
padding: 0.5rem;
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
background: transparent;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.row label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
button {
|
||||
margin: 0.5rem 0 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--background);
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
textarea {
|
||||
height: 16rem;
|
||||
font: 13px/1.4 monospace;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 0.25rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.status {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.result {
|
||||
padding: 0.5rem 1rem;
|
||||
border-left: 4px solid var(--accent);
|
||||
}
|
||||
Reference in New Issue
Block a user