Fusion de main dans revision-ux
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user