fix : improve engine workers management

This commit is contained in:
GuillaumeSD
2025-05-14 04:00:04 +02:00
parent bff407fe2d
commit 8af6194895
14 changed files with 240 additions and 210 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 126 KiB

View File

@@ -12,44 +12,40 @@ const LinearProgressBar = (
if (props.value === 0) return null; if (props.value === 0) return null;
return ( return (
<Grid container alignItems="center" justifyContent="center" size={12}> <Grid
container
alignItems="center"
justifyContent="center"
wrap="nowrap"
width="90%"
columnGap={2}
size={12}
>
<Typography variant="caption" align="center"> <Typography variant="caption" align="center">
{props.label} {props.label}
</Typography> </Typography>
<Grid <Grid sx={{ width: "100%" }}>
container <LinearProgress
width="90%" variant="determinate"
alignItems="center" {...props}
justifyContent="center" sx={(theme) => ({
wrap="nowrap" borderRadius: "5px",
columnGap={2} height: "5px",
size={12} [`&.${linearProgressClasses.colorPrimary}`]: {
> backgroundColor:
<Grid sx={{ width: "100%" }}> theme.palette.grey[theme.palette.mode === "light" ? 200 : 700],
<LinearProgress },
variant="determinate" [`& .${linearProgressClasses.bar}`]: {
{...props} borderRadius: 5,
sx={(theme) => ({ backgroundColor: "#308fe8",
borderRadius: "5px", },
height: "5px", })}
[`&.${linearProgressClasses.colorPrimary}`]: { />
backgroundColor: </Grid>
theme.palette.grey[ <Grid>
theme.palette.mode === "light" ? 200 : 700 <Typography variant="body2" color="text.secondary">{`${Math.round(
], props.value
}, )}%`}</Typography>
[`& .${linearProgressClasses.bar}`]: {
borderRadius: 5,
backgroundColor: "#308fe8",
},
})}
/>
</Grid>
<Grid>
<Typography variant="body2" color="text.secondary">{`${Math.round(
props.value
)}%`}</Typography>
</Grid>
</Grid> </Grid>
</Grid> </Grid>
); );

View File

@@ -7,10 +7,7 @@ import { UciEngine } from "@/lib/engine/uciEngine";
import { EngineName } from "@/types/enums"; import { EngineName } from "@/types/enums";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
export const useEngine = ( export const useEngine = (engineName: EngineName | undefined) => {
engineName: EngineName | undefined,
workersNb?: number
) => {
const [engine, setEngine] = useState<UciEngine | null>(null); const [engine, setEngine] = useState<UciEngine | null>(null);
useEffect(() => { useEffect(() => {
@@ -20,35 +17,32 @@ export const useEngine = (
return; return;
} }
pickEngine(engineName, workersNb).then((newEngine) => { pickEngine(engineName).then((newEngine) => {
setEngine((prev) => { setEngine((prev) => {
prev?.shutdown(); prev?.shutdown();
return newEngine; return newEngine;
}); });
}); });
}, [engineName, workersNb]); }, [engineName]);
return engine; return engine;
}; };
const pickEngine = ( const pickEngine = (engine: EngineName): Promise<UciEngine> => {
engine: EngineName,
workersNb?: number
): Promise<UciEngine> => {
switch (engine) { switch (engine) {
case EngineName.Stockfish17: case EngineName.Stockfish17:
return Stockfish17.create(false, workersNb); return Stockfish17.create(false);
case EngineName.Stockfish17Lite: case EngineName.Stockfish17Lite:
return Stockfish17.create(true, workersNb); return Stockfish17.create(true);
case EngineName.Stockfish16_1: case EngineName.Stockfish16_1:
return Stockfish16_1.create(false, workersNb); return Stockfish16_1.create(false);
case EngineName.Stockfish16_1Lite: case EngineName.Stockfish16_1Lite:
return Stockfish16_1.create(true, workersNb); return Stockfish16_1.create(true);
case EngineName.Stockfish16: case EngineName.Stockfish16:
return Stockfish16.create(false, workersNb); return Stockfish16.create(false);
case EngineName.Stockfish16NNUE: case EngineName.Stockfish16NNUE:
return Stockfish16.create(true, workersNb); return Stockfish16.create(true);
case EngineName.Stockfish11: case EngineName.Stockfish11:
return Stockfish11.create(workersNb); return Stockfish11.create();
} }
}; };

View File

@@ -1,12 +1,11 @@
import { EngineName } from "@/types/enums"; import { EngineName } from "@/types/enums";
import { UciEngine } from "./uciEngine"; import { UciEngine } from "./uciEngine";
import { getEngineWorkers } from "./worker";
export class Stockfish11 { export class Stockfish11 {
public static async create(workersNb?: number): Promise<UciEngine> { public static async create(): Promise<UciEngine> {
const workers = getEngineWorkers("engines/stockfish-11.js", workersNb); const enginePath = "engines/stockfish-11.js";
return UciEngine.create(EngineName.Stockfish11, workers); return UciEngine.create(EngineName.Stockfish11, enginePath);
} }
public static isSupported() { public static isSupported() {

View File

@@ -1,13 +1,11 @@
import { EngineName } from "@/types/enums"; import { EngineName } from "@/types/enums";
import { UciEngine } from "./uciEngine"; import { UciEngine } from "./uciEngine";
import { isMultiThreadSupported, isWasmSupported } from "./shared"; import { isMultiThreadSupported, isWasmSupported } from "./shared";
import { getEngineWorkers } from "./worker"; import { sendCommandsToWorker } from "./worker";
import { EngineWorker } from "@/types/engine";
export class Stockfish16 { export class Stockfish16 {
public static async create( public static async create(nnue?: boolean): Promise<UciEngine> {
nnue?: boolean,
workersNb?: number
): Promise<UciEngine> {
if (!Stockfish16.isSupported()) { if (!Stockfish16.isSupported()) {
throw new Error("Stockfish 16 is not supported"); throw new Error("Stockfish 16 is not supported");
} }
@@ -19,10 +17,9 @@ export class Stockfish16 {
? "engines/stockfish-16/stockfish-nnue-16.js" ? "engines/stockfish-16/stockfish-nnue-16.js"
: "engines/stockfish-16/stockfish-nnue-16-single.js"; : "engines/stockfish-16/stockfish-nnue-16-single.js";
const customEngineInit = async ( const customEngineInit = async (worker: EngineWorker) => {
sendCommands: UciEngine["sendCommands"] await sendCommandsToWorker(
) => { worker,
await sendCommands(
[`setoption name Use NNUE value ${!!nnue}`, "isready"], [`setoption name Use NNUE value ${!!nnue}`, "isready"],
"readyok" "readyok"
); );
@@ -32,9 +29,7 @@ export class Stockfish16 {
? EngineName.Stockfish16NNUE ? EngineName.Stockfish16NNUE
: EngineName.Stockfish16; : EngineName.Stockfish16;
const workers = getEngineWorkers(enginePath, workersNb); return UciEngine.create(engineName, enginePath, customEngineInit);
return UciEngine.create(engineName, workers, customEngineInit);
} }
public static isSupported() { public static isSupported() {

View File

@@ -1,13 +1,9 @@
import { EngineName } from "@/types/enums"; import { EngineName } from "@/types/enums";
import { UciEngine } from "./uciEngine"; import { UciEngine } from "./uciEngine";
import { isMultiThreadSupported, isWasmSupported } from "./shared"; import { isMultiThreadSupported, isWasmSupported } from "./shared";
import { getEngineWorkers } from "./worker";
export class Stockfish16_1 { export class Stockfish16_1 {
public static async create( public static async create(lite?: boolean): Promise<UciEngine> {
lite?: boolean,
workersNb?: number
): Promise<UciEngine> {
if (!Stockfish16_1.isSupported()) { if (!Stockfish16_1.isSupported()) {
throw new Error("Stockfish 16.1 is not supported"); throw new Error("Stockfish 16.1 is not supported");
} }
@@ -23,9 +19,7 @@ export class Stockfish16_1 {
? EngineName.Stockfish16_1Lite ? EngineName.Stockfish16_1Lite
: EngineName.Stockfish16_1; : EngineName.Stockfish16_1;
const workers = getEngineWorkers(enginePath, workersNb); return UciEngine.create(engineName, enginePath);
return UciEngine.create(engineName, workers);
} }
public static isSupported() { public static isSupported() {

View File

@@ -1,13 +1,9 @@
import { EngineName } from "@/types/enums"; import { EngineName } from "@/types/enums";
import { UciEngine } from "./uciEngine"; import { UciEngine } from "./uciEngine";
import { isMultiThreadSupported, isWasmSupported } from "./shared"; import { isMultiThreadSupported, isWasmSupported } from "./shared";
import { getEngineWorkers } from "./worker";
export class Stockfish17 { export class Stockfish17 {
public static async create( public static async create(lite?: boolean): Promise<UciEngine> {
lite?: boolean,
workersNb?: number
): Promise<UciEngine> {
if (!Stockfish17.isSupported()) { if (!Stockfish17.isSupported()) {
throw new Error("Stockfish 17 is not supported"); throw new Error("Stockfish 17 is not supported");
} }
@@ -23,9 +19,7 @@ export class Stockfish17 {
? EngineName.Stockfish17Lite ? EngineName.Stockfish17Lite
: EngineName.Stockfish17; : EngineName.Stockfish17;
const workers = getEngineWorkers(enginePath, workersNb); return UciEngine.create(engineName, enginePath);
return UciEngine.create(engineName, workers);
} }
public static isSupported() { public static isSupported() {

View File

@@ -15,38 +15,41 @@ import { getLichessEval } from "../lichess";
import { getMovesClassification } from "./helpers/moveClassification"; import { getMovesClassification } from "./helpers/moveClassification";
import { computeEstimatedElo } from "./helpers/estimateElo"; import { computeEstimatedElo } from "./helpers/estimateElo";
import { EngineWorker, WorkerJob } from "@/types/engine"; import { EngineWorker, WorkerJob } from "@/types/engine";
import { getEngineWorker, sendCommandsToWorker } from "./worker";
export class UciEngine { export class UciEngine {
private workers: EngineWorker[]; public readonly name: EngineName;
private workers: EngineWorker[] = [];
private workerQueue: WorkerJob[] = []; private workerQueue: WorkerJob[] = [];
private isReady = false; private isReady = false;
private engineName: EngineName; private enginePath: string;
private customEngineInit?:
| ((worker: EngineWorker) => Promise<void>)
| undefined = undefined;
private multiPv = 3; private multiPv = 3;
private elo: number | undefined = undefined; private elo: number | undefined = undefined;
private constructor(engineName: EngineName, workers: EngineWorker[]) { private constructor(
this.engineName = engineName; engineName: EngineName,
this.workers = workers; enginePath: string,
customEngineInit: UciEngine["customEngineInit"]
) {
this.name = engineName;
this.enginePath = enginePath;
this.customEngineInit = customEngineInit;
} }
public static async create( public static async create(
engineName: EngineName, engineName: EngineName,
workers: EngineWorker[], enginePath: string,
customEngineInit?: ( customEngineInit?: UciEngine["customEngineInit"]
sendCommands: UciEngine["sendCommands"]
) => Promise<void>
): Promise<UciEngine> { ): Promise<UciEngine> {
const engine = new UciEngine(engineName, workers); const engine = new UciEngine(engineName, enginePath, customEngineInit);
await engine.broadcastCommands(["uci"], "uciok"); await engine.addNewWorker();
await engine.setMultiPv(engine.multiPv, true);
await customEngineInit?.(engine.sendCommands.bind(engine));
for (const worker of workers) {
worker.isReady = true;
}
engine.isReady = true; engine.isReady = true;
console.log(`${engineName} initialized with ${workers.length} workers`); console.log(`${engineName} initialized`);
return engine; return engine;
} }
@@ -61,31 +64,34 @@ export class UciEngine {
return undefined; return undefined;
} }
private releaseWorker(worker: EngineWorker) { private async releaseWorker(worker: EngineWorker) {
worker.isReady = true;
const nextJob = this.workerQueue.shift(); const nextJob = this.workerQueue.shift();
if (!nextJob) {
if (nextJob) { worker.isReady = true;
this.sendCommands( return;
nextJob.commands,
nextJob.finalMessage,
nextJob.onNewMessage
).then(nextJob.resolve);
} }
const res = await sendCommandsToWorker(
worker,
nextJob.commands,
nextJob.finalMessage,
nextJob.onNewMessage
);
this.releaseWorker(worker);
nextJob.resolve(res);
} }
private async setMultiPv(multiPv: number, initCase = false) { private async setMultiPv(multiPv: number) {
if (!initCase) { if (multiPv === this.multiPv) return;
if (multiPv === this.multiPv) return;
this.throwErrorIfNotReady(); this.throwErrorIfNotReady();
}
if (multiPv < 2 || multiPv > 6) { if (multiPv < 2 || multiPv > 6) {
throw new Error(`Invalid MultiPV value : ${multiPv}`); throw new Error(`Invalid MultiPV value : ${multiPv}`);
} }
await this.broadcastCommands( await this.sendCommandsToEachWorker(
[`setoption name MultiPV value ${multiPv}`, "isready"], [`setoption name MultiPV value ${multiPv}`, "isready"],
"readyok" "readyok"
); );
@@ -93,23 +99,21 @@ export class UciEngine {
this.multiPv = multiPv; this.multiPv = multiPv;
} }
private async setElo(elo: number, initCase = false) { private async setElo(elo: number) {
if (!initCase) { if (elo === this.elo) return;
if (elo === this.elo) return;
this.throwErrorIfNotReady(); this.throwErrorIfNotReady();
}
if (elo < 1320 || elo > 3190) { if (elo < 1320 || elo > 3190) {
throw new Error(`Invalid Elo value : ${elo}`); throw new Error(`Invalid Elo value : ${elo}`);
} }
await this.broadcastCommands( await this.sendCommandsToEachWorker(
["setoption name UCI_LimitStrength value true", "isready"], ["setoption name UCI_LimitStrength value true", "isready"],
"readyok" "readyok"
); );
await this.broadcastCommands( await this.sendCommandsToEachWorker(
[`setoption name UCI_Elo value ${elo}`, "isready"], [`setoption name UCI_Elo value ${elo}`, "isready"],
"readyok" "readyok"
); );
@@ -117,9 +121,13 @@ export class UciEngine {
this.elo = elo; this.elo = elo;
} }
public getIsReady(): boolean {
return this.isReady;
}
private throwErrorIfNotReady() { private throwErrorIfNotReady() {
if (!this.isReady) { if (!this.isReady) {
throw new Error(`${this.engineName} is not ready`); throw new Error(`${this.name} is not ready`);
} }
} }
@@ -128,21 +136,21 @@ export class UciEngine {
this.workerQueue = []; this.workerQueue = [];
for (const worker of this.workers) { for (const worker of this.workers) {
worker.uci("quit"); this.terminateWorker(worker);
worker.terminate?.();
worker.isReady = false;
} }
console.log(`${this.engineName} shutdown`); console.log(`${this.name} shutdown`);
} }
public getIsReady(): boolean { private terminateWorker(worker: EngineWorker) {
return this.isReady; worker.uci("quit");
worker.terminate?.();
worker.isReady = false;
} }
public async stopSearch(): Promise<void> { public async stopSearch(): Promise<void> {
this.workerQueue = []; this.workerQueue = [];
await this.broadcastCommands(["stop", "isready"], "readyok"); await this.sendCommandsToEachWorker(["stop", "isready"], "readyok");
for (const worker of this.workers) { for (const worker of this.workers) {
this.releaseWorker(worker); this.releaseWorker(worker);
@@ -167,46 +175,74 @@ export class UciEngine {
}); });
} }
return this.sendCommandsToWorker( const res = await sendCommandsToWorker(
worker, worker,
commands, commands,
finalMessage, finalMessage,
onNewMessage onNewMessage
); );
this.releaseWorker(worker);
return res;
} }
private async sendCommandsToWorker( private async sendCommandsToEachWorker(
worker: EngineWorker,
commands: string[],
finalMessage: string,
onNewMessage?: (messages: string[]) => void
): Promise<string[]> {
return new Promise((resolve) => {
const messages: string[] = [];
worker.listen = (data) => {
messages.push(data);
onNewMessage?.(messages);
if (data.startsWith(finalMessage)) {
this.releaseWorker(worker);
resolve(messages);
}
};
for (const command of commands) {
worker.uci(command);
}
});
}
private async broadcastCommands(
commands: string[], commands: string[],
finalMessage: string, finalMessage: string,
onNewMessage?: (messages: string[]) => void onNewMessage?: (messages: string[]) => void
): Promise<void> { ): Promise<void> {
await Promise.all( await Promise.all(
this.workers.map((worker) => this.workers.map(async (worker) => {
this.sendCommandsToWorker(worker, commands, finalMessage, onNewMessage) await sendCommandsToWorker(
) worker,
commands,
finalMessage,
onNewMessage
);
this.releaseWorker(worker);
})
);
}
private async addNewWorker() {
const worker = getEngineWorker(this.enginePath);
await sendCommandsToWorker(worker, ["uci"], "uciok");
await sendCommandsToWorker(
worker,
[`setoption name MultiPV value ${this.multiPv}`, "isready"],
"readyok"
);
await this.customEngineInit?.(worker);
await sendCommandsToWorker(worker, ["ucinewgame", "isready"], "readyok");
this.workers.push(worker);
this.releaseWorker(worker);
}
private async setWorkersNb(workersNb: number) {
if (workersNb === this.workers.length) return;
if (workersNb < 1) {
throw new Error(
`Number of workers must be greater than 0, got ${workersNb} instead`
);
}
if (workersNb < this.workers.length) {
const workersToRemove = this.workers.slice(workersNb);
this.workers = this.workers.slice(0, workersNb);
for (const worker of workersToRemove) {
this.terminateWorker(worker);
}
return;
}
const workersNbToCreate = workersNb - this.workers.length;
await Promise.all(
new Array(workersNbToCreate).fill(0).map(() => this.addNewWorker())
); );
} }
@@ -217,13 +253,15 @@ export class UciEngine {
multiPv = this.multiPv, multiPv = this.multiPv,
setEvaluationProgress, setEvaluationProgress,
playersRatings, playersRatings,
workersNb = 1,
}: EvaluateGameParams): Promise<GameEval> { }: EvaluateGameParams): Promise<GameEval> {
this.throwErrorIfNotReady(); this.throwErrorIfNotReady();
setEvaluationProgress?.(1); setEvaluationProgress?.(1);
await this.setMultiPv(multiPv); await this.setMultiPv(multiPv);
this.isReady = false; this.isReady = false;
await this.broadcastCommands(["ucinewgame", "isready"], "readyok"); await this.sendCommandsToEachWorker(["ucinewgame", "isready"], "readyok");
this.setWorkersNb(workersNb);
const positions: PositionEval[] = new Array(fens.length); const positions: PositionEval[] = new Array(fens.length);
let completed = 0; let completed = 0;
@@ -267,10 +305,11 @@ export class UciEngine {
return; return;
} }
const result = await this.evaluatePosition(fen, depth); const result = await this.evaluatePosition(fen, depth, workersNb);
updateEval(i, result); updateEval(i, result);
}) })
); );
await this.setWorkersNb(1);
const positionsWithClassification = getMovesClassification( const positionsWithClassification = getMovesClassification(
positions, positions,
@@ -290,7 +329,7 @@ export class UciEngine {
estimatedElo, estimatedElo,
accuracy, accuracy,
settings: { settings: {
engine: this.engineName, engine: this.name,
date: new Date().toISOString(), date: new Date().toISOString(),
depth, depth,
multiPv, multiPv,
@@ -300,9 +339,10 @@ export class UciEngine {
private async evaluatePosition( private async evaluatePosition(
fen: string, fen: string,
depth = 16 depth = 16,
workersNb: number
): Promise<PositionEval> { ): Promise<PositionEval> {
if (this.workers.length < 2) { if (workersNb < 2) {
const lichessEval = await getLichessEval(fen, this.multiPv); const lichessEval = await getLichessEval(fen, this.multiPv);
if ( if (
lichessEval.lines.length >= this.multiPv && lichessEval.lines.length >= this.multiPv &&

View File

@@ -1,45 +1,59 @@
import { EngineWorker } from "@/types/engine"; import { EngineWorker } from "@/types/engine";
export const getEngineWorkers = ( export const getEngineWorker = (enginePath: string): EngineWorker => {
enginePath: string, console.log(`Creating worker from ${enginePath}`);
workersInputNb?: number
): EngineWorker[] => {
if (workersInputNb !== undefined && workersInputNb < 1) {
throw new Error(
`Number of workers must be greater than 0, got ${workersInputNb} instead`
);
}
const engineWorkers: EngineWorker[] = []; const worker = new window.Worker(enginePath);
const maxWorkersNb = Math.max( const engineWorker: EngineWorker = {
isReady: false,
uci: (command: string) => worker.postMessage(command),
listen: () => null,
terminate: () => worker.terminate(),
};
worker.onmessage = (event) => {
engineWorker.listen(event.data);
};
return engineWorker;
};
export const sendCommandsToWorker = (
worker: EngineWorker,
commands: string[],
finalMessage: string,
onNewMessage?: (messages: string[]) => void
): Promise<string[]> => {
return new Promise((resolve) => {
const messages: string[] = [];
worker.listen = (data) => {
messages.push(data);
onNewMessage?.(messages);
if (data.startsWith(finalMessage)) {
resolve(messages);
}
};
for (const command of commands) {
worker.uci(command);
}
});
};
export const getRecommendedWorkersNb = (): number => {
const maxWorkersNbFromThreads = Math.max(
1, 1,
navigator.hardwareConcurrency - 4, navigator.hardwareConcurrency - 4,
Math.ceil((navigator.hardwareConcurrency * 2) / 3) Math.ceil((navigator.hardwareConcurrency * 2) / 3)
); );
const deviceMemory =
const maxWorkersNbFromMemory =
"deviceMemory" in navigator && typeof navigator.deviceMemory === "number" "deviceMemory" in navigator && typeof navigator.deviceMemory === "number"
? navigator.deviceMemory ? navigator.deviceMemory
: 4; : 4;
const workersNb = workersInputNb ?? Math.min(maxWorkersNb, deviceMemory, 10);
console.log(`Starting ${workersNb} workers from ${enginePath}`);
for (let i = 0; i < workersNb; i++) { return Math.min(maxWorkersNbFromThreads, maxWorkersNbFromMemory, 10);
const worker = new window.Worker(enginePath);
const engineWorker: EngineWorker = {
isReady: false,
uci: (command: string) => worker.postMessage(command),
listen: () => null,
terminate: () => worker.terminate(),
};
worker.onmessage = (event) => {
engineWorker.listen(event.data);
};
engineWorkers.push(engineWorker);
}
return engineWorkers;
}; };

View File

@@ -10,15 +10,13 @@ import {
import { CurrentPosition, PositionEval } from "@/types/eval"; import { CurrentPosition, PositionEval } from "@/types/eval";
import { useAtom, useAtomValue } from "jotai"; import { useAtom, useAtomValue } from "jotai";
import { useEffect } from "react"; import { useEffect } from "react";
import { useEngine } from "../../../hooks/useEngine";
import { EngineName } from "@/types/enums";
import { getEvaluateGameParams } from "@/lib/chess"; import { getEvaluateGameParams } from "@/lib/chess";
import { getMovesClassification } from "@/lib/engine/helpers/moveClassification"; import { getMovesClassification } from "@/lib/engine/helpers/moveClassification";
import { openings } from "@/data/openings"; import { openings } from "@/data/openings";
import { UciEngine } from "@/lib/engine/uciEngine";
export const useCurrentPosition = (engineName?: EngineName) => { export const useCurrentPosition = (engine: UciEngine | null) => {
const [currentPosition, setCurrentPosition] = useAtom(currentPositionAtom); const [currentPosition, setCurrentPosition] = useAtom(currentPositionAtom);
const engine = useEngine(engineName, 1);
const gameEval = useAtomValue(gameEvalAtom); const gameEval = useAtomValue(gameEvalAtom);
const game = useAtomValue(gameAtom); const game = useAtomValue(gameAtom);
const board = useAtomValue(boardAtom); const board = useAtomValue(boardAtom);
@@ -77,7 +75,7 @@ export const useCurrentPosition = (engineName?: EngineName) => {
if ( if (
!position.eval && !position.eval &&
engine?.getIsReady() && engine?.getIsReady() &&
engineName && engine.name &&
!board.isCheckmate() && !board.isCheckmate() &&
!board.isStalemate() !board.isStalemate()
) { ) {
@@ -85,12 +83,13 @@ export const useCurrentPosition = (engineName?: EngineName) => {
fen: string, fen: string,
setPartialEval?: (positionEval: PositionEval) => void setPartialEval?: (positionEval: PositionEval) => void
) => { ) => {
if (!engine?.getIsReady() || !engineName) if (!engine.getIsReady()) {
throw new Error("Engine not ready"); throw new Error("Engine not ready");
}
const savedEval = savedEvals[fen]; const savedEval = savedEvals[fen];
if ( if (
savedEval && savedEval &&
savedEval.engine === engineName && savedEval.engine === engine.name &&
(savedEval.lines?.length ?? 0) >= multiPv && (savedEval.lines?.length ?? 0) >= multiPv &&
(savedEval.lines[0].depth ?? 0) >= depth (savedEval.lines[0].depth ?? 0) >= depth
) { ) {
@@ -111,7 +110,7 @@ export const useCurrentPosition = (engineName?: EngineName) => {
setSavedEvals((prev) => ({ setSavedEvals((prev) => ({
...prev, ...prev,
[fen]: { ...rawPositionEval, engine: engineName }, [fen]: { ...rawPositionEval, engine: engine.name },
})); }));
return rawPositionEval; return rawPositionEval;
@@ -165,7 +164,9 @@ export const useCurrentPosition = (engineName?: EngineName) => {
} }
return () => { return () => {
engine?.stopSearch(); if (engine?.getIsReady()) {
engine?.stopSearch();
}
}; };
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [gameEval, board, game, engine, depth, multiPv]); }, [gameEval, board, game, engine, depth, multiPv]);

View File

@@ -7,13 +7,12 @@ import {
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { import {
boardAtom, boardAtom,
currentPositionAtom,
engineMultiPvAtom, engineMultiPvAtom,
engineNameAtom,
gameAtom, gameAtom,
gameEvalAtom, gameEvalAtom,
} from "../../states"; } from "../../states";
import LineEvaluation from "./lineEvaluation"; import LineEvaluation from "./lineEvaluation";
import { useCurrentPosition } from "../../hooks/useCurrentPosition";
import { LineEval } from "@/types/eval"; import { LineEval } from "@/types/eval";
import PlayersMetric from "./playersMetric"; import PlayersMetric from "./playersMetric";
import MoveInfo from "./moveInfo"; import MoveInfo from "./moveInfo";
@@ -21,8 +20,7 @@ import Opening from "./opening";
export default function AnalysisTab(props: GridProps) { export default function AnalysisTab(props: GridProps) {
const linesNumber = useAtomValue(engineMultiPvAtom); const linesNumber = useAtomValue(engineMultiPvAtom);
const engineName = useAtomValue(engineNameAtom); const position = useAtomValue(currentPositionAtom);
const position = useCurrentPosition(engineName);
const game = useAtomValue(gameAtom); const game = useAtomValue(gameAtom);
const board = useAtomValue(boardAtom); const board = useAtomValue(boardAtom);
const gameEval = useAtomValue(gameEvalAtom); const gameEval = useAtomValue(gameEvalAtom);

View File

@@ -18,10 +18,13 @@ import { SavedEvals } from "@/types/eval";
import { useEffect, useCallback } from "react"; import { useEffect, useCallback } from "react";
import { usePlayersData } from "@/hooks/usePlayersData"; import { usePlayersData } from "@/hooks/usePlayersData";
import { Typography } from "@mui/material"; import { Typography } from "@mui/material";
import { useCurrentPosition } from "../hooks/useCurrentPosition";
import { getRecommendedWorkersNb } from "@/lib/engine/worker";
export default function AnalyzeButton() { export default function AnalyzeButton() {
const engineName = useAtomValue(engineNameAtom); const engineName = useAtomValue(engineNameAtom);
const engine = useEngine(engineName); const engine = useEngine(engineName);
useCurrentPosition(engine);
const [evaluationProgress, setEvaluationProgress] = useAtom( const [evaluationProgress, setEvaluationProgress] = useAtom(
evaluationProgressAtom evaluationProgressAtom
); );
@@ -55,6 +58,7 @@ export default function AnalyzeButton() {
white: white?.rating, white: white?.rating,
black: black?.rating, black: black?.rating,
}, },
workersNb: getRecommendedWorkersNb(),
}); });
setEval(newGameEval); setEval(newGameEval);

View File

@@ -19,7 +19,7 @@ import { usePlayersData } from "@/hooks/usePlayersData";
export default function BoardContainer() { export default function BoardContainer() {
const screenSize = useScreenSize(); const screenSize = useScreenSize();
const engineName = useAtomValue(enginePlayNameAtom); const engineName = useAtomValue(enginePlayNameAtom);
const engine = useEngine(engineName, 1); const engine = useEngine(engineName);
const game = useAtomValue(gameAtom); const game = useAtomValue(gameAtom);
const { white, black } = usePlayersData(gameAtom); const { white, black } = usePlayersData(gameAtom);
const playerColor = useAtomValue(playerColorAtom); const playerColor = useAtomValue(playerColorAtom);

View File

@@ -62,6 +62,7 @@ export interface EvaluateGameParams {
multiPv?: number; multiPv?: number;
setEvaluationProgress?: (value: number) => void; setEvaluationProgress?: (value: number) => void;
playersRatings?: { white?: number; black?: number }; playersRatings?: { white?: number; black?: number };
workersNb?: number;
} }
export interface SavedEval { export interface SavedEval {