Author SHA1 Message Date
Pierre MARTINandClaude Opus 5.5 091cb4a8b0 Fusion de main dans moteur-analyse ; plan 1a clos
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 15:06:09 +01:00
Pierre MARTINandClaude Opus 5.5 fa558667f7 Plan 1a : résultat
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 15:04:50 +01:00
Pierre MARTINandClaude Opus 5.5 cbef27ae9c Fusion de la MR !31 : plan 0e.5, accueil, installation, tutoriel, aide
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 15:02:19 +01:00
Pierre MARTINandClaude Opus 5.5 c061b6d41b Plan 0e.5 : accueil, installation, tutoriel, aide
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 15:02:01 +01:00
Pierre MARTINandClaude Opus 5.5 6b5e0d2b5e Mesure : temps de l'analyse du coup joué à 2 plis
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 15:00:39 +01:00
Pierre MARTINandClaude Opus 5.5 23f4d47fcf Domaine et adaptateur : niveaux, analyses et chance ; cache vidé à chaque calcul
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 14:59:07 +01:00
pierre 31159a5707 Fusion de la MR !30 : accueil, tutoriel, aide 2026-11-03 13:58:27 +00:00
Pierre MARTINandClaude Opus 5.5 f24315fb98 Charte : accueil, installation, tutoriel des règles, aide en questions
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 14:58:25 +01:00
Pierre MARTINandClaude Opus 5.5 2f6eb86972 Tests du moteur : niveaux, analyses et chance
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 14:52:08 +01:00
Pierre MARTINandClaude Opus 5.5 7e958fab8e Références : format des analyses documenté et vérifié
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 14:51:41 +01:00
Pierre MARTINandClaude Opus 5.5 7c1163054a Fusion de la MR !29 : ADR 0013, position dans l'adresse ; plan 0e.4
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 14:51:02 +01:00
Pierre MARTINandClaude Opus 5.5 242a2433d2 Références : analyses de coups, de videau, chance et niveaux, comparées à gnubg
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 10:23:18 +01:00
Pierre MARTINandClaude Opus 5.5 34c3eaeca5 Moteur : niveaux de jeu, analyse d'un coup et du videau, chance d'un jet
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 10:04:51 +01:00
64 changed files with 5614 additions and 116 deletions
+2 -2
View File
@@ -163,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
+135
View File
@@ -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;
}
/**
+132 -2
View File
@@ -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 };
}
+126
View File
@@ -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"],
);
});
+10
View File
@@ -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,
};
}
+4 -3
View File
@@ -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
+1 -1
View File
@@ -25,7 +25,7 @@ Demandes de Pierre du 2026-11-03, traitées avant d'ouvrir l'étape 1.
| --- | --- | --- | --- |
| 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 (ADR 0013) | ⏳ | `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 | ⏳ | |
| 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
+3
View File
@@ -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 |
+14
View File
@@ -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)
+15
View File
@@ -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)
+18
View File
@@ -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)
+1
View File
@@ -14,6 +14,7 @@ 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 |
@@ -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).
@@ -0,0 +1,59 @@
# 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.
`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.
-60
View File
@@ -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.
+199 -10
View File
@@ -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
View File
@@ -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");
+17 -4
View File
@@ -17,6 +17,9 @@ 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.
@@ -35,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
View File
@@ -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
View File
@@ -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
+89 -3
View File
@@ -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.
+283
View File
@@ -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"
}
}
+126
View File
@@ -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"
}
}
+54
View File
@@ -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"
}
}
+142
View File
@@ -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"
}
}
+126
View File
@@ -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"
}
}
+202
View File
@@ -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
View File
@@ -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)
+254
View File
@@ -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
View File
@@ -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
+47
View File
@@ -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 ]
+2
View File
@@ -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)] : [])],
};