Compare commits

...
6 Commits
60 changed files with 5399 additions and 52 deletions
+2 -2
View File
@@ -165,8 +165,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,
};
}
+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)
+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");
+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)] : [])],
};
+1
View File
@@ -43,6 +43,7 @@ async function measure() {
const times = await measureTimes({
bestMove: (...args) => engine.bestMove(...args).then(keepMemory),
cubeDecision: (...args) => engine.cubeDecision(...args).then(keepMemory),
analyseMove: (...args) => engine.analyseMove(...args).then(keepMemory),
}, positions, (done, total) => {
$("status").textContent = "Calcul " + done + " sur " + total + "…";
});
+17 -4
View File
@@ -15,6 +15,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.
@@ -34,19 +37,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)]);
}
+2
View File
@@ -58,6 +58,8 @@ export function startEngine() {
run(commands.bestMove(positionId, matchId, dice, ply)),
cubeDecision: (positionId, matchId, ply) =>
run(commands.cubeDecision(positionId, matchId, ply)),
analyseMove: (positionId, matchId, dice, playedPositionId, ply) =>
run(commands.analyseMove(positionId, matchId, dice, playedPositionId, ply)),
evaluate: (positionId, matchId, ply) =>
run(commands.evaluate(positionId, matchId, ply)),
reference: (positionId, matchId, dice, ply) =>