diff --git a/app/src/domain/analysis/analyse.ts b/app/src/domain/analysis/analyse.ts index 665249b..3f563b8 100644 --- a/app/src/domain/analysis/analyse.ts +++ b/app/src/domain/analysis/analyse.ts @@ -102,10 +102,9 @@ export async function analyseDecision( if (analysis === null) { return null; } - const ranking = - options.ranking === false - ? [] - : await engine.rankMoves(before.position, cube, before.roll, ply); + // Le classement est celui de l'analyse même : mêmes coups, mêmes + // niveaux et mêmes équités que le coup joué et le meilleur coup. + const ranking = options.ranking === false ? [] : analysis.ranking; const start = await engine.board(before.position, cube); const facts = async (move: EvaluatedMove) => factsOf(start.onRoll, start.opponent, await engine.board(move.position, cube)); diff --git a/app/src/domain/analysis/numbers.ts b/app/src/domain/analysis/numbers.ts index 8c29335..3798562 100644 --- a/app/src/domain/analysis/numbers.ts +++ b/app/src/domain/analysis/numbers.ts @@ -7,6 +7,7 @@ // Licence : GPLv3 ou ultérieure (voir LICENSE). import type { AdvisedMove, MoveAdvice } from "../game/advice.js"; +import type { Ply } from "../game/engine.js"; /** Un nombre à la française, signé, à trois décimales : « −0,123 ». */ export function formatEquity(value: number): string { @@ -34,18 +35,21 @@ export interface RankingRow { /** * Le classement d'un conseil tel que l'affiche la feuille du maître : les - * coups du classement du moteur, chacun avec son écart par rapport au - * premier ; puis le coup joué, s'il n'y figure pas, avec l'écart de - * l'analyse du coup joué. + * coups du classement de l'analyse, chacun avec son écart par rapport au + * premier ; puis le coup joué, s'il n'y figure pas. Le coup joué a toujours + * l'écart de l'analyse (equityLoss), celui que dit le maître. */ export function rankingRows(advice: MoveAdvice): RankingRow[] { const first = advice.ranking[0]?.equity ?? advice.best.equity; - const rows = advice.ranking.map((move, i) => ({ - move, - rank: i + 1, - loss: move.equity - first, - played: move.notation === advice.played.notation, - })); + const rows = advice.ranking.map((move, i) => { + const played = move.notation === advice.played.notation; + return { + move, + rank: i + 1, + loss: played ? advice.equityLoss : move.equity - first, + played, + }; + }); if (!rows.some((row) => row.played)) { rows.push({ move: advice.played, @@ -56,3 +60,18 @@ export function rankingRows(advice: MoveAdvice): RankingRow[] { } return rows; } + +/** + * Le niveau du classement d'un conseil : celui auquel le moteur a évalué le + * meilleur coup. C'est le niveau demandé (advice.ply), sauf quand gnubg + * s'arrête avant, parce qu'un seul coup a passé ses filtres : tout le + * classement reste alors au niveau inférieur. + */ +export function rankingPly(advice: MoveAdvice): Ply { + return advice.best.ply; +} + +/** « 1 pli », « 2 plis » : gnubg compte les plis à partir de 0. */ +export function pliesText(ply: Ply): string { + return `${ply} ${ply > 1 ? "plis" : "pli"}`; +} diff --git a/app/src/domain/game/advice.ts b/app/src/domain/game/advice.ts index c9a876c..8eb96d2 100644 --- a/app/src/domain/game/advice.ts +++ b/app/src/domain/game/advice.ts @@ -15,8 +15,10 @@ import type { HalfMove, PositionId } from "./position.js"; import type { Roll } from "./roll.js"; /** Version du format d'un conseil enregistré. Un conseil d'une autre - * version est ignoré à la reprise (il peut être recalculé). */ -export const ADVICE_VERSION = 1; + * version est ignoré à la reprise (il peut être recalculé). Version 2 : le + * classement des coups vient de l'analyse du coup joué, et non plus d'un + * second classement comme la commande « hint » de gnubg. */ +export const ADVICE_VERSION = 2; /** * Le conseil du maître sur une décision du joueur : un coup ou une décision @@ -66,8 +68,8 @@ export interface MoveAdvice { readonly best: AdvisedMove; readonly playedFacts: MoveFacts; readonly bestFacts: MoveFacts; - /** Les meilleurs coups du classement du moteur, du meilleur au moins - * bon (cinq au plus). */ + /** Les meilleurs coups du classement de l'analyse, du meilleur au + * moins bon (cinq au plus), chacun au niveau où le moteur l'a évalué. */ readonly ranking: readonly AdvisedMove[]; /** Équité du coup joué moins celle du meilleur : 0 ou négative. */ readonly equityLoss: number; diff --git a/app/src/domain/game/engine.ts b/app/src/domain/game/engine.ts index e8429e1..b667d17 100644 --- a/app/src/domain/game/engine.ts +++ b/app/src/domain/game/engine.ts @@ -80,17 +80,6 @@ export interface Engine { ply: Ply, ): Promise; - /** Les coups légaux d'un jet classés du meilleur au moins bon, au - * niveau ply, comme la commande « hint » de gnubg (les coups écartés - * par ses filtres gardent l'évaluation d'un niveau inférieur, voir leur - * ply). */ - rankMoves( - position: PositionId, - cube: EngineCube, - roll: Roll, - ply: Ply, - ): Promise; - /** 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). */ @@ -222,6 +211,12 @@ export interface MoveAnalysis { /** Équité du coup joué moins celle du meilleur : 0 ou négative. */ readonly equityLoss: number; readonly skill: Skill; + /** Le classement de l'analyse, du meilleur au moins bon (dix coups au + * plus) : la liste de l'analyse de partie de gnubg, chaque coup au + * niveau où gnubg l'a évalué (ply). Les coups écartés par ses filtres + * gardent l'évaluation d'un niveau inférieur ; le meilleur coup et le + * coup joué y ont les évaluations de best et played. */ + readonly ranking: readonly EvaluatedMove[]; } /** Décision de videau : doubler ou non (joueur au trait), prendre ou passer diff --git a/app/src/domain/prompts/prompts.ts b/app/src/domain/prompts/prompts.ts index 15f82c6..881bca0 100644 --- a/app/src/domain/prompts/prompts.ts +++ b/app/src/domain/prompts/prompts.ts @@ -20,7 +20,13 @@ import { type ReviewedDecision, summarizeAnalysis, } from "../analysis/game-analysis.js"; -import { formatEquity, formatPercent, rankingRows } from "../analysis/numbers.js"; +import { + formatEquity, + formatPercent, + pliesText, + rankingPly, + rankingRows, +} from "../analysis/numbers.js"; import { diceWords, type MasterPhrases, masterWords } from "../analysis/phrases.js"; import { type Advice, @@ -83,7 +89,7 @@ const plural = (n: number, one: string, many: string) => `${n} ${n > 1 ? many : /** « analyse à 2 plis ». */ export function plyText(ply: Ply): string { - return `analyse à ${ply} ${ply > 1 ? "plis" : "pli"}`; + return `analyse à ${pliesText(ply)}`; } /** Nombre de décisions analysées à partir duquel le niveau du joueur se @@ -146,8 +152,14 @@ export function decisionPrompt( niveau_eval: plyText(data.advice.ply), appreciation: masterWords(phrases, data.advice).word, }; + // Le niveau d'un classement de coups : celui de son meilleur coup, que + // le moteur a pu évaluer moins profondément que demandé. return data.advice.kind === "move" - ? fillPrompt(templates.move, { ...common, ...moveValues(data.advice) }) + ? fillPrompt(templates.move, { + ...common, + niveau_eval: plyText(rankingPly(data.advice)), + ...moveValues(data.advice), + }) : fillPrompt(templates.cube, { ...common, ...cubeValues(data.advice) }); } @@ -163,7 +175,10 @@ function moveValues(advice: MoveAdvice): Record { // de « Voir les chiffres du moteur », avec les mêmes chiffres. const rows = rankingRows(advice).filter((row) => row.rank <= 3 || row.played); const ranking = rows.map((row) => { - const ply = row.move.ply === advice.ply ? "" : `, évalué à ${row.move.ply} pli`; + // Un coup écarté par les filtres du moteur garde l'évaluation d'un + // niveau inférieur à celui du classement : la ligne le dit. + const ply = + row.move.ply === rankingPly(advice) ? "" : `, évalué à ${pliesText(row.move.ply)}`; const mine = row.played ? " (mon coup)" : ""; return `${row.rank}. ${row.move.notation}${mine} : équité ${formatEquity(row.move.equity)}, écart ${formatEquity(row.loss)}${ply}`; }); diff --git a/app/src/engine/wasm-engine.ts b/app/src/engine/wasm-engine.ts index 58e73f1..7712aec 100644 --- a/app/src/engine/wasm-engine.ts +++ b/app/src/engine/wasm-engine.ts @@ -182,6 +182,7 @@ export function createWasmEngine(run: RunCommand): Engine { best: CliEvaluatedMove | null; equity_loss: number; skill: CliSkill; + ranking: CliEvaluatedMove[]; }; if (answer.rank === null || answer.played === null || answer.best === null) { return null; @@ -193,17 +194,10 @@ export function createWasmEngine(run: RunCommand): Engine { best: toEvaluatedMove(answer.best), equityLoss: answer.equity_loss, skill: toSkill(answer.skill), + ranking: answer.ranking.map(toEvaluatedMove), }; }, - async rankMoves(position: PositionId, cube: EngineCube, roll: Roll, ply: Ply) { - const args = ["--ply", String(ply), "moves", position, await matchId(cube)]; - const answer = (await run([...args, diceText(roll)])) as { - moves: CliEvaluatedMove[]; - }; - return answer.moves.map(toEvaluatedMove); - }, - async analyseCube( position: PositionId, cube: EngineCube, diff --git a/app/src/ui/advice-texts.ts b/app/src/ui/advice-texts.ts index cc309de..dac6fc0 100644 --- a/app/src/ui/advice-texts.ts +++ b/app/src/ui/advice-texts.ts @@ -26,8 +26,8 @@ export const ADVICE_TEXTS = { replay: "Rejouer le coup", numbers: "Voir les chiffres du moteur", you: "(vous)", - moveNote: (ply: number) => - `Écart d'équité à ${ply} plis, par rapport au meilleur coup. Un écart de 0,03 ou moins reste un bon coup.`, + moveNote: (plies: string) => + `Écart d'équité à ${plies}, par rapport au meilleur coup. Un écart de 0,03 ou moins reste un bon coup.`, cubeNote: (ply: number) => `Équités de celui qui double, pour un videau de 1, à ${ply} plis. Écart de votre décision : `, cubeRows: { diff --git a/app/src/ui/advice-view.ts b/app/src/ui/advice-view.ts index c546657..56656e9 100644 --- a/app/src/ui/advice-view.ts +++ b/app/src/ui/advice-view.ts @@ -7,7 +7,7 @@ // Copyright (C) 2026 les auteurs d'Ustagammon. // Licence : GPLv3 ou ultérieure (voir LICENSE). -import { rankingRows } from "../domain/analysis/numbers.js"; +import { pliesText, rankingPly, rankingRows } from "../domain/analysis/numbers.js"; import { type MasterPhrases, masterWords } from "../domain/analysis/phrases.js"; import { type Advice, @@ -304,7 +304,7 @@ export function createAdviceView(options: AdviceViewOptions): AdviceView { } element(details, "p", { className: "explain-note", - text: ADVICE_TEXTS.moveNote(advice.ply), + text: ADVICE_TEXTS.moveNote(pliesText(rankingPly(advice))), }); } else { // Les lignes de votre décision, en gras : doubler mène à l'une des diff --git a/app/tests/engine-analysis.test.ts b/app/tests/engine-analysis.test.ts index ddb1241..02f76f5 100644 --- a/app/tests/engine-analysis.test.ts +++ b/app/tests/engine-analysis.test.ts @@ -35,6 +35,35 @@ test("ouverture 3-1 : 24/23 13/10 est une erreur très mauvaise, 8/5 6/5 le meil Math.abs(analysis.equityLoss - (analysis.played.equity - analysis.best.equity)) < 1e-6, ); assert.ok(analysis.equityLoss < -SKILL_THRESHOLDS.veryBad); + // Le classement de l'analyse, comme gnubg de bureau + // (tests/reference/analysis/move-opening-31-error.json) : le meilleur + // coup et le coup joué, une erreur, réévalués à 2 plis ; les suivants + // restés à 1 pli, puis à 0, écartés par les filtres. + assert.deepEqual(analysis.ranking[0], analysis.best); + assert.deepEqual(analysis.ranking[1], analysis.played); + assert.deepEqual( + analysis.ranking.map((move) => move.ply), + [2, 2, 1, 1, 1, 0, 0, 0, 0, 0], + ); +}); + +test("ouverture 3-1 jouée 8/5 6/5 : un seul coup passe le filtre, tout reste à 1 pli", async () => { + const engine = await loadNodeEngine(); + const analysis = await engine.analyseMove( + START_POSITION, + centered, + [3, 1], + "4HPwATCwZ/ABMA", + 2, + ); + assert.ok(analysis !== null); + assert.equal(analysis.rank, 1); + assert.deepEqual(analysis.ranking[0], analysis.best); + assert.deepEqual(analysis.best, analysis.played); + assert.deepEqual( + analysis.ranking.slice(0, 5).map((move) => move.ply), + [1, 1, 1, 1, 1], + ); }); test("sans coup possible, il n'y a rien à analyser", async () => { diff --git a/app/tests/game.test.ts b/app/tests/game.test.ts index 77ede5f..901657f 100644 --- a/app/tests/game.test.ts +++ b/app/tests/game.test.ts @@ -73,7 +73,6 @@ function fakeEngine(overrides: Partial = {}): Engine { }; }, analyseMove: async () => null, - rankMoves: async () => [], analyseCube: async () => ({ analysed: false, equities: null, diff --git a/app/tests/prompts.test.ts b/app/tests/prompts.test.ts index 3583492..fe7b40c 100644 --- a/app/tests/prompts.test.ts +++ b/app/tests/prompts.test.ts @@ -239,7 +239,97 @@ test("prompt d'un coup : la position, les identifiants et le top 3 du moteur, m assert.ok(shown.includes(`${row.move.notation}|${formatEquity(row.loss)}`)); } assert.ok(prompt.includes(`1. ${advice.best.notation}`), prompt); - assert.ok(prompt.includes("analyse à 2 plis"), prompt); + assert.match(prompt, /\(analyse à [12] plis?\)/); + } +}); + +/** Une équité écrite (« −0,212 ») proche d'une équité de gnubg de bureau : + * à l'arrondi près, plus la tolérance des positions de référence (0,002 ; + * tests/reference/README.md), le moteur WebAssembly ne calculant pas au + * bit près comme gnubg de bureau (ADR 0005). */ +function assertNear(text: string, value: number, message: string): void { + const written = Number(text.replace("−", "-").replace("+", "").replace(",", ".")); + assert.ok(Math.abs(written - value) <= 0.0005 + 0.002, `${text} ≠ ${value}\n${message}`); +} + +/** Le niveau du titre du classement, et chaque ligne du classement : coup, + * équité, écart, niveau propre (null : celui du titre). */ +function rankingOfPrompt(prompt: string) { + const title = /^Classement de GNU Backgammon \(analyse à (\d) plis?\)/m.exec(prompt); + assert.ok(title, prompt); + const lines = [ + ...prompt.matchAll( + /^(\d+)\. (.+?)( \(mon coup\))? : équité (\S+), écart (\S+?)(?:, évalué à (\d) plis?)?$/gm, + ), + ].map((m) => ({ + rank: Number(m[1]), + notation: m[2], + mine: m[3] !== undefined, + equity: m[4], + loss: m[5], + ply: m[6] === undefined ? null : Number(m[6]), + })); + return { ply: Number(title[1]), lines }; +} + +test("prompt d'un coup : le niveau du titre est celui des lignes, les équités celles de l'analyse", async () => { + const engine = await loadNodeEngine(); + // gnubg de bureau, analyse de partie à 2 plis (tests/reference/analysis/). + const reference = (id: string) => + JSON.parse(readFileSync(new URL(`tests/reference/analysis/${id}.json`, root), "utf8")) + .plies["2"]; + for (const [dice, move, id] of [ + // Erreur : gnubg réévalue à 2 plis le meilleur coup et le coup joué ; + // les autres restent au niveau où ses filtres les ont laissés. + [[3, 1], "24/23 13/10", "move-opening-31-error"], + // Meilleur coup : un seul coup passe le filtre de 1 pli, gnubg + // s'arrête là, tout le classement est à 1 pli. + [[3, 1], "8/5 6/5", "move-opening-31-best"], + [[6, 5], "13/8 13/7", "move-opening-65-error"], + ] as const) { + const played = await playOpening(engine, dice, move); + const advice = await adviceOn(engine, played); + assert.ok(advice.kind === "move"); + played.game.recordAdvice(advice); + const position = await positionBefore(engine, played.game.journal, played.eventId); + const prompt = decisionPrompt(templates, phrases, { + journal: played.game.journal, + advice, + position, + }); + const { ply, lines } = rankingOfPrompt(prompt); + const expected = reference(id); + + // Le titre dit le niveau du meilleur coup, celui que gnubg de bureau + // donne ; une ligne sans niveau propre est à ce niveau, une ligne + // avec un niveau propre, à un niveau inférieur. + assert.equal(ply, expected.ranking[0].ply, prompt); + assert.equal(ply, advice.best.ply); + assert.ok(lines.length >= 3, prompt); + for (const line of lines) { + const reference = expected.ranking.find( + (m: { move: string }) => m.move === line.notation, + ); + assert.ok(reference, `${line.notation} absent du classement de gnubg`); + assert.equal(line.ply ?? ply, reference.ply, `${line.notation}\n${prompt}`); + assert.ok(line.ply === null || line.ply < ply, prompt); + assertNear(line.equity, reference.equity, `${line.notation}\n${prompt}`); + } + + // Les équités et l'écart du meilleur coup et du vôtre sont ceux de + // l'analyse, que disent la feuille du maître et l'écran d'analyse. + const best = lines[0]; + const mine = lines.find((line) => line.mine); + assert.ok(best && mine, prompt); + assert.equal(best.notation, advice.best.notation); + assert.equal(best.equity, formatEquity(advice.best.equity)); + assert.equal(best.loss, formatEquity(0)); + assert.equal(mine.notation, advice.played.notation); + assert.equal(mine.rank, advice.rank); + assert.equal(mine.equity, formatEquity(advice.played.equity)); + assert.equal(mine.loss, formatEquity(advice.equityLoss)); + assertNear(mine.loss, expected.equity_loss, prompt); + assert.ok(shownRanking(advice).includes(`${mine.notation}|${mine.loss}`)); } }); diff --git a/app/tests/storage.test.ts b/app/tests/storage.test.ts index a6bc7ec..1f4ed8d 100644 --- a/app/tests/storage.test.ts +++ b/app/tests/storage.test.ts @@ -222,6 +222,57 @@ test("les identifiants de partie sont distincts", () => { assert.match(newGameId(), /^[0-9a-z]+-[0-9a-z]{6}$/); }); +// Régression du test navigateur « mes parties » sous Firefox (disque lent) : +// la nouvelle partie n'est pas perdue quand son enregistrement attend +// derrière ceux, lents, de l'analyse de la partie finie ; seule une lecture +// directe du stockage, hors de la file, peut la précéder. +test("stockage lent : la nouvelle partie est enregistrée après l'analyse de l'ancienne", async () => { + const storage = memoryStorage(); + const raw = createKeyValueBackend(storage); + const pause = () => new Promise((resolve) => setTimeout(resolve, 30)); + const slow = { + ...raw, + write: async (...args: Parameters) => { + await pause(); + await raw.write(...args); + }, + setCurrentId: async (id: string) => { + await pause(); + await raw.setCurrentId(id); + }, + }; + const games = createGameLibrary(slow, { now: clock() }); + const engine = {} as Engine; + const session = createGameSession(games, { + start: () => Game.start(engine, { dice: fixedDice([5, 2]) }), + resume: (j) => Game.resume(engine, j), + }); + const finished = session.newGame(); + const finishedId = session.shownId as string; + session.save(finished.journal); + assert.equal((await games.current())?.summary.id, finishedId); + + // L'analyse de la partie finie s'enregistre en arrière-plan, puis + // « Rejouer » : nouvelle partie, enregistrée sans attendre. + for (let i = 0; i < 5; i++) { + void games.saveAnalyses(finishedId, finished.journal, { level: "2ply" }); + } + const next = session.newGame(); + session.save(next.journal); + const nextId = session.shownId as string; + assert.notEqual(nextId, finishedId); + + // Lue directement, tout de suite : encore l'ancienne partie. + assert.equal(await raw.currentId(), finishedId); + // Lue par la bibliothèque, après les écritures en attente : la nouvelle, + // avec son journal ; l'ancienne reste là. + const current = await games.current(); + assert.equal(current?.summary.id, nextId); + assert.deepEqual(current?.journal, next.journal); + assert.equal(await raw.currentId(), nextId); + assert.deepEqual((await games.load(finishedId))?.journal, finished.journal); +}); + test("session : « Reprendre l'ancienne » par l'identifiant, et reprise d'une branche", async () => { const games = library(); // Game.start et Game.resume n'interrogent pas le moteur. diff --git a/docs/adr/0007-architecture-et-conventions-de-code.md b/docs/adr/0007-architecture-et-conventions-de-code.md index f16099b..1d7fe97 100644 --- a/docs/adr/0007-architecture-et-conventions-de-code.md +++ b/docs/adr/0007-architecture-et-conventions-de-code.md @@ -184,3 +184,21 @@ a plus besoin (`app/src/ui/review-runner.ts`). La bibliothèque gagne `saveAnalyses`, qui enregistre le journal analysé d'une partie sans en faire la partie en cours ni changer la date de sa dernière action (l'ordre de « Mes parties »). + +### 2026-11-11 — Version 2 des conseils : le classement de l'analyse + +Le classement des coups d'un conseil (`ranking`) venait d'un second appel au +moteur, comme la commande « hint » de gnubg, alors que le coup joué et le +meilleur coup venaient de l'analyse de partie. Les deux listes diffèrent : +quand le coup joué est une erreur, l'analyse réévalue le meilleur coup et le +coup joué au niveau demandé, ce que « hint » ne fait pas quand un seul coup +passe ses filtres (ouverture 3-1 jouée 24/23 13/10 : 8/5 6/5 à +0,200 à +2 plis dans l'analyse, +0,204 à 1 pli dans « hint »). Le classement vient +désormais de l'analyse même (`engine_analyse_move`, champ `ranking`), comme +le panneau d'analyse de gnubg de bureau. + +- `ADVICE_VERSION` passe à 2 (`app/src/domain/game/advice.ts`). Selon la + règle de l'amendement du 2026-11-10, un conseil de la version 1 est oublié + à la reprise et recalculé en arrière-plan ; les analyses de l'ordinateur + (`review.computer`), du même format, aussi. +- Ni les événements (version 2) ni le schéma IndexedDB ne changent. diff --git a/docs/prompts/README.md b/docs/prompts/README.md index 1758b5b..4a2a905 100644 --- a/docs/prompts/README.md +++ b/docs/prompts/README.md @@ -54,5 +54,5 @@ Repères communs à plusieurs gabarits : | `{diagramme}` | le plateau en texte : vos pions X, ceux de l'ordinateur O, dans votre numérotation | | `{score}` | « partie libre » ou le score du match | | `{videau}` | sa valeur et qui le possède | -| `{niveau_eval}` | le niveau d'évaluation de GNU Backgammon, « analyse à 2 plis » | +| `{niveau_eval}` | le niveau d'évaluation de GNU Backgammon, « analyse à 2 plis » ; pour un classement de coups, celui auquel le moteur a évalué le meilleur coup (voir [coup.md](coup.md)) | | `{langue}` | la langue de la réponse : « français » | diff --git a/docs/prompts/coup.md b/docs/prompts/coup.md index 47fc1a5..d54b028 100644 --- a/docs/prompts/coup.md +++ b/docs/prompts/coup.md @@ -12,7 +12,15 @@ Repères propres à ce gabarit : | `{coup_joue}` | votre coup, dans la notation de GNU Backgammon | | `{appreciation}` | le mot du maître : « Bon coup », « Imprécision », « Erreur » ou « Grosse erreur » | | `{rang}` | « le meilleur coup » ou « le 4e choix sur 15 coups possibles » | -| `{classement}` | le classement du moteur : les trois meilleurs coups, et le vôtre s'il n'en est pas, avec leur équité et leur écart au meilleur, comme dans « Voir les chiffres du moteur » | +| `{classement}` | le classement de l'analyse de votre coup : les trois meilleurs coups, et le vôtre s'il n'en est pas, avec leur équité et leur écart au meilleur, comme dans « Voir les chiffres du moteur » | + +Le niveau du titre (`{niveau_eval}`) est celui auquel GNU Backgammon a +évalué le meilleur coup. C'est d'ordinaire le niveau de l'analyse (2 plis), +mais le moteur s'arrête plus tôt quand un seul coup passe ses filtres : à +l'ouverture 3-1 jouée 8/5 6/5, tout le classement est à 1 pli. Les coups que +ses filtres ont écartés gardent l'évaluation d'un niveau inférieur, et leur +ligne le dit (« évalué à 1 pli »). Les équités et l'écart de votre coup sont +ceux de l'analyse, que disent la feuille du maître et l'écran d'analyse. ## Gabarit diff --git a/engine-cli/main.c b/engine-cli/main.c index bd05cda..421133e 100644 --- a/engine-cli/main.c +++ b/engine-cli/main.c @@ -43,7 +43,8 @@ * 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) + * moves et legal), avec le classement + * de l'analyse (dix coups au plus) * analysecube POSITION_ID MATCH_ID DÉCISION * analyse d'une décision de videau : * no_double ou double (joueur au trait), @@ -684,7 +685,7 @@ run_analyse_move_command(const options *opt) const char *played_id = opt->args[3]; engine_move_analysis a; engine_status status; - int die1, die2; + int die1, die2, i; if (opt->arg_count != 4) return usage_error("arguments attendus : POSITION_ID MATCH_ID DÉS POSITION_JOUÉE"); @@ -705,6 +706,13 @@ run_analyse_move_command(const options *opt) printf(", \"best\": "); print_move(&a.best, 1); } + printf(", \"ranking\": ["); + for (i = 0; i < a.ranking_count; i++) { + if (i > 0) + printf(", "); + print_move(&a.ranking[i], 1); + } + printf("]"); printf(", \"equity_loss\": " NUMBER ", \"skill\": \"%s\"}\n", a.equity_loss, skill_names[a.skill]); return EXIT_SUCCESS; diff --git a/engine/engine.c b/engine/engine.c index d038d74..b40be99 100644 --- a/engine/engine.c +++ b/engine/engine.c @@ -512,6 +512,8 @@ engine_level_best_moves(const char *position_id, const char *match_id, int die1, /* ------------------------------------------------- Analyse d'un coup joué */ +static float luck_analysis(const char *position_id, position *pos, int die1, int die2); + 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) @@ -546,6 +548,17 @@ engine_analyse_move(const char *position_id, const char *match_id, int die1, int * premier. */ memset(analysis, 0, sizeof *analysis); + /* + * gnubg analyse la chance du jet juste avant le coup (AnalyzeMove, + * l. 679-682), à 0 pli sans élagage, et garde ces évaluations dans son + * cache, qui ne distingue pas une évaluation à 0 pli avec ou sans + * réseaux d'élagage : le tri à 0 pli de FindnSaveBestMoves les reprend + * ensuite, ce qui change l'équité des coups restés à 0 pli, et parfois + * ceux que les filtres retiennent. Faire de même, sans garder le + * résultat, donne le classement de gnubg de bureau. + */ + if (luck_analysis(position_id, &pos, die1, die2) == ERR_VAL) + return ENGINE_ERROR_INTERNAL; if (FindnSaveBestMoves(&list, die1, die2, (ConstTanBoard) pos.board, &played_key, TRUE, DOUBTFUL_THRESHOLD, &pos.cube, &context, aaamfMoveFilterSettings[0]) < 0) @@ -565,6 +578,10 @@ engine_analyse_move(const char *position_id, const char *match_id, int die1, int status = ENGINE_OK; break; } + /* Le classement : le début de la même liste, dans l'ordre de gnubg. */ + for (i = 0; i < list.cMoves && i < ENGINE_ANALYSIS_RANKING; i++) + fill_move(&analysis->ranking[i], &list.amMoves[i], &pos, 1); + analysis->ranking_count = (int) i; g_free(list.amMoves); return status; } @@ -1005,13 +1022,25 @@ luck_first(ConstTanBoard board, int i0, int j0, cubeinfo *cube) return equities[i0][j0] - mean / 30.0f; } +/* LuckAnalysis (analysis.c l. 252-268) : dés rangés du plus fort au plus + * faible ; LuckFirst à la position de départ hors doubles. */ +static float +luck_analysis(const char *position_id, position *pos, int die1, int die2) +{ + int high = (die1 > die2 ? die1 : die2) - 1; + int low = (die1 > die2 ? die2 : die1) - 1; + + if (strcmp(position_id, START_POSITION_ID) == 0 && high != low) + return luck_first((ConstTanBoard) pos->board, high, low, &pos->cube); + return luck_normal((ConstTanBoard) pos->board, high, low, &pos->cube); +} + 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); @@ -1021,14 +1050,7 @@ engine_luck_of(const char *position_id, const char *match_id, int die1, int die2 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); + value = luck_analysis(position_id, &pos, die1, die2); if (value == ERR_VAL) return ENGINE_ERROR_INTERNAL; luck->luck = value; diff --git a/engine/include/engine.h b/engine/include/engine.h index 4e99ed4..66197ad 100644 --- a/engine/include/engine.h +++ b/engine/include/engine.h @@ -207,6 +207,10 @@ typedef struct { * (double_take <= double_pass) */ } engine_cube; +/* Coups gardés dans le classement d'une analyse : les dix premiers, comme + * les positions de référence (tests/reference/README.md). */ +#define ENGINE_ANALYSIS_RANKING 10 + /* 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 @@ -218,6 +222,14 @@ typedef struct { 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 */ + /* Le classement de l'analyse, du meilleur au moins bon : les premiers + * coups de la liste que gnubg garde pour l'analyse de partie (ce que + * montre son panneau d'analyse), chacun au niveau où gnubg l'a + * évalué. Le meilleur coup et le coup joué y ont les mêmes évaluations + * que best et played ; le coup joué n'y figure que s'il est parmi les + * premiers. */ + engine_move ranking[ENGINE_ANALYSIS_RANKING]; + int ranking_count; } engine_move_analysis; /* Analyse d'une décision de videau, comme l'analyse de partie de gnubg. */ @@ -452,7 +464,15 @@ 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 + * filtre « Normal », sans RefreshMoveList), classement compris. + * + * Niveaux : gnubg n'évalue au niveau ply que les coups retenus par ses + * filtres ; les autres gardent l'évaluation d'un niveau inférieur (champ + * ply de chaque coup, même convention que gnubg de bureau : 0 = statique). + * Si un seul coup passe le filtre du niveau ply - 1, gnubg s'arrête là, et + * tout le classement reste à ce niveau, sauf quand le coup joué est une + * erreur (écart au meilleur supérieur au seuil « douteux ») : le meilleur + * coup et le coup joué sont alors réévalués au niveau ply. 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. diff --git a/tests/browser/play.mjs b/tests/browser/play.mjs index 2b47443..ea75380 100644 --- a/tests/browser/play.mjs +++ b/tests/browser/play.mjs @@ -164,6 +164,45 @@ async function readJournal() { ); } +/** L'identifiant de la partie en cours enregistrée dans IndexedDB + * (magasin settings, clé « current »), ou null. */ +async function savedGameId() { + return page.evaluate( + () => + new Promise((resolve, reject) => { + const open = indexedDB.open("ustagammon"); + open.onerror = () => reject(open.error); + open.onsuccess = () => { + const db = open.result; + const current = db.transaction("settings").objectStore("settings").get("current"); + current.onsuccess = () => { + db.close(); + resolve(current.result?.value ?? null); + }; + }; + }), + ); +} + +/** + * Attend que la partie en cours enregistrée ne soit plus oldId, et renvoie + * le temps d'attente en ms (null au bout de 30 s). L'application enregistre + * sans attendre l'écriture, dans une file (app/src/storage/game-library.ts) : + * sous Firefox, sur un disque lent et chargé, l'enregistrement de la + * nouvelle partie peut attendre derrière ceux de l'analyse de la partie + * finie, et une lecture directe d'IndexedDB trouver encore l'ancienne. + */ +async function waitForSavedGameOtherThan(oldId) { + const start = Date.now(); + while (Date.now() - start < 30_000) { + if ((await savedGameId()) !== oldId) { + return Date.now() - start; + } + await sleep(100); + } + return null; +} + /** Le nombre de parties enregistrées dans IndexedDB. */ async function savedGameCount() { await savedJournal(); // les enregistrements en cours terminés @@ -587,17 +626,41 @@ async function checkTraces() { * (les dés en ont une de chaque côté du plateau), chacune un bouton * accessible d'au moins 44 px de côté. Renvoie le locator de la zone index. */ async function touchTarget(action, label, count = 1, index = 0) { - const zones = page.locator(`[data-action="${action}"]`); - check((await zones.count()) === count, - `zone ${action} : ${await zones.count()} sur le plateau, ${count} attendues`); - for (const zone of await zones.all()) { - check((await zone.getAttribute("role")) === (action === "menu" ? "link" : "button") && - (await zone.getAttribute("aria-label")) === label, `zone ${action} sans rôle accessible`); - const box = await zone.boundingBox(); - check(box.width >= 44 && box.height >= 44, - `zone ${action} trop petite : ${Math.round(box.width)} x ${Math.round(box.height)} px`); + // Les zones sont relevées d'un coup dans la page, où le plateau ne peut + // pas se redessiner entre deux lectures : mesurées une à une depuis + // Playwright (boundingBox), une zone remplacée entre-temps par un + // nouveau dessin du plateau n'avait plus de boîte (null, vu sous + // Firefox). On attend l'état observable : le nombre de zones attendu, + // toutes dessinées (5 s au plus). + const read = () => + page.evaluate( + (name) => + [...document.querySelectorAll(`[data-action="${name}"]`)].map((zone) => { + const box = zone.getBoundingClientRect(); + return { + role: zone.getAttribute("role"), + label: zone.getAttribute("aria-label"), + width: box.width, + height: box.height, + }; + }), + action, + ); + let zones = await read(); + const start = Date.now(); + while ((zones.length !== count || zones.some((z) => z.width === 0 && z.height === 0)) && + Date.now() - start < 5000) { + await sleep(100); + zones = await read(); } - return zones.nth(index); + check(zones.length === count, `zone ${action} : ${zones.length} sur le plateau, ${count} attendues`); + for (const zone of zones) { + check(zone.role === (action === "menu" ? "link" : "button") && zone.label === label, + `zone ${action} sans rôle accessible`); + check(zone.width >= 44 && zone.height >= 44, + `zone ${action} trop petite : ${Math.round(zone.width)} x ${Math.round(zone.height)} px`); + } + return page.locator(`[data-action="${action}"]`).nth(index); } /** Le nombre de dés montrés, et de dés estompés (déjà joués). */ @@ -1026,6 +1089,7 @@ async function rematch() { * immédiat, bandeau 10 s. Renvoie le journal de la partie quittée. */ async function newGameFromMenu() { const before = await savedJournal(); + const beforeId = await savedGameId(); const started = () => page.evaluate(() => JSON.parse(localStorage.getItem("ustagammon.settings") ?? "{}").gamesStarted); const startedBefore = await started(); @@ -1038,6 +1102,8 @@ async function newGameFromMenu() { `bandeau : « ${await page.textContent("#banner")} »`); check(!(await page.isVisible("#menu")), "le menu reste ouvert après « Nouvelle partie »"); await waitForPlayer(); + check((await waitForSavedGameOtherThan(beforeId)) !== null, + "« Nouvelle partie » : la partie n'a pas été enregistrée (30 s)"); const after = await savedJournal(); // (Les identifiants d'événements se suivent : une partie neuve peut avoir // la même tête que la précédente ; on compte les parties commencées.) @@ -1617,11 +1683,17 @@ step("mes parties"); // recharger la page. { const finished = await savedJournal(); + const finishedId = await savedGameId(); await rematch(); await waitForPlayer(); + // L'état observable, et non un délai : la nouvelle partie devient la + // partie en cours enregistrée. + const waited = await waitForSavedGameOtherThan(finishedId); + check(waited !== null, "la nouvelle partie n'a pas été enregistrée (30 s)"); + log(`nouvelle partie enregistrée ${waited} ms après la main au joueur`); const second = await savedJournal(); check(second.head !== finished.head || second.events.length !== finished.events.length, - "la nouvelle partie n'a pas été enregistrée"); + "la nouvelle partie enregistrée a le journal de la partie finie"); await openMenu(); await page.click("#menu-games"); await page.waitForSelector("#games-veil:not([hidden]) .games-item"); @@ -1744,6 +1816,12 @@ if (offline === "roll") { await page.click("#roll"); await waitForPlayer(); } +// Si la partie reprise est finie, son bilan se rouvre au rechargement et +// couvre l'écran (c'est voulu) : on le ferme avant d'ouvrir l'aide. +if (await page.locator("#summary-veil").isVisible()) { + await page.click("#summary-close"); + await page.waitForSelector("#summary-veil", { state: "hidden" }); +} await page.click("#help-button"); await page.waitForSelector(".glossary dt", { state: "attached" }); await page.click("#help-back"); diff --git a/tests/reference/README.md b/tests/reference/README.md index 31b7f88..302285a 100644 --- a/tests/reference/README.md +++ b/tests/reference/README.md @@ -256,7 +256,11 @@ Un fichier de résultat reprend le cas, avec `type` (`move`, `cube` ou 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 + `ranking` (les dix premiers coups, et jusqu'au coup joué, chacun avec + `move`, `ply` et `equity` : comme pour `hint`, `ply` est le niveau auquel + gnubg a réellement évalué le coup ; à la différence de `hint`, l'analyse + réévalue au niveau demandé le meilleur coup et le coup joué quand celui-ci + est une erreur, au-delà du seuil « douteux ») ; 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 @@ -337,6 +341,10 @@ Pour chaque analyse (`engine-cli analysemove`, `analysecube`, `luck`, et é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 ; +- classement de l'analyse (`ranking`, dix coups au plus) : chaque coup de + gnubg présent (sauf égalité avec le dixième), au même `ply` et à la même + équité à la tolérance près ; son premier coup et le coup joué y sont + exactement `best` et `played` ; - 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 ; diff --git a/tests/reference/analysis/move-bearoff-doubles.json b/tests/reference/analysis/move-bearoff-doubles.json index cb72f8f..115bd53 100644 --- a/tests/reference/analysis/move-bearoff-doubles.json +++ b/tests/reference/analysis/move-bearoff-doubles.json @@ -25,6 +25,7 @@ "ranking": [ { "move": "6/off(2) 4/off 1/off", + "ply": 0, "equity": 0.8888838 } ] @@ -42,6 +43,7 @@ "ranking": [ { "move": "6/off(2) 4/off 1/off", + "ply": 0, "equity": 0.8888838 } ] diff --git a/tests/reference/analysis/move-blitz-doubles.json b/tests/reference/analysis/move-blitz-doubles.json index 3360416..0d73843 100644 --- a/tests/reference/analysis/move-blitz-doubles.json +++ b/tests/reference/analysis/move-blitz-doubles.json @@ -25,438 +25,547 @@ "ranking": [ { "move": "13/10 6/3 4/1*(2)", + "ply": 0, "equity": 0.9465638 }, { "move": "8/5 6/3 4/1*(2)", + "ply": 0, "equity": 0.9422797 }, { "move": "13/7 13/10 6/3", + "ply": 0, "equity": 0.94128984 }, { "move": "13/7 8/5 6/3", + "ply": 0, "equity": 0.9370698 }, { "move": "13/10(2) 7/4 6/3", + "ply": 0, "equity": 0.93615985 }, { "move": "13/10(2) 8/5 6/3", + "ply": 0, "equity": 0.9330082 }, { "move": "13/10 8/5 7/4 6/3", + "ply": 0, "equity": 0.932737 }, { "move": "13/10 7/1* 6/3", + "ply": 0, "equity": 0.9313148 }, { "move": "8/5 7/1* 6/3", + "ply": 0, "equity": 0.93129885 }, { "move": "13/4 6/3", + "ply": 0, "equity": 0.92980635 }, { "move": "13/10 8/5(2) 6/3", + "ply": 0, "equity": 0.9264537 }, { "move": "13/10(3) 6/3", + "ply": 0, "equity": 0.9262361 }, { "move": "13/10(2) 4/1*(2)", + "ply": 0, "equity": 0.92529553 }, { "move": "8/5(2) 7/4 6/3", + "ply": 0, "equity": 0.9252107 }, { "move": "7/1* 6/3 4/1", + "ply": 0, "equity": 0.92395884 }, { "move": "13/10 6/3 5/2(2)", + "ply": 0, "equity": 0.9204093 }, { "move": "13/7 4/1*(2)", + "ply": 0, "equity": 0.9190106 }, { "move": "13/7(2)", + "ply": 0, "equity": 0.9184398 }, { "move": "13/10 8/5 4/1*(2)", + "ply": 0, "equity": 0.9182539 }, { "move": "7/4 6/3 5/2(2)", + "ply": 0, "equity": 0.91752934 }, { "move": "13/7 13/10(2)", + "ply": 0, "equity": 0.91483337 }, { "move": "13/10 8/2 6/3", + "ply": 0, "equity": 0.912639 }, { "move": "13/7 13/10 8/5", + "ply": 0, "equity": 0.91152656 }, { "move": "13/10(2) 7/1*", + "ply": 0, "equity": 0.9115156 }, { "move": "13/1*", + "ply": 0, "equity": 0.9111869 }, { "move": "13/10(2) 6/3 4/1*", + "ply": 0, "equity": 0.91055506 }, { "move": "8/2 8/5 6/3", + "ply": 0, "equity": 0.9091699 }, { "move": "8/2 7/4 6/3", + "ply": 0, "equity": 0.90897554 }, { "move": "13/7 8/5(2)", + "ply": 0, "equity": 0.90817225 }, { "move": "13/7 6/3 4/1*", + "ply": 0, "equity": 0.90714073 }, { "move": "13/10(2) 8/5 7/4", + "ply": 0, "equity": 0.9070756 }, { "move": "8/2(2)", + "ply": 0, "equity": 0.9065211 }, { "move": "13/10 8/5 7/1*", + "ply": 0, "equity": 0.90571 }, { "move": "13/10(3) 7/4", + "ply": 0, "equity": 0.9040942 }, { "move": "13/10 8/5 6/3 4/1*", + "ply": 0, "equity": 0.90343916 }, { "move": "8/5(2) 4/1*(2)", + "ply": 0, "equity": 0.90325713 }, { "move": "5/2(2) 4/1*(2)", + "ply": 0, "equity": 0.90183336 }, { "move": "13/10 7/1* 4/1", + "ply": 0, "equity": 0.9013542 }, { "move": "13/4 7/4", + "ply": 0, "equity": 0.9010268 }, { "move": "13/7 5/2(2)", + "ply": 0, "equity": 0.90037674 }, { "move": "8/5(2) 7/1*", + "ply": 0, "equity": 0.89956194 }, { "move": "13/10 8/5(2) 7/4", + "ply": 0, "equity": 0.8988826 }, { "move": "8/2 6/3 5/2", + "ply": 0, "equity": 0.8984738 }, { "move": "13/4 13/10", + "ply": 0, "equity": 0.89749956 }, { "move": "6/3(2) 4/1*(2)", + "ply": 0, "equity": 0.8951012 }, { "move": "13/10(2) 8/5(2)", + "ply": 0, "equity": 0.89493525 }, { "move": "13/7 8/2", + "ply": 0, "equity": 0.8946096 }, { "move": "13/7 6/3(2)", + "ply": 0, "equity": 0.89403474 }, { "move": "7/1* 5/2(2)", + "ply": 0, "equity": 0.8924208 }, { "move": "8/5 7/1* 4/1", + "ply": 0, "equity": 0.891961 }, { "move": "13/10(2) 6/3(2)", + "ply": 0, "equity": 0.89077055 }, { "move": "13/4 8/5", + "ply": 0, "equity": 0.8907301 }, { "move": "8/5(2) 6/3 4/1*", + "ply": 0, "equity": 0.8900244 }, { "move": "13/10(2) 5/2(2)", + "ply": 0, "equity": 0.8890666 }, { "move": "7/4 6/3(3)", + "ply": 0, "equity": 0.8890004 }, { "move": "13/10 7/4 5/2(2)", + "ply": 0, "equity": 0.8888954 }, { "move": "8/2 8/5 7/4", + "ply": 0, "equity": 0.8876569 }, { "move": "13/10(4)", + "ply": 0, "equity": 0.88716996 }, { "move": "7/1* 6/3(2)", + "ply": 0, "equity": 0.8856077 }, { "move": "8/2 4/1*(2)", + "ply": 0, "equity": 0.8853629 }, { "move": "13/10 6/3(3)", + "ply": 0, "equity": 0.8853096 }, { "move": "13/7 6/3 5/2", + "ply": 0, "equity": 0.8831558 }, { "move": "13/10 8/2 7/4", + "ply": 0, "equity": 0.8819176 }, { "move": "7/1* 4/1(2)", + "ply": 0, "equity": 0.88113916 }, { "move": "13/10 7/4 6/3(2)", + "ply": 0, "equity": 0.88064635 }, { "move": "13/10(3) 8/5", + "ply": 0, "equity": 0.88049895 }, { "move": "6/3 5/2(2) 4/1*", + "ply": 0, "equity": 0.8790827 }, { "move": "13/10(2) 8/2", + "ply": 0, "equity": 0.8782023 }, { "move": "6/3 5/2 4/1*(2)", + "ply": 0, "equity": 0.8774586 }, { "move": "8/5 6/3(3)", + "ply": 0, "equity": 0.8774268 }, { "move": "8/2 7/1*", + "ply": 0, "equity": 0.8772669 }, { "move": "13/7 13/10 4/1*", + "ply": 0, "equity": 0.87700987 }, { "move": "13/10 8/5 6/3(2)", + "ply": 0, "equity": 0.8757389 }, { "move": "13/10 8/2 8/5", + "ply": 0, "equity": 0.87530077 }, { "move": "13/10(2) 6/3 5/2", + "ply": 0, "equity": 0.87215745 }, { "move": "13/7 8/5 4/1*", + "ply": 0, "equity": 0.87202764 }, { "move": "8/5 7/4 6/3(2)", + "ply": 0, "equity": 0.86896765 }, { "move": "7/1* 6/3 5/2", + "ply": 0, "equity": 0.8687051 }, { "move": "13/10 7/4 6/3 5/2", + "ply": 0, "equity": 0.8679464 }, { "move": "8/2 6/3 4/1*", + "ply": 0, "equity": 0.86750233 }, { "move": "13/10(2) 8/5 4/1*", + "ply": 0, "equity": 0.86701244 }, { "move": "8/5(2) 6/3(2)", + "ply": 0, "equity": 0.86167765 }, { "move": "13/10(3) 4/1*", + "ply": 0, "equity": 0.8568015 }, { "move": "6/3(2) 5/2(2)", + "ply": 0, "equity": 0.85501754 }, { "move": "8/2 7/4 5/2", + "ply": 0, "equity": 0.8537997 }, { "move": "8/2 5/2(2)", + "ply": 0, "equity": 0.8524791 }, { "move": "13/10 8/5(2) 4/1*", + "ply": 0, "equity": 0.84969926 }, { "move": "13/10 8/2 5/2", + "ply": 0, "equity": 0.8466199 }, { "move": "8/2 6/3(2)", + "ply": 0, "equity": 0.80814815 }, { "move": "13/10 5/2(2) 4/1*", + "ply": 0, "equity": 0.8037353 }, { "move": "13/10 5/2 4/1*(2)", + "ply": 0, "equity": 0.77350885 }, { "move": "13/10 6/3(2) 4/1*", + "ply": 0, "equity": 0.74067366 }, { "move": "13/7 13/10 5/2", + "ply": 0, "equity": 0.72703636 }, { "move": "13/10(2) 7/4 5/2", + "ply": 0, "equity": 0.7053956 }, { "move": "6/3(3) 4/1*", + "ply": 0, "equity": 0.66575944 }, { "move": "13/10 7/1* 5/2", + "ply": 0, "equity": 0.6462204 }, { "move": "8/5 6/3(2) 4/1*", + "ply": 0, "equity": 0.6374039 }, { "move": "8/2 8/5 4/1*", + "ply": 0, "equity": 0.62353325 }, { "move": "13/10 8/2 4/1*", + "ply": 0, "equity": 0.61257124 }, { "move": "13/10 6/3 5/2 4/1*", + "ply": 0, "equity": 0.5349428 }, { "move": "7/1* 5/2 4/1", + "ply": 0, "equity": 0.45721298 }, { "move": "8/2 5/2 4/1*", + "ply": 0, "equity": 0.44362232 }, { "move": "13/4 5/2", + "ply": 0, "equity": 0.43828273 }, { "move": "13/10(3) 5/2", + "ply": 0, "equity": 0.2798175 }, { "move": "13/10 6/3(2) 5/2", + "ply": 0, "equity": 0.24207813 }, { "move": "6/3(3) 5/2", + "ply": 0, "equity": 0.23385207 }, { "move": "7/4 6/3(2) 5/2", + "ply": 0, "equity": 0.22096781 }, { "move": "13/7 5/2 4/1*", + "ply": 0, "equity": 0.18849733 }, { "move": "13/10(2) 5/2 4/1*", + "ply": 0, "equity": 0.084023334 } ] @@ -474,42 +583,52 @@ "ranking": [ { "move": "13/10 7/1* 6/3", + "ply": 2, "equity": 1.0 }, { "move": "13/7 13/10 6/3", + "ply": 2, "equity": 1.0 }, { "move": "13/10(2) 8/5 6/3", + "ply": 2, "equity": 1.0 }, { "move": "13/7 8/5 6/3", + "ply": 2, "equity": 1.0 }, { "move": "13/10(2) 7/4 6/3", + "ply": 2, "equity": 1.0 }, { "move": "13/10 8/5 7/4 6/3", + "ply": 2, "equity": 1.0 }, { "move": "13/10(2) 5/2 4/1*", + "ply": 2, "equity": -0.022027964 }, { "move": "13/10 8/5(2) 6/3", + "ply": 2, "equity": 1.0 }, { "move": "13/4 6/3", + "ply": 2, "equity": 1.0 }, { "move": "13/10 6/3 4/1*(2)", + "ply": 2, "equity": 0.99692726 } ] diff --git a/tests/reference/analysis/move-crawford.json b/tests/reference/analysis/move-crawford.json index 90985fc..2ebbe8e 100644 --- a/tests/reference/analysis/move-crawford.json +++ b/tests/reference/analysis/move-crawford.json @@ -25,42 +25,52 @@ "ranking": [ { "move": "8/5 6/2", + "ply": 0, "equity": 0.7345603 }, { "move": "5/2 5/1*", + "ply": 0, "equity": 0.7229391 }, { "move": "8/1*", + "ply": 0, "equity": 0.71705425 }, { "move": "13/10 6/2", + "ply": 0, "equity": 0.7142835 }, { "move": "13/9 8/5", + "ply": 0, "equity": 0.6963019 }, { "move": "8/5 8/4", + "ply": 0, "equity": 0.68610644 }, { "move": "13/10 8/4", + "ply": 0, "equity": 0.6855368 }, { "move": "13/10 13/9", + "ply": 0, "equity": 0.6818689 }, { "move": "13/6", + "ply": 0, "equity": 0.6633271 }, { "move": "13/9 5/2", + "ply": 0, "equity": 0.6538005 } ] @@ -78,42 +88,52 @@ "ranking": [ { "move": "8/5 6/2", + "ply": 2, "equity": 0.7697691 }, { "move": "13/10 6/2", + "ply": 2, "equity": 0.7615671 }, { "move": "13/9 8/5", + "ply": 2, "equity": 0.69608325 }, { "move": "5/2 5/1*", + "ply": 2, "equity": 0.6911188 }, { "move": "8/1*", + "ply": 2, "equity": 0.6836242 }, { "move": "13/10 13/9", + "ply": 2, "equity": 0.6807338 }, { "move": "13/10 8/4", + "ply": 1, "equity": 0.6733323 }, { "move": "13/6", + "ply": 1, "equity": 0.66055346 }, { "move": "8/5 8/4", + "ply": 1, "equity": 0.6556259 }, { "move": "13/9 5/2", + "ply": 1, "equity": 0.645951 } ] diff --git a/tests/reference/analysis/move-forced.json b/tests/reference/analysis/move-forced.json index 755a2e4..acdd967 100644 --- a/tests/reference/analysis/move-forced.json +++ b/tests/reference/analysis/move-forced.json @@ -25,6 +25,7 @@ "ranking": [ { "move": "bar/24", + "ply": 0, "equity": -1.0 } ] @@ -42,6 +43,7 @@ "ranking": [ { "move": "bar/24", + "ply": 0, "equity": -1.0 } ] diff --git a/tests/reference/analysis/move-jacoby.json b/tests/reference/analysis/move-jacoby.json index 1deab70..2a98cb3 100644 --- a/tests/reference/analysis/move-jacoby.json +++ b/tests/reference/analysis/move-jacoby.json @@ -25,58 +25,72 @@ "ranking": [ { "move": "13/3", + "ply": 0, "equity": 0.9328213 }, { "move": "13/9 13/7", + "ply": 0, "equity": 0.9175232 }, { "move": "8/2 7/3", + "ply": 0, "equity": 0.91263336 }, { "move": "13/7 8/4", + "ply": 0, "equity": 0.90736973 }, { "move": "13/9 7/1*", + "ply": 0, "equity": 0.9053596 }, { "move": "8/2 6/2", + "ply": 0, "equity": 0.9017469 }, { "move": "8/4 7/1*", + "ply": 0, "equity": 0.89993495 }, { "move": "13/7 6/2", + "ply": 0, "equity": 0.8962312 }, { "move": "7/1* 5/1", + "ply": 0, "equity": 0.8907822 }, { "move": "7/1* 6/2", + "ply": 0, "equity": 0.87481093 }, { "move": "13/9 8/2", + "ply": 0, "equity": 0.87251335 }, { "move": "8/4 8/2", + "ply": 0, "equity": 0.8716274 }, { "move": "13/7 5/1*", + "ply": 0, "equity": 0.87005275 }, { "move": "8/2 5/1*", + "ply": 0, "equity": 0.50889754 } ] @@ -94,42 +108,52 @@ "ranking": [ { "move": "13/3", + "ply": 2, "equity": 1.0 }, { "move": "13/9 13/7", + "ply": 2, "equity": 0.99894416 }, { "move": "8/2 7/3", + "ply": 2, "equity": 0.99641544 }, { "move": "13/7 8/4", + "ply": 2, "equity": 0.98718643 }, { "move": "13/7 6/2", + "ply": 2, "equity": 0.9808178 }, { "move": "13/9 7/1*", + "ply": 2, "equity": 0.9452111 }, { "move": "8/2 5/1*", + "ply": 2, "equity": 0.3304975 }, { "move": "8/4 7/1*", + "ply": 1, "equity": 0.93114305 }, { "move": "7/1* 5/1", + "ply": 1, "equity": 0.91711015 }, { "move": "8/2 6/2", + "ply": 1, "equity": 0.9160911 } ] diff --git a/tests/reference/analysis/move-luck-lucky.json b/tests/reference/analysis/move-luck-lucky.json index f98e789..892af6b 100644 --- a/tests/reference/analysis/move-luck-lucky.json +++ b/tests/reference/analysis/move-luck-lucky.json @@ -25,42 +25,52 @@ "ranking": [ { "move": "13/5 11/3", + "ply": 0, "equity": 0.16933289 }, { "move": "13/9(2) 11/3", + "ply": 0, "equity": 0.16701066 }, { "move": "13/5 9/5 7/3", + "ply": 0, "equity": 0.16552198 }, { "move": "13/5(2)", + "ply": 0, "equity": 0.16473158 }, { "move": "13/5 13/9 11/7", + "ply": 0, "equity": 0.16365689 }, { "move": "13/5 13/9 7/3", + "ply": 0, "equity": 0.16245355 }, { "move": "13/5 11/7 9/5", + "ply": 0, "equity": 0.16167049 }, { "move": "11/3 9/5(2)", + "ply": 0, "equity": 0.16043647 }, { "move": "13/9 11/3 11/7", + "ply": 0, "equity": 0.1603657 }, { "move": "13/5 11/7(2)", + "ply": 0, "equity": 0.1602792 } ] @@ -78,42 +88,52 @@ "ranking": [ { "move": "13/5 11/3", + "ply": 2, "equity": 0.15129912 }, { "move": "13/5 13/9 11/7", + "ply": 2, "equity": 0.15059008 }, { "move": "13/9(2) 11/3", + "ply": 2, "equity": 0.14950007 }, { "move": "13/5 11/7 9/5", + "ply": 2, "equity": 0.14858595 }, { "move": "11/3 11/7 9/5", + "ply": 2, "equity": 0.14829463 }, { "move": "13/9 11/3 11/7", + "ply": 2, "equity": 0.1463402 }, { "move": "13/5 9/5 7/3", + "ply": 1, "equity": 0.13938732 }, { "move": "13/5 11/7(2)", + "ply": 1, "equity": 0.13921705 }, { "move": "13/9(2) 11/7(2)", + "ply": 1, "equity": 0.13873637 }, { "move": "13/5 13/9 6/2", + "ply": 1, "equity": 0.13852078 } ] diff --git a/tests/reference/analysis/move-luck-unlucky-bearoff.json b/tests/reference/analysis/move-luck-unlucky-bearoff.json index a96a1ff..2d15e71 100644 --- a/tests/reference/analysis/move-luck-unlucky-bearoff.json +++ b/tests/reference/analysis/move-luck-unlucky-bearoff.json @@ -25,18 +25,22 @@ "ranking": [ { "move": "3/off", + "ply": 0, "equity": -0.8607614 }, { "move": "2/off 1/off", + "ply": 0, "equity": -0.8607614 }, { "move": "2/1 2/off", + "ply": 0, "equity": -0.95561147 }, { "move": "3/1 2/1", + "ply": 0, "equity": -1.0 } ] @@ -54,18 +58,22 @@ "ranking": [ { "move": "3/off", + "ply": 2, "equity": -0.8607614 }, { "move": "2/off 1/off", + "ply": 2, "equity": -0.8607614 }, { "move": "2/1 2/off", + "ply": 2, "equity": -0.95561147 }, { "move": "3/1 2/1", + "ply": 1, "equity": -1.0 } ] diff --git a/tests/reference/analysis/move-luck-unlucky.json b/tests/reference/analysis/move-luck-unlucky.json index 16cb6c9..9298b95 100644 --- a/tests/reference/analysis/move-luck-unlucky.json +++ b/tests/reference/analysis/move-luck-unlucky.json @@ -25,42 +25,52 @@ "ranking": [ { "move": "18/9", + "ply": 0, "equity": 0.2544027 }, { "move": "18/14 13/8", + "ply": 0, "equity": 0.22496489 }, { "move": "24/20 18/13", + "ply": 0, "equity": 0.20747662 }, { "move": "24/20 13/8", + "ply": 0, "equity": 0.1158272 }, { "move": "13/9 13/8", + "ply": 0, "equity": 0.09201278 }, { "move": "24/15", + "ply": 0, "equity": 0.061017293 }, { "move": "18/13 6/2", + "ply": 0, "equity": 0.03581623 }, { "move": "13/4", + "ply": 0, "equity": -0.028887823 }, { "move": "13/8 6/2", + "ply": 0, "equity": -0.033572298 }, { "move": "18/13 8/4", + "ply": 0, "equity": -0.09662818 } ] @@ -78,42 +88,52 @@ "ranking": [ { "move": "18/9", + "ply": 2, "equity": 0.27687487 }, { "move": "18/14 13/8", + "ply": 2, "equity": 0.21801135 }, { "move": "24/20 18/13", + "ply": 2, "equity": 0.21000355 }, { "move": "24/20 13/8", + "ply": 2, "equity": 0.1144446 }, { "move": "13/9 13/8", + "ply": 2, "equity": 0.09412725 }, { "move": "24/15", + "ply": 2, "equity": 0.07892294 }, { "move": "18/13 6/2", + "ply": 1, "equity": -0.020656409 }, { "move": "13/4", + "ply": 0, "equity": -0.028887823 }, { "move": "13/8 6/2", + "ply": 0, "equity": -0.033572298 }, { "move": "18/13 8/4", + "ply": 0, "equity": -0.09662818 } ] diff --git a/tests/reference/analysis/move-match.json b/tests/reference/analysis/move-match.json index 6eb7a82..dc63991 100644 --- a/tests/reference/analysis/move-match.json +++ b/tests/reference/analysis/move-match.json @@ -25,42 +25,52 @@ "ranking": [ { "move": "13/5", + "ply": 0, "equity": 0.7553502 }, { "move": "18/13 8/5", + "ply": 0, "equity": 0.5814649 }, { "move": "24/16*", + "ply": 0, "equity": 0.4581785 }, { "move": "18/10", + "ply": 0, "equity": 0.31719947 }, { "move": "8/5 8/3", + "ply": 0, "equity": 0.31371918 }, { "move": "18/15 13/8", + "ply": 0, "equity": 0.26037523 }, { "move": "24/21 18/13", + "ply": 0, "equity": 0.23547156 }, { "move": "8/3 6/3", + "ply": 0, "equity": 0.15432969 }, { "move": "13/10 13/8", + "ply": 0, "equity": 0.13837479 }, { "move": "18/13 5/2", + "ply": 0, "equity": 0.12815686 } ] @@ -78,42 +88,52 @@ "ranking": [ { "move": "13/5", + "ply": 2, "equity": 0.85497326 }, { "move": "24/16*", + "ply": 2, "equity": 0.46805125 }, { "move": "18/13 8/5", + "ply": 1, "equity": 0.6511238 }, { "move": "18/10", + "ply": 0, "equity": 0.31719947 }, { "move": "8/5 8/3", + "ply": 0, "equity": 0.31371918 }, { "move": "18/15 13/8", + "ply": 0, "equity": 0.26037523 }, { "move": "24/21 18/13", + "ply": 0, "equity": 0.23547156 }, { "move": "8/3 6/3", + "ply": 0, "equity": 0.15432969 }, { "move": "13/10 13/8", + "ply": 0, "equity": 0.13837479 }, { "move": "18/13 5/2", + "ply": 0, "equity": 0.12815686 } ] diff --git a/tests/reference/analysis/move-middlegame-blunder.json b/tests/reference/analysis/move-middlegame-blunder.json index e581af8..3861a4d 100644 --- a/tests/reference/analysis/move-middlegame-blunder.json +++ b/tests/reference/analysis/move-middlegame-blunder.json @@ -25,58 +25,72 @@ "ranking": [ { "move": "24/14", + "ply": 0, "equity": 0.15439394 }, { "move": "8/2 6/2", + "ply": 0, "equity": 0.10821653 }, { "move": "24/18 13/9", + "ply": 0, "equity": 0.10337136 }, { "move": "24/20 24/18", + "ply": 0, "equity": 0.072762795 }, { "move": "13/9 13/7", + "ply": 0, "equity": 0.0029048775 }, { "move": "24/20 13/7", + "ply": 0, "equity": 9.1438415e-05 }, { "move": "13/3", + "ply": 0, "equity": -0.012697897 }, { "move": "24/18 6/2", + "ply": 0, "equity": -0.075399 }, { "move": "24/18 8/4", + "ply": 0, "equity": -0.088734664 }, { "move": "24/20 8/2", + "ply": 0, "equity": -0.095189385 }, { "move": "13/9 8/2", + "ply": 0, "equity": -0.09528468 }, { "move": "13/7 8/4", + "ply": 0, "equity": -0.12819159 }, { "move": "13/7 6/2", + "ply": 0, "equity": -0.14467186 }, { "move": "8/4 8/2", + "ply": 0, "equity": -0.35321844 } ] @@ -94,42 +108,52 @@ "ranking": [ { "move": "24/14", + "ply": 2, "equity": 0.13111727 }, { "move": "8/2 6/2", + "ply": 2, "equity": 0.11303549 }, { "move": "24/18 13/9", + "ply": 2, "equity": 0.105096616 }, { "move": "24/20 24/18", + "ply": 2, "equity": 0.049068402 }, { "move": "13/3", + "ply": 2, "equity": -0.0040049786 }, { "move": "8/4 8/2", + "ply": 2, "equity": -0.36824828 }, { "move": "13/9 13/7", + "ply": 2, "equity": -0.014309813 }, { "move": "24/20 13/7", + "ply": 1, "equity": -0.029367238 }, { "move": "24/18 8/4", + "ply": 1, "equity": -0.081171095 }, { "move": "24/18 6/2", + "ply": 1, "equity": -0.09313507 } ] diff --git a/tests/reference/analysis/move-middlegame-close.json b/tests/reference/analysis/move-middlegame-close.json index 59487b5..1ef6e82 100644 --- a/tests/reference/analysis/move-middlegame-close.json +++ b/tests/reference/analysis/move-middlegame-close.json @@ -25,42 +25,52 @@ "ranking": [ { "move": "24/14", + "ply": 0, "equity": 0.15439394 }, { "move": "8/2 6/2", + "ply": 0, "equity": 0.10821653 }, { "move": "24/18 13/9", + "ply": 0, "equity": 0.10337136 }, { "move": "24/20 24/18", + "ply": 0, "equity": 0.072762795 }, { "move": "13/9 13/7", + "ply": 0, "equity": 0.0029048775 }, { "move": "24/20 13/7", + "ply": 0, "equity": 9.1438415e-05 }, { "move": "13/3", + "ply": 0, "equity": -0.012697897 }, { "move": "24/18 6/2", + "ply": 0, "equity": -0.075399 }, { "move": "24/18 8/4", + "ply": 0, "equity": -0.088734664 }, { "move": "24/20 8/2", + "ply": 0, "equity": -0.095189385 } ] @@ -78,42 +88,52 @@ "ranking": [ { "move": "24/14", + "ply": 2, "equity": 0.13111727 }, { "move": "8/2 6/2", + "ply": 2, "equity": 0.11303549 }, { "move": "24/18 13/9", + "ply": 2, "equity": 0.105096616 }, { "move": "24/20 24/18", + "ply": 2, "equity": 0.049068402 }, { "move": "13/3", + "ply": 2, "equity": -0.0040049786 }, { "move": "13/9 13/7", + "ply": 2, "equity": -0.014309813 }, { "move": "24/20 13/7", + "ply": 1, "equity": -0.029367238 }, { "move": "24/18 8/4", + "ply": 1, "equity": -0.081171095 }, { "move": "24/18 6/2", + "ply": 1, "equity": -0.09313507 }, { "move": "24/20 8/2", + "ply": 0, "equity": -0.095189385 } ] diff --git a/tests/reference/analysis/move-opening-31-best.json b/tests/reference/analysis/move-opening-31-best.json index 50dbcd5..ba22887 100644 --- a/tests/reference/analysis/move-opening-31-best.json +++ b/tests/reference/analysis/move-opening-31-best.json @@ -25,42 +25,52 @@ "ranking": [ { "move": "8/5 6/5", + "ply": 0, "equity": 0.21258597 }, { "move": "24/23 13/10", + "ply": 0, "equity": -0.010789523 }, { "move": "24/20", + "ply": 0, "equity": -0.017392516 }, { "move": "24/23 24/21", + "ply": 0, "equity": -0.023609947 }, { "move": "13/9", + "ply": 0, "equity": -0.027675275 }, { "move": "24/21 6/5", + "ply": 0, "equity": -0.04369401 }, { "move": "13/10 6/5", + "ply": 0, "equity": -0.04984435 }, { "move": "24/23 8/5", + "ply": 0, "equity": -0.104100645 }, { "move": "24/23 6/3", + "ply": 0, "equity": -0.10542354 }, { "move": "24/21 8/7", + "ply": 0, "equity": -0.107585564 } ] @@ -78,42 +88,52 @@ "ranking": [ { "move": "8/5 6/5", + "ply": 1, "equity": 0.20373425 }, { "move": "24/23 13/10", + "ply": 1, "equity": -0.022293653 }, { "move": "24/20", + "ply": 1, "equity": -0.031027086 }, { "move": "24/23 24/21", + "ply": 1, "equity": -0.03213631 }, { "move": "13/9", + "ply": 1, "equity": -0.040205758 }, { "move": "24/21 6/5", + "ply": 0, "equity": -0.04369401 }, { "move": "13/10 6/5", + "ply": 0, "equity": -0.04984435 }, { "move": "24/23 8/5", + "ply": 0, "equity": -0.104100645 }, { "move": "24/23 6/3", + "ply": 0, "equity": -0.10542354 }, { "move": "24/21 8/7", + "ply": 0, "equity": -0.107585564 } ] diff --git a/tests/reference/analysis/move-opening-31-error.json b/tests/reference/analysis/move-opening-31-error.json index 0ac69a9..7ac6e98 100644 --- a/tests/reference/analysis/move-opening-31-error.json +++ b/tests/reference/analysis/move-opening-31-error.json @@ -25,42 +25,52 @@ "ranking": [ { "move": "8/5 6/5", + "ply": 0, "equity": 0.21258597 }, { "move": "24/23 13/10", + "ply": 0, "equity": -0.010789523 }, { "move": "24/20", + "ply": 0, "equity": -0.017392516 }, { "move": "24/23 24/21", + "ply": 0, "equity": -0.023609947 }, { "move": "13/9", + "ply": 0, "equity": -0.027675275 }, { "move": "24/21 6/5", + "ply": 0, "equity": -0.04369401 }, { "move": "13/10 6/5", + "ply": 0, "equity": -0.04984435 }, { "move": "24/23 8/5", + "ply": 0, "equity": -0.104100645 }, { "move": "24/23 6/3", + "ply": 0, "equity": -0.10542354 }, { "move": "24/21 8/7", + "ply": 0, "equity": -0.107585564 } ] @@ -78,42 +88,52 @@ "ranking": [ { "move": "8/5 6/5", + "ply": 2, "equity": 0.1998143 }, { "move": "24/23 13/10", + "ply": 2, "equity": -0.0118339015 }, { "move": "24/20", + "ply": 1, "equity": -0.031027086 }, { "move": "24/23 24/21", + "ply": 1, "equity": -0.03213631 }, { "move": "13/9", + "ply": 1, "equity": -0.040205758 }, { "move": "24/21 6/5", + "ply": 0, "equity": -0.04369401 }, { "move": "13/10 6/5", + "ply": 0, "equity": -0.04984435 }, { "move": "24/23 8/5", + "ply": 0, "equity": -0.104100645 }, { "move": "24/23 6/3", + "ply": 0, "equity": -0.10542354 }, { "move": "24/21 8/7", + "ply": 0, "equity": -0.107585564 } ] diff --git a/tests/reference/analysis/move-opening-65-error.json b/tests/reference/analysis/move-opening-65-error.json index 0fbce8c..b7fdd2e 100644 --- a/tests/reference/analysis/move-opening-65-error.json +++ b/tests/reference/analysis/move-opening-65-error.json @@ -25,30 +25,37 @@ "ranking": [ { "move": "24/13", + "ply": 0, "equity": 0.082985915 }, { "move": "24/18 13/8", + "ply": 0, "equity": 0.03631018 }, { "move": "13/8 13/7", + "ply": 0, "equity": -0.05142035 }, { "move": "13/2", + "ply": 0, "equity": -0.13522762 }, { "move": "24/18 8/3", + "ply": 0, "equity": -0.14635906 }, { "move": "13/7 8/3", + "ply": 0, "equity": -0.17384717 }, { "move": "8/3 8/2", + "ply": 0, "equity": -0.37779096 } ] @@ -66,30 +73,37 @@ "ranking": [ { "move": "24/13", + "ply": 2, "equity": 0.07996891 }, { "move": "24/18 13/8", + "ply": 2, "equity": 0.03882884 }, { "move": "13/8 13/7", + "ply": 2, "equity": -0.061850794 }, { "move": "13/2", + "ply": 1, "equity": -0.12083713 }, { "move": "24/18 8/3", + "ply": 1, "equity": -0.13999933 }, { "move": "13/7 8/3", + "ply": 0, "equity": -0.17384717 }, { "move": "8/3 8/2", + "ply": 0, "equity": -0.37779096 } ] diff --git a/tests/reference/analysis/move-opening-double.json b/tests/reference/analysis/move-opening-double.json index 5ab51f1..c0b255e 100644 --- a/tests/reference/analysis/move-opening-double.json +++ b/tests/reference/analysis/move-opening-double.json @@ -25,42 +25,52 @@ "ranking": [ { "move": "24/18(2) 13/7(2)", + "ply": 0, "equity": 0.539388 }, { "move": "24/18 13/7(3)", + "ply": 0, "equity": 0.33099693 }, { "move": "24/18(2) 8/2(2)", + "ply": 0, "equity": 0.27119082 }, { "move": "13/7(2) 8/2(2)", + "ply": 0, "equity": 0.1563462 }, { "move": "24/18 13/7(2) 8/2", + "ply": 0, "equity": 0.14750561 }, { "move": "13/7(4)", + "ply": 0, "equity": 0.14310652 }, { "move": "24/18(2) 13/7 8/2", + "ply": 0, "equity": 0.12353878 }, { "move": "13/7(3) 8/2", + "ply": 0, "equity": 0.101389736 }, { "move": "24/18 13/7 8/2(2)", + "ply": 0, "equity": -0.05435923 }, { "move": "24/18 8/2(3)", + "ply": 0, "equity": -0.12482335 } ] @@ -78,42 +88,52 @@ "ranking": [ { "move": "24/18(2) 13/7(2)", + "ply": 1, "equity": 0.5886327 }, { "move": "24/18 13/7(3)", + "ply": 1, "equity": 0.33312234 }, { "move": "24/18(2) 8/2(2)", + "ply": 0, "equity": 0.27119082 }, { "move": "13/7(2) 8/2(2)", + "ply": 0, "equity": 0.1563462 }, { "move": "24/18 13/7(2) 8/2", + "ply": 0, "equity": 0.14750561 }, { "move": "13/7(4)", + "ply": 0, "equity": 0.14310652 }, { "move": "24/18(2) 13/7 8/2", + "ply": 0, "equity": 0.12353878 }, { "move": "13/7(3) 8/2", + "ply": 0, "equity": 0.101389736 }, { "move": "24/18 13/7 8/2(2)", + "ply": 0, "equity": -0.05435923 }, { "move": "24/18 8/2(3)", + "ply": 0, "equity": -0.12482335 } ] diff --git a/tests/reference/analysis/move-race.json b/tests/reference/analysis/move-race.json index 09dea1e..bd3b039 100644 --- a/tests/reference/analysis/move-race.json +++ b/tests/reference/analysis/move-race.json @@ -25,118 +25,147 @@ "ranking": [ { "move": "13/9 11/6", + "ply": 0, "equity": -0.22230339 }, { "move": "11/6 9/5", + "ply": 0, "equity": -0.22993731 }, { "move": "13/8 9/5", + "ply": 0, "equity": -0.23005769 }, { "move": "13/9 13/8", + "ply": 0, "equity": -0.23073325 }, { "move": "13/8 11/7", + "ply": 0, "equity": -0.23153114 }, { "move": "11/7 11/6", + "ply": 0, "equity": -0.2335976 }, { "move": "13/8 7/3", + "ply": 0, "equity": -0.23374501 }, { "move": "11/6 7/3", + "ply": 0, "equity": -0.23394687 }, { "move": "13/4", + "ply": 0, "equity": -0.23507257 }, { "move": "13/9 7/2", + "ply": 0, "equity": -0.23529215 }, { "move": "7/3 7/2", + "ply": 0, "equity": -0.23544219 }, { "move": "11/2", + "ply": 0, "equity": -0.235758 }, { "move": "11/7 9/4", + "ply": 0, "equity": -0.23589191 }, { "move": "13/8 6/2", + "ply": 0, "equity": -0.24026787 }, { "move": "9/5 7/2", + "ply": 0, "equity": -0.24087214 }, { "move": "13/8 5/1", + "ply": 0, "equity": -0.24252185 }, { "move": "9/5 9/4", + "ply": 0, "equity": -0.2440444 }, { "move": "9/4 6/2", + "ply": 0, "equity": -0.24487257 }, { "move": "9/4 7/3", + "ply": 0, "equity": -0.24512678 }, { "move": "11/6 5/1", + "ply": 0, "equity": -0.25016445 }, { "move": "9/4 5/1", + "ply": 0, "equity": -0.25572774 }, { "move": "7/2 5/1", + "ply": 0, "equity": -0.258138 }, { "move": "7/3 6/1", + "ply": 0, "equity": -0.25878713 }, { "move": "7/2 6/2", + "ply": 0, "equity": -0.26088732 }, { "move": "13/9 6/1", + "ply": 0, "equity": -0.26104733 }, { "move": "9/5 6/1", + "ply": 0, "equity": -0.26680842 }, { "move": "11/7 6/1", + "ply": 0, "equity": -0.2670043 }, { "move": "6/2 6/1", + "ply": 0, "equity": -0.27360544 }, { "move": "6/1 5/1", + "ply": 0, "equity": -0.31466243 } ] @@ -154,42 +183,52 @@ "ranking": [ { "move": "13/9 11/6", + "ply": 2, "equity": -0.21922025 }, { "move": "13/9 13/8", + "ply": 2, "equity": -0.2202243 }, { "move": "11/6 9/5", + "ply": 2, "equity": -0.22022447 }, { "move": "13/8 9/5", + "ply": 2, "equity": -0.22088149 }, { "move": "11/7 11/6", + "ply": 2, "equity": -0.22277355 }, { "move": "13/4", + "ply": 2, "equity": -0.2230124 }, { "move": "6/1 5/1", + "ply": 2, "equity": -0.29347342 }, { "move": "11/7 9/4", + "ply": 1, "equity": -0.22985312 }, { "move": "13/8 11/7", + "ply": 1, "equity": -0.23169218 }, { "move": "13/9 7/2", + "ply": 1, "equity": -0.23561361 } ] diff --git a/tests/reference/compare.py b/tests/reference/compare.py index 677da81..8bdd272 100644 --- a/tests/reference/compare.py +++ b/tests/reference/compare.py @@ -210,6 +210,36 @@ def compare_move_analysis(report, cli, case): 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) + compare_analysis_ranking(report, where, expected, actual) + + +def compare_analysis_ranking(report, where, expected, actual): + """Classement de l'analyse : les dix premiers coups de gnubg (le moteur + n'en rend pas plus), au même niveau et à la même équité, à la tolérance + près ; le meilleur coup et le coup joué y ont exactement les évaluations + de best et played.""" + engine = actual["ranking"] + reference = expected["ranking"][:10] + report.same(where, "taille du classement", len(reference), len(engine)) + by_move = {m["move"]: m for m in engine} + for m in reference: + other = by_move.get(m["move"]) + here = "%s classement %s" % (where, m["move"]) + if other is None: + # Un coup à égalité avec le dixième peut changer de côté. + tied = abs(m["equity"] - reference[-1]["equity"]) <= report.tolerance + if not tied: + report.same(here, "présent", True, False) + continue + report.same(here, "ply", m["ply"], other["ply"]) + report.number(here, "equity", m["equity"], other["equity"]) + if engine: + report.same(where, "classement, meilleur coup", actual["best"], engine[0]) + for m in engine: + if m["move"] == actual["played"]["move"]: + report.same(where, "classement, coup joué", actual["played"], m) + report.same(where, "classement, rang du coup joué", actual["rank"], + engine.index(m) + 1) def compare_cube_analysis(report, cli, case): diff --git a/tests/reference/generate.py b/tests/reference/generate.py index 858abaf..2a6b9b6 100644 --- a/tests/reference/generate.py +++ b/tests/reference/generate.py @@ -221,15 +221,25 @@ def set_analysis(ply): def analyse_game(): - """Analyse la partie en cours ; renvoie ses enregistrements et ses - statistiques par joueur (« X », « O »). Le cache est vidé avant : les + """Analyse la partie en cours ; renvoie ses enregistrements, ses + statistiques par joueur (« X », « O ») et les informations du match. 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"] + match = gnubg.match(analysis=1, verbose=1, statistics=1, boards=1) + game = match["games"][-1] + return game["game"], game["stats"], match["match-info"] + + +def move_plies(match_info, move): + """Niveau auquel gnubg a évalué un coup de l'analyse : gnubg.match ne + donne le réglage d'un coup (« evalcontext ») que s'il diffère du réglage + par défaut de la partie (« default-eval-context », celui du premier coup + évalué rencontré).""" + context = move.get("evalcontext", {}) + return context.get("plies", match_info["default-eval-context"]["plies"]) def last_record(records, action): @@ -280,7 +290,7 @@ def analyse_move(case, ply): # vient de jouer est la position retournée. played_id = gnubg.positionid(swapped(gnubg.board())) set_analysis(ply) - records, stats = analyse_game() + records, stats, match_info = analyse_game() record = last_record(records, "move") analysis = record["analysis"] moves, index = analysis["moves"], analysis["imove"] @@ -304,7 +314,10 @@ def analyse_move(case, ply): # 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], + # Avec le niveau de chaque coup : les coups écartés par les filtres + # gardent celui d'un niveau inférieur (voir README.md). + "ranking": [{"move": notation(m["move"]), "ply": move_plies(match_info, m), + "equity": f32(m["score"])} for m in kept], } luck = {"value": f32(analysis["luck-value"]), "rating": LUCKS[analysis.get("luck")]} return level, luck @@ -322,7 +335,7 @@ def analyse_cube(case, ply): # 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() + records, stats, _ = analyse_game() record = last_record(records, "move") analysed = "nd-cubeful-eq" in record["analysis"] skill = record["analysis"].get("cube-skill") @@ -332,7 +345,7 @@ def analyse_cube(case, ply): # du double. gnubg.command("double") gnubg.command("drop" if action == "pass" else "take") - records, stats = analyse_game() + 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") diff --git a/tests/test_reference.sh b/tests/test_reference.sh index 12bb5db..ca49b0c 100644 --- a/tests/test_reference.sh +++ b/tests/test_reference.sh @@ -130,7 +130,8 @@ if .type == "move" then 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)) + and (.ranking | length) >= ([.rank, ([.move_count, 10] | min)] | max) + and all(.ranking[]; (.move | type) == "string" and (.ply | nombre) and (.equity | nombre))) 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"))