feat : add engines choices

This commit is contained in:
GuillaumeSD
2024-04-14 04:07:18 +02:00
parent 56b267762e
commit 1546709452
20 changed files with 172 additions and 37 deletions

View File

@@ -21,7 +21,7 @@
] ]
}, },
{ {
"source": "/engines/stockfish-wasm/stockfish-nnue-16.js", "source": "/engines/**",
"headers": [ "headers": [
{ {
"key": "Cross-Origin-Embedder-Policy", "key": "Cross-Origin-Embedder-Policy",
@@ -30,6 +30,19 @@
{ {
"key": "Cross-Origin-Opener-Policy", "key": "Cross-Origin-Opener-Policy",
"value": "same-origin" "value": "same-origin"
},
{
"key": "Cache-Control",
"value": "public, max-age=3600, immutable"
}
]
},
{
"source": "/engines/stockfish-16-wasm/nn-5af11540bbfe.nnue",
"headers": [
{
"key": "Cache-Control",
"value": "public, max-age=31536000, immutable"
} }
] ]
}, },

View File

@@ -26,7 +26,7 @@ const nextConfig = (phase) =>
], ],
}, },
{ {
source: "/engines/stockfish-wasm/stockfish-nnue-16.js", source: "/engines/:blob*",
headers: [ headers: [
{ {
key: "Cross-Origin-Embedder-Policy", key: "Cross-Origin-Embedder-Policy",

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -1,3 +1,4 @@
import { Stockfish11 } from "@/lib/engine/stockfish11";
import { Stockfish16 } from "@/lib/engine/stockfish16"; import { Stockfish16 } from "@/lib/engine/stockfish16";
import { UciEngine } from "@/lib/engine/uciEngine"; import { UciEngine } from "@/lib/engine/uciEngine";
import { EngineName } from "@/types/enums"; import { EngineName } from "@/types/enums";
@@ -9,6 +10,10 @@ export const useEngine = (engineName: EngineName | undefined) => {
useEffect(() => { useEffect(() => {
if (!engineName) return; if (!engineName) return;
if (engineName.includes("stockfish_16") && !Stockfish16.isSupported()) {
return;
}
const engine = pickEngine(engineName); const engine = pickEngine(engineName);
engine.init().then(() => { engine.init().then(() => {
setEngine(engine); setEngine(engine);
@@ -25,6 +30,12 @@ export const useEngine = (engineName: EngineName | undefined) => {
const pickEngine = (engine: EngineName): UciEngine => { const pickEngine = (engine: EngineName): UciEngine => {
switch (engine) { switch (engine) {
case EngineName.Stockfish16: case EngineName.Stockfish16:
return new Stockfish16(); return new Stockfish16(false);
case EngineName.Stockfish16NNUE:
return new Stockfish16(true);
case EngineName.Stockfish11:
return new Stockfish11();
default:
throw new Error(`Engine ${engine} does not exist ?!`);
} }
}; };

View File

@@ -0,0 +1,8 @@
import { EngineName } from "@/types/enums";
import { UciEngine } from "./uciEngine";
export class Stockfish11 extends UciEngine {
constructor() {
super(EngineName.Stockfish11, "engines/stockfish-11.js");
}
}

View File

@@ -2,17 +2,29 @@ import { EngineName } from "@/types/enums";
import { UciEngine } from "./uciEngine"; import { UciEngine } from "./uciEngine";
export class Stockfish16 extends UciEngine { export class Stockfish16 extends UciEngine {
constructor() { constructor(nnue?: boolean) {
const isWasmSupported = Stockfish16.isWasmSupported(); if (!Stockfish16.isSupported()) {
throw new Error("Stockfish 16 is not supported");
const enginePath = isWasmSupported
? "engines/stockfish-wasm/stockfish-nnue-16.js"
: "engines/stockfish.js";
super(EngineName.Stockfish16, enginePath);
} }
public static isWasmSupported() { const isMultiThreadSupported = Stockfish16.isMultiThreadSupported();
if (!isMultiThreadSupported) console.log("Single thread mode");
const enginePath = isMultiThreadSupported
? "engines/stockfish-16-wasm/stockfish-nnue-16.js"
: "engines/stockfish-16-wasm/stockfish-nnue-16-single.js";
const customEngineInit = async () => {
await this.sendCommands(
[`setoption name Use NNUE value ${!!nnue}`, "isready"],
"readyok"
);
};
super(EngineName.Stockfish16, enginePath, customEngineInit);
}
public static isSupported() {
return ( return (
typeof WebAssembly === "object" && typeof WebAssembly === "object" &&
WebAssembly.validate( WebAssembly.validate(
@@ -20,4 +32,8 @@ export class Stockfish16 extends UciEngine {
) )
); );
} }
public static isMultiThreadSupported() {
return SharedArrayBuffer !== undefined;
}
} }

View File

@@ -20,10 +20,16 @@ export abstract class UciEngine {
private engineName: EngineName; private engineName: EngineName;
private multiPv = 3; private multiPv = 3;
private skillLevel: number | undefined = undefined; private skillLevel: number | undefined = undefined;
private customEngineInit?: () => Promise<void>;
constructor(engineName: EngineName, enginePath: string) { constructor(
engineName: EngineName,
enginePath: string,
customEngineInit?: () => Promise<void>
) {
this.engineName = engineName; this.engineName = engineName;
this.worker = new Worker(enginePath); this.worker = new Worker(enginePath);
this.customEngineInit = customEngineInit;
console.log(`${engineName} created`); console.log(`${engineName} created`);
} }
@@ -31,6 +37,7 @@ export abstract class UciEngine {
public async init(): Promise<void> { public async init(): Promise<void> {
await this.sendCommands(["uci"], "uciok"); await this.sendCommands(["uci"], "uciok");
await this.setMultiPv(this.multiPv, true); await this.setMultiPv(this.multiPv, true);
await this.customEngineInit?.();
this.ready = true; this.ready = true;
console.log(`${this.engineName} initialized`); console.log(`${this.engineName} initialized`);
} }
@@ -94,7 +101,7 @@ export abstract class UciEngine {
await this.sendCommands(["stop", "isready"], "readyok"); await this.sendCommands(["stop", "isready"], "readyok");
} }
private async sendCommands( protected async sendCommands(
commands: string[], commands: string[],
finalMessage: string, finalMessage: string,
onNewMessage?: (messages: string[]) => void onNewMessage?: (messages: string[]) => void

View File

@@ -1,11 +1,15 @@
import { Icon } from "@iconify/react"; import { Icon } from "@iconify/react";
import { Grid, List, Typography } from "@mui/material"; import { Grid, List, Typography } from "@mui/material";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { boardAtom, engineMultiPvAtom, gameAtom } from "../states"; import {
boardAtom,
engineMultiPvAtom,
engineNameAtom,
gameAtom,
} from "../states";
import LineEvaluation from "./lineEvaluation"; import LineEvaluation from "./lineEvaluation";
import { useCurrentPosition } from "../hooks/useCurrentPosition"; import { useCurrentPosition } from "../hooks/useCurrentPosition";
import { LineEval } from "@/types/eval"; import { LineEval } from "@/types/eval";
import { EngineName } from "@/types/enums";
import EngineSettingsButton from "@/sections/engineSettings/engineSettingsButton"; import EngineSettingsButton from "@/sections/engineSettings/engineSettingsButton";
import Accuracies from "./accuracies"; import Accuracies from "./accuracies";
import MoveInfo from "./moveInfo"; import MoveInfo from "./moveInfo";
@@ -13,7 +17,8 @@ import Opening from "./opening";
export default function ReviewPanelBody() { export default function ReviewPanelBody() {
const linesNumber = useAtomValue(engineMultiPvAtom); const linesNumber = useAtomValue(engineMultiPvAtom);
const position = useCurrentPosition(EngineName.Stockfish16); const engineName = useAtomValue(engineNameAtom);
const position = useCurrentPosition(engineName);
const game = useAtomValue(gameAtom); const game = useAtomValue(gameAtom);
const board = useAtomValue(boardAtom); const board = useAtomValue(boardAtom);

View File

@@ -2,6 +2,7 @@ import { Icon } from "@iconify/react";
import { import {
engineDepthAtom, engineDepthAtom,
engineMultiPvAtom, engineMultiPvAtom,
engineNameAtom,
evaluationProgressAtom, evaluationProgressAtom,
gameAtom, gameAtom,
gameEvalAtom, gameEvalAtom,
@@ -11,11 +12,11 @@ import { getEvaluateGameParams } from "@/lib/chess";
import { useGameDatabase } from "@/hooks/useGameDatabase"; import { useGameDatabase } from "@/hooks/useGameDatabase";
import { LoadingButton } from "@mui/lab"; import { LoadingButton } from "@mui/lab";
import { useEngine } from "@/hooks/useEngine"; import { useEngine } from "@/hooks/useEngine";
import { EngineName } from "@/types/enums";
import { logAnalyticsEvent } from "@/lib/firebase"; import { logAnalyticsEvent } from "@/lib/firebase";
export default function AnalyzeButton() { export default function AnalyzeButton() {
const engine = useEngine(EngineName.Stockfish16); const engineName = useAtomValue(engineNameAtom);
const engine = useEngine(engineName);
const [evaluationProgress, setEvaluationProgress] = useAtom( const [evaluationProgress, setEvaluationProgress] = useAtom(
evaluationProgressAtom evaluationProgressAtom
); );
@@ -49,7 +50,7 @@ export default function AnalyzeButton() {
} }
logAnalyticsEvent("analyze_game", { logAnalyticsEvent("analyze_game", {
engine: EngineName.Stockfish16, engine: engineName,
depth: engineDepth, depth: engineDepth,
multiPv: engineMultiPv, multiPv: engineMultiPv,
nbPositions: params.fens.length, nbPositions: params.fens.length,

View File

@@ -1,3 +1,4 @@
import { EngineName } from "@/types/enums";
import { CurrentPosition, GameEval } from "@/types/eval"; import { CurrentPosition, GameEval } from "@/types/eval";
import { Chess } from "chess.js"; import { Chess } from "chess.js";
import { atom } from "jotai"; import { atom } from "jotai";
@@ -11,6 +12,7 @@ export const boardOrientationAtom = atom(true);
export const showBestMoveArrowAtom = atom(true); export const showBestMoveArrowAtom = atom(true);
export const showPlayerMoveIconAtom = atom(true); export const showPlayerMoveIconAtom = atom(true);
export const engineNameAtom = atom<EngineName>(EngineName.Stockfish16);
export const engineDepthAtom = atom(16); export const engineDepthAtom = atom(16);
export const engineMultiPvAtom = atom(3); export const engineMultiPvAtom = atom(3);
export const evaluationProgressAtom = atom(0); export const evaluationProgressAtom = atom(0);

View File

@@ -14,9 +14,15 @@ import {
Typography, Typography,
Grid, Grid,
} from "@mui/material"; } from "@mui/material";
import { engineDepthAtom, engineMultiPvAtom } from "../analysis/states"; import {
engineNameAtom,
engineDepthAtom,
engineMultiPvAtom,
} from "../analysis/states";
import ArrowOptions from "./arrowOptions"; import ArrowOptions from "./arrowOptions";
import { useAtomLocalStorage } from "@/hooks/useAtomLocalStorage"; import { useAtomLocalStorage } from "@/hooks/useAtomLocalStorage";
import { Stockfish16 } from "@/lib/engine/stockfish16";
import { useEffect } from "react";
interface Props { interface Props {
open: boolean; open: boolean;
@@ -32,6 +38,16 @@ export default function EngineSettingsDialog({ open, onClose }: Props) {
"engine-multi-pv", "engine-multi-pv",
engineMultiPvAtom engineMultiPvAtom
); );
const [engineName, setEngineName] = useAtomLocalStorage(
"engine-name",
engineNameAtom
);
useEffect(() => {
if (!Stockfish16.isSupported()) {
setEngineName(EngineName.Stockfish11);
}
}, [setEngineName]);
return ( return (
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth> <Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
@@ -40,8 +56,10 @@ export default function EngineSettingsDialog({ open, onClose }: Props) {
</DialogTitle> </DialogTitle>
<DialogContent sx={{ paddingBottom: 0 }}> <DialogContent sx={{ paddingBottom: 0 }}>
<Typography> <Typography>
Stockfish 16 is the only engine available now, more engine choices Stockfish 16 Lite (HCE) is the default engine. It offers the best
will come soon ! balance between speed and strength. Stockfish 16 is the strongest
engine available, but please note that it requires a one time download
of 40MB.
</Typography> </Typography>
<Grid <Grid
marginTop={4} marginTop={4}
@@ -60,12 +78,20 @@ export default function EngineSettingsDialog({ open, onClose }: Props) {
id="dialog-select" id="dialog-select"
displayEmpty displayEmpty
input={<OutlinedInput label="Engine" />} input={<OutlinedInput label="Engine" />}
value={EngineName.Stockfish16} value={engineName}
disabled={true} onChange={(e) => setEngineName(e.target.value as EngineName)}
sx={{ width: 200 }} sx={{ width: 280, maxWidth: "100%" }}
> >
{Object.values(EngineName).map((engine) => ( {Object.values(EngineName).map((engine) => (
<MenuItem key={engine} value={engine}> <MenuItem
key={engine}
value={engine}
disabled={
engine.includes("stockfish_16")
? !Stockfish16.isSupported()
: false
}
>
{engineLabel[engine]} {engineLabel[engine]}
</MenuItem> </MenuItem>
))} ))}
@@ -104,5 +130,7 @@ export default function EngineSettingsDialog({ open, onClose }: Props) {
} }
const engineLabel: Record<EngineName, string> = { const engineLabel: Record<EngineName, string> = {
[EngineName.Stockfish16]: "Stockfish 16", [EngineName.Stockfish16]: "Stockfish 16 Lite (HCE)",
[EngineName.Stockfish16NNUE]: "Stockfish 16 (40MB download)",
[EngineName.Stockfish11]: "Stockfish 11",
}; };

View File

@@ -5,11 +5,12 @@ import {
playerColorAtom, playerColorAtom,
isGameInProgressAtom, isGameInProgressAtom,
gameDataAtom, gameDataAtom,
enginePlayNameAtom,
} from "./states"; } from "./states";
import { useChessActions } from "@/hooks/useChessActions"; import { useChessActions } from "@/hooks/useChessActions";
import { useEffect, useMemo } from "react"; import { useEffect, useMemo } from "react";
import { useScreenSize } from "@/hooks/useScreenSize"; import { useScreenSize } from "@/hooks/useScreenSize";
import { Color, EngineName } from "@/types/enums"; import { Color } from "@/types/enums";
import { useEngine } from "@/hooks/useEngine"; import { useEngine } from "@/hooks/useEngine";
import { uciMoveParams } from "@/lib/chess"; import { uciMoveParams } from "@/lib/chess";
import Board from "@/components/board"; import Board from "@/components/board";
@@ -17,7 +18,8 @@ import { useGameData } from "@/hooks/useGameData";
export default function BoardContainer() { export default function BoardContainer() {
const screenSize = useScreenSize(); const screenSize = useScreenSize();
const engine = useEngine(EngineName.Stockfish16); const engineName = useAtomValue(enginePlayNameAtom);
const engine = useEngine(engineName);
const game = useAtomValue(gameAtom); const game = useAtomValue(gameAtom);
const playerColor = useAtomValue(playerColorAtom); const playerColor = useAtomValue(playerColorAtom);
const { makeMove: makeGameMove } = useChessActions(gameAtom); const { makeMove: makeGameMove } = useChessActions(gameAtom);

View File

@@ -24,10 +24,13 @@ import {
playerColorAtom, playerColorAtom,
isGameInProgressAtom, isGameInProgressAtom,
gameAtom, gameAtom,
enginePlayNameAtom,
} from "../states"; } from "../states";
import { useChessActions } from "@/hooks/useChessActions"; import { useChessActions } from "@/hooks/useChessActions";
import { playGameStartSound } from "@/lib/sounds"; import { playGameStartSound } from "@/lib/sounds";
import { logAnalyticsEvent } from "@/lib/firebase"; import { logAnalyticsEvent } from "@/lib/firebase";
import { Stockfish16 } from "@/lib/engine/stockfish16";
import { useEffect } from "react";
interface Props { interface Props {
open: boolean; open: boolean;
@@ -39,6 +42,10 @@ export default function GameSettingsDialog({ open, onClose }: Props) {
"engine-skill-level", "engine-skill-level",
engineSkillLevelAtom engineSkillLevelAtom
); );
const [engineName, setEngineName] = useAtomLocalStorage(
"engine-play-name",
enginePlayNameAtom
);
const [playerColor, setPlayerColor] = useAtom(playerColorAtom); const [playerColor, setPlayerColor] = useAtom(playerColorAtom);
const setIsGameInProgress = useSetAtom(isGameInProgressAtom); const setIsGameInProgress = useSetAtom(isGameInProgressAtom);
const { reset: resetGame } = useChessActions(gameAtom); const { reset: resetGame } = useChessActions(gameAtom);
@@ -55,12 +62,18 @@ export default function GameSettingsDialog({ open, onClose }: Props) {
setIsGameInProgress(true); setIsGameInProgress(true);
logAnalyticsEvent("play_game", { logAnalyticsEvent("play_game", {
engine: EngineName.Stockfish16, engine: engineName,
skillLevel, skillLevel,
playerColor, playerColor,
}); });
}; };
useEffect(() => {
if (!Stockfish16.isSupported()) {
setEngineName(EngineName.Stockfish11);
}
}, [setEngineName]);
return ( return (
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth> <Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
<DialogTitle marginY={1} variant="h5"> <DialogTitle marginY={1} variant="h5">
@@ -68,8 +81,10 @@ export default function GameSettingsDialog({ open, onClose }: Props) {
</DialogTitle> </DialogTitle>
<DialogContent sx={{ paddingBottom: 0 }}> <DialogContent sx={{ paddingBottom: 0 }}>
<Typography> <Typography>
Stockfish 16 is the only engine available now, more engine choices Stockfish 16 Lite (HCE) is the default engine. It offers the best
will come soon ! balance between speed and strength. Stockfish 16 is the strongest
engine available, but please note that it requires a one time download
of 40MB.
</Typography> </Typography>
<Grid <Grid
marginTop={4} marginTop={4}
@@ -88,12 +103,20 @@ export default function GameSettingsDialog({ open, onClose }: Props) {
id="dialog-select" id="dialog-select"
displayEmpty displayEmpty
input={<OutlinedInput label="Engine" />} input={<OutlinedInput label="Engine" />}
value={EngineName.Stockfish16} value={engineName}
disabled={true} onChange={(e) => setEngineName(e.target.value as EngineName)}
sx={{ width: 200 }} sx={{ width: 280, maxWidth: "100%" }}
> >
{Object.values(EngineName).map((engine) => ( {Object.values(EngineName).map((engine) => (
<MenuItem key={engine} value={engine}> <MenuItem
key={engine}
value={engine}
disabled={
engine.includes("stockfish_16")
? !Stockfish16.isSupported()
: false
}
>
{engineLabel[engine]} {engineLabel[engine]}
</MenuItem> </MenuItem>
))} ))}
@@ -145,5 +168,7 @@ export default function GameSettingsDialog({ open, onClose }: Props) {
} }
const engineLabel: Record<EngineName, string> = { const engineLabel: Record<EngineName, string> = {
[EngineName.Stockfish16]: "Stockfish 16", [EngineName.Stockfish16]: "Stockfish 16 Lite (HCE)",
[EngineName.Stockfish16NNUE]: "Stockfish 16 (40MB download)",
[EngineName.Stockfish11]: "Stockfish 11",
}; };

View File

@@ -1,4 +1,4 @@
import { Color } from "@/types/enums"; import { Color, EngineName } from "@/types/enums";
import { CurrentPosition } from "@/types/eval"; import { CurrentPosition } from "@/types/eval";
import { Chess } from "chess.js"; import { Chess } from "chess.js";
import { atom } from "jotai"; import { atom } from "jotai";
@@ -6,5 +6,6 @@ import { atom } from "jotai";
export const gameAtom = atom(new Chess()); export const gameAtom = atom(new Chess());
export const gameDataAtom = atom<CurrentPosition>({}); export const gameDataAtom = atom<CurrentPosition>({});
export const playerColorAtom = atom<Color>(Color.White); export const playerColorAtom = atom<Color>(Color.White);
export const enginePlayNameAtom = atom<EngineName>(EngineName.Stockfish16);
export const engineSkillLevelAtom = atom<number>(1); export const engineSkillLevelAtom = atom<number>(1);
export const isGameInProgressAtom = atom(false); export const isGameInProgressAtom = atom(false);

View File

@@ -6,6 +6,8 @@ export enum GameOrigin {
export enum EngineName { export enum EngineName {
Stockfish16 = "stockfish_16", Stockfish16 = "stockfish_16",
Stockfish16NNUE = "stockfish_16_nnue",
Stockfish11 = "stockfish_11",
} }
export enum MoveClassification { export enum MoveClassification {