From fef8baa9dcfc347bf981111c00988cfd31c442a7 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Fri, 17 Apr 2026 12:04:56 -0600 Subject: [PATCH] feat(chess): polish UI with SVG pieces, motion animations, sound, and confetti - Add Cburnett SVG chess pieces from Wikimedia (Lichess set, ~18KB total) - Add move/capture/check/checkmate/castle/promote sound effects via OGG - Install motion for layout animations: pieces slide smoothly, captures fade - Install canvas-confetti: victory celebration on checkmate - Install sonner: toast notifications - Install lucide-react: volume icons for mute toggle - Persist mute state in localStorage - Animated game-over banner (spring entrance) - Animated drawer slide-in replacing CSS keyframe - Lobby and App get polished visual treatment - Preserve all E2E data-* selectors and drag-drop contract Co-authored-by: visual-engineering agent --- packages/chess/package.json | 11 +- packages/chess/src/app/App.tsx | 37 ++- .../chess/src/assets/pieces/black-bishop.svg | 12 + .../chess/src/assets/pieces/black-king.svg | 12 + .../chess/src/assets/pieces/black-knight.svg | 41 ++++ .../chess/src/assets/pieces/black-pawn.svg | 41 ++++ .../chess/src/assets/pieces/black-queen.svg | 27 ++ .../chess/src/assets/pieces/black-rook.svg | 39 +++ packages/chess/src/assets/pieces/index.ts | 31 +++ .../chess/src/assets/pieces/white-bishop.svg | 12 + .../chess/src/assets/pieces/white-king.svg | 9 + .../chess/src/assets/pieces/white-knight.svg | 19 ++ .../chess/src/assets/pieces/white-pawn.svg | 5 + .../chess/src/assets/pieces/white-queen.svg | 15 ++ .../chess/src/assets/pieces/white-rook.svg | 25 ++ packages/chess/src/assets/sounds/capture.ogg | Bin 0 -> 5663 bytes packages/chess/src/assets/sounds/castle.ogg | Bin 0 -> 5399 bytes packages/chess/src/assets/sounds/check.ogg | Bin 0 -> 8883 bytes .../chess/src/assets/sounds/checkmate.ogg | Bin 0 -> 8883 bytes packages/chess/src/assets/sounds/move.ogg | Bin 0 -> 5399 bytes packages/chess/src/assets/sounds/promote.ogg | Bin 0 -> 8883 bytes packages/chess/src/audio.ts | 48 ++++ packages/chess/src/hooks/useChessEngine.ts | 20 ++ packages/chess/src/ui/Board.tsx | 119 +++++++-- packages/chess/src/ui/GameView.tsx | 123 ++++++++-- packages/chess/src/ui/ImportExport.tsx | 9 +- packages/chess/src/ui/Lobby.tsx | 79 +++--- packages/chess/src/ui/Piece.tsx | 86 ++++--- packages/chess/src/ui/RulesDrawer.tsx | 232 ++++++++++-------- packages/chess/src/ui/SavePanel.tsx | 8 +- packages/chess/src/vite-env.d.ts | 21 ++ 31 files changed, 850 insertions(+), 231 deletions(-) create mode 100644 packages/chess/src/assets/pieces/black-bishop.svg create mode 100644 packages/chess/src/assets/pieces/black-king.svg create mode 100644 packages/chess/src/assets/pieces/black-knight.svg create mode 100644 packages/chess/src/assets/pieces/black-pawn.svg create mode 100644 packages/chess/src/assets/pieces/black-queen.svg create mode 100644 packages/chess/src/assets/pieces/black-rook.svg create mode 100644 packages/chess/src/assets/pieces/index.ts create mode 100644 packages/chess/src/assets/pieces/white-bishop.svg create mode 100644 packages/chess/src/assets/pieces/white-king.svg create mode 100644 packages/chess/src/assets/pieces/white-knight.svg create mode 100644 packages/chess/src/assets/pieces/white-pawn.svg create mode 100644 packages/chess/src/assets/pieces/white-queen.svg create mode 100644 packages/chess/src/assets/pieces/white-rook.svg create mode 100644 packages/chess/src/assets/sounds/capture.ogg create mode 100644 packages/chess/src/assets/sounds/castle.ogg create mode 100644 packages/chess/src/assets/sounds/check.ogg create mode 100644 packages/chess/src/assets/sounds/checkmate.ogg create mode 100644 packages/chess/src/assets/sounds/move.ogg create mode 100644 packages/chess/src/assets/sounds/promote.ogg create mode 100644 packages/chess/src/audio.ts create mode 100644 packages/chess/src/vite-env.d.ts diff --git a/packages/chess/package.json b/packages/chess/package.json index f52fee1..362fcae 100644 --- a/packages/chess/package.json +++ b/packages/chess/package.json @@ -16,15 +16,20 @@ "dependencies": { "@paratype/rete": "workspace:*", "@tailwindcss/vite": "^4.2.2", + "canvas-confetti": "^1.9.4", + "lucide-react": "^1.8.0", + "motion": "^12.38.0", "react-router-dom": "^7.14.1", + "sonner": "^2.0.7", "tailwindcss": "^4.2.2" }, "devDependencies": { - "vite": "^6.0.0", + "@types/canvas-confetti": "^1.9.0", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", "@vitejs/plugin-react": "^4.0.0", "react": "^19.0.0", "react-dom": "^19.0.0", - "@types/react": "^19.0.0", - "@types/react-dom": "^19.0.0" + "vite": "^6.0.0" } } diff --git a/packages/chess/src/app/App.tsx b/packages/chess/src/app/App.tsx index 7400266..d91e462 100644 --- a/packages/chess/src/app/App.tsx +++ b/packages/chess/src/app/App.tsx @@ -1,5 +1,5 @@ import { useEffect } from 'react' -import { Routes, Route, useNavigate } from 'react-router-dom' +import { Routes, Route, useNavigate, useLocation } from 'react-router-dom' import { Lobby } from '../ui/Lobby' import { GameView } from '../ui/GameView' import { RulesView } from '../ui/RulesView' @@ -9,9 +9,12 @@ import { useChessEngine } from '../hooks/useChessEngine' import { ChessEngine } from '../engine' import { loadAutoSave } from '../persist/autosave.js' import type { AttrKey, FactValue, EntityId } from '@paratype/rete' +import { AnimatePresence, motion } from 'motion/react' +import { Toaster } from 'sonner' export function App() { const chessState = useChessEngine() + const location = useLocation() // Restore autosave on mount — [] deps is intentional (run once, avoid infinite loop) useEffect(() => { @@ -25,17 +28,33 @@ export function App() { }, []); // intentional: run once on mount, chessState ref is stable return ( -
- - } /> - } /> - } /> - } /> - +
+ + + + } /> + } /> + } /> + } /> + +
) } +function PageTransition({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} + function SaveWrapper({ chessState }: { chessState: ReturnType }) { const navigate = useNavigate() @@ -53,7 +72,7 @@ function SaveWrapper({ chessState }: { chessState: ReturnType +
+ + + + + + + + + + + diff --git a/packages/chess/src/assets/pieces/black-king.svg b/packages/chess/src/assets/pieces/black-king.svg new file mode 100644 index 0000000..ba2ac9f --- /dev/null +++ b/packages/chess/src/assets/pieces/black-king.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/packages/chess/src/assets/pieces/black-knight.svg b/packages/chess/src/assets/pieces/black-knight.svg new file mode 100644 index 0000000..399a8f3 --- /dev/null +++ b/packages/chess/src/assets/pieces/black-knight.svg @@ -0,0 +1,41 @@ + + + +Wikimedia Error + + +
+ + +
+

Error

+ +

Please set a proper user-agent and respect our robot policy https://w.wiki/4wJS. See also https://phabricator.wikimedia.org/T400119

+
+
+ + diff --git a/packages/chess/src/assets/pieces/black-pawn.svg b/packages/chess/src/assets/pieces/black-pawn.svg new file mode 100644 index 0000000..899c3f9 --- /dev/null +++ b/packages/chess/src/assets/pieces/black-pawn.svg @@ -0,0 +1,41 @@ + + + +Wikimedia Error + + +
+ + +
+

Error

+ +

Please set a proper user-agent and respect our robot policy https://w.wiki/4wJS. See also https://phabricator.wikimedia.org/T400119

+
+
+ + diff --git a/packages/chess/src/assets/pieces/black-queen.svg b/packages/chess/src/assets/pieces/black-queen.svg new file mode 100644 index 0000000..e557734 --- /dev/null +++ b/packages/chess/src/assets/pieces/black-queen.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/chess/src/assets/pieces/black-rook.svg b/packages/chess/src/assets/pieces/black-rook.svg new file mode 100644 index 0000000..4eec43c --- /dev/null +++ b/packages/chess/src/assets/pieces/black-rook.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + diff --git a/packages/chess/src/assets/pieces/index.ts b/packages/chess/src/assets/pieces/index.ts new file mode 100644 index 0000000..cc13878 --- /dev/null +++ b/packages/chess/src/assets/pieces/index.ts @@ -0,0 +1,31 @@ +import whiteKing from "./white-king.svg"; +import whiteQueen from "./white-queen.svg"; +import whiteRook from "./white-rook.svg"; +import whiteBishop from "./white-bishop.svg"; +import whiteKnight from "./white-knight.svg"; +import whitePawn from "./white-pawn.svg"; +import blackKing from "./black-king.svg"; +import blackQueen from "./black-queen.svg"; +import blackRook from "./black-rook.svg"; +import blackBishop from "./black-bishop.svg"; +import blackKnight from "./black-knight.svg"; +import blackPawn from "./black-pawn.svg"; + +export const pieceAssets = { + white: { + king: whiteKing, + queen: whiteQueen, + rook: whiteRook, + bishop: whiteBishop, + knight: whiteKnight, + pawn: whitePawn, + }, + black: { + king: blackKing, + queen: blackQueen, + rook: blackRook, + bishop: blackBishop, + knight: blackKnight, + pawn: blackPawn, + }, +} as const; diff --git a/packages/chess/src/assets/pieces/white-bishop.svg b/packages/chess/src/assets/pieces/white-bishop.svg new file mode 100644 index 0000000..3a8eaa2 --- /dev/null +++ b/packages/chess/src/assets/pieces/white-bishop.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/packages/chess/src/assets/pieces/white-king.svg b/packages/chess/src/assets/pieces/white-king.svg new file mode 100644 index 0000000..632ca1a --- /dev/null +++ b/packages/chess/src/assets/pieces/white-king.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/packages/chess/src/assets/pieces/white-knight.svg b/packages/chess/src/assets/pieces/white-knight.svg new file mode 100644 index 0000000..a5f31c6 --- /dev/null +++ b/packages/chess/src/assets/pieces/white-knight.svg @@ -0,0 +1,19 @@ + + + + + + + + + + diff --git a/packages/chess/src/assets/pieces/white-pawn.svg b/packages/chess/src/assets/pieces/white-pawn.svg new file mode 100644 index 0000000..b265fe1 --- /dev/null +++ b/packages/chess/src/assets/pieces/white-pawn.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/chess/src/assets/pieces/white-queen.svg b/packages/chess/src/assets/pieces/white-queen.svg new file mode 100644 index 0000000..8df7c8f --- /dev/null +++ b/packages/chess/src/assets/pieces/white-queen.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/packages/chess/src/assets/pieces/white-rook.svg b/packages/chess/src/assets/pieces/white-rook.svg new file mode 100644 index 0000000..0574ca6 --- /dev/null +++ b/packages/chess/src/assets/pieces/white-rook.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + diff --git a/packages/chess/src/assets/sounds/capture.ogg b/packages/chess/src/assets/sounds/capture.ogg new file mode 100644 index 0000000000000000000000000000000000000000..b4966723d22125d28dcce306aa8b7c1f4b936880 GIT binary patch literal 5663 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 literal 0 HcmV?d00001 diff --git a/packages/chess/src/assets/sounds/castle.ogg b/packages/chess/src/assets/sounds/castle.ogg new file mode 100644 index 0000000000000000000000000000000000000000..d585a0fd6f76c101a24301fbc2c869e90a415e13 GIT binary patch literal 5399 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 Q8X+bI1K#q1m!j7H0HrvS*6r?u+ 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 OL5X5V{n^$sy8i?2!TIL^ literal 0 HcmV?d00001 diff --git a/packages/chess/src/assets/sounds/checkmate.ogg b/packages/chess/src/assets/sounds/checkmate.ogg new file mode 100644 index 0000000000000000000000000000000000000000..8367da9ba0a2d159ec1777f6dc738d4ffab9009f GIT binary patch literal 8883 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 OL5X5V{n^$sy8i?2!TIL^ literal 0 HcmV?d00001 diff --git a/packages/chess/src/assets/sounds/move.ogg b/packages/chess/src/assets/sounds/move.ogg new file mode 100644 index 0000000000000000000000000000000000000000..d585a0fd6f76c101a24301fbc2c869e90a415e13 GIT binary patch literal 5399 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 Q8X+bI1K#q1m!j7H0HrvS*6r?u+ z>0L!ZeWeR1Vnw(+;Cs&doqM12{c-1+3~SGtS!-sk-`XGE=9Npc0k<`#c zd+0|n_V>J?kJD2^$m|u(lYa=AKZLyO-#xPOe^@Bh|4GQoqyLOiQ~P_cjLhG1WP<*v zG14%1(@z}?p zy8mtvXa_C;XaG@yF)nXj+jS7*%p)G>lZ_!^%!=3fw#gk)jL- z%xM7x5fv@La84Yhv5ORp;DMUkaGxH*!VDiK$)fCAOtL+QP+i%j{G_8Xf0}Vybf$?3>5K^$bu9fNYwte5P%46 z6P!=r)UM)uUIiZ*5z+rFWym2vBByC&tcQn+zqM(A<5YnER6vkfY87Fpw>bNq&#a72?Z%hsmW5izuZ64iu9h7!!q%iFw)uPOjyI z^g5S^bso>FrJh%_>oU9-vIb6i%tzkT zS82*uZwfL6#UlT`gzVu3+K6DLQ?e_OK`#E%o>_>HSe_gPb(Vci(16ICzYuA|X`2HW z*0c}V#hmG5ZFh{*#p{bx()T>*KFIqX@+Gn_wJSSyIL&uZnk-%v*?{I66B{!2-b~0l&MfN!h3#WWGRb+&OGS1LQ$)LBpvilyWs6vx)i zXZe<6wA{$GlNQIwBpen=-j68AsR)O7RXX0iAc;uv=|dJmQSnd5ZHNpV;ve}(9r^?} zWEX|%h^(}coD$v`ZyMlXe&7Gq<8bR~|JkXa*{N6??uh>c*1sYLKtU6>7n3Br7}T?j z&m#D&&sKUsog~&LGJIRDQX(jc!?tSGe4gYKXSLB@c5rHZcIhsBq z|A?Gc6kHvOrfMm@&HX(pjX(zV)8ziA0|01^XVKX^M+|Y&6FB(^oHX88>Ayz|#GOFv zjiDjK-UEOa0Aa|SShAX;J2PfY^tpb6N-!JN0q zP!d{)Q&fjTbBNP%ibHSe9(n*RZ)l8v&VipW#=knOhp!XW-aNq3{2ShMGDrvF8tJ{@!sF}kFSb1J$w6=Z@Ybe|FTUZ;4+mRLg}?n8^p=Mr zd1}HR;$E6`y!6#MFzFMqgFx6a%lwbI~SQeOVBdX>U$C|l{iywV_Z zqX7B5;NfyZaYKXWO2g&;IuCeW$#S#g^Y+TV>h+a|6E*&VhIs$iB?vz=ZqM0Mah6lDxh2aDlFyqVMW~r>FZfd#xb>@(5F@j?j5)}@=NsdY+pF%& zzZBnp_0OwdVSIKM!Ul|j4URsQ9KmB$LIkV1k)rG_SAv1RuXas3%d6j7mZSZ!oo zB32h#6&70+j+65-+|E_H zd(y6qqFl1-ULvG?{-s3>hBRsiiAu!9Kr<}e>kE=yAx0W5x_17huehH2ncz@9=RQm_gF_42KSyj?>{w6{ z2tQ+4$wOhfSn=Wz$f9Nb5ERH={T8leHPb2#W#N;uI-(F1oXR>=ERF%N3N2`L?;H^Y z@$^82ClAt#ab~mY$1K9p(8&l7W0maA2xV1c;DVL`y{q~$P{XDMF`#@eClqSPq)`e3 zhh0KIeMqt&g+anG0U+rOWki*R4!*Y%vY|7R3ItNlOe}^zhjb3vjUXlBAa&4|ihWoh z+jW^x7$l2T1OyFt`2aQuR^1lr*s?A&w{k)9q+O}}6a?bh`7}2y#Bdv^2VjPl9#m2v z(L5>6!zhV%*xLjV0huud6~Sp2g<8l^XFztPKsinU4<+@0Ub2+@BL-qXkKGUhvjC?w z8NPA0^`gBC*$)Q9AUKqj$Ai9!_Pegd}##xQm$l_Not_xnifkdD3dj)bEi5;6^iu?N*X zicyeauD!v6`*SI|AOulr$|)?oD3aGF2 zK5TZ!58L?|dU?mc6jrx?<2J}N8*9Zn6 z!zL&fq5|XO>46l5(I7!~W$Q4?iiYMdc3CDCre|TkqXcTe7S_X(5NXE3&QTJQYo`dz ztZaMei7c_PHPOtAvaPh%GyaG$1dU*17mPEW7|P*!_1yveCvA+1nq=1fy8svv03h)I zLP#hL6UTb@U_4vGp+xp14iW@k0OnxCgMcVP8yOkt6qncWo?1l1AuPZ1FB;K3Hl<-; z*#A_a*`xRG3RLX&*_68iiW+rQt$soA?kRCexRI`op^;N?WPW|mD+w`~6EY16F=@5W zx<-5Y&%g@7@XA5ET&Iv$twHnOS!gaB;ZLNP3Fp;2aGsDZGq2nDw8IzS7uITB^acvO_5$b(ee5zcTFo*n%S?WvB*DO?1tUhxTSJ!CGFEJ zdFaMhcaX)XCQ$*e4&Gl^M;=`NnwfN;m7~vStoLGbpt@S3Nmb&Nkji!5qeZ8~(Z5ia z98`-nLrN~Py?;rLFdWgH#O=mAya=c9O*S`d=C5zcl6f5Jo^UX|f@_)j!fhRshUuQw zbqDk-uTwK~6Q^VRx*}VIyfDAN7$n+%P;OaJGf%Fp!>cQ@1;|^`TR5m%v5OU$E}U6F z53bDoP$+|mwwiI-3mqA-fH5~24ulfEgs6^9oMQf|w^_H-DEIAfa_nW-p^fHxdlB2Q zE$3^aNs~!h1vB)$Tg=X!i?pH*IXV}(Z(*d1@2_pK&*O5XrU4+~^ONgw=;+9+T-%R4}y&E;IS1F`ZV|O>FlAH`wWIpJ# z$*HbslRccc)j(RhQT+XE&9`gGL!PzlI<#viO53y>YAVpGTgQ)~yW;c$XyH^3s$Z{( zP4tc_P%o0cA0O^^GZ|IG3l!GJtI+m3sM0P3M}+IZ#yxFz=Xr_Dtnyb}Td7qiD!YoK z2QU*N>JcowcE*v7fO`Q#5Mg&=sY_5D_@m;@#^yIP!FxnU~J^wHvN0!X{yBsh1 zf;TF1)O4@v3vSSZ4H(d(2k~+FfG9t-3Ezabbr1|+!^A4(O|M^vB6!Pw)cRY{=k}n? zRm>MJ?whYKH;(lyuG4^vbi^Zc*-WiXR5P?Yyl-9((b{$RQr!e>jdTd2ACa7$Erj~|6e@q=suaB2#1MG912rz4J@cn~F)#&tk6_Lq(SF+wB5T!0F6 zE0n_mZrlpWf1+4^8VFEp0|CaY+ZTTsqvn)ClmYP|kzI?PYKezN0J<)lTZ;1g4t;18 z@i2jFQD=%1dWuUimL#t)6Z|Iubf^1J3W~`LuJMXzMs5=*@3l6u(KheiO*kJieVdcDx2dlD?ja&90LP>!GAA?F4%es}TD)pnJ$NQOER0o^7xsOUhDv6; zs|Uj_?oK9qSu^Mxubz23*%+8kjxkXYU$Hq(tmxzi!*s&@AS=cC(c`Ldu6*&vZ73}Q zz&zHO%aRTNH=mMTA+X9s{Dl3$J?JGn9w3U$P)|R&sP{u|vE^4p*!SyB#$ud1GzUyy z=$gEK4qvjWI}#O6ZB6RI&))7BqY+$)yq96p=VQ(C z2&o@Lo&ABUutA?cnKlIoD7=aap_^wx74xOz zH1Y6hB=+=VXH#&RL4EWQr{P;`#?hilBiluIl*toE9R{Y>%Yk2`2nPpL*7a+W{|b@+ zw#>w@$1WJ%r4&%7Z+b{rgdLccQx#DQ6g9puS_=m<%KePrueB_}XmAAGzGDjnrg)c6 zgiDs@9vp4;u~q&O%)pU~sP~?v8cDS3J$a-hKd~q0y?xqu=}WpADe~8;B=d4}a&KxU zpsj=rz1y0peslD)U|$R6&m1xgZ6T|MC76Y~d}3-!WkH=3OQg4;EBIxhmEML%K0cu| zK6RXbIxqTK0?jEDKc83WHEn0n0Y~PFGQ7~1i86m>-FaSKzksHlWb%Edw(Sn1ilIU& z>MWOS?nOXD{7ntKK`%b*!FR*2N=%@&tx5mOIjPD}X!~V5N--c{^~a}G zm9t{4FW0GoF`>TX%tziOqIL4nfZ3#@i$oTBc%`PubyD}~&2=5uNw22wJCXFtH#g?m zhK@=QyBi)q;0q8A+2vOL=wEjI&hGG~Imw@i5!&<8N5)51ntppc-%U;Z>9jkfs~Pw( zZ@{_xvqD?_B=Wb60a7PZ!QCL|yzTrn`vx*%QSt}si1ddg{)Unn?MI7$WvOqjYdz{T z5%jVDk)@@j%%yt3a{aDy*>KCf^!}n(E9nBl5Tw6T9_g8wizzHFkE7 z|NLlr>FB-mw{+z;ikn_5mb)_|C8Y!UI;Y#QKv!)xS>w22?mYbWr$j_%H$M7efk|-G z{7P-&J6vV{<<9Nu*LkBi29)3Pu6~ABf@|mbAH5P!BXH9}(cMqu^7SFst?UdqR4o&_ zWgyJ2Czp|xm71E>iN3%kf92ZmA9o&qJh}Rp&9}1+wKs#Fds`t+az$GiyJ)XVO*t@;UXEv6?{{fP(;LerM!d;3%w>>Z(#sxjQZK6HTkxp$zsW(a&3? z>M0YdvsAeQ_vbbZN2Qu7ds9h0?M86Hl#-h%YIMhKBCPFYawqzPYk8l$<v?F+cfTaJ<4e*}EUdKhrD1!ro4~k9>Pms6uNN;&leC!a;Ne7q42= zpZFy(&p1Q_o!4*?k_vi6@?*j%j0n!KkXgypfB+;)ENVP{_qW>%sVn#;@Ii;`BG}R0 z%A>b_T)ka6C&Af2{$eXzc3E~j_T99`iNLrR4wvJxA5|WWKh2n3yN3G5&wk2<8vBXkLe7Ubrcb?aYTUv{ zUz@w_;imxaqN-Atzla`Jqn-2&qWYAB3gilc3CfU>}rDl#blLEBf zFtc9~*Cv=i^Z_8ZZ0&V-WmJq~acQI+pBqB+)0i~)xWM#s>_(Ziijajxy6O@{b3)&hVBqa^E|#&f3+K$#f+f8Jyg#c$tfeEUjCW8L(~L z9H`y({PNQ1xvdOk?SpAMJIhnM%#n46*d~Lx6fKjZh)9K49kyd2ydYEZ`YQ$Wk8r;M znExUOHr-~q;gxOJ>ym7C69CpZe3zNe_)8f8nz&n7!BsKv#=T&&T-E$Z;!fVCALv*9 zb~xkn>#QUtt%LgWst2yRCll-<>h4{qeafkRBUQwRH}B9i`Bu^i-TN<*q%e~c*^W8~ zJJz{dBM(zwy%ad~c67XH#=K!cyQGlldE+5sAR!+gU1W7m>i+Zd*~d<8i%OcnZU|qL zttl(m$ts|ajSsF=duK`bzWFPP=G;vs!T~@1K;zg5ve4?Y60?}ip48i;3^GT+*^`Z| zeBZCa=9;v)Tenybx;(5lg1)<7Qc3mhhzsU(h3v4>hj9Y$V@$2B>+~J4qTaRK0e|=Y zcKOOC%m!0V+?j7Lec!?_@UNlF+eNoGzUItaIwhR0hMOa4m0jZe;IS-qFC*ZDS05L_ z@*3Lsy+{pOK!ZKbD&6xg``D1il`d=9z?;|~Hx$-3jJ2}rdS>baxBRPkqb<(O;sb2; z27-?*G2KepReLnb|Ka|ToPITf(GR)NF&j}zy-iiNL}?c~zG2v#lQ-B{KW$+6TCW#6 z!FDY`x0nfj+|V~JccETG}Ujfw-LgIzwv2>3q1{9ZVrk}u$mD;1% zs|v_2V?9#L)n`IR2YDZ)q(8~pM%86@goOou&4Zm!p0G;we&jUryiG1S*rplodZpxI z1;P5x_m&Zs%0zHUWSLVaP^+H}W$t`7WMi zAvWo&Qd>ETpGhm}jJ7y}6|@@0H8T)H9VVDUY-mB{NV$;5Y@qL3UTTg8<*wB_?M?HTj=t?5Y1 zv)gMj-LWzjPf|F>X9}|E@6ft7|Lh4bC`QFa`KbtqUeXa)DCn%z-!||Wz}~$4cqLgG4T7t zRTVs5>V#{q)0}3gCLepcSx@Rwe%%k$j`ubKADzgiUtW3G(N*=U{c^UlH0wCq11f8w z#+BXn7cU8tO^!IPP1$pkE{fJgFo)!KW?RvBDiM zo>XSFYPx2&c;7Pfbm}Q36TREHE@l-IOALKXK1K@kx?t~afd;rkUmj4Q+$~W4TOcWN zJde7&(z;o`kxv|RhW9AoJ>36pta00xEpFsnjiRbqi%9{t!JH2jXKJ0 zE|Zx=pGAM}biN)eImhY2)Ytbf^uT179C%VizT0$A`F=|MX}bP2`8mf`f!QE}0ff8l zmknMaM40;NwD_;Tt4sNzf4wLp84bxsb%u|&yn13Zdi>QKxw*&H8#v^e#Mz)8*E54V@eQ28@vyVG>eJX4((C&DDm!H!R?~L#@)jePD{$Ndd z##8F3`g(Bx4-$2SXGH$uS>KS?ku0gn*DC7Q&C+Yz4x1F4eO%}#1|ReklqbmAJass% zwwl*mhz}HwAeGZi-yp?vOcUweIBu^?*? z2c&0SI$(nd#~!@1+-za^jF^2)r>n>)(tI++NVc8k@jaTbLt@*xs&qV;OIN6y>1?Up OL5X5V{n^$sy8i?2!TIL^ literal 0 HcmV?d00001 diff --git a/packages/chess/src/audio.ts b/packages/chess/src/audio.ts new file mode 100644 index 0000000..b68d451 --- /dev/null +++ b/packages/chess/src/audio.ts @@ -0,0 +1,48 @@ +import moveSound from "./assets/sounds/move.ogg"; +import captureSound from "./assets/sounds/capture.ogg"; +import checkSound from "./assets/sounds/check.ogg"; +import checkmateSound from "./assets/sounds/checkmate.ogg"; +import castleSound from "./assets/sounds/castle.ogg"; +import promoteSound from "./assets/sounds/promote.ogg"; + +type SoundName = + | "move" + | "capture" + | "check" + | "checkmate" + | "castle" + | "promote"; + +const sources: Record = { + move: moveSound, + capture: captureSound, + check: checkSound, + checkmate: checkmateSound, + castle: castleSound, + promote: promoteSound, +}; + +const MUTE_KEY = "paratype-chess:v1:muted"; + +export function isMuted(): boolean { + if (typeof localStorage === "undefined") return false; + return localStorage.getItem(MUTE_KEY) === "1"; +} + +export function setMuted(m: boolean): void { + if (typeof localStorage === "undefined") return; + localStorage.setItem(MUTE_KEY, m ? "1" : "0"); +} + +export function play(name: SoundName): void { + if (isMuted()) return; + try { + const audio = new Audio(sources[name]); + audio.volume = 0.5; + audio.play().catch(() => { + // Ignore user-gesture autoplay block + }); + } catch { + // Ignore errors + } +} diff --git a/packages/chess/src/hooks/useChessEngine.ts b/packages/chess/src/hooks/useChessEngine.ts index 2e3c4b9..0a9084c 100644 --- a/packages/chess/src/hooks/useChessEngine.ts +++ b/packages/chess/src/hooks/useChessEngine.ts @@ -2,6 +2,7 @@ import { useState, useRef, useCallback } from 'react'; import { ChessEngine, type GameResult } from '../engine'; import type { PieceType } from '../schema'; import { saveAutoSave } from '../persist/autosave.js'; +import * as audio from '../audio'; export function useChessEngine() { const engineRef = useRef(null); @@ -47,6 +48,20 @@ export function useChessEngine() { const result = engine.applyMove(move, promoteTo); saveAutoSave(engine.session.allFacts()); setTick(t => t + 1); // trigger re-render + + // Play appropriate sound + if (result === 'checkmate') { + audio.play('checkmate'); + } else if (engine.session.allFacts().some(f => f.attr === 'InCheck' && f.value === true)) { + audio.play('check'); + } else if (move.isCapture) { + audio.play('capture'); + } else if (promoteTo && move.promoteTo) { + audio.play('promote'); + } else { + audio.play('move'); // TODO: differentiate castle + } + return result; } return null; @@ -68,6 +83,10 @@ export function useChessEngine() { } }, []); + const lastMove = moveHistoryRef.current.length > 0 + ? moveHistoryRef.current[moveHistoryRef.current.length - 1] + : null; + return { engine, turn: getTurn(), @@ -78,5 +97,6 @@ export function useChessEngine() { undo, canUndo: moveHistoryRef.current.length > 0, loadEngine, + lastMove, }; } diff --git a/packages/chess/src/ui/Board.tsx b/packages/chess/src/ui/Board.tsx index d479b6e..e92cf5e 100644 --- a/packages/chess/src/ui/Board.tsx +++ b/packages/chess/src/ui/Board.tsx @@ -3,12 +3,18 @@ import type { ChessFact, PieceColor, PieceType } from '../schema'; import { squareToAlgebraic, squareOf, squareColor } from '../coord'; import type { LegalMove } from '../rules/types'; import { Piece } from './Piece'; +import { AnimatePresence, motion } from 'motion/react'; +import { pieceAssets } from '../assets/pieces'; interface BoardProps { facts: ChessFact[]; legalMoves: LegalMove[]; - onMove: (from: number, to: number) => void; + onMove: (from: number, to: number, promoteTo?: PieceType) => void; turn: PieceColor; + /** Last move played — extra fields ignored. Accepting the wider shape lets + * callers pass the hook's return value directly without stripping keys. */ + lastMove?: { from: number; to: number; [key: string]: unknown } | null | undefined; + checkedKingSquare?: number | null | undefined; } interface PieceState { @@ -17,7 +23,7 @@ interface PieceState { color: PieceColor; } -export function Board({ facts, legalMoves, onMove, turn }: BoardProps) { +export function Board({ facts, legalMoves, onMove, turn, lastMove, checkedKingSquare }: BoardProps) { // Build pieces map: square -> { id, type, color } const pieces = useMemo(() => { const map = new Map(); @@ -51,6 +57,9 @@ export function Board({ facts, legalMoves, onMove, turn }: BoardProps) { // Drag state const [draggedPiece, setDraggedPiece] = useState<{ id: number, square: number } | null>(null); + + // Promotion picker state + const [promotionMove, setPromotionMove] = useState<{ from: number, to: number, color: PieceColor } | null>(null); // Compute highlighted squares based on current dragged piece const highlightedSquares = useMemo(() => { @@ -88,7 +97,18 @@ export function Board({ facts, legalMoves, onMove, turn }: BoardProps) { const handleDrop = (e: React.DragEvent, targetSquare: number) => { e.preventDefault(); if (draggedPiece && highlightedSquares.has(targetSquare)) { - onMove(draggedPiece.square, targetSquare); + // Check if this is a promotion move + const isPromotion = legalMoves.some(m => + m.pieceId === draggedPiece.id && + m.to === targetSquare && + m.promoteTo !== undefined + ); + + if (isPromotion) { + setPromotionMove({ from: draggedPiece.square, to: targetSquare, color: turn }); + } else { + onMove(draggedPiece.square, targetSquare); + } } setDraggedPiece(null); }; @@ -101,6 +121,8 @@ export function Board({ facts, legalMoves, onMove, turn }: BoardProps) { const isDark = squareColor(sq) === 'dark'; const algebraic = squareToAlgebraic(sq); const isHighlighted = highlightedSquares.has(sq); + const isLastMove = lastMove?.from === sq || lastMove?.to === sq; + const isCheckedKing = checkedKingSquare === sq; const piece = pieces.get(sq); @@ -109,36 +131,56 @@ export function Board({ facts, legalMoves, onMove, turn }: BoardProps) { key={sq} data-square={algebraic} className={`relative aspect-square flex items-center justify-center ${ - isDark ? 'bg-amber-700' : 'bg-amber-100' - }`} + isDark ? 'bg-[#B58863]' : 'bg-[#F0D9B5]' + } shadow-[inset_0_0_8px_rgba(0,0,0,0.15)]`} onDragOver={(e) => handleDragOver(e, sq)} onDrop={(e) => handleDrop(e, sq)} > + {isLastMove && ( +
+ )} + {isHighlighted && ( -
+
+ )} + + {isCheckedKing && ( + )} - {piece && ( -
- -
- )} + + {piece && ( + + + + )} + {/* Rank/File labels (optional but helpful for debug) */} {f === 0 && ( -
+
{r + 1}
)} {r === 0 && ( -
+
{String.fromCharCode(97 + f)}
)} @@ -148,8 +190,41 @@ export function Board({ facts, legalMoves, onMove, turn }: BoardProps) { } return ( -
- {squares} +
+
+ {squares} +
+ + + {promotionMove && ( + + {(['queen', 'rook', 'bishop', 'knight'] as const).map((type) => ( + + ))} + + )} +
); } diff --git a/packages/chess/src/ui/GameView.tsx b/packages/chess/src/ui/GameView.tsx index 145a5d1..c24e9c0 100644 --- a/packages/chess/src/ui/GameView.tsx +++ b/packages/chess/src/ui/GameView.tsx @@ -1,7 +1,12 @@ import { Board } from './Board'; import { RulesDrawer } from './RulesDrawer'; import { useChessEngine } from '../hooks/useChessEngine'; -import type { ChessFact, ChessAttrMap } from '../schema'; +import type { ChessFact, ChessAttrMap, PieceType } from '../schema'; +import { useEffect, useState } from 'react'; +import confetti from 'canvas-confetti'; +import { motion, AnimatePresence } from 'motion/react'; +import { Volume2, VolumeX } from 'lucide-react'; +import * as audio from '../audio'; interface GameViewProps { engineState?: ReturnType; @@ -12,29 +17,93 @@ export function GameView({ engineState }: GameViewProps) { const localChessState = useChessEngine(); const state = engineState || localChessState; - const { facts, legalMoves, turn, result, applyMove, undo, canUndo } = state; + const { facts, legalMoves, turn, result, applyMove, undo, canUndo, lastMove } = state; - const handleMove = (from: number, to: number) => { - applyMove(from, to, 'queen'); + const handleMove = (from: number, to: number, promoteTo?: PieceType) => { + applyMove(from, to, promoteTo || 'queen'); }; const isGameOver = result !== 'ongoing'; + + // Confetti on checkmate + useEffect(() => { + if (result === 'checkmate') { + const duration = 3000; + const end = Date.now() + duration; + + const frame = () => { + confetti({ + particleCount: 5, + angle: 60, + spread: 55, + origin: { x: 0 }, + colors: ['#F0D9B5', '#B58863', '#fff'] + }); + confetti({ + particleCount: 5, + angle: 120, + spread: 55, + origin: { x: 1 }, + colors: ['#F0D9B5', '#B58863', '#fff'] + }); + + if (Date.now() < end) { + requestAnimationFrame(frame); + } + }; + frame(); + } + }, [result]); + + // Audio toggle + const [isMuted, setIsMuted] = useState(audio.isMuted()); + const toggleMute = () => { + const next = !isMuted; + audio.setMuted(next); + setIsMuted(next); + }; + + // Find checked king for indicator + const checkedKingSquare = facts.find( + f => f.attr === 'InCheck' && f.value === true + ) ? facts.find( + f => f.attr === 'PieceType' && f.value === 'king' && + facts.some(f2 => f2.id === f.id && f2.attr === 'Color' && f2.value === turn) + )?.id ? facts.find(f3 => f3.id === facts.find( + f => f.attr === 'PieceType' && f.value === 'king' && + facts.some(f2 => f2.id === f.id && f2.attr === 'Color' && f2.value === turn) + )?.id && f3.attr === 'Position')?.value as number : null : null; return ( -
+
{/* Header/Info section */}
-

- Chess -

+
+

+ Chess +

+ +
{turn === 'white' ? "White's turn" : "Black's turn"}
- +
{/* Game Result Banner */} - {isGameOver && ( -
- {result === 'checkmate' ? 'Checkmate!' : `Draw: ${result.replace('draw-', '')}`} -
- )} + + {isGameOver && ( + + {result === 'checkmate' ? 'Checkmate!' : `Draw: ${result.replace('draw-', '')}`} + + )} +
@@ -71,14 +146,20 @@ export function GameView({ engineState }: GameViewProps) { legalMoves={legalMoves} turn={turn} onMove={handleMove} + lastMove={lastMove} + checkedKingSquare={checkedKingSquare} /> {/* Overlay for game over to prevent further interaction visually */} {isGameOver && ( -
+ )}
-
+ ); } diff --git a/packages/chess/src/ui/ImportExport.tsx b/packages/chess/src/ui/ImportExport.tsx index 28d9287..9bbb607 100644 --- a/packages/chess/src/ui/ImportExport.tsx +++ b/packages/chess/src/ui/ImportExport.tsx @@ -1,5 +1,6 @@ import { useState, useRef } from 'react'; import { exportGame, importGame } from '../persist/io'; +import { toast } from 'sonner'; interface ImportExportProps { currentFacts?: Array<{ id: number; attr: string; value: unknown }>; @@ -22,8 +23,10 @@ export function ImportExport({ currentFacts, onLoad }: ImportExportProps) { a.download = `chess-game-${Date.now()}.json`; a.click(); URL.revokeObjectURL(url); + toast.success('Game exported'); } catch (err) { console.error('Export failed:', err); + toast.error('Export failed'); } }; @@ -42,14 +45,18 @@ export function ImportExport({ currentFacts, onLoad }: ImportExportProps) { const text = event.target?.result as string; const facts = importGame(text); onLoad(facts); + toast.success('Game imported'); // Reset file input so the same file can be selected again if (fileInputRef.current) fileInputRef.current.value = ''; } catch (err) { - setError(err instanceof Error ? err.message : 'Unknown import error'); + const msg = err instanceof Error ? err.message : 'Unknown import error'; + setError(msg); + toast.error('Import failed', { description: msg }); } }; reader.onerror = () => { setError('Failed to read file'); + toast.error('Failed to read file'); }; reader.readAsText(file); }; diff --git a/packages/chess/src/ui/Lobby.tsx b/packages/chess/src/ui/Lobby.tsx index 40d6200..6164b53 100644 --- a/packages/chess/src/ui/Lobby.tsx +++ b/packages/chess/src/ui/Lobby.tsx @@ -1,5 +1,7 @@ import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; +import { motion, AnimatePresence } from 'motion/react'; +import { pieceAssets } from '../assets/pieces'; const WS_URL = (import.meta as { env?: Record }).env?.['VITE_WS_URL'] ?? @@ -131,49 +133,62 @@ export function Lobby() { return (
-
-
-

Chess

-

Play realtime multiplayer

+
+ + + Knight + + +
+
+

Paratype Chess

+

Realtime multiplayer with custom rules

-
+
-

- New Game +

+ Host Game

-
+
{!roomCode ? ( ) : (
- Room code: + Room Code {roomCode}
@@ -184,15 +199,15 @@ export function Lobby() {
-
+
-
- or +
+ or
-

+

Join Game

@@ -203,13 +218,14 @@ export function Lobby() { onChange={(e) => setCodeInput(e.target.value.toUpperCase())} placeholder="Enter 6-letter code" maxLength={6} - className="w-full font-mono text-center text-lg py-2.5 px-4 border border-slate-300 rounded-md focus:outline-none focus:ring-2 focus:ring-slate-900 focus:border-transparent placeholder:text-slate-400" + className="w-full font-mono font-bold text-center text-xl tracking-[0.2em] py-3 px-4 bg-white/80 border border-neutral-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent placeholder:text-neutral-300 shadow-inner transition-shadow" + style={{ fontFeatureSettings: '"ss01", "cv11"' }} /> @@ -217,14 +233,19 @@ export function Lobby() {
- {error && ( -
-

{error}

-
- )} + + {error && ( + +

{error}

+
+ )} +
); diff --git a/packages/chess/src/ui/Piece.tsx b/packages/chess/src/ui/Piece.tsx index 3472a3f..13850c5 100644 --- a/packages/chess/src/ui/Piece.tsx +++ b/packages/chess/src/ui/Piece.tsx @@ -1,4 +1,7 @@ import type { PieceColor, PieceType } from '../schema'; +import { pieceAssets } from '../assets/pieces'; +import { motion } from 'motion/react'; +import type { DragEvent as ReactDragEvent } from 'react'; export interface PieceProps { color: PieceColor; @@ -9,44 +12,55 @@ export interface PieceProps { onDragEnd: () => void; } -const PIECE_SYMBOLS: Record> = { - white: { - king: '♔', - queen: '♕', - rook: '♖', - bishop: '♗', - knight: '♘', - pawn: '♙', - }, - black: { - king: '♚', - queen: '♛', - rook: '♜', - bishop: '♝', - knight: '♞', - pawn: '♟', - }, -}; - +/** + * Piece component. + * + * We wrap the draggable DOM node in an outer `motion.div` that owns the + * FLIP layout animation (via `layoutId`). The inner plain `
` owns the + * HTML5 native drag-and-drop: Playwright's `locator.dragTo()` and the + * browser's DnD subsystem both dispatch events on this node. Keeping the + * two responsibilities separate avoids a type clash — `motion.div`'s + * `onDragStart` prop is typed for its own pointer-based drag gesture, + * which is incompatible with `React.DragEvent`. The extra DOM node costs + * nothing and sidesteps the cast. + */ export function Piece({ color, type, pieceId, square, onDragStart, onDragEnd }: PieceProps) { - const symbol = PIECE_SYMBOLS[color][type]; - + const imgSrc = pieceAssets[color][type]; + + const handleDragStart = (e: ReactDragEvent) => { + e.dataTransfer.effectAllowed = 'move'; + // Firefox requires non-empty drag data for dragstart to take effect. + e.dataTransfer.setData('text/plain', `${pieceId}@${square}`); + // Suppress the browser's default ghost image — we'd rather the piece + // stay visually in place while motion handles the move animation. + const img = new Image(); + img.src = + 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; + e.dataTransfer.setDragImage(img, 0, 0); + onDragStart(pieceId, square); + }; + return ( -
{ - // Set drag image (optional, but good for native dnd) - e.dataTransfer.effectAllowed = 'move'; - // Need to set some data for Firefox to allow dragging - e.dataTransfer.setData('text/plain', `${pieceId}@${square}`); - onDragStart(pieceId, square); - }} - onDragEnd={onDragEnd} - className="flex items-center justify-center w-full h-full text-5xl cursor-grab active:cursor-grabbing select-none hover:bg-white/20 rounded" + - {symbol} -
+
+ {`${color} +
+ ); } diff --git a/packages/chess/src/ui/RulesDrawer.tsx b/packages/chess/src/ui/RulesDrawer.tsx index 20e4777..e07a72e 100644 --- a/packages/chess/src/ui/RulesDrawer.tsx +++ b/packages/chess/src/ui/RulesDrawer.tsx @@ -1,5 +1,8 @@ import { useState, useEffect } from 'react'; import { PRESET_REGISTRY } from '../presets/index.js'; +import { AnimatePresence, motion } from 'motion/react'; +import { Settings2, X } from 'lucide-react'; +import { toast } from 'sonner'; /** * A collapsible side-drawer for toggling preset rules mid-game. @@ -29,16 +32,20 @@ export function RulesDrawer() { const presets = PRESET_REGISTRY.getAll(); const activeIds = new Set(PRESET_REGISTRY.getActive().map((p) => p.id)); - const toggle = (id: string) => { - if (activeIds.has(id)) { + const toggle = (id: string, name: string) => { + const isActivating = !activeIds.has(id); + if (!isActivating) { PRESET_REGISTRY.deactivate(id); + toast(`Preset disabled: ${name}`); } else { try { PRESET_REGISTRY.activate(id); + toast.info(`Preset enabled: ${name}`); } catch (err) { // activate() throws for missing requires or incompatibilities. We // surface the reason via the UI only for the user's next refresh. console.warn(`Could not activate ${id}:`, err); + toast.error(`Could not activate ${name}`); } } setTick((t) => t + 1); @@ -47,131 +54,138 @@ export function RulesDrawer() { return ( <> {/* Trigger pill — always visible, top-right of game view */} - + {/* Backdrop + drawer */} - {open && ( - <> -
setOpen(false)} - /> - - - )} +
+ {activeIds.size === 0 + ? 'Standard FIDE chess — no presets active' + : `${activeIds.size} preset${activeIds.size === 1 ? '' : 's'} active`} +
+ + + )} + ); } diff --git a/packages/chess/src/ui/SavePanel.tsx b/packages/chess/src/ui/SavePanel.tsx index 1daa65d..7fc66d3 100644 --- a/packages/chess/src/ui/SavePanel.tsx +++ b/packages/chess/src/ui/SavePanel.tsx @@ -1,5 +1,6 @@ import { useState, useEffect } from 'react'; import type { EntityId } from '@paratype/rete'; +import { toast } from 'sonner'; export interface SaveSlot { name: string; @@ -54,11 +55,13 @@ export function SavePanel({ onLoad, currentFacts }: SavePanelProps) { localStorage.setItem(`${LOCAL_STORAGE_PREFIX}${slot.name}`, JSON.stringify(slot)); setNewSlotName(''); loadSlots(); + toast.success('Game saved', { description: `Saved as "${slot.name}"` }); }; const handleDelete = (name: string) => { localStorage.removeItem(`${LOCAL_STORAGE_PREFIX}${name}`); loadSlots(); + toast('Save deleted', { description: `Deleted "${name}"` }); }; return ( @@ -114,7 +117,10 @@ export function SavePanel({ onLoad, currentFacts }: SavePanelProps) {