diff --git a/assets/showcase.png b/assets/showcase.png index 63280eb..f9eaaba 100644 Binary files a/assets/showcase.png and b/assets/showcase.png differ diff --git a/src/components/LinearProgressBar.tsx b/src/components/LinearProgressBar.tsx index d81e717..26c35c7 100644 --- a/src/components/LinearProgressBar.tsx +++ b/src/components/LinearProgressBar.tsx @@ -12,44 +12,40 @@ const LinearProgressBar = ( if (props.value === 0) return null; return ( - + {props.label} - - - ({ - borderRadius: "5px", - height: "5px", - [`&.${linearProgressClasses.colorPrimary}`]: { - backgroundColor: - theme.palette.grey[ - theme.palette.mode === "light" ? 200 : 700 - ], - }, - [`& .${linearProgressClasses.bar}`]: { - borderRadius: 5, - backgroundColor: "#308fe8", - }, - })} - /> - - - {`${Math.round( - props.value - )}%`} - + + ({ + borderRadius: "5px", + height: "5px", + [`&.${linearProgressClasses.colorPrimary}`]: { + backgroundColor: + theme.palette.grey[theme.palette.mode === "light" ? 200 : 700], + }, + [`& .${linearProgressClasses.bar}`]: { + borderRadius: 5, + backgroundColor: "#308fe8", + }, + })} + /> + + + {`${Math.round( + props.value + )}%`} ); diff --git a/src/hooks/useEngine.ts b/src/hooks/useEngine.ts index 6bd2d44..e51a6f7 100644 --- a/src/hooks/useEngine.ts +++ b/src/hooks/useEngine.ts @@ -7,10 +7,7 @@ import { UciEngine } from "@/lib/engine/uciEngine"; import { EngineName } from "@/types/enums"; import { useEffect, useState } from "react"; -export const useEngine = ( - engineName: EngineName | undefined, - workersNb?: number -) => { +export const useEngine = (engineName: EngineName | undefined) => { const [engine, setEngine] = useState(null); useEffect(() => { @@ -20,35 +17,32 @@ export const useEngine = ( return; } - pickEngine(engineName, workersNb).then((newEngine) => { + pickEngine(engineName).then((newEngine) => { setEngine((prev) => { prev?.shutdown(); return newEngine; }); }); - }, [engineName, workersNb]); + }, [engineName]); return engine; }; -const pickEngine = ( - engine: EngineName, - workersNb?: number -): Promise => { +const pickEngine = (engine: EngineName): Promise => { switch (engine) { case EngineName.Stockfish17: - return Stockfish17.create(false, workersNb); + return Stockfish17.create(false); case EngineName.Stockfish17Lite: - return Stockfish17.create(true, workersNb); + return Stockfish17.create(true); case EngineName.Stockfish16_1: - return Stockfish16_1.create(false, workersNb); + return Stockfish16_1.create(false); case EngineName.Stockfish16_1Lite: - return Stockfish16_1.create(true, workersNb); + return Stockfish16_1.create(true); case EngineName.Stockfish16: - return Stockfish16.create(false, workersNb); + return Stockfish16.create(false); case EngineName.Stockfish16NNUE: - return Stockfish16.create(true, workersNb); + return Stockfish16.create(true); case EngineName.Stockfish11: - return Stockfish11.create(workersNb); + return Stockfish11.create(); } }; diff --git a/src/lib/engine/stockfish11.ts b/src/lib/engine/stockfish11.ts index f554e31..9b2db68 100644 --- a/src/lib/engine/stockfish11.ts +++ b/src/lib/engine/stockfish11.ts @@ -1,12 +1,11 @@ import { EngineName } from "@/types/enums"; import { UciEngine } from "./uciEngine"; -import { getEngineWorkers } from "./worker"; export class Stockfish11 { - public static async create(workersNb?: number): Promise { - const workers = getEngineWorkers("engines/stockfish-11.js", workersNb); + public static async create(): Promise { + const enginePath = "engines/stockfish-11.js"; - return UciEngine.create(EngineName.Stockfish11, workers); + return UciEngine.create(EngineName.Stockfish11, enginePath); } public static isSupported() { diff --git a/src/lib/engine/stockfish16.ts b/src/lib/engine/stockfish16.ts index a93e927..5f9586d 100644 --- a/src/lib/engine/stockfish16.ts +++ b/src/lib/engine/stockfish16.ts @@ -1,13 +1,11 @@ import { EngineName } from "@/types/enums"; import { UciEngine } from "./uciEngine"; import { isMultiThreadSupported, isWasmSupported } from "./shared"; -import { getEngineWorkers } from "./worker"; +import { sendCommandsToWorker } from "./worker"; +import { EngineWorker } from "@/types/engine"; export class Stockfish16 { - public static async create( - nnue?: boolean, - workersNb?: number - ): Promise { + public static async create(nnue?: boolean): Promise { if (!Stockfish16.isSupported()) { 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-single.js"; - const customEngineInit = async ( - sendCommands: UciEngine["sendCommands"] - ) => { - await sendCommands( + const customEngineInit = async (worker: EngineWorker) => { + await sendCommandsToWorker( + worker, [`setoption name Use NNUE value ${!!nnue}`, "isready"], "readyok" ); @@ -32,9 +29,7 @@ export class Stockfish16 { ? EngineName.Stockfish16NNUE : EngineName.Stockfish16; - const workers = getEngineWorkers(enginePath, workersNb); - - return UciEngine.create(engineName, workers, customEngineInit); + return UciEngine.create(engineName, enginePath, customEngineInit); } public static isSupported() { diff --git a/src/lib/engine/stockfish16_1.ts b/src/lib/engine/stockfish16_1.ts index 8caeddd..f402067 100644 --- a/src/lib/engine/stockfish16_1.ts +++ b/src/lib/engine/stockfish16_1.ts @@ -1,13 +1,9 @@ import { EngineName } from "@/types/enums"; import { UciEngine } from "./uciEngine"; import { isMultiThreadSupported, isWasmSupported } from "./shared"; -import { getEngineWorkers } from "./worker"; export class Stockfish16_1 { - public static async create( - lite?: boolean, - workersNb?: number - ): Promise { + public static async create(lite?: boolean): Promise { if (!Stockfish16_1.isSupported()) { throw new Error("Stockfish 16.1 is not supported"); } @@ -23,9 +19,7 @@ export class Stockfish16_1 { ? EngineName.Stockfish16_1Lite : EngineName.Stockfish16_1; - const workers = getEngineWorkers(enginePath, workersNb); - - return UciEngine.create(engineName, workers); + return UciEngine.create(engineName, enginePath); } public static isSupported() { diff --git a/src/lib/engine/stockfish17.ts b/src/lib/engine/stockfish17.ts index 3932334..f034313 100644 --- a/src/lib/engine/stockfish17.ts +++ b/src/lib/engine/stockfish17.ts @@ -1,13 +1,9 @@ import { EngineName } from "@/types/enums"; import { UciEngine } from "./uciEngine"; import { isMultiThreadSupported, isWasmSupported } from "./shared"; -import { getEngineWorkers } from "./worker"; export class Stockfish17 { - public static async create( - lite?: boolean, - workersNb?: number - ): Promise { + public static async create(lite?: boolean): Promise { if (!Stockfish17.isSupported()) { throw new Error("Stockfish 17 is not supported"); } @@ -23,9 +19,7 @@ export class Stockfish17 { ? EngineName.Stockfish17Lite : EngineName.Stockfish17; - const workers = getEngineWorkers(enginePath, workersNb); - - return UciEngine.create(engineName, workers); + return UciEngine.create(engineName, enginePath); } public static isSupported() { diff --git a/src/lib/engine/uciEngine.ts b/src/lib/engine/uciEngine.ts index ff6e0be..49402e9 100644 --- a/src/lib/engine/uciEngine.ts +++ b/src/lib/engine/uciEngine.ts @@ -15,38 +15,41 @@ import { getLichessEval } from "../lichess"; import { getMovesClassification } from "./helpers/moveClassification"; import { computeEstimatedElo } from "./helpers/estimateElo"; import { EngineWorker, WorkerJob } from "@/types/engine"; +import { getEngineWorker, sendCommandsToWorker } from "./worker"; export class UciEngine { - private workers: EngineWorker[]; + public readonly name: EngineName; + private workers: EngineWorker[] = []; private workerQueue: WorkerJob[] = []; private isReady = false; - private engineName: EngineName; + private enginePath: string; + private customEngineInit?: + | ((worker: EngineWorker) => Promise) + | undefined = undefined; private multiPv = 3; private elo: number | undefined = undefined; - private constructor(engineName: EngineName, workers: EngineWorker[]) { - this.engineName = engineName; - this.workers = workers; + private constructor( + engineName: EngineName, + enginePath: string, + customEngineInit: UciEngine["customEngineInit"] + ) { + this.name = engineName; + this.enginePath = enginePath; + this.customEngineInit = customEngineInit; } public static async create( engineName: EngineName, - workers: EngineWorker[], - customEngineInit?: ( - sendCommands: UciEngine["sendCommands"] - ) => Promise + enginePath: string, + customEngineInit?: UciEngine["customEngineInit"] ): Promise { - const engine = new UciEngine(engineName, workers); + const engine = new UciEngine(engineName, enginePath, customEngineInit); - await engine.broadcastCommands(["uci"], "uciok"); - await engine.setMultiPv(engine.multiPv, true); - await customEngineInit?.(engine.sendCommands.bind(engine)); - for (const worker of workers) { - worker.isReady = true; - } + await engine.addNewWorker(); engine.isReady = true; - console.log(`${engineName} initialized with ${workers.length} workers`); + console.log(`${engineName} initialized`); return engine; } @@ -61,31 +64,34 @@ export class UciEngine { return undefined; } - private releaseWorker(worker: EngineWorker) { - worker.isReady = true; + private async releaseWorker(worker: EngineWorker) { const nextJob = this.workerQueue.shift(); - - if (nextJob) { - this.sendCommands( - nextJob.commands, - nextJob.finalMessage, - nextJob.onNewMessage - ).then(nextJob.resolve); + if (!nextJob) { + worker.isReady = true; + return; } + + const res = await sendCommandsToWorker( + worker, + nextJob.commands, + nextJob.finalMessage, + nextJob.onNewMessage + ); + + this.releaseWorker(worker); + nextJob.resolve(res); } - private async setMultiPv(multiPv: number, initCase = false) { - if (!initCase) { - if (multiPv === this.multiPv) return; + private async setMultiPv(multiPv: number) { + if (multiPv === this.multiPv) return; - this.throwErrorIfNotReady(); - } + this.throwErrorIfNotReady(); if (multiPv < 2 || multiPv > 6) { throw new Error(`Invalid MultiPV value : ${multiPv}`); } - await this.broadcastCommands( + await this.sendCommandsToEachWorker( [`setoption name MultiPV value ${multiPv}`, "isready"], "readyok" ); @@ -93,23 +99,21 @@ export class UciEngine { this.multiPv = multiPv; } - private async setElo(elo: number, initCase = false) { - if (!initCase) { - if (elo === this.elo) return; + private async setElo(elo: number) { + if (elo === this.elo) return; - this.throwErrorIfNotReady(); - } + this.throwErrorIfNotReady(); if (elo < 1320 || elo > 3190) { throw new Error(`Invalid Elo value : ${elo}`); } - await this.broadcastCommands( + await this.sendCommandsToEachWorker( ["setoption name UCI_LimitStrength value true", "isready"], "readyok" ); - await this.broadcastCommands( + await this.sendCommandsToEachWorker( [`setoption name UCI_Elo value ${elo}`, "isready"], "readyok" ); @@ -117,9 +121,13 @@ export class UciEngine { this.elo = elo; } + public getIsReady(): boolean { + return this.isReady; + } + private throwErrorIfNotReady() { 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 = []; for (const worker of this.workers) { - worker.uci("quit"); - worker.terminate?.(); - worker.isReady = false; + this.terminateWorker(worker); } - console.log(`${this.engineName} shutdown`); + console.log(`${this.name} shutdown`); } - public getIsReady(): boolean { - return this.isReady; + private terminateWorker(worker: EngineWorker) { + worker.uci("quit"); + worker.terminate?.(); + worker.isReady = false; } public async stopSearch(): Promise { this.workerQueue = []; - await this.broadcastCommands(["stop", "isready"], "readyok"); + await this.sendCommandsToEachWorker(["stop", "isready"], "readyok"); for (const worker of this.workers) { this.releaseWorker(worker); @@ -167,46 +175,74 @@ export class UciEngine { }); } - return this.sendCommandsToWorker( + const res = await sendCommandsToWorker( worker, commands, finalMessage, onNewMessage ); + + this.releaseWorker(worker); + return res; } - private async sendCommandsToWorker( - worker: EngineWorker, - commands: string[], - finalMessage: string, - onNewMessage?: (messages: string[]) => void - ): Promise { - 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( + private async sendCommandsToEachWorker( commands: string[], finalMessage: string, onNewMessage?: (messages: string[]) => void ): Promise { await Promise.all( - this.workers.map((worker) => - this.sendCommandsToWorker(worker, commands, finalMessage, onNewMessage) - ) + this.workers.map(async (worker) => { + 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, setEvaluationProgress, playersRatings, + workersNb = 1, }: EvaluateGameParams): Promise { this.throwErrorIfNotReady(); setEvaluationProgress?.(1); await this.setMultiPv(multiPv); 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); let completed = 0; @@ -267,10 +305,11 @@ export class UciEngine { return; } - const result = await this.evaluatePosition(fen, depth); + const result = await this.evaluatePosition(fen, depth, workersNb); updateEval(i, result); }) ); + await this.setWorkersNb(1); const positionsWithClassification = getMovesClassification( positions, @@ -290,7 +329,7 @@ export class UciEngine { estimatedElo, accuracy, settings: { - engine: this.engineName, + engine: this.name, date: new Date().toISOString(), depth, multiPv, @@ -300,9 +339,10 @@ export class UciEngine { private async evaluatePosition( fen: string, - depth = 16 + depth = 16, + workersNb: number ): Promise { - if (this.workers.length < 2) { + if (workersNb < 2) { const lichessEval = await getLichessEval(fen, this.multiPv); if ( lichessEval.lines.length >= this.multiPv && diff --git a/src/lib/engine/worker.ts b/src/lib/engine/worker.ts index 9b85a12..b3e3e9f 100644 --- a/src/lib/engine/worker.ts +++ b/src/lib/engine/worker.ts @@ -1,45 +1,59 @@ import { EngineWorker } from "@/types/engine"; -export const getEngineWorkers = ( - enginePath: string, - workersInputNb?: number -): EngineWorker[] => { - if (workersInputNb !== undefined && workersInputNb < 1) { - throw new Error( - `Number of workers must be greater than 0, got ${workersInputNb} instead` - ); - } +export const getEngineWorker = (enginePath: string): EngineWorker => { + console.log(`Creating worker from ${enginePath}`); - 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 => { + 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, navigator.hardwareConcurrency - 4, Math.ceil((navigator.hardwareConcurrency * 2) / 3) ); - const deviceMemory = + + const maxWorkersNbFromMemory = "deviceMemory" in navigator && typeof navigator.deviceMemory === "number" ? navigator.deviceMemory : 4; - const workersNb = workersInputNb ?? Math.min(maxWorkersNb, deviceMemory, 10); - console.log(`Starting ${workersNb} workers from ${enginePath}`); - for (let i = 0; i < workersNb; i++) { - 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; + return Math.min(maxWorkersNbFromThreads, maxWorkersNbFromMemory, 10); }; diff --git a/src/sections/analysis/hooks/useCurrentPosition.ts b/src/sections/analysis/hooks/useCurrentPosition.ts index 5fd3470..3c18ba3 100644 --- a/src/sections/analysis/hooks/useCurrentPosition.ts +++ b/src/sections/analysis/hooks/useCurrentPosition.ts @@ -10,15 +10,13 @@ import { import { CurrentPosition, PositionEval } from "@/types/eval"; import { useAtom, useAtomValue } from "jotai"; import { useEffect } from "react"; -import { useEngine } from "../../../hooks/useEngine"; -import { EngineName } from "@/types/enums"; import { getEvaluateGameParams } from "@/lib/chess"; import { getMovesClassification } from "@/lib/engine/helpers/moveClassification"; 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 engine = useEngine(engineName, 1); const gameEval = useAtomValue(gameEvalAtom); const game = useAtomValue(gameAtom); const board = useAtomValue(boardAtom); @@ -77,7 +75,7 @@ export const useCurrentPosition = (engineName?: EngineName) => { if ( !position.eval && engine?.getIsReady() && - engineName && + engine.name && !board.isCheckmate() && !board.isStalemate() ) { @@ -85,12 +83,13 @@ export const useCurrentPosition = (engineName?: EngineName) => { fen: string, setPartialEval?: (positionEval: PositionEval) => void ) => { - if (!engine?.getIsReady() || !engineName) + if (!engine.getIsReady()) { throw new Error("Engine not ready"); + } const savedEval = savedEvals[fen]; if ( savedEval && - savedEval.engine === engineName && + savedEval.engine === engine.name && (savedEval.lines?.length ?? 0) >= multiPv && (savedEval.lines[0].depth ?? 0) >= depth ) { @@ -111,7 +110,7 @@ export const useCurrentPosition = (engineName?: EngineName) => { setSavedEvals((prev) => ({ ...prev, - [fen]: { ...rawPositionEval, engine: engineName }, + [fen]: { ...rawPositionEval, engine: engine.name }, })); return rawPositionEval; @@ -165,7 +164,9 @@ export const useCurrentPosition = (engineName?: EngineName) => { } return () => { - engine?.stopSearch(); + if (engine?.getIsReady()) { + engine?.stopSearch(); + } }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [gameEval, board, game, engine, depth, multiPv]); diff --git a/src/sections/analysis/panelBody/analysisTab/index.tsx b/src/sections/analysis/panelBody/analysisTab/index.tsx index 82f8990..20030ec 100644 --- a/src/sections/analysis/panelBody/analysisTab/index.tsx +++ b/src/sections/analysis/panelBody/analysisTab/index.tsx @@ -7,13 +7,12 @@ import { import { useAtomValue } from "jotai"; import { boardAtom, + currentPositionAtom, engineMultiPvAtom, - engineNameAtom, gameAtom, gameEvalAtom, } from "../../states"; import LineEvaluation from "./lineEvaluation"; -import { useCurrentPosition } from "../../hooks/useCurrentPosition"; import { LineEval } from "@/types/eval"; import PlayersMetric from "./playersMetric"; import MoveInfo from "./moveInfo"; @@ -21,8 +20,7 @@ import Opening from "./opening"; export default function AnalysisTab(props: GridProps) { const linesNumber = useAtomValue(engineMultiPvAtom); - const engineName = useAtomValue(engineNameAtom); - const position = useCurrentPosition(engineName); + const position = useAtomValue(currentPositionAtom); const game = useAtomValue(gameAtom); const board = useAtomValue(boardAtom); const gameEval = useAtomValue(gameEvalAtom); diff --git a/src/sections/analysis/panelHeader/analyzeButton.tsx b/src/sections/analysis/panelHeader/analyzeButton.tsx index 53988d1..cc6d808 100644 --- a/src/sections/analysis/panelHeader/analyzeButton.tsx +++ b/src/sections/analysis/panelHeader/analyzeButton.tsx @@ -18,10 +18,13 @@ import { SavedEvals } from "@/types/eval"; import { useEffect, useCallback } from "react"; import { usePlayersData } from "@/hooks/usePlayersData"; import { Typography } from "@mui/material"; +import { useCurrentPosition } from "../hooks/useCurrentPosition"; +import { getRecommendedWorkersNb } from "@/lib/engine/worker"; export default function AnalyzeButton() { const engineName = useAtomValue(engineNameAtom); const engine = useEngine(engineName); + useCurrentPosition(engine); const [evaluationProgress, setEvaluationProgress] = useAtom( evaluationProgressAtom ); @@ -55,6 +58,7 @@ export default function AnalyzeButton() { white: white?.rating, black: black?.rating, }, + workersNb: getRecommendedWorkersNb(), }); setEval(newGameEval); diff --git a/src/sections/play/board.tsx b/src/sections/play/board.tsx index 4fdac6e..d49c634 100644 --- a/src/sections/play/board.tsx +++ b/src/sections/play/board.tsx @@ -19,7 +19,7 @@ import { usePlayersData } from "@/hooks/usePlayersData"; export default function BoardContainer() { const screenSize = useScreenSize(); const engineName = useAtomValue(enginePlayNameAtom); - const engine = useEngine(engineName, 1); + const engine = useEngine(engineName); const game = useAtomValue(gameAtom); const { white, black } = usePlayersData(gameAtom); const playerColor = useAtomValue(playerColorAtom); diff --git a/src/types/eval.ts b/src/types/eval.ts index 396f9c9..467aeb9 100644 --- a/src/types/eval.ts +++ b/src/types/eval.ts @@ -62,6 +62,7 @@ export interface EvaluateGameParams { multiPv?: number; setEvaluationProgress?: (value: number) => void; playersRatings?: { white?: number; black?: number }; + workersNb?: number; } export interface SavedEval {