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";
|
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}`}
|
||||||
|
|||||||
@@ -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}`);
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user