From 99a90def9c04be57f564f2de068e61a4c2bbac38 Mon Sep 17 00:00:00 2001 From: GuillaumeSD Date: Thu, 7 Mar 2024 00:28:32 +0100 Subject: [PATCH] feat : add move classification icons --- public/icons/best.png | Bin 0 -> 1482 bytes public/icons/blunder.png | Bin 0 -> 1688 bytes public/icons/book.png | Bin 0 -> 1341 bytes public/icons/excellent.png | Bin 0 -> 1328 bytes public/icons/good.png | Bin 0 -> 1452 bytes public/icons/inaccuracy.png | Bin 0 -> 1523 bytes public/icons/mistake.png | Bin 0 -> 1360 bytes ...seCurrentMove.ts => useCurrentPosition.ts} | 30 +++---- src/hooks/useSquareRenderer.tsx | 73 ++++++++++++++++++ src/lib/engine/helpers/accuracy.ts | 10 +-- src/lib/engine/helpers/moveClassification.ts | 6 +- src/lib/engine/helpers/parseResults.ts | 6 +- src/lib/engine/helpers/winPercentage.ts | 12 +-- src/lib/engine/uciEngine.ts | 21 ++--- src/lib/lichess.ts | 4 +- src/sections/analysis/board/evaluationBar.tsx | 12 ++- src/sections/analysis/board/index.tsx | 58 ++++---------- .../analysis/reviewPanelBody/index.tsx | 8 +- .../analysis/reviewPanelBody/moveInfo.tsx | 11 ++- .../analysis/reviewPanelBody/opening.tsx | 6 +- src/sections/analysis/states.ts | 6 +- src/sections/engineSettings/arrowOptions.tsx | 14 ++-- src/types/eval.ts | 15 ++-- 23 files changed, 170 insertions(+), 122 deletions(-) create mode 100644 public/icons/best.png create mode 100644 public/icons/blunder.png create mode 100644 public/icons/book.png create mode 100644 public/icons/excellent.png create mode 100644 public/icons/good.png create mode 100644 public/icons/inaccuracy.png create mode 100644 public/icons/mistake.png rename src/hooks/{useCurrentMove.ts => useCurrentPosition.ts} (57%) create mode 100644 src/hooks/useSquareRenderer.tsx diff --git a/public/icons/best.png b/public/icons/best.png new file mode 100644 index 0000000000000000000000000000000000000000..f9bc8af6c796bc67b0a8dbaeb8775092579ce5d6 GIT binary patch literal 1482 zcmV;*1vUDKP)0s2HK#sB~S8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H11y@N# zK~z|UomX2-6jvDj&Y78IFYZl*Sq*jlHO=)?PG?Z6BIyOl@srd@(U? zQv1+oZPS>5(x$0tv?WbTL|3c|g?a&71+O4V6;}loS-QZk3+&G2^nr-%1$LJIWzPJU z@B7X_GiS~rP-%P5tgs@8R~hge0a*Yr8vrW+9>4&Ay8tdThGSYCSG~UYh+hpZiI-4X zv#N-IZ3Ea!07)wKh5#H^DBZDniL+%=0!r%~X;8=>!mw72&~Z#*KslAgt(%sf>`^VC ztj6)O%y5tpk~&G^Fk=i|93g8ycX^hr+^$&Lrw?*A@Q3-2r^_Sr}kDm z-j0fmz1dgoc#{$GD*!GoDuCDL?FNH_%BF2<(!sz)O4zbZFaA;c$T!-7vXd)k$_ziL zCAXRFNVCjBx@9&}OfyvpARvMwlS6xJmlcKyU<^c3lyXXmK@}2!Lfg{Ov3<-qt_Vs@ zQjmNXCZM#&v6k{=o>~wMZ$Lr%3y;NQEQW^Hs}{ivbWusQWA&&2MN)RCt_DzGTLOW9 zyd(&`7Bf;7B?19OX1f4@mesDD%L|-6kz#v#bjxRFE>B1R&(p$^6OM(v-xqqU-&#he=jm}1Vuil;tDP<#kc9!5|axr)*dg3?2@_cTD1 zhr()x0@50uf-H;#%bEHtiVHR-G$R1WPM$w*Oq4{ldrpV>I3C4R(9(j)W zjj*QZ5br-F1(jYp=$Bg60Ow~!eJrsBP!2QFi&W+M2SQhK3%n1XZ76=-$UM~k%;fspZ^JphQJ;ytqeS|xv_ zwxg@#jhsU{cFTL?C1+W}TiWGm!{PdG;FH=ROA0Qu7GYKK7FdmG004cG88(w0-2>Oh z72v+(bGljr-5dajvf`Rkl=>Fsh%Q$0yxezSF{EQC=tosU363{?3yozEI)eb6K|nxq zqyBOwAX8-7W`Pn4f4~d(z}48Z56S3rzn0HE`>-;kEQ0z+~0L-2=)JgRgHGNm?FyqYxFp*VbRL^TYa$0K7o>_G|b; zP0v41FSgA!7&WX}!?Ck*13*Z4pPRwy_vWHG! kHBLDyZ literal 0 HcmV?d00001 diff --git a/public/icons/blunder.png b/public/icons/blunder.png new file mode 100644 index 0000000000000000000000000000000000000000..783af846dc99e9046cd76c7aeea855ed4d29cada GIT binary patch literal 1688 zcmV;J250$+P)0s2HK#sB~S8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H11|>;E zK~z|Ut(R?VTh$%Lf9Lx8+OZS6d2z_{(mI4fo6wRpNwcjTnf z+KT2Y$B4(79y~{=ryFB>I>V8W#1I?WdV7yp40N_NE*B|25JI>CoPIym_dm#z^^a6J-XZ0~MHcl-ls7yuX$vZ1eSJ=QlsdZZG> zP({U?HX)V)xRx)ce$PH!b=M&j%POHboOoJVaa7kZdiZs&*?KRU*K6fz8lIL`R9_jv z_IJ}Gb|ek4YRBl4w`_vZk4VJ%os2PkInr+jm7AA;Aey{TtJV-`0vXv(Y5VSKHB{pQrAt%OYYu8 z?AUQq(I^8i?4$d)+c^KPLlzURn{PH@UGfBg+O@XnCSkB*Zl?gsLP6f%x&_^4L%Vbl zw_+nC4ICa1*WUG=&l3Cn$O}Upy!94bQz0}(Vf>X>DDe5HX#4VKNj>HzvI}uTNAv17 zHJsA76e%{cLlJ`VU;qi&?DiQkMWwi|&cusPUO_Vqs-~kTvpkQcQCLx7;!TfVwrGTP zHY-TlEIo5f*u>065jGpOk8Y!Q)oK%G_>IGeSuTL$a8UQeQxq=qXE;*Vx%lrlEcsQz zdey0j>+=~%ht1`p?#bWdojaw#ME@xQ2M^#*C-it({>Nu1Tx~Xade;ur;bF+yQJPS$ zRTQCk7BW!#`0p&@!SjRkJoIziv-?{p3Uz;Y+9H1Lzf`0n4yo%XTVlkYQmFUUHDX+rHra&WhNZ% z>c*?iJGsQAF)m)avHsN>r(;t-(qvH)`kds>kx`1`F$-_Py9A@8ID-p^hbhP^SlRJO zEcT*09FM&TCH&!uyy6LIO#bxD*#`B<#Kg!CeWm*w3f(`-MJ)a9 z_wcM-nZdfI#_awYI|-Y>r3crOrmwmlsF2+UWq;=T1Z?7ZIPy3@qI zbpx*2+6)Z$^q?F+W=Sf(y^(^dstlfbzXMyJbqUTyqpxi}J@6A?GTQ)NsNH#E!;WgF z_KkE~)G*NJOs6J9&SN#I8>Z!Q2!WQx8;Zr>{_&~P_XXj6y3JIF;}Cc~5bSO%Dy%58 z+v@@Bvx4cLDsn#&?24IZ%i;}0qX+KoKl4xoOjj+-MIfQ;2$8Rzp~=?dbg{kT+>$PJG7EghAzbueJ3N4&fOzpZ=H?D zPXePr-aYww-h4}da-giu<5{}W?X2{wb{9Zgmx=CB=zqQ8@GvkATm*uPNqV)9PMEnz iXv~Z*38bzj$^QYwqAH_PPYNXf0000 literal 0 HcmV?d00001 diff --git a/public/icons/book.png b/public/icons/book.png new file mode 100644 index 0000000000000000000000000000000000000000..65c83312b1a8d4c26a9194746fb55ce7a56aee80 GIT binary patch literal 1341 zcmV-D1;YA?P)0s2HK#sB~S8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H11jEpRYerWe>3;C(DrrT;IUY!mQ@x}7A0!pzJ$iGMA2Z31)?#=m>3We6JkP3 zO^h02AP|hfQV@+G0TB>vM2Zl0D3rC9+Lpe)*X7Zc_uid5{!v<6+Slb)zrXICGiSbU z&dfP8Bj_Uva^FfcrJAF)o&{n$=yV_vFo6c(FW?kH?~cOWm%VJGzc1s8-a2f_UHlM& zWx$KTpg!siBG_STRxMn<{=6dr-{rqOMA@Ow1{Y4+Ft?9FX&1-U= z5?XvKAk86hCunq~VH@)ntXNlkw@u#Nur)X56{*GU|B4(yAlypYMTL1w7Iw9DB40dL z3$_Bre?{IA8bVs@XBU3B;XoGw1^G+w6H1+wLd5(Z|v@ zvmAi<6gLxR&tYih{TOCMw|=!=FP=I-sfn>f7{+4-c{$myF5j?20@zxuYU`+!jk0yv z5K6=KY6_@%5+PjJY0wPRfw3JfBIil9#H)>xY4H+-FlZpd@(+CIpR)Yt9?7m$~@I3p!Xau`E2Rj+1U}Mk!YF*d$+A;@3!^-_Le3IsYAJU+!V%7 zok>R4#CB0_DX6vPdg)otpD5;9=^5&4t3X+uNK+DB00b?|yKT>je7I8s4xPMocIuen zpFT1r^W&5R7byv@E;CXpK*hNtN{;+OWLzqn1n@!UyWj0Yr?3AxbKPWJVS-`EsDTh$mhv4ucJY(^Z}zTMifeb z-p1xLm&^N|0|BV{oS)hs@ibj9RSOTrDrIbloBSvNpOLDuSqM`u;AuD+9N<~@8 z_38?s7Pv|4i@8?};4H%gB59pn2(WtY-aY;S-{jwHb=)2F00000NkvXXu0mjfJT!#F literal 0 HcmV?d00001 diff --git a/public/icons/excellent.png b/public/icons/excellent.png new file mode 100644 index 0000000000000000000000000000000000000000..3e61fe1be4a29e87dd213551ae87e0dbfd3656c7 GIT binary patch literal 1328 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 Z{{hf(5&%lFdW`@8002ovPDHLkV1n3evBCfV literal 0 HcmV?d00001 diff --git a/public/icons/mistake.png b/public/icons/mistake.png new file mode 100644 index 0000000000000000000000000000000000000000..3151cab3507214cd6438e9920540885e2f5ed4e1 GIT binary patch literal 1360 zcmV-W1+V&vP)0s2HK#sB~S8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H11l>tQ zK~z|Ut(R$RR8<&;pL6e>nNGKv(uGnef-KSwLWl@TNQA_NO-xwgMhsew{4fMChG_hQ zF+?zIZiz&RiaHW*-{k)I&N=V< zy?4%;?>krE5;wkjG)s*-3lZ{3A()0_D2Ob9)FiZmxPhch2yw_`n1AH1y%Kh#D-tcJ z-x_+{cH~+iSOEB3`ZWZU3dy=*Yc5s{%D|nCp?uRLw*gDt7<~X7V2^5PYx3VKzvJ4# zt*u4RNJsoA1bKr@+zS%wz*#b4_2rX&=ahcQw}f6sibMZbasUAZpd7E;9C~Gdxo*^L z4K0?Ey&%;@qKYpA#XkgT8`$l4K-vk7NH9Un8L_(JSU&?dw-t@U7H1Vf+5_K2^N|`_ zL~3Xep1h(M-7(tHD~}QWXFFDNRa_H}lx>RQOdG!H$`$C^0x1!W$)0%0wX9L(yt<{te75DUx;!Ve2w2<6o2k=jLKCS^ph)ly&zK+EJHCu|8YN~iF9?{UU&@xBF6&H#| z@Y#>?5O^d~&@0HpgAIx6>}DuN+d_c;z$_%_gO!02psrPrz3OEd}7U9Ijyny+{1@INw- ztfHrTjdj$P;c=m*sbZ$$_bTyEOguAFUL$MH1`yq*qOG^-EZ^&j@6~u%Qw5oWNgb+e8 zZP}5m0d>rLgwlO*qpb~;Z-0q=YS3IJ2QVzBRf!m~E?EO2v#?j(aAqF`o+dOUxvs67 zvPo;{v@Qixy~W88n<{>%rSVoQwyTemHi#R;-45%VR@I~*8I|K*oD5N|If{P$NG$ey z70{A*PVcw?tohHmSi}{24*~89`KOfRnf#_k>}|=C^m(uhf1$ z7pYq@Ti-wBp_Jur$=OT3!;?KG7KhL7B7A&(QU)&7MGsa~cidJ0`R$43t-2-exg}eD z>Ri-L?w_oi^6sX?jT@n>8}2{da{f?x+vcQHdO~s6OYY-vMfkI$744T|1_0-sowvTc za7!L+dl%8!qJ!4!(ettlEU~LHaRkG8y*weQ|4ulJ} z(F1e0Rj%sLsq8u4Z}TDsO}kFsJ2^7j8yTDD3u&tGCsdCE!;;OrPqlu!{F|Efw$1gv z^YN-jU}Ax9`X?_IluR6+GS{PujARmly zH9mc0s4%5qNSc}o5H+Ro*VS { - const [currentMove, setCurrentMove] = useAtom(currentMoveAtom); +export const useCurrentPosition = (engineName?: EngineName) => { + const [currentPosition, setCurrentPosition] = useAtom(currentPositionAtom); const engine = useEngine(engineName); const gameEval = useAtomValue(gameEvalAtom); const game = useAtomValue(gameAtom); @@ -22,8 +22,8 @@ export const useCurrentMove = (engineName?: EngineName) => { const multiPv = useAtomValue(engineMultiPvAtom); useEffect(() => { - const move: CurrentMove = { - ...board.history({ verbose: true }).at(-1), + const position: CurrentPosition = { + lastMove: board.history({ verbose: true }).at(-1), }; if (gameEval) { @@ -36,15 +36,15 @@ export const useCurrentMove = (engineName?: EngineName) => { ) { const evalIndex = board.history().length; - move.eval = gameEval.moves[evalIndex]; - move.lastEval = - evalIndex > 0 ? gameEval.moves[evalIndex - 1] : undefined; + position.eval = gameEval.positions[evalIndex]; + position.lastEval = + evalIndex > 0 ? gameEval.positions[evalIndex - 1] : undefined; } } - if (!move.eval && engine?.isReady()) { - const setPartialEval = (moveEval: MoveEval) => { - setCurrentMove({ ...move, eval: moveEval }); + if (!position.eval && engine?.isReady()) { + const setPartialEval = (positionEval: PositionEval) => { + setCurrentPosition({ ...position, eval: positionEval }); }; engine.evaluatePositionWithUpdate({ @@ -55,8 +55,8 @@ export const useCurrentMove = (engineName?: EngineName) => { }); } - setCurrentMove(move); - }, [gameEval, board, game, engine, depth, multiPv, setCurrentMove]); + setCurrentPosition(position); + }, [gameEval, board, game, engine, depth, multiPv, setCurrentPosition]); - return currentMove; + return currentPosition; }; diff --git a/src/hooks/useSquareRenderer.tsx b/src/hooks/useSquareRenderer.tsx new file mode 100644 index 0000000..bec282d --- /dev/null +++ b/src/hooks/useSquareRenderer.tsx @@ -0,0 +1,73 @@ +import { showPlayerMoveIconAtom } from "@/sections/analysis/states"; +import { MoveClassification } from "@/types/enums"; +import { CurrentPosition } from "@/types/eval"; +import { useAtomValue } from "jotai"; +import Image from "next/image"; +import { CSSProperties, forwardRef, useMemo } from "react"; +import { CustomSquareProps } from "react-chessboard/dist/chessboard/types"; + +export const useSquareRenderer = (position: CurrentPosition) => { + const showPlayerMoveIcon = useAtomValue(showPlayerMoveIconAtom); + + const CustomSquareRenderer = useMemo(() => { + const fromSquare = position.lastMove?.from; + const toSquare = position.lastMove?.to; + const moveClassification = position?.eval?.moveClassification; + + if (!showPlayerMoveIcon || !moveClassification || !fromSquare || !toSquare) + return undefined; + + const squareRenderer = forwardRef( + (props, ref) => { + const { children, square, style } = props; + + const customSquareStyle: CSSProperties | undefined = + fromSquare === square || toSquare === square + ? { + position: "absolute", + width: "100%", + height: "100%", + backgroundColor: moveClassificationColors[moveClassification], + opacity: 0.5, + } + : undefined; + + return ( +
+ {children} + {customSquareStyle &&
} + {square === toSquare && ( + move-icon + )} +
+ ); + } + ); + + squareRenderer.displayName = "CustomSquareRenderer"; + + return squareRenderer; + }, [showPlayerMoveIcon, position]); + + return CustomSquareRenderer; +}; + +export const moveClassificationColors: Record = { + [MoveClassification.Best]: "#3aab18", + [MoveClassification.Book]: "#d5a47d", + [MoveClassification.Excellent]: "#3aab18", + [MoveClassification.Good]: "#81b64c", + [MoveClassification.Inaccuracy]: "#f7c631", + [MoveClassification.Mistake]: "#ffa459", + [MoveClassification.Blunder]: "#fa412d", +}; diff --git a/src/lib/engine/helpers/accuracy.ts b/src/lib/engine/helpers/accuracy.ts index d589a20..d2a6661 100644 --- a/src/lib/engine/helpers/accuracy.ts +++ b/src/lib/engine/helpers/accuracy.ts @@ -4,15 +4,15 @@ import { getStandardDeviation, getWeightedMean, } from "@/lib/helpers"; -import { Accuracy, MoveEval } from "@/types/eval"; +import { Accuracy, PositionEval } from "@/types/eval"; import { getPositionWinPercentage } from "./winPercentage"; -export const computeAccuracy = (moves: MoveEval[]): Accuracy => { - const movesWinPercentage = moves.map(getPositionWinPercentage); +export const computeAccuracy = (positions: PositionEval[]): Accuracy => { + const positionsWinPercentage = positions.map(getPositionWinPercentage); - const weights = getAccuracyWeights(movesWinPercentage); + const weights = getAccuracyWeights(positionsWinPercentage); - const movesAccuracy = getMovesAccuracy(movesWinPercentage); + const movesAccuracy = getMovesAccuracy(positionsWinPercentage); const whiteAccuracy = getPlayerAccuracy(movesAccuracy, weights, "white"); const blackAccuracy = getPlayerAccuracy(movesAccuracy, weights, "black"); diff --git a/src/lib/engine/helpers/moveClassification.ts b/src/lib/engine/helpers/moveClassification.ts index 267beab..7269d3c 100644 --- a/src/lib/engine/helpers/moveClassification.ts +++ b/src/lib/engine/helpers/moveClassification.ts @@ -1,13 +1,13 @@ -import { MoveEval } from "@/types/eval"; +import { PositionEval } from "@/types/eval"; import { getPositionWinPercentage } from "./winPercentage"; import { MoveClassification } from "@/types/enums"; import { openings } from "@/data/openings"; export const getMovesClassification = ( - rawMoves: MoveEval[], + rawMoves: PositionEval[], uciMoves: string[], fens: string[] -): MoveEval[] => { +): PositionEval[] => { const positionsWinPercentage = rawMoves.map(getPositionWinPercentage); let currentOpening: string | undefined = undefined; diff --git a/src/lib/engine/helpers/parseResults.ts b/src/lib/engine/helpers/parseResults.ts index bd40f73..abd3ead 100644 --- a/src/lib/engine/helpers/parseResults.ts +++ b/src/lib/engine/helpers/parseResults.ts @@ -1,10 +1,10 @@ -import { LineEval, MoveEval } from "@/types/eval"; +import { LineEval, PositionEval } from "@/types/eval"; export const parseEvaluationResults = ( results: string[], whiteToPlay: boolean -): MoveEval => { - const parsedResults: MoveEval = { +): PositionEval => { + const parsedResults: PositionEval = { lines: [], }; const tempResults: Record = {}; diff --git a/src/lib/engine/helpers/winPercentage.ts b/src/lib/engine/helpers/winPercentage.ts index de12fe5..8f3074b 100644 --- a/src/lib/engine/helpers/winPercentage.ts +++ b/src/lib/engine/helpers/winPercentage.ts @@ -1,13 +1,13 @@ import { ceilsNumber } from "@/lib/helpers"; -import { MoveEval } from "@/types/eval"; +import { PositionEval } from "@/types/eval"; -export const getPositionWinPercentage = (move: MoveEval): number => { - if (move.lines[0].cp !== undefined) { - return getWinPercentageFromCp(move.lines[0].cp); +export const getPositionWinPercentage = (position: PositionEval): number => { + if (position.lines[0].cp !== undefined) { + return getWinPercentageFromCp(position.lines[0].cp); } - if (move.lines[0].mate !== undefined) { - return getWinPercentageFromMate(move.lines[0].mate); + if (position.lines[0].mate !== undefined) { + return getWinPercentageFromMate(position.lines[0].mate); } throw new Error("No cp or mate in move"); diff --git a/src/lib/engine/uciEngine.ts b/src/lib/engine/uciEngine.ts index 45b57c8..d7a8425 100644 --- a/src/lib/engine/uciEngine.ts +++ b/src/lib/engine/uciEngine.ts @@ -3,7 +3,7 @@ import { EvaluateGameParams, EvaluatePositionWithUpdateParams, GameEval, - MoveEval, + PositionEval, } from "@/types/eval"; import { parseEvaluationResults } from "./helpers/parseResults"; import { computeAccuracy } from "./helpers/accuracy"; @@ -108,11 +108,11 @@ export abstract class UciEngine { await this.sendCommands(["ucinewgame", "isready"], "readyok"); this.worker.postMessage("position startpos"); - const moves: MoveEval[] = []; + const positions: PositionEval[] = []; for (const fen of fens) { const whoIsCheckmated = getWhoIsCheckmated(fen); if (whoIsCheckmated) { - moves.push({ + positions.push({ lines: [ { pv: [], @@ -125,19 +125,19 @@ export abstract class UciEngine { continue; } const result = await this.evaluatePosition(fen, depth); - moves.push(result); + positions.push(result); } - const movesWithClassification = getMovesClassification( - moves, + const positionsWithClassification = getMovesClassification( + positions, uciMoves, fens ); - const accuracy = computeAccuracy(moves); + const accuracy = computeAccuracy(positions); this.ready = true; return { - moves: movesWithClassification, + positions: positionsWithClassification, accuracy, settings: { engine: this.engineName, @@ -148,7 +148,10 @@ export abstract class UciEngine { }; } - private async evaluatePosition(fen: string, depth = 16): Promise { + private async evaluatePosition( + fen: string, + depth = 16 + ): Promise { console.log(`Evaluating position: ${fen}`); const lichessEval = await getLichessEval(fen, this.multiPv); diff --git a/src/lib/lichess.ts b/src/lib/lichess.ts index 404c03c..46ef28b 100644 --- a/src/lib/lichess.ts +++ b/src/lib/lichess.ts @@ -1,4 +1,4 @@ -import { LineEval, MoveEval } from "@/types/eval"; +import { LineEval, PositionEval } from "@/types/eval"; import { sortLines } from "./engine/helpers/parseResults"; import { LichessError, @@ -9,7 +9,7 @@ import { export const getLichessEval = async ( fen: string, multiPv = 1 -): Promise => { +): Promise => { try { const res = await fetch( `https://lichess.org/api/cloud-eval?fen=${fen}&multiPv=${multiPv}` diff --git a/src/sections/analysis/board/evaluationBar.tsx b/src/sections/analysis/board/evaluationBar.tsx index 52a078f..99f4e91 100644 --- a/src/sections/analysis/board/evaluationBar.tsx +++ b/src/sections/analysis/board/evaluationBar.tsx @@ -1,7 +1,11 @@ import { Box, Grid, Typography } from "@mui/material"; import { useAtomValue } from "jotai"; import { useEffect, useState } from "react"; -import { boardAtom, boardOrientationAtom, currentMoveAtom } from "../states"; +import { + boardAtom, + boardOrientationAtom, + currentPositionAtom, +} from "../states"; import { getEvaluationBarValue } from "@/lib/chess"; interface Props { @@ -15,17 +19,17 @@ export default function EvaluationBar({ height }: Props) { }); const board = useAtomValue(boardAtom); const boardOrientation = useAtomValue(boardOrientationAtom); - const currentMove = useAtomValue(currentMoveAtom); + const position = useAtomValue(currentPositionAtom); const isWhiteToPlay = board.turn() === "w"; useEffect(() => { - const bestLine = currentMove?.eval?.lines[0]; + const bestLine = position?.eval?.lines[0]; if (!bestLine || bestLine.depth < 6) return; const evalBar = getEvaluationBarValue(bestLine, isWhiteToPlay); setEvalBar(evalBar); - }, [currentMove, isWhiteToPlay]); + }, [position, isWhiteToPlay]); return ( (null); @@ -22,9 +25,9 @@ export default function Board() { const board = useAtomValue(boardAtom); const boardOrientation = useAtomValue(boardOrientationAtom); const showBestMoveArrow = useAtomValue(showBestMoveArrowAtom); - const showPlayerMoveArrow = useAtomValue(showPlayerMoveArrowAtom); const { makeMove: makeBoardMove } = useChessActions(boardAtom); - const currentMove = useAtomValue(currentMoveAtom); + const position = useAtomValue(currentPositionAtom); + const squareRenderer = useSquareRenderer(position); const onPieceDrop = ( source: Square, @@ -45,9 +48,8 @@ export default function Board() { }; const customArrows: Arrow[] = useMemo(() => { - const arrows: Arrow[] = []; - const bestMove = currentMove?.lastEval?.bestMove; - const moveClassification = currentMove?.eval?.moveClassification; + const bestMove = position?.lastEval?.bestMove; + const moveClassification = position?.eval?.moveClassification; if ( bestMove && @@ -60,36 +62,11 @@ export default function Board() { moveClassificationColors[MoveClassification.Best], ] as Arrow; - arrows.push(bestMoveArrow); + return [bestMoveArrow]; } - if ( - currentMove.from && - currentMove.to && - showPlayerMoveArrow && - moveClassification !== MoveClassification.Best - ) { - const arrowColor = moveClassification - ? moveClassificationColors[moveClassification] - : "#ffaa00"; - const playerMoveArrow: Arrow = [ - currentMove.from, - currentMove.to, - arrowColor, - ]; - - if ( - arrows.every( - (arrow) => - arrow[0] !== playerMoveArrow[0] || arrow[1] !== playerMoveArrow[1] - ) - ) { - arrows.push(playerMoveArrow); - } - } - - return arrows; - }, [currentMove, showBestMoveArrow, showPlayerMoveArrow]); + return []; + }, [position, showBestMoveArrow]); return ( @@ -139,13 +117,3 @@ export default function Board() { ); } - -const moveClassificationColors: Record = { - [MoveClassification.Best]: "#26c2a3", - [MoveClassification.Book]: "#d5a47d", - [MoveClassification.Excellent]: "#3aab18", - [MoveClassification.Good]: "#81b64c", - [MoveClassification.Inaccuracy]: "#f7c631", - [MoveClassification.Mistake]: "#ffa459", - [MoveClassification.Blunder]: "#fa412d", -}; diff --git a/src/sections/analysis/reviewPanelBody/index.tsx b/src/sections/analysis/reviewPanelBody/index.tsx index cb8a0f8..6e7828d 100644 --- a/src/sections/analysis/reviewPanelBody/index.tsx +++ b/src/sections/analysis/reviewPanelBody/index.tsx @@ -3,7 +3,7 @@ import { Grid, List, Typography } from "@mui/material"; import { useAtomValue } from "jotai"; import { boardAtom, engineMultiPvAtom, gameAtom } from "../states"; import LineEvaluation from "./lineEvaluation"; -import { useCurrentMove } from "@/hooks/useCurrentMove"; +import { useCurrentPosition } from "@/hooks/useCurrentPosition"; import { LineEval } from "@/types/eval"; import { EngineName } from "@/types/enums"; import EngineSettingsButton from "@/sections/engineSettings/engineSettingsButton"; @@ -13,7 +13,7 @@ import Opening from "./opening"; export default function ReviewPanelBody() { const linesNumber = useAtomValue(engineMultiPvAtom); - const move = useCurrentMove(EngineName.Stockfish16); + const position = useCurrentPosition(EngineName.Stockfish16); const game = useAtomValue(gameAtom); const board = useAtomValue(boardAtom); @@ -30,8 +30,8 @@ export default function ReviewPanelBody() { (_, i) => ({ pv: [`${i}`], depth: 0, multiPv: i + 1 }) ); - const engineLines = move?.eval?.lines?.length - ? move.eval.lines + const engineLines = position?.eval?.lines?.length + ? position.eval.lines : linesSkeleton; return ( diff --git a/src/sections/analysis/reviewPanelBody/moveInfo.tsx b/src/sections/analysis/reviewPanelBody/moveInfo.tsx index 8b48d9d..caffbff 100644 --- a/src/sections/analysis/reviewPanelBody/moveInfo.tsx +++ b/src/sections/analysis/reviewPanelBody/moveInfo.tsx @@ -1,16 +1,15 @@ -import { useCurrentMove } from "@/hooks/useCurrentMove"; import { Grid, Typography } from "@mui/material"; import { useAtomValue } from "jotai"; -import { boardAtom } from "../states"; +import { boardAtom, currentPositionAtom } from "../states"; import { useMemo } from "react"; import { moveLineUciToSan } from "@/lib/chess"; import { MoveClassification } from "@/types/enums"; export default function MoveInfo() { - const move = useCurrentMove(); + const position = useAtomValue(currentPositionAtom); const board = useAtomValue(boardAtom); - const bestMove = move?.lastEval?.bestMove; + const bestMove = position?.lastEval?.bestMove; const bestMoveSan = useMemo(() => { if (!bestMove) return undefined; @@ -23,9 +22,9 @@ export default function MoveInfo() { if (!bestMoveSan) return null; - const moveClassification = move.eval?.moveClassification; + const moveClassification = position.eval?.moveClassification; const moveLabel = moveClassification - ? `${move.san} is ${moveClassificationLabels[moveClassification]}` + ? `${position.lastMove?.san} is ${moveClassificationLabels[moveClassification]}` : null; const bestMoveLabel = diff --git a/src/sections/analysis/reviewPanelBody/opening.tsx b/src/sections/analysis/reviewPanelBody/opening.tsx index 76dc3ba..60aed5a 100644 --- a/src/sections/analysis/reviewPanelBody/opening.tsx +++ b/src/sections/analysis/reviewPanelBody/opening.tsx @@ -1,10 +1,10 @@ -import { useCurrentMove } from "@/hooks/useCurrentMove"; +import { useCurrentPosition } from "@/hooks/useCurrentPosition"; import { Grid, Typography } from "@mui/material"; export default function Opening() { - const move = useCurrentMove(); + const position = useCurrentPosition(); - const opening = move?.eval?.opening; + const opening = position?.eval?.opening; if (!opening) return null; return ( diff --git a/src/sections/analysis/states.ts b/src/sections/analysis/states.ts index cb8cd77..f75f0ce 100644 --- a/src/sections/analysis/states.ts +++ b/src/sections/analysis/states.ts @@ -1,15 +1,15 @@ -import { CurrentMove, GameEval } from "@/types/eval"; +import { CurrentPosition, GameEval } from "@/types/eval"; import { Chess } from "chess.js"; import { atom } from "jotai"; export const gameEvalAtom = atom(undefined); export const gameAtom = atom(new Chess()); export const boardAtom = atom(new Chess()); -export const currentMoveAtom = atom({}); +export const currentPositionAtom = atom({}); export const boardOrientationAtom = atom(true); export const showBestMoveArrowAtom = atom(true); -export const showPlayerMoveArrowAtom = atom(true); +export const showPlayerMoveIconAtom = atom(true); export const engineDepthAtom = atom(16); export const engineMultiPvAtom = atom(3); diff --git a/src/sections/engineSettings/arrowOptions.tsx b/src/sections/engineSettings/arrowOptions.tsx index 3410246..b2c4be4 100644 --- a/src/sections/engineSettings/arrowOptions.tsx +++ b/src/sections/engineSettings/arrowOptions.tsx @@ -1,7 +1,7 @@ import { Checkbox, FormControlLabel, Grid } from "@mui/material"; import { showBestMoveArrowAtom, - showPlayerMoveArrowAtom, + showPlayerMoveIconAtom, } from "../analysis/states"; import { useAtomLocalStorage } from "@/hooks/useAtomLocalStorage"; @@ -10,9 +10,9 @@ export default function ArrowOptions() { "show-arrow-best-move", showBestMoveArrowAtom ); - const [showPlayerMove, setShowPlayerMove] = useAtomLocalStorage( - "show-arrow-player-move", - showPlayerMoveArrowAtom + const [showPlayerMoveIcon, setShowPlayerMoveIcon] = useAtomLocalStorage( + "show-icon-player-move", + showPlayerMoveIconAtom ); return ( @@ -37,11 +37,11 @@ export default function ArrowOptions() { setShowPlayerMove(checked)} + checked={showPlayerMoveIcon} + onChange={(_, checked) => setShowPlayerMoveIcon(checked)} /> } - label="Show played move arrow" + label="Show played move icon" sx={{ marginX: 0 }} /> diff --git a/src/types/eval.ts b/src/types/eval.ts index 779b7bb..418db6a 100644 --- a/src/types/eval.ts +++ b/src/types/eval.ts @@ -1,7 +1,7 @@ import { Move } from "chess.js"; import { EngineName, MoveClassification } from "./enums"; -export interface MoveEval { +export interface PositionEval { bestMove?: string; moveClassification?: MoveClassification; opening?: string; @@ -29,7 +29,7 @@ export interface EngineSettings { } export interface GameEval { - moves: MoveEval[]; + positions: PositionEval[]; accuracy: Accuracy; settings: EngineSettings; } @@ -38,13 +38,14 @@ export interface EvaluatePositionWithUpdateParams { fen: string; depth?: number; multiPv?: number; - setPartialEval: (moveEval: MoveEval) => void; + setPartialEval: (positionEval: PositionEval) => void; } -export type CurrentMove = Partial & { - eval?: MoveEval; - lastEval?: MoveEval; -}; +export interface CurrentPosition { + lastMove?: Move; + eval?: PositionEval; + lastEval?: PositionEval; +} export interface EvaluateGameParams { fens: string[];