Elo play level selection (#17)

* feat(play): switch engine UI and logic from skill level to Elo rating (100–3200)
This commit is contained in:
titanium_machine
2025-05-08 23:47:09 +02:00
committed by GitHub
parent 9245125e13
commit 1ac075f1f0
5 changed files with 40 additions and 35 deletions

View File

@@ -1,13 +1,13 @@
import { Grid2 as Grid, Slider as MuiSlider, Typography } from "@mui/material"; import { Grid2 as Grid, Slider as MuiSlider, Typography } from "@mui/material";
interface Props { export interface Props {
value: number; value: number;
setValue: (value: number) => void; setValue: (value: number) => void;
min: number; min: number;
max: number; max: number;
label: string; label: string;
size?: number; size?: number;
marksFilter?: number; step?: number;
} }
export default function Slider({ export default function Slider({
@@ -17,7 +17,7 @@ export default function Slider({
value, value,
setValue, setValue,
size, size,
marksFilter = 1, step = 1,
}: Props) { }: Props) {
return ( return (
<Grid <Grid
@@ -27,18 +27,14 @@ export default function Slider({
size={size ?? 11} size={size ?? 11}
> >
<Typography id={`input-${label}`} textAlign="left" width="100%"> <Typography id={`input-${label}`} textAlign="left" width="100%">
{label} {`${label}: ${value}`}
</Typography> </Typography>
<MuiSlider <MuiSlider
min={min} min={min}
max={max} max={max}
marks={Array.from({ length: max - min + 1 }, (_, i) => ({ step={step}
value: i + min, valueLabelDisplay="auto"
label: `${i + min}`,
})).filter((_, i) => i % marksFilter === 0)}
step={1}
valueLabelDisplay="off"
value={value} value={value}
onChange={(_, value) => setValue(value as number)} onChange={(_, value) => setValue(value as number)}
aria-labelledby={`input-${label}`} aria-labelledby={`input-${label}`}

View File

@@ -92,6 +92,20 @@ export class UciEngine {
this.multiPv = multiPv; this.multiPv = multiPv;
} }
private async setLimitStrength(on: boolean) {
await this.broadcastCommands(
[`setoption name UCI_LimitStrength value ${on}`, "isready"],
"readyok"
);
}
private async setElo(elo: number) {
await this.broadcastCommands(
[`setoption name UCI_Elo value ${elo}`, "isready"],
"readyok"
);
}
private async setSkillLevel(skillLevel: number, initCase = false) { private async setSkillLevel(skillLevel: number, initCase = false) {
if (!initCase) { if (!initCase) {
if (skillLevel === this.skillLevel) return; if (skillLevel === this.skillLevel) return;
@@ -352,11 +366,12 @@ export class UciEngine {
public async getEngineNextMove( public async getEngineNextMove(
fen: string, fen: string,
skillLevel: number, elo: number,
depth = 16 depth = 16
): Promise<string | undefined> { ): Promise<string | undefined> {
this.throwErrorIfNotReady(); this.throwErrorIfNotReady();
await this.setSkillLevel(skillLevel); await this.setLimitStrength(true);
await this.setElo(elo);
console.log(`Evaluating position: ${fen}`); console.log(`Evaluating position: ${fen}`);

View File

@@ -1,6 +1,6 @@
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { import {
engineSkillLevelAtom, engineEloAtom,
gameAtom, gameAtom,
playerColorAtom, playerColorAtom,
isGameInProgressAtom, isGameInProgressAtom,
@@ -24,7 +24,7 @@ export default function BoardContainer() {
const { white, black } = usePlayersData(gameAtom); const { white, black } = usePlayersData(gameAtom);
const playerColor = useAtomValue(playerColorAtom); const playerColor = useAtomValue(playerColorAtom);
const { makeMove: makeGameMove } = useChessActions(gameAtom); const { makeMove: makeGameMove } = useChessActions(gameAtom);
const engineSkillLevel = useAtomValue(engineSkillLevelAtom); const engineElo = useAtomValue(engineEloAtom);
const isGameInProgress = useAtomValue(isGameInProgressAtom); const isGameInProgress = useAtomValue(isGameInProgressAtom);
const gameFen = game.fen(); const gameFen = game.fen();
@@ -40,10 +40,7 @@ export default function BoardContainer() {
) { ) {
return; return;
} }
const move = await engine.getEngineNextMove( const move = await engine.getEngineNextMove(gameFen, engineElo);
gameFen,
engineSkillLevel - 1
);
if (move) makeGameMove(uciMoveParams(move)); if (move) makeGameMove(uciMoveParams(move));
}; };
playEngineMove(); playEngineMove();

View File

@@ -20,7 +20,7 @@ import {
import { useAtomLocalStorage } from "@/hooks/useAtomLocalStorage"; import { useAtomLocalStorage } from "@/hooks/useAtomLocalStorage";
import { useAtom, useSetAtom } from "jotai"; import { useAtom, useSetAtom } from "jotai";
import { import {
engineSkillLevelAtom, engineEloAtom,
playerColorAtom, playerColorAtom,
isGameInProgressAtom, isGameInProgressAtom,
gameAtom, gameAtom,
@@ -40,9 +40,9 @@ interface Props {
} }
export default function GameSettingsDialog({ open, onClose }: Props) { export default function GameSettingsDialog({ open, onClose }: Props) {
const [skillLevel, setSkillLevel] = useAtomLocalStorage( const [engineElo, setEngineElo] = useAtomLocalStorage(
"engine-skill-level", "engine-elo",
engineSkillLevelAtom engineEloAtom
); );
const [engineName, setEngineName] = useAtomLocalStorage( const [engineName, setEngineName] = useAtomLocalStorage(
"engine-play-name", "engine-play-name",
@@ -56,20 +56,16 @@ export default function GameSettingsDialog({ open, onClose }: Props) {
onClose(); onClose();
resetGame({ resetGame({
whiteName: whiteName:
playerColor === Color.White playerColor === Color.White ? "You" : engineLabel[engineName].small,
? "You"
: `${engineLabel[engineName].small} level ${skillLevel}`,
blackName: blackName:
playerColor === Color.Black playerColor === Color.Black ? "You" : engineLabel[engineName].small,
? "You"
: `${engineLabel[engineName].small} level ${skillLevel}`,
}); });
playGameStartSound(); playGameStartSound();
setIsGameInProgress(true); setIsGameInProgress(true);
logAnalyticsEvent("play_game", { logAnalyticsEvent("play_game", {
engine: engineName, engine: engineName,
skillLevel, engineElo,
playerColor, playerColor,
}); });
}; };
@@ -130,12 +126,12 @@ export default function GameSettingsDialog({ open, onClose }: Props) {
</Grid> </Grid>
<Slider <Slider
label="Bot skill level" label="Bot Elo rating"
value={skillLevel} value={engineElo}
setValue={setSkillLevel} setValue={setEngineElo}
min={1} min={100}
max={21} max={3200}
marksFilter={2} step={100}
/> />
<FormGroup> <FormGroup>

View File

@@ -8,4 +8,5 @@ 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.Stockfish17Lite); export const enginePlayNameAtom = atom<EngineName>(EngineName.Stockfish17Lite);
export const engineSkillLevelAtom = atom<number>(1); export const engineSkillLevelAtom = atom<number>(1);
export const engineEloAtom = atom<number>(1200);
export const isGameInProgressAtom = atom(false); export const isGameInProgressAtom = atom(false);