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:
@@ -1,13 +1,13 @@
|
||||
import { Grid2 as Grid, Slider as MuiSlider, Typography } from "@mui/material";
|
||||
|
||||
interface Props {
|
||||
export interface Props {
|
||||
value: number;
|
||||
setValue: (value: number) => void;
|
||||
min: number;
|
||||
max: number;
|
||||
label: string;
|
||||
size?: number;
|
||||
marksFilter?: number;
|
||||
step?: number;
|
||||
}
|
||||
|
||||
export default function Slider({
|
||||
@@ -17,7 +17,7 @@ export default function Slider({
|
||||
value,
|
||||
setValue,
|
||||
size,
|
||||
marksFilter = 1,
|
||||
step = 1,
|
||||
}: Props) {
|
||||
return (
|
||||
<Grid
|
||||
@@ -27,18 +27,14 @@ export default function Slider({
|
||||
size={size ?? 11}
|
||||
>
|
||||
<Typography id={`input-${label}`} textAlign="left" width="100%">
|
||||
{label}
|
||||
{`${label}: ${value}`}
|
||||
</Typography>
|
||||
|
||||
<MuiSlider
|
||||
min={min}
|
||||
max={max}
|
||||
marks={Array.from({ length: max - min + 1 }, (_, i) => ({
|
||||
value: i + min,
|
||||
label: `${i + min}`,
|
||||
})).filter((_, i) => i % marksFilter === 0)}
|
||||
step={1}
|
||||
valueLabelDisplay="off"
|
||||
step={step}
|
||||
valueLabelDisplay="auto"
|
||||
value={value}
|
||||
onChange={(_, value) => setValue(value as number)}
|
||||
aria-labelledby={`input-${label}`}
|
||||
|
||||
@@ -92,6 +92,20 @@ export class UciEngine {
|
||||
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) {
|
||||
if (!initCase) {
|
||||
if (skillLevel === this.skillLevel) return;
|
||||
@@ -352,11 +366,12 @@ export class UciEngine {
|
||||
|
||||
public async getEngineNextMove(
|
||||
fen: string,
|
||||
skillLevel: number,
|
||||
elo: number,
|
||||
depth = 16
|
||||
): Promise<string | undefined> {
|
||||
this.throwErrorIfNotReady();
|
||||
await this.setSkillLevel(skillLevel);
|
||||
await this.setLimitStrength(true);
|
||||
await this.setElo(elo);
|
||||
|
||||
console.log(`Evaluating position: ${fen}`);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useAtomValue } from "jotai";
|
||||
import {
|
||||
engineSkillLevelAtom,
|
||||
engineEloAtom,
|
||||
gameAtom,
|
||||
playerColorAtom,
|
||||
isGameInProgressAtom,
|
||||
@@ -24,7 +24,7 @@ export default function BoardContainer() {
|
||||
const { white, black } = usePlayersData(gameAtom);
|
||||
const playerColor = useAtomValue(playerColorAtom);
|
||||
const { makeMove: makeGameMove } = useChessActions(gameAtom);
|
||||
const engineSkillLevel = useAtomValue(engineSkillLevelAtom);
|
||||
const engineElo = useAtomValue(engineEloAtom);
|
||||
const isGameInProgress = useAtomValue(isGameInProgressAtom);
|
||||
|
||||
const gameFen = game.fen();
|
||||
@@ -40,10 +40,7 @@ export default function BoardContainer() {
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const move = await engine.getEngineNextMove(
|
||||
gameFen,
|
||||
engineSkillLevel - 1
|
||||
);
|
||||
const move = await engine.getEngineNextMove(gameFen, engineElo);
|
||||
if (move) makeGameMove(uciMoveParams(move));
|
||||
};
|
||||
playEngineMove();
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
import { useAtomLocalStorage } from "@/hooks/useAtomLocalStorage";
|
||||
import { useAtom, useSetAtom } from "jotai";
|
||||
import {
|
||||
engineSkillLevelAtom,
|
||||
engineEloAtom,
|
||||
playerColorAtom,
|
||||
isGameInProgressAtom,
|
||||
gameAtom,
|
||||
@@ -40,9 +40,9 @@ interface Props {
|
||||
}
|
||||
|
||||
export default function GameSettingsDialog({ open, onClose }: Props) {
|
||||
const [skillLevel, setSkillLevel] = useAtomLocalStorage(
|
||||
"engine-skill-level",
|
||||
engineSkillLevelAtom
|
||||
const [engineElo, setEngineElo] = useAtomLocalStorage(
|
||||
"engine-elo",
|
||||
engineEloAtom
|
||||
);
|
||||
const [engineName, setEngineName] = useAtomLocalStorage(
|
||||
"engine-play-name",
|
||||
@@ -56,20 +56,16 @@ export default function GameSettingsDialog({ open, onClose }: Props) {
|
||||
onClose();
|
||||
resetGame({
|
||||
whiteName:
|
||||
playerColor === Color.White
|
||||
? "You"
|
||||
: `${engineLabel[engineName].small} level ${skillLevel}`,
|
||||
playerColor === Color.White ? "You" : engineLabel[engineName].small,
|
||||
blackName:
|
||||
playerColor === Color.Black
|
||||
? "You"
|
||||
: `${engineLabel[engineName].small} level ${skillLevel}`,
|
||||
playerColor === Color.Black ? "You" : engineLabel[engineName].small,
|
||||
});
|
||||
playGameStartSound();
|
||||
setIsGameInProgress(true);
|
||||
|
||||
logAnalyticsEvent("play_game", {
|
||||
engine: engineName,
|
||||
skillLevel,
|
||||
engineElo,
|
||||
playerColor,
|
||||
});
|
||||
};
|
||||
@@ -130,12 +126,12 @@ export default function GameSettingsDialog({ open, onClose }: Props) {
|
||||
</Grid>
|
||||
|
||||
<Slider
|
||||
label="Bot skill level"
|
||||
value={skillLevel}
|
||||
setValue={setSkillLevel}
|
||||
min={1}
|
||||
max={21}
|
||||
marksFilter={2}
|
||||
label="Bot Elo rating"
|
||||
value={engineElo}
|
||||
setValue={setEngineElo}
|
||||
min={100}
|
||||
max={3200}
|
||||
step={100}
|
||||
/>
|
||||
|
||||
<FormGroup>
|
||||
|
||||
@@ -8,4 +8,5 @@ export const gameDataAtom = atom<CurrentPosition>({});
|
||||
export const playerColorAtom = atom<Color>(Color.White);
|
||||
export const enginePlayNameAtom = atom<EngineName>(EngineName.Stockfish17Lite);
|
||||
export const engineSkillLevelAtom = atom<number>(1);
|
||||
export const engineEloAtom = atom<number>(1200);
|
||||
export const isGameInProgressAtom = atom(false);
|
||||
|
||||
Reference in New Issue
Block a user