Domaine « partie » en journal d'événements, branché au moteur WebAssembly
app/src/domain/game/ : une partie libre contre l'ordinateur, enregistrée comme un arbre d'événements immuables et versionnés (GameStarted, DiceRolled, MovePlayed, CubeOffered, CubeTaken, CubeDropped, GameEnded) ; l'état se recalcule en les rejouant, annuler repart du parent, une action après un retour ouvre une branche. Dés injectés. Le joueur saisit son coup demi-coup par demi-coup ; l'ordinateur joue et décide du videau à 2 plis. Toutes les règles viennent du moteur, derrière l'interface Engine que le domaine définit. app/src/engine/wasm-engine.ts : l'adaptateur Engine sur les commandes d'engine-cli du moteur WebAssembly. Tests : unitaires avec un faux moteur, et deux parties complètes jusqu'à GameEnded sur le vrai moteur, avec des dés imposés. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
54883f4ca3
commit
602d0eafed
@@ -0,0 +1,144 @@
|
||||
// L'interface Engine du domaine, implémentée avec le moteur WebAssembly.
|
||||
//
|
||||
// Copyright (C) 2026 les auteurs d'Ustagammon.
|
||||
// Licence : GPLv3 ou ultérieure (voir LICENSE).
|
||||
//
|
||||
// Le moteur WebAssembly répond aux mêmes commandes qu'engine-cli, en JSON
|
||||
// (web/engine-wasm.mjs). Cet adaptateur traduit chaque méthode en commande et
|
||||
// chaque réponse en types du domaine. Il ne sait pas comment la commande est
|
||||
// exécutée : directement sous Node, dans un Web Worker dans le navigateur.
|
||||
|
||||
import type {
|
||||
Board,
|
||||
BoardResult,
|
||||
BoardSide,
|
||||
CubeDecision,
|
||||
Engine,
|
||||
EngineCube,
|
||||
HalfMovesCheck,
|
||||
Move,
|
||||
Ply,
|
||||
} from "../domain/game/engine.js";
|
||||
import { formatHalfMoves, type HalfMove, type PositionId } from "../domain/game/position.js";
|
||||
import type { Roll } from "../domain/game/roll.js";
|
||||
|
||||
/** Exécute une commande d'engine-cli et renvoie son JSON décodé ; la
|
||||
* promesse est rejetée si le moteur signale une erreur. */
|
||||
export type RunCommand = (args: string[]) => Promise<unknown>;
|
||||
|
||||
// Les réponses d'engine-cli utilisées ici (voir engine-cli/main.c).
|
||||
interface CliMove {
|
||||
move: string;
|
||||
position_id: string;
|
||||
half_moves: [number, number][];
|
||||
}
|
||||
interface CliSide {
|
||||
points: number[];
|
||||
bar: number;
|
||||
off: number;
|
||||
pip_count: number;
|
||||
}
|
||||
interface CliBoard {
|
||||
on_roll: CliSide;
|
||||
opponent: CliSide;
|
||||
swapped_position_id: string;
|
||||
result: { winner: "on_roll" | "opponent"; kind: BoardResult["kind"]; points: number } | null;
|
||||
}
|
||||
|
||||
export function createWasmEngine(run: RunCommand): Engine {
|
||||
// Un Match ID par état du videau : le moteur les code une fois.
|
||||
const matchIds = new Map<string, string>();
|
||||
|
||||
async function matchId(cube: EngineCube): Promise<string> {
|
||||
// Partie libre, joueur 0 au trait : le propriétaire 0 est le joueur au
|
||||
// trait, 1 son adversaire, -1 le centre.
|
||||
const owner = { centered: -1, onRoll: 0, opponent: 1 }[cube.owner];
|
||||
const key = `${cube.value} ${owner}`;
|
||||
let id = matchIds.get(key);
|
||||
if (id === undefined) {
|
||||
const answer = (await run(["matchid", String(cube.value), String(owner)])) as {
|
||||
match_id: string;
|
||||
};
|
||||
id = answer.match_id;
|
||||
matchIds.set(key, id);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
return {
|
||||
async legalMoves(position: PositionId, roll: Roll): Promise<Move[]> {
|
||||
// Le Match ID n'a pas d'effet sur les coups légaux.
|
||||
const answer = (await run(["legal", position, "MAEAAAAAAAAA", diceText(roll)])) as {
|
||||
moves: CliMove[];
|
||||
};
|
||||
return answer.moves.map(toMove);
|
||||
},
|
||||
|
||||
async bestMove(position: PositionId, cube: EngineCube, roll: Roll, ply: Ply) {
|
||||
const args = ["--ply", String(ply), "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 cubeDecision(
|
||||
position: PositionId,
|
||||
cube: EngineCube,
|
||||
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 };
|
||||
},
|
||||
|
||||
async checkHalfMoves(
|
||||
position: PositionId,
|
||||
roll: Roll,
|
||||
halfMoves: readonly HalfMove[],
|
||||
): Promise<HalfMovesCheck> {
|
||||
const args = ["halfmoves", position, diceText(roll), formatHalfMoves(halfMoves)];
|
||||
const answer = (await run(args)) as {
|
||||
valid: boolean;
|
||||
complete: boolean;
|
||||
result_position_id: string | null;
|
||||
};
|
||||
return {
|
||||
valid: answer.valid,
|
||||
complete: answer.complete,
|
||||
position: answer.result_position_id,
|
||||
};
|
||||
},
|
||||
|
||||
async board(position: PositionId, cube: EngineCube): Promise<Board> {
|
||||
const answer = (await run(["position", position, await matchId(cube)])) as CliBoard;
|
||||
return {
|
||||
onRoll: toSide(answer.on_roll),
|
||||
opponent: toSide(answer.opponent),
|
||||
swappedPosition: answer.swapped_position_id,
|
||||
result:
|
||||
answer.result === null
|
||||
? null
|
||||
: {
|
||||
winner: answer.result.winner === "on_roll" ? "onRoll" : "opponent",
|
||||
kind: answer.result.kind,
|
||||
points: answer.result.points,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function diceText(roll: Roll): string {
|
||||
return `${roll[0]}${roll[1]}`;
|
||||
}
|
||||
|
||||
function toMove(move: CliMove): Move {
|
||||
return {
|
||||
notation: move.move,
|
||||
halfMoves: move.half_moves.map(([from, to]) => ({ from, to })),
|
||||
position: move.position_id,
|
||||
};
|
||||
}
|
||||
|
||||
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