From 153ebbf8b4709289d13ecf1d48aaf27abd178143 Mon Sep 17 00:00:00 2001 From: Sainava Date: Thu, 2 Oct 2025 22:53:06 +0530 Subject: [PATCH 1/6] feat: Add Simon Says memory sequence game - Implement complete Simon Says game with two-page structure - Landing page with game setup (difficulty, button count) - Gameplay page with full game logic and audio - Support for 4, 7, and 10 button configurations - Normal and Strict game modes - Keyboard controls (number keys, WASD, arrow keys) - Audio feedback with Web Audio API - Local storage for high score persistence - Responsive design with mobile support - SSR-safe implementation with proper hydration guards - TypeScript interfaces for type safety - Error boundaries and client-side API protection Resolves #105 --- public/SimonSays.png | Bin 0 -> 28929 bytes src/app/game/page.tsx | 16 +- src/app/game/simon-says/page.module.css | 713 +++++++++++++++++++ src/app/game/simon-says/page.tsx | 129 ++++ src/app/game/simon-says/play/page.module.css | 349 +++++++++ src/app/game/simon-says/play/page.tsx | 503 +++++++++++++ 6 files changed, 1709 insertions(+), 1 deletion(-) create mode 100644 public/SimonSays.png create mode 100644 src/app/game/simon-says/page.module.css create mode 100644 src/app/game/simon-says/page.tsx create mode 100644 src/app/game/simon-says/play/page.module.css create mode 100644 src/app/game/simon-says/play/page.tsx diff --git a/public/SimonSays.png b/public/SimonSays.png new file mode 100644 index 0000000000000000000000000000000000000000..ed982244194bf4bb016c7ba4fc778afa98b39b08 GIT binary patch literal 28929 zcmZsC1yIyc+wblI(w%~&G!jbpN=Ww-0@B?`_tG6ADGdTkgQU_RARt}R3y4T}*Zuo` z_no=l%-tDg*`4!0=Q&UOp4@14RXJQNN-PivgsUJgqX_~*fPWz%j3>a`nfLS!@P_Ip zr2xYKUj7&sQNU+R7x}ktAQ0a7e}CXq4m>L0%ct(LdhS}zR_R$`JM_Rzvb5 zmSjUh`49w>)Eq>JM)d3>JSGOqcc$e7YBSaJE}Gzy`iG=2lc>Eftg&H?^=jPK6*u=2 zywXIxnYoz~0XObJl4C{q%2ukK7FOR@%6)e^;ZgtlRURBA0xD$9k?3rf$ih-ZbrFM0 z(~n~nQ-wiwq|d;xW*0l$Cx$B-AF~{_7NKF4YbPj>t>DntWPfO&2rnwij*1u*6aJCg zNm6DP`J&1d4_>eCzh+eIt7?Duguu)C^kgov+TQCwOR)ixmG}4GAGmcZ=*PN&0tDch zh?TrK9JnOZB!?sJaq+#MpVu^)P~*H}RuC3bbm1v{`S!4J{Cx*e1`8ae5RVNK459&@ zi7<`OI(};pF!;CD!wdrb@JTYLXgPq*`3J?99xh#!P>O{Hxe6uI%hLTy z>-_HaTKOM!V!)ZnS@$cu_{qWE=@ELU$j^w4GJ0W*rK(gtgUgV+&GyTtTMcv>nbcU| z@bB5-b4{3z*)`#9pz%_;q=Cj_30fd)%FRlDqo4$Z2agQQ=Zaayd~SuyT5>m!9v%+Q zK&K=1kZ;gUMDcAb8#!F*Yzpw3?k3Q%G=V^i1TK3n*cOhsQNLRvI|8#?KnKBO_LJN{ z^4`b75lw)c3!l|_yB0RKZcT0FI>2UoGX;6tAHGZl};<(;E zRsWp8>dJ%6KZ;s1K;ubY$+^WfIj$w3K_IC8^s%}->JfQWUwY_t)8lfRT`IYUm<-HU z8DzJt&eW*~LGB~RQ1=fkp_LPFj%HL=+mFK$q4b55YgqyDpKfz^neCN~aH1X|Jp4P} zi1n`oSDCZEGbQ{SLFiC9Y68M|d}25yuh+zvw~A|;^tA-gS3BOUX}Kih#!8z%omX3{ z+a-5QqHG*8K-2Jzt>#6KZKS`AAsX4a#`YQLs%g8z;PCMw{xMF3f`O8n8KmuS5WXMJ zb9ZSMl?V}H1&*|119gI2ruz9gX62}H2uUL`p`@ScO|}aNPF_KS2%uWP4fiR;#CiB1 zuheF%49I1u@dohfo`!R}RFEM`O{~bXLh(J+@6EWV%~i&38%i4J(lRw>5CX0L;6TaUrBo~jor~V| zF+n^XJ`%6&oD;wD-L*HsdqQIPFP~+mIlilofM~LHeH-Nj5t>4cNl;)lQ=0R)fW8ud zt>u==ATzVk9{WX*{mXnwNInxF`+g-*XE}tV#ILqU8}75``cb4=IyHLe)ffb+IK=R%>@P<*D5+_4 zF8yk-7xx$qh8?c^CmtGdQZU>MP|7^pp}jM%n8!$$tHi-Ear}yJmFs>M3myGT|b!Fd>{ZAp;b)cXhq*86eW4XYY^I z75MKumVBrOBvQtmYZs|Oi6Rt*LZ%RcGyuxBo_f&aOZFPw%M_e8^n0lkq@7OE7q0Vw zS;b%ab>muN7wACD^Z{YbQ0Uq(Id$uKJ}zAB12TpU0_2WoqY2)jhjkEkRU*vk%U#7O zZ?HXtPh&5u?GOK*qWB#(AJxchVzQ{1#29pR0W2z9-Xj_3z#kRACKVuNSOE0NRcJF{ zziJ3qq6|#}Rp6MuTu=XRWd3(;F%kJQc4m+{&+eoW=TzpMQI+d+I3{oz8QE5Yto~$` zRofte9cWZxKdMQJ6}RDy75JD5G$2QfO&A}|!Pjw6x3TMB=L20g+TSYhh2m;Jv$3Cs23?AsWI|Z zl;9?2q(WDM7!bl{7pe(&%a|IhUaa7le->Sx(*#l*D~!FEILUAoGcLrtqZh?01*rkG zSZ(Z(+JpZhdg-xHh(VyzpPjErKT}HJ{Z#|W|Fbx4j9^JBNP10YK;LR1UN~afP6C|K zj*M^%8*hdPF#`J_7_Ox@It$_~sXFFXr~sTT)~v_u`sXo;16??Nb)-kdoW4Sp=yn8` zlkum6{s%QnH}#=G&;Ef*6su=oX-dakx2TT9Q-Jgh4K2*B?AWv%tY~MDkib7eXT_jP zlk?N(gQd=`Bhi4w>JZVWCjEW;BhhqMDP&hXAOHqrVEnnWQvNPeJDZ)cA_RpK|~V>Zw>VuQYLfdOvJF8!`)yH&7*0NC?ZQyaLe>~KQ5zQ3IG zShNTkL%Ho+DEF#&#E6_g;i((HA{GPNPa^75XWib6g{XF_kXt^?WG4VL- z&KL_Rjz|g%i$c7LQqbR6XPZ*n-Ew`+-YAd0B#fPI^d!olYa;+)^z~ch)5F!f(2E>^ zXEXH4s3!kB+7M0$J>O{J`gwGenF5lu>RdK!w(`z#xpwP0uVZ4*H%XTx+ zk)bf7f}DVyM(E!CdZCE<{<>~}W(tU3j z{TUb*DyS)MAh|^OW+`Ie47Z6d+et0NU4+Gf0)&L|tf22h@sWd@HqN@h)~OYr0p$k- zO4KFT)h{6PBTvk+RgzSF2E1Y<54&*)-p zNq{||G@DPoAUwD2bF_?*uXtTcMBk->$UilbjCQ5P^SZeRFN}S-g$OQ;?R15YaUTGx zVv323it3>|I#-_6JNf&~Q*cKe@hVOjoB_lH#rhi+NJ2?~%>lYR}*T(4I3wa<642A0LjQpd1Ci^kCfG{U0XDp#^+zxr`^jD-|-UfPLLP8A$ zWn)(e8?S26c5&om3)7AXtB7n3=;u4{``yMQFDdi)8UI%K*u-nIpASXOOSK4)rh;(x zE?1wdtjo#3EWhU|b8fBvC!+kI?z7MPPqB;BGV{)~agT%FqshRyFtLAi_6>wR37?Gs zDNrHQ+I-u;ZJ_w5#o%O3w@n^5*2LeaHzgdS(Zrvbb`#i_fN@PtBz5TO6`pQ@c zeRq!FdRoKwb#CD5n{{)|omzj4GqEPmJ0tqbHu0hmFbA!T>5XUEzS*Jkaw3RDOHX5V zMHICTi*@cqmgCkcg|;RB@^AWuiXSJW+8w8Ms2)iG9qnjm0s#S#r5C6rf2o@wwI{d= z{HI+Yme=~XM&66ncdUviK>?%vYP9kj4+F&N#bbg|Lj_uF_Y3HUde;_!CbT?of-~w} zwpqXvG_POCY`ljm!If5)5_wgBO227ie@xtGc6tn;J_ulZ>PRPJetPLXn&ET!?cxDI zmZQBUBcBu5ecr}#`-?jv4W@dP_}H-@Cfh+5>pxKZqmqr^i~EjvV3p<}k)L}F#f}TZ z4{&x-S#=h~upPO@?*>`{P3z(18_x^$ZQX7{y#)Q@?(vR#vw3tM;P0d%xZ)CmQUn?J zb^nQUMSvqQo8#yzR%d#RAilp6z>CKzCH%)@T7aEv6u!ehX+D#^-e_SBrKgP7N*vy( zNcUVH9}Ju>*!$>c=YQoGaP_;@FUbTCdQ8vf!mH@+D5gz=!kT@V{(3nP%qRN{UqfRw zH>c0z)Yr7nrhX?$K|(cqXKh6;`G|B?i;WCV$fn;Gzy0bcPme$^oDq~U8h8kDars_H zK)R_ib<(m!9Z)3EYm902Y{#rGe3^&Cr8UIHIfX1;jv6Ig8Uok3xd#Y^#YwZ=Uj6#o zUia({eMF+C?{?+qx?N1XV4lD0ExiW%>tYsTuR(rX0&lJ9V1oGXJaEfXzYh)TL0u1b zTD%#77>C9MdP(}%kK8+Sh%*ai?2zg(F zX+m{d1yb0BcDW4bsCkR?&b;-h2L;pRDY}+MBoy|H-WOP?sKlO^^MjQp(A`*Mhd^;> zVvAGkE^%jRfv#|fC14dr4@3TC?LHc~MsSB!QQ8*C(TOZ$XNXA%n>`9(*%6Am*sf%I z`XZ~2__|aqA8SZQ=dl1Uu~RR=fKrBpmI&k@dHD%X~#iUgf^I(IaMH=Gc}D z;tA(|Fn;du`S83Di|1^1TU0nBngl0Qdyu56?3^ZTWT)%CAx5xlXy-&=WcU+Bvs|(hq*j&Mm zh(3u;lIqW8uICMSKXdiI#r8L#3q9f>#c1htt=APYzhg%F*x0iMoac~ZIQWeA7}KaB z@e>(+>S+o4`gzW$D6b!A7M{=-|i%i6%uM zC{a0gx$CCE%lW;5jHV$fLio&3{+e|}=7}l;AQ&x|+kE^No9w-+;H&^{ z5MWhyfZ3GctLIJEs^CtIAEAJ8^Cej(az6Y$ApbRM-h;`aOV7aGN~osZ`c(qa#c_s# z+y=1+YcX%RY&NJeS_mz>9Qbz7topP)ea?BsfxEYt`fHv;<9iK+P(zz-=iQjssX9Bf zuo^RY8DjfvG;<+QDPj8!h3IB5L`Qrg#JF9Zyk{yps$6Meie}UA$E7EO#wu#BML2JK z0)4Pj;&zV#-cZ=dT^!4CX5?aRV`zAVj7%TJ%(v26tm_W0Uu92b(bOk)UWD6QyCc8P zx;W)5rSaCS7hrnVYO@a6;Gk&)jJ;p%4Ug4h@c6P zq70w@hCZ)$N2G%@da_3)cK&kCH)Ap-)x8i>sp8JruEbo&@A!GPVSgIG$;g<$dQj2Q zMqt+_pVw+tc2s*vfwpK@V!dc*sfNll)WF6*4WyS3vHYw+1gY_*oQ-=GpVV7^mLIZ2 zgiuIBCCeL+7Tn9j-MkLS{a*Sg0#5XFZH$hxX;ah}p!UXlACMCs2<~U~t<>D(*Tb62 znGW92?H-+*m3TL!l<-CbD6cQniFU_^?}*A6gskGV^KT5AxGfY6NdDu=7Z)$JLEF z=h53eAY{@k@Kim{d6u$84@l0oRzayz*k18TI`teHZ`hqv?c^i6nqb<)nv4v|11_uL zbvRj1n|_NOj?PV9khe4I*cdvb`L2qT%4m&@h)_ds=oRN2iY|6mN2p9pGgklG$?+CD zeU?9}L*XZPs&ec7ovPob#9v%IFK3iW^zLTvi^HT>uXbYZ_$Y_G1z2{H0>x=$h8PA- zsy>?_Q8ic%x-ihI`-&@ivc$zu&ZnO*`Og6;gA4fF=Z7jhkmRbEjJu{MQU$++K6m#` zX69sMoJspU7<)0hp7vJ#_%fM?U{Hwa_~(LNg{KHpBfc~pP3iXzo_FJmF;3w+JcF%S zss7k7pA!9mS2p>WOTK$_K43uL68r&ShBiJ1IPcUnwy>oq$Rk!GF_+UHH(bdcDoJfh z`3yO{jkM{{mT{j-%|}djHRn0>FF#gt-}go%Qc^11)MTkqfhTbtaPMr0K%j9&`nfqY zjt>zz)#x?k)i?lvNtRGKOA2K>cD8XJT~|5&Y6xYKjxs|Y8q5U(r33<){;Tn5ehqXm zDEFgmbu%$LhIL|QO!_OnYDrsbo6w+AbL4``N1-d?zK9l<9Sp?T@72J&@JC|Jh^bzW z6d3aP+#*<#cGU6}-tU6+ns< zvk?fC`P7fQp&z${XDhKdN{c?dTY7)OA=JbYt%^aS2a@(vqV zP|{b}k1RDF!IE#Ne^&boCU&`*&hhs&5NSkeovUVM_ohyXd`Mb53v1cE_VwR7w(=yR zkD=mpT5Yq_78;Rc)g%Hv+vRtQEMl`Q$bX(Nm187S9DU|87tMR;e z7^N+O8w&ouN>mTCgQU+2g^0S+=W4zl7b7-+r#+DKUk-*lUshVk&Or7QD?k7kwal-4 zY2>Q5I!}|jt;{LFec-lMF|0RbKo2Us@?BB;mKQC8fi>o&GFQ(Qp!15yXo4N_>-25z z$I!#C^3(j?JZ}{w$ZksmUgXRNvw5sJ?24~gZcG_4=MPv$$`xizj;~LSR7flWWg?Q? z<&}cGhZV>6o-d>5GO}uQ*cIR$LtKhba|1(v&x4EcCy$cOO6K#S#KM4pGsat7fl{N` zjqc{dzS!)=fPk0}vO&uahU{dDfxnSPlkn%px6MdOmK%Qae&^w>p=c3`ZT*?T$Nzyb z!91Hc`ST+mSesC19S!T(PY}KNNBbVrOBcUT@Sh3oPy3R3*Km44i%R(Y1@UdrY!?VZ zLdSB$Kr3!C+!8Lzf^h`Qe{vyC0S&3*_t9pmAjxfqoeqTC#+xRKDsZoARErZP4Fc!Rog@ydW~|nWVxuKi(c1`M2zr;|^Ue^xz?2VM(=9=RErh z*_ku|T+aW*Q&tSGPi`)L9-P(OK)+G{E0ML|w~>t8QNQ)B8pvo)IDBKof*$;D%S<{Y zZ*tv$bTjpnDU(Slm7Se|Nq?AFb>>R62yga^U#CwUo^enmn#2~CjO&F3UA&i5NBey= zu!l{YZ@m9aTjJ7A>*#t5b!YuQx8b1?N%*b4#KL9X^&OA&uX~Ehv&ZhK_~IZT!qlnd z*OTp0TX*)084=Ojr|E45!Uxof=yeNssS(jK-!LcRw13GGLWCZVjI#eX=9Z0ck`}TT zfL%}fdw+Y_IB%Ul<9GUb`h9!L$RlNP6ovgrU-7BBuwFFZ9!y+{EjAJQ|Hb3IHhB{B z0JvU*>&Qvo8)^*g2-kRcJy$#5{=V`Ox%a3-F&4rwETvZKn48E!1yxU^9wk!n8xZ!; z29W7UOh-^(?-=mYmp_FhqV)(R-rhGd1N&BIqP@iLmWf6_lbprb6P` zy41N_u`n|Ojwr~+nkdh&h4ih?4WQ-^uSKkleE*YLJ}(&3`~vp7Y2z5vG^|N&OS?5-ti2waD8R`-fV+;rFxDucY*Yki7hG3HuP6vL~ zS=l4!POxA<7q&bDE-A|J6|j!w7SDf!&xoW7R$TwxXsf$BpQdxBn7Z@^&Bw3X2~EXNf&y;cF!W-C=l8F7ru+W5YsU%luFv! zW_NC1TsQ*RUMgq9`N79O#&um@IftyIHANwG|?rhk8w*?S$tdT&dGMaI@ zCMrH4Dp+j*fw~VoNwo8Hy^qVWQ4Q0IZ+=K6>1b&rO^A^@eonZra5o5b>Av9?>>%-A zB;mI7o69@S)8#tJxodWhYeofi@l^IKLo5;a-uu6wRg2XTHxa6?yU(4TUyW{zm=s8A zy!d|#V#ip>5n%R{rBE+4wQN>fffdQJ2der@|kC2>6!v9^=s5-y=Kh0$Iq zB0FjAyAO>NBahq}*hBZn&DQ!Ghl8jA`I{?dy-~?7Um`yx9Vb(B<7A`N zwI93&yRjCFR&%!PT@@iN5X*~_y1a#=kvn#5l%xZNhIP&_@vHw4hg*)bJt=JVZR<;d zNq9~d!!{s?T zRWpMu?SqhyMn0owtQ)MPwx4Q0;__5Z&=xOB?wfA|AXA2I&2f0lMU}UrXt(G8-&ufH zU~#82Im&38>4ySTE?<}Pm`)kM*b-w{?{i&OlCuH?gBlao-a-7dY#3qqy)U>3g_`EO z8(BQ^2k}_S9#5P2I_{duyerRGV(8mvUo8(mp5$cR7AAz1EA_=;OpO}9(iMHENjc<~ zqgE=TD0wIJvNka`LUma(mWjo^JGr>#Y>x73qCWm=jW>S$SUgw(R^i(~knXJCe&+{h z$V{0T5ge>N!E2({n?vEf*nQt}mf;jOVf;g@%Q3NGJ(0V9GLJQ4)_$tRX3A?JFCH)v zqtw|O6=y$(jl>dV$@90xA{GpW@pZwO>SN#t=ffjW{xj}Ii2q)9?0G;KvUMIHLRv5yZjg8)fMeC|R z>y;3q<81$1-eGg?dVzq$Sl_o*B8`1QZ)Fg+V5L!=Fi?*afH^>T{(kqN{raZ$Kt;cz zTn`rnCuX+u-&QH61+vd!HtDo_wFp3p|JNez^GtN6ZU5mp*r0?vH+U7i}h6(-d$hg!cf$G;4;`R(w5` z^fesy3Z!mcL>p*@93@I%bI{uLCWvtW?tI-6d~8fL`{MB}P;kPHS;p$B7O1@KF*gXh zNe%rev#iKwX-}>~Cw91H$pW8s+BV-Hq$ToeC7& z(d(Jb(*3)=AFjjy;gFzj-c3J>SFq}x>wLsvgdxcfhwbiR-j2lsak0dis0Q}!gqec z!+W%hke;tNo(736GaYpsA9mb1XOLA|P~&iD#2iK0#KDlgBbTBELH<;?E?RvRL-7_> zjYW_e7wo)Qb?pvAnk$-+2PgSnxMis+vk#a+L)f-zZL|I;J5b)XmYTdzN}CxJ3RAaf zw$BiNAz$nIls+Rxb$K7)a{4qQny=CVOA0o^g|8ut<&Ur0L@vS_;OAV(qwN9VVLf_> zVGC7!nL+~Nts-snmlP|dEy)Q>MQ~I`5NHhA0H@p_g}(wlM9Pn!o{~0T<%0n$!MiIr z#sz`?N_FQS{x;Re%oyaunMx30FZ^}7{A|fVvf+=NWS{N`BPcZG@HGaww?U&!HpKKu z9KB-;O2OF0aV6K6_IGlO#NUp|28A z{-%Ci%AdcAL-~lR*8bEVoje8c*ZRuy7nH1C8*>shWepq7YPE9pCYQ!4*W!tDG>O2jcCiF-4HFc_M6sGuw^MWuwRz@{^bw{lsfCf3F zFwl!2!D0rXU@bz7RGhGJCKX9XUv1U%>@n`iU?t0j!p!;j?hKz)e_XSZ)aGXf`TYto zphL{FuXc?*-}*z3ixQCMvlRqZMMnob2fEj7%P<*ktX z@k^J=F|Swrt>5InqnePoBRdyV)X~OcZo`HykpNlax?Q!&Je*{Bnwuwp*bF6ug3A89 z1XG~!!)VpB2A*6h+Fz;Sb$krvK*bRSPpQ_O^JvF)d9D4qu@B@(v>_md*HH=yu!e>4 z;B=JjN!g3LqX#Dum86Dc%GYhs$x5-&8VR7RZb(Q@R9_{CsfM0|8Fc8$|@dMDGnF6tbO%LFMwt0LjMjXX@nx+{2qNDsnukOJWN*sb5l&0jwv5fuK- zxPvAt(1H(1V}hRpei^(nna9LnMu9P|4wH*qyQMm@EN1 zp~R)XL4FN?O*e;5&xA7^0fg_L1n7Pxw=4j*k-qUZuuu~iz^W@vrL+_Kov-d~t0(Z; zwwuSn$dJOE{b)`2snM)7Nl3XeC@e`FD4h1ci1ch+uzMT?!r?*2P1QL#(NYVTyCcfK+@f%(fbumAKV~53+iNEBY3{+alR4 z3==dXMtTKBn!C*Ws3lSc4SjaWu90jiHOJ`()rrPQf_Qf{AR-mYu-x@P zOyUR3)5zQ7?)!jHai&7ni=|=Re0(APc)1_LV~)qlcyJgs z0_k)DLRl3Kw6Vc%ulhRUvR5B%f0g-S2&Cq-_n_u0;=Ge~$RLoyS%UsZ2!#a#92R|h zZ^F!`;xK0EiO-_er83%@gbIZEa| z_vbiMF(M>|U(6utp$%9B2$S@EFj`d$JJhe#!!&c%tK1?p2%bcw^Ce-#-$qYd{2S?y z&gD5F-0}GF1tO^qGmPV?P3s5l_KU(0l+IP+9-f>A6pS3|_gFS91&{U7y1|on- zBG4Z|Zyl?sQPuPOw?r8UnMM0vaO)}y7wB*BV{ylcq8yhs#(6FM^p?IpWgwBtck=;M zIA|}>7fQ(-`@}vi;!ybL^y{yStyOZG-J-Rth2k+n<{c`}3HkouxZZ zf_iI{9BgF26oiLL;!fx4RB$yxt-#TI|M`03+i%HA4NKPvnUjDppuij+W#4#OYLf?=j`ZB&358`d2m z>=4I%sg6unHvMdASdOc2UteGUcmt)NJqy^=9eN$)?9Zg1A7XsCH?%S{o9(O1Oi5N$nptSIo{d-0A}^FDh{O7=uMb3fx524Z)p&Gbo9YCso%QE>(3}`fk&F= z@{$gTCS+}`dtki0yzG{hm6gY_RgnmoW4wZ?c0NxMhOLyMu*)OVW0ZA+{U|#)c=h!! z-dsecXZiS#$kcm&HG4-uu+>;3WTIGF`GxoMm!!<>ztlWE&U1_vO3{Gf!WP@j=M)r| zK2J5yBGRsw{Z$w(N^;<-mDaAb1@-lZsPdLmaX&~zHqDD2sV@JeGsQ_NFbO+l&16JQK`=x@#%cJ<` z#idHu@p*C1yRRq)2AbA#)VRbTtngV%-&z5vJVnHyZ!nP-JN!NMl~JILXbBk*ndI1+ z?5{Bn#pF56hYZb-;^OWIg9H@b&Y`%zzB%N{X zX`Lp+2A%NCj;xx6BEMcXoV#-#WzijVR;iqvdvaS_!}Qacy6{BUNnDV3b>c_SZ}n6Q zpQhZ3VuFF0(ixK?dXpnf2FxlC?eh$7{;C=1%e>Gow?e$qJph(cD??5D?2_e}re|jO zwV&5b>YC*ixqqqsN&BcpQ=KR*+WJ1RiJzrZ&Q&=H6C`T|?1Y@L0O^za;!F!wL9TLR zyDCOr)0z;|`g%0wC0;uO8Mi@c=jK1Q;&%-K10&2J7@?=y6UNVccGXr;wATf^d090F z^*7IXaIme)@tCde_}LaGT-A+AoK7G=-AKxb z0XS+ubpA;hg!p2M+c?Y)Ef|BxITNly4K};nkbbH&Onl}VEd@2X;ehhTMDj>@85IsEt6NOh0y>Gd6L^5AbR(@siaIK>yO z^P_BskujP^&GDU?BA3)JB0S!cuYI_zul-E*74*tPJP#^fhSmodueD_-!q$o!MHg48 z=;0z%5$bk1vo|F4G6Fy6Hk?g>Jda4{74GUt@33V73@5!ux^{5~og~I0g*>s z157hh>T1y9*(ZUlhNq9s?zz^rPw2nCtr=|#Cm@()9mEQr#TvTA^1JTPJ?- z0*L+mv=UuCSn>bnva{^OuZCJfOv!=HiEdsT*(Z^xGHk#1s+cho(35 zXbm*Uq13;zA0>Xqe8GTZyar`rm{8ky4K5zt#?t185!60OGer@^9~+#Vu5u;eqG$YR zft8jMkyPZXRFtu^GdZI8$4NcdD8)5&78px+mjb1sCR>?)4XWfjTJx6((tVYAr?OpW z4pG-R$BeJx{$b*4kH^4jMn_KMWFQ`LBgO5`i`%wc1-XGs_+KBqIopg0n&RJM#1X}$ zzH(W9=Fc)P&lMld$C=@YrNcn#!3tx}5x;J>tx6LPbgIUq^Y(XM?H605%_UH*n$*u@ z4g1SI@;noiGGa*So64|NT22&*QewJ|6o_&9zA~M04cga*cpH-2f9AqbZ)W z>uKq-qX7HH)TNK) zYL;)k1Y^vn2*h*eE#lVyc-z#3BlIyVfAfcug9m1OD!J!IlEQ{)npK8C^7)9oU{klE zi)1>MqwmLmpVY!qE4kg-3d(0D3aCVVzclat{pl{0dlSC9!T)LZu9t`vb|T0OFJVl2 zv8?$h|Cwl=N`E)Ga^ljqGktY=8gAO7 zDoD31L>PU;Oi?0!V6Ewo!Dh-dB`>H7{nC+HA;q8HDjH-Z=jGYXeey1QKT$ai;N*)c zHTU;lAKCkgfYIl_B|gG@djL<6Iih0kfU3BYPVdLeE8=<#sBgRLjdm@X@G}-%d zZ2}ApMwx>a7qo|NS1R~}pWnm_s&RJcY0Wd=MbznPcY0Xi$gcmI}yPV^vCFgYkUe4TIa|p5st-t@nYJJ;FSU7>|^z6_axWezL z!y&1J!pFwdjDBxlTMV$_S;Wtb&W0;u%s>*-lr?=TK z>nW#eC2QJL6SRAiQNg!*dD+K@h4O%O=rl8UhFSMx#KJ0Ji%{$d1w;!{&%71P;2b=o zNr#|%4BXvK@EC`cj(^0wyfmPA@}h`F=L-#x@3snGIpsRKalqD=0*Oj;*2Bg4&8*d| znx;>*7De5x**G&cAQ1$5W8hM;|L$q z(LxdR-ATa-zs-jdw8aLKVoHK5Jb#uk156Hh(i7!>sj~EYwN7T6s{QKRCDPy?v$0+h zqwl=0rdV-qJe7lF`CQ&~bUtzp-rN|+Z|-J-!jLKhaZcPuOp47l68Gvgm6%=m-J6zE zic%c>4eVW6r$h4c89ERvhC%&!$}dHtyu)zAx+0DstNg262_kwd^5gFgfa=h1s_5?; zW7EIMIX;eLLXB*Qa~ju*p5)gRc6fh=;tG8wHM)`t2H18X{P(ab z>?ndMHF)TtGGSkbFEMVC(qWc!@*qCl?sUZ;o4e%oSm6!kM;IQ(6z{B%c#s75p$ zyF5=>lTGJ&DjCu5DeHF~BxzloJ1Mlq7FR{96208M`hXs+`HXdd)HY&N_V%X8-;HbS z(2d|@T?0?XeC-qz>37sF1`))sGCQGFXRgsXVwsO)6Od6@3HtW!#@fbosR4-YOV%BN zWcYa(X%h&P8fSOkXkn`tOTyk;r_>y<((!+yg%-n!wCygp)-OK-bvv9Xh$gOjF#77q za~Eoaep|6R289l!#J0EL#+m?;Y-cDM zX+8TtGi<849~j|Bp)VUjD_P{C>-Pq9xY(X{JWr1Zy}5w0!Df2LuxT-YR8K7tj{{Xi zCh{b3$@VUw!FY0hUJ`fm3dj1k@9C#~s9%HqG6p@4J! zlc1zlj$jK=G+@?5adcpzpC5;CWG0Lu*?_b>BF70@ZVSv_FdFR->N0>_yyNRK#SRiJP|#$F>uN$W-E(Cw&z z3M;}DdA`$V())*hf9!A{Uqtkldr~TJ2-hX!4W`r1r%ASSK1Y4{rF8)!F=WhTMHl>{^ zm*8LVE5}LZo6kCp>k)M&8HUMB_2UW<_BYmZv|gl+8m?%(ucAWo;NACrWPNcgSPbvC zSj!W>O8jJN6Y%zBp59DQHEmW?n zb(u4(iW>tFJAF3PVD!ya46)|VRnf(KPpqzi+R$rC?biG^eLh?tc@uox-?c8z&j)=- z>-VUxNvJw$o=*lqw1Kbo`3FVn7hyjRcTdYb(LkX5cIh)vN&470pR}^N z&UouK-j7n~>M%Sh{Nc*899-5fFpWEn57YtxmsTEg`_B3b`0Dc~W;%^IRNfEkNHrnH z8;!j7!>qZbmwbzt{D9XcKsrdifMopLJUxxQYTFSASRsuKVDYnu)jW>KKFxkrb``vw z^D8Q!OSpWl#af9-aCx3@VcZ6OB5p$pW$RRz%;)Dr4I?1X#LIbO@|?`?J!}zu^2V*YYRI1f~?U84%F;> zgZ#a9s}!~S;U2k;`H(hU;#I}M7@o4P;Mh?K>QZG%vL`hLG#{V^$&#j!jY^M_bz9C& z!a<9Z`8A0Gt2go6TbvK=ha%2#>y1>&UElD}%oN|}Exwd)AaP;gmu9*QkZ~Ax6Og#> zWj?dGqLPK-S-1W1Zo93VGwsBzLKHNQz1(n+G?Qvz(JWKLy&c$$W3rpt;=B$%{0;m# zpfMce6rlFRS*-TQK8POKDA48n>(;YtIcxIjWcmCk2rNJa#TnU>sx?bNOvpd-FcnD_hW8XjC<8x+78B|jbJ?yi}ap3_IK+YyJR z;MO#nBD9Z&bGmec=Ha(AYLQQwHAX}aucVLCN*I&hEg#+9fI)AhspDpK7!yP(XD8(9 z@%Ooe7HgC>qo%50JBfa`^`GtpM!e^e=Yvlk^C30WOEsLRwSQ2aD(Arvm`T;f(*d+b z1}JwCBnLqxSHBY%kZhweQHoV)Sn2!}LNBJ!xtk9;McL5y6o`{Pi4$)N=7BXrD^Rlc z2jH`-3ez+Djci6L%pefYMvg8Y$C@O?)xp}P|FW}jcK1?rjT4HEv=r47*igt3N>qLl zt_@qh3d&GZzUHjcJvgwLKR9*wlLkHRVU*b+$^Lp3du&sFOPIgfBwv!mfeoM0QC<>m zkkxlTsNYj@>XPs>xuFPryfXS~FQC zA1ffjZx&?-eh&qLKRrz|+!DP`75To1q9)GX71UWzY{ot1*~x}mV@WcH4pFCUvGTyC z@&ER1#UiRlK(5t0Yt$@xE79_cG0Bv`#M!7@l5g*>v~T#y%imqEI<@z6HpE9v z!ZEbgVavNcM|;~A*!f`kDCTt}15MaBI3AAFyxLQv9H6As(9S9|%r1JL@!4NSo6mfK zdEsFfr8Yr?A$g7#cl4qnon#%xk^0SfF7YM!S1iDjrFG$J4)qYu;B9pDmW{i1?t2vT z#mCht(p9JKw5=9WC@P2zw+&_f%h<)GtYYvVx3SkZae_dZx^vB;)5@RggQ{`;E_wzC z$9(p#V)ei`leU6RK5k3+^2K&H_|^txPw+FcG2VG@Z9ARQ@Mg6NNxA_WR-L99)e(M1 z(vs>U^#+8?LK9muyh7{22my2 zVLJ)kL6#@FVVsf=la?hp{uO+2g{AYS)_%|N9zglAZRgI6A3u>igjRrHeaYkA&7#k_+)1XO@l z!A{_~!8Fm%v$D?(rAoaE9nF%Rces6=eY*q~k1HPUn|_ZmvZtFP*B0{0ja@`sKV?$qWGgl-=1!o1PPu`SmuRwJfJr1t>dSi*tAcV7HgUs9|(UNd1JO$98z2p zqi4J^?Q&KH+aBysv9baUuOga`(_w5P&t>5VQ!6mi+@?}g^37(=au~Bw&VEqOevd{j@uK2`<`n4L zVF&s_Z{psC>*z(<_Y&KxT`(CqE#>hD4Ld$}Z>Lb=EynK>RUFPV(#NT=)jEnK_U~&) z$JKPP(&e?uozpl|UB7oFBF<8xifcl&kQXcKk4o+0qG6_1?iBM zk`iGEC5G0W z0o#3v-vnDz`Ot}rD06ZsmRb*ujlk5oe^;RI4Sj7Xjn2+c89EhyEoSZ8#!Y{7ojlsJ zrvQ-s;i8k5fq|li{nsGTTo~^s@i2~C(Hs04&0*f@{a@9Gv{^=sOvBwynEA%aR`gPSg&L#oqZFDJ}K8`9%wjIAB@(u|7YDg8Y59$GMvK`JwKE9@?mk&{KvkyY@lVl1W~~MUC(}C}CBKua zMsd!n5Ue938%(PqYcws=B$I7JOS37Ss( zGjhfRDATMl+S_kQ(qI%Pv-`W-f3o~{MVl=IolMVAF*zSI5)nxu^f8IdQ->slax-z+kwyRfRia4L_4rGY z**(mb{D{ym{QUfxE!zVo^hbr^WTZdIZH~WIOdyo;`MMn?wM|M>cM-QY5i*{0_<}2! z60lb}g5_hjmKHMI&;6d5$Dbi3jsbzgnJ4VfVl_@51=QNfg+R_-%SRnXC$8i}11-D0 zdmqlw)BY}f1sMqrO=|_s5@iC<^$mO=o@l)56Io)IWxC)EUbi{nUqLnBIQMeIs`IaO zsHrin+hz6`qRA7{%augCau7zn;-}!g0?g`$yfmJAyl5y(_iW4rxcF2?EZzV(M2jDs zhh09j0_kr?sZls7IGlX2?5WIm=o~xuuS=G#5Gd1kX{;7wnOIH>1aq&`<6ac1@&M=& zT&?(vta9Xrvq0Q)&9q{b4vA2Q~CF(|AfGGjwG(6NRS>ngxHPiWk_)yhD zJS%Gb_cf^b6j%F}q-V(}nvBE~9dhAJajywmvawuYh+qZo?30MT;BR!wc=C$6PlV>! z`_w_~x2kw|D75Ewjk1+#EN@1l&^MubrxSCCuW-N^XLaN6qYJ~?F#{D4>SO!J_N)=H z!D;nYh!l|3(>95S>U#eSZ^fV1&gh!a?rd zRqrAzP3*7C0Dy;U$TN}jj5TMe246>M8p2oHGo?DCw6yY; ztM;m$+UdtBH}_&oXxkrp(a)rse*$|W;bHeuE^wSL72V9G)Ist#M?ormt#P%k_jP=+ z(od=5q+(MCbKy>CMRvtJ4riZy9lLZwaI@lXceAGOx$Job1_02Og93_g%{lMd{rR8v zp}|+F&sxjV{m)*L>5EfsJDMexW(W6KxJ|pC9}YP$Yc-uGXR$<*J{SbcK#t;#X&xwv zFvft^doTWT$tY~NEJ5a17~L@=T8yqTPm38`Rxd-Kc5Sxk{Z^$fVN^=v`c}T(++11f zQTv|P1^+YTHkL|WCAkcnR5NYd6NNW^%XWV3f!9_$WYGNP!@N>Hd$6 zxN9*_I@OGS9Q~CV%62`8E`NUAC(rNwm#if2kX7SWqVUI+(&qSO?b&Y=Eu$s=q&zz4 z^5J#yWnHQHSkAa7#Gs3CimX#3viI+9r(=S3+C_Q81=$(nm(l;qjY}U^*Ik;e`~9kO zlT(<=S>Hbnhi$fq<^|6aueH0(?=g)#2yY=V4Ye z3;Y#D7b-eB<~MQG%I8&=cK)7KGh9pU4=t0qOT5ijrH|jyfCCX|(+3EjiU7xBghVVa zxa!s4cRhlf`qhoVa!gy>e;73llfW=7O8Sm7L6eD=#@fb@*q(%jD|rUfGb62cm;w8o zNNUS|m#MoA4t)*kfw~8+ZRtmn)=Q+E&xUD55g%>50xlQ@dOFJ($p%3B(No~Us3|HIvD^9-a|M*^=LU5pYvOwSTJ&lT;o%iE*eV!Vz zj6?-J%}K`nD|`Qa!#>gvknTGrEx(~WHjIVIyadJ{9QIu+e_JS2##6Aw?M*}+>~GGF zN;!YdMFjm^EF~JIg{E&S<=66WxQSs&yLHJ_^uP_uaXHAjZ z3hz`YQr^-tl}O}Bo8a;3=RBnUOb{s@GVhoY;QgefPD z#SmCkjPu8>fK5q2&y;!$?yU z=?m|I&UtFiY>A!a^8=283{tfz5g^#3;`H~Ym>x7%+K>!w)-xVQ69ew5g|ptgo{_^g z%l!NeMxTgu%xw z@piik-YDMg8(^mp{9aQ*G8bp)V=edR1IF;7jY(^AgPPoLM#XNvQd+ip`)oK>?=O~_ zwu>T8tsY~_CfqN&CDYs2r11l|J6Qq*AuY|uF>0Tqzw|4X(!_vA0{RXruN4W*t4(9Z zP&jofy@uPSv|h|U-XQWJM}{MT5pZVy-j=rW)r=p-l~D^do#&xLTcy``VlU~$s|V`N ziB1d{Qg5HkRqwt3oB!b69oV+t;$+GsYlgx1{qS6zodLDeXZcO`$390LeKF&FG+hN0 zB0{>K-c|Mwqr`T;7g%2l$U;p#=r1x56`&IYJATsRO8JPp2F-`i60pMdP1%{{T&X`C6P%ot`dR8PD`yHZrvX+Q$3Ml_F5-x_fJ|7ZsO@gX@wo6ytPuidlcmRJ9ArM#dqu_>P+U=j10tv3pD8Eed4zM zApHmIC&^0V(oHLM>)x4Odg9;HZM&YJ&A&_zd>Y>e1vYhe|KNEdN|wGY9Y@S92aZd( z6MBekzT#`bh*adiE1eJ8VCL2tfvT!{8!=?u|F|-9gRBLPeIbpFqerkN@c7`Z{5{I8 zDGyHJ@SeuQTJxkkeQCAq^xzK|hqQ_DU-$M{G?9=JSV#f#u;O7WCCl&jgKKWuEGIPk z4^s}!t5IVIE(NZCzHMU<+&yz#_u0c3)1(e!SL*)3-}~ITdDY69y;ADh&ce7k($PY! zt&)OGC}4T7;9-T54u7h*sar$a%7u3C%wdG8Hosbor>dd2ZyT^__dW_Ic<%O_{`vOF zVp-+dHqKp9*NUG@rXPY%Y^2T>I3TsUf{?YXQGu1ih-L9A+koTis8A3#fVOpDMq#md zD#o$Es{NfxBbEEF0qQNGZv4vRf)n(_6YJwLQ9??Jm5aT*=m*2G){{F!XbWohEilr= zG1E8Q>pJ{r(6xD0L%huw_x*-++Sqp8ZD^LcGj3m8)iI`Iz-#GmcqHrn61}QWWCCBb z+2JGN4d5knG~Q5}#Jodj7p-gUSR0(mJj4QZO3J*<<@fkgS|OPFgCBdJhR&Tknq&AN35dI%^6;rFmi1cxEG6m9xB`pb&7Gq9r10^W7j3#^g+p6qo*xJ z>@UcO5Z0iI5RU{uF3-c+R#s@isv_ZV)d~|b>br|XonYyCy|=w)_ne=lj z>J4w(qcG{7LN+o#eIZ{0Zxvo_B13b=+uY=uf`lD0mF9f)!TiMleX0z5pj3E!eMT|R zalD4zB@)f{0iB5_x!VG;8#|$IxoNcG|Fl|8AknnS8ZZ&%N22-l+!-M_S#bMZc{W>O z>z)6JlR_xIHg! zWMPA_ht5aSdk8!a+akBg$r$0o-gs>Lvfa%L4FvWB+ADtemYahN-g{K0l-<^?Uuorj z2}?hr#QCB!a#v=ttgL|Y8xevH?1=su7;rT#KsK7MbBa=8Ef7f<%?Zk#w)&=6AxbL5 zwo!}Cd~pS8+z%o(>jVu+q?UpgTBa+CY6I+(6uSOv@-^nlxGyEf*n+~n2dd!_fv=03Oc$B-ZyU=r5`Hy*$NVodsaNaz3r!FBqhT;&$DjOcS>gLMOm^&?>o$Y zkc%4cgJ>?c%`fu zz~{bLXX%_=(~hO!`6sZi1_ldVN3bHSI1Eo(vu8GpXm>-*Jw&@3`cME`i$ zKG`}w^AlQ(j}C;=hA3;l3<~&IoXKokeNQoN@Bq%FSUgdVw8{H4_CKSd#uWX|d6&VM z;gK8XcPlQK(({?Sf-!jd6L1urv7=Q{c`??x z@CK-rb$FLJ*EDqB-L$3H+99a}qp87m1DVcNLRkJ~jfVWrSw#`5sl}$v9AS^8G6>=a z^OzVWVe;Uk5Qjf}8ba8&2On%~svBh_3Tiw-{j2YZC1OyAU)h@JP&1#5y)*{cgU+8v zTzBA`dyhR!du?qUw9of#9STn=c4*F<*lJxmM)5>Eq>1gkZA=lvQS$QMVp2D`PpmHeH!cgDj);u>1-Ct1LfLX#Y$f~dbbPg;Ef z^v#xiOi)-?$dty@7q_VJ2Fo@nZf7zP)a?!Y6Kde6y}X0N_~+2EW7+4Hnv+KuJ}@(S zxWevP2Sc~NMoKiwweo0?e_+mdMJ$nlo*+KtVqFlpHtOb961a`n=iBZ<1A1+Z8-B34 zAjyM=gQN@q-bhrs3J{$dvpo%QT~lwAM%IJsV)i}`Ia*-a)=n&w`h0*-_zkIdPg3)!B4jZI5onDBPa(-%f#yc2Df+D8DE6L`bed>53V;Cjqhww=x-% zfh%v8E>HV8-EfUx*sUrth0#u4~EP&^dLi0grohgircLPcnEbKDzs>vd+Ml5Wp zW$BvM7fJpGNQR(!>N(6iBP|#>%J8p^UXH8t7lwJ8%6ucTs0W)G z-8O)T3jJpf@}=DgpoZgoxTw{&OkEBYyS<2v*r` zKqAT}_{m;F`ielQgzp;JJD$}Bv8Cux7x4c7WkgNEe$FB=5*tnsL4Zd+rsQ9NN#eO0 zr3{I2IS$kExzphRsJ4d10R?U%FF4o1ju*$}=l$mv-DooD3m-!BY>-L{yCG)SW5w3O zGN<<1kyesTV9?VkgyQN_7ksg7t~ppfY>&o<^U?p(hXp^*plT*yZXNYF0>Rac!hSes zv}hg<9{P&Pa}~IB)?Y|A{45Nc2eiuP=R&b~c>p=Zqun&M3Tu=MIafVtD}W$RECU+| zQ+TIbCU$vM_kaj5lh?)6>qC<%n_`;7Qcd7?$3r;@mJZ!``rWBQ9u?>>49B6|_>JZ1 zf#`}Y(W+_nwERg5u4{RqXK2BYq?uEs^M{IH`gYL|y^keUo0^`$l6au~^6%d(XQYV2 z?9l+0p?wR0iM2F=8-_1#Pbu@Mvn5si1UQ3X~2Bijrk1XOFcuDlcDRHm4r^zLb zIqA!&H-oSjXn^3^zv~r(ILY=9!-keVUhL+DzHco6S@F$9`2?-bh$qu)-#7DV#DsCP zH>foQPl>ctG$gOJE7Sw>04o`v9yhdUS>WX9)poU()acQK&1i+5JPv zr;2zwN`BgENMlg4j54HbtpKhW6-`kt7FhEKgZBB&sXzNF+Vo1D+&W%G#l{l9)Bu`{ z$^zXgZ=rrIJlk*?1sGL0E$5|WbojE+OAcF9Z2yL=;wh0f^i9O(co|0g(JqGPyd5Ne=K52`lM(Qf9r z8PN3Mpv%IDPTyxzPuVL=7;=SZ){i8`9UrT?0gr4UWpRU>8bq%FBaENa;6rjx3}jnS z0Fpe+aK*?d)Vh<1*;tvQ2UvyGjjH8C>In(Um;td-3s1zXC6&g`IK<$n=b0U-vOP zwH{J_5R$hJE+CD7QOne?*&G~A9O3;E@GdENxu6ve6n6}Ots0kLvno(tnNq7nEYO81 zPJ(2(*JJE{5_0qHrTZs_gd1^B#J}ZhUpSQHt3&tRj&7L1QIE;UI1=9R0C$X_TFaz1 zv4Il^yu^F7t)Z~cGHSzMO#YukC8#|cQc;583|eE$aE-*Fg+`RA1TYY){LrE9&kEOs z-R&MmdMitFgtV{^SRpDwoF1&;#0_(FR1tx^ccq;=Yk18C#p%Q*pivK@K;^(ui& ze5-*@eEhDPj;x4q&vEmI9x$>o??|*Jbf|mn>)m+Z6P^$`c_uGyOaiH(;!hhrU!?{OOStAI4x@GE%^T z;WS|%bDM!Vp7X>6UQ6(?MK2(+8y#9jm)@1zc#-N~KG7Q(0A`AWD{)yrJ1Kp(2{gtU<8aRrsEj#HF`je*wb2_XR*33b!tfQ%LAl1I-aX7d{q((m{ECgF4 z6OZ~fbsu#*mAmX!_sICMJb?B?>C=FmTN~W{cjb1Jv^IF2sRDf+R<`CqT!Zg+1cvy3 z-@JkaCoXC+7}riOTNH9T(7P~2Dg6U zzQy&-lVmEP!Ti##TJC%OV5bPO;$f63uFm8=gy0o#1>RaH6+Q}T;@oeD{m0U_BrS%l z=BY%Ud}b>K{o2?2K!5V>?z@4X?^BosWJUlK+(Cr@x1>FB!{a~5# z{xn6b@luUuG6|BA1qK8??!XT~?zHwz?lBbgmBsl}>)qd?O{*nKo@GOgCDD5sd(GLR z6jr{QV4)AiFBMklC>9H&{P4R~8$^1V?=j46o4df~xx_BJ!P&?eRYsj82y65LA3uu) zQgTt&dXdp-j!g=;eaNjAyv;ef`ebSJ;UW{eI$Qg91K+>;p@KD}Ndf@i(IRrj>5L`~>UZuEvcmIX>C!&? z?7RHyOLZitm?SnKXqL!uE+~y|PA_iiR!Kk*L%U5?(cseWLlKw<;xooTlq0240r{$4 zP7oR`2qJ7y@XIIGo|=-d0mTYCF`iVsu1(SM+Ok#w($<@&d7U8?}zKa`9I6x4nxG?~kPv`fO2m0{ojpLe5K;sF2x zqkrEKkS`ZU_jK_zMSbQck9J1P|3p?M!mj{D#;>9oRedY`i~;d8^ResqQTFhvsm!OJ za^F#`5GJt0tC$va*~UQ!)c2=%XxKzgHtOO*jJH)NhZKZY?o&ChLA$$*Xdcz_8j$TC zh${Q9g17CA;gKtyb;`}F+*mjbjQ^Q^V5he6!mp?6VU zU;vyiyj62$mH0}f>YeU`(S4qo`w5NG&x27x_?wkuSMTUb=lYxG)u8>#D*x6u7`E&E zXaEWjO`Ypy2IVcAcXC$gdl z47Q#Wjow@S_<7NqctSZRI#MNor|@i!-9W$frSa)7b(ic;Z=9rIzC8s!Az`@Ua_gF> zGBhu%-tpA<-NZy7TcH?m9-AO^hje^5b`R~gn=>3KHwW8*XfsXIr$-oeUK_M_z7H#s zHg{i?Q2Q95pmK|Sny(QrRoZa6g(}=?*EEsZ;}X_ZRy!E+XJKGg(pE>&>R7B02>JZ< zSy$~7Tn(wPo+g_)*Q?s1B~L$rRdb;OBagAYySv7HIxaGhi7)B?z85O|bq%0;U}^oP zFo3X6KTa4udwfuAto}$mV}&h105{N~qah5pqiO$ptdrA3WDCXYZ=|!-=7FhzPb97dWG_g zUXQU`npPJ}X4BSuq{yMU>+YpW+{y=fhCYUV*zM@ep6Mbcz0J#x&4GEHUGv95Phf#& zAXepD6If+z0W-~lgOvY@YyIuC(>RfWy8t4S3nh%MTKxW|oSn ziJnNnzw;aanz~r#wDZ|P+iG^!rnbe;r0WxBy)6Ie+~xCDSDDLG7`|PR#OJ>Wg8mc*7o*0MoZw__*A4Odcw);FlN$;W?szW>l7s28 z^*cej`x>zNtkntGb~qEmDT@iTt%kR-76*~8PZxiC$7&FxGFBIGkjruugsg#`vFBcg z{btgdFw7knkmH*s^m!6=Z=Zj2yFKH2p(O=bt4yzsi=!O)8lkWu52SxwP1K{WHL`v# z=bF;_L^6GRc@+%@<7l#TzyK6r1y;7Cm8Dj9mtnLT;fzp zW)w_I6q6`24NMhb*5l!n1K9=wh8hYPkDa2>wlC3(AcK>~0jm$g3)2EnX5L4II={vG zl5anWEV<#y`&_TeX9D6)we=E`FJJc>ydFXG_jGvGRb03dE~56woKaRr9eY&W&sL=; zn_Z2xMhkQg{8fnggMg*NJ7Ok3ur4LZ!7$)uibQk0O}2Q&OUDyZ{%?MNw!erZK7AH2 z;Vi8KRzLb}hB?A?X%Gn9RW&Jf5#~38Qma3I0^eBhW8fDnIEdOPpUrwjv)puejDIuU z9(|}RK6rK8W5xl^X}`Mv!l4U#a__&lg7`0KKX_3$+lPtY7zV(kwi%g@=dTy3RI zzcwssoT>W_5_VRILCywylD}o$ z`SNcKX|sSaw*fsp;l$+5<@n&Cb7J#)Ku?wa)rnE_DsfidA;>M!6YeL6M@eb3W3uMr z&rspcz}goy4=*x!O3x-J0=}^DTW7T58XJ0SXa#JJy^Vgmovuw>k8N32SC?7RbQkU5 zsp*3-+*fy(46d7GBr;Kw!g;;-IM3TssP}G~k$zj^#dEZ71GL+_@zjml@=+HbSs31q zFX0>{9r;y+xif(xQ53VAYR>R()T+6>0ia2cn`I@6VATmD$6+wyDY*~Xr(&*08|G|p zzd`BlYj<8jHMicC_ChAQs1x;MDifUp*+BlUZ)LtehD=f6*uYO#7qDBs`m2^(FxRn_ zqMHv~nz0%(+_Fz%fU?HZ<$+l7zV4~PjZ*xo0b$ay{Ug%jl$6FpGeyniy_4~)t>QIC zeDb->GkhXts3sywaj36Y*kifg3~j?UG6kO$7~`jG_Q-xxsJs@G$H|D!G`ox(f!tUT zv_0fj8hc&(0zP#D)Q}i7?zYLsJEZgb4KGTbuH~Wh(_R@%8oFJ`bAl&Q_=#IW+NnH- zf3LhVy1X8vGH*u*s;H%*)0Y))7bOf3g_RsAf(9;b^|>{ggOj>-!D}uIyGB%1?$HDm z`xvifDq($_*ggWg|4M2m^oDpNec~2{6`HPD?FM5G@MPsR?x02U>Ua3pL*P?>!EV3V zH0I0XTD{Npt^B7+1_oFH|3C6<*BL#}-LcONztYYr!k>I))O5op!kgGWPWzl@`^`)3 znS`XY)eFbP*h!x_R)~NyVSl;8!44H>Xsb@by*HKT;hAf|P_lEdsw1F(qF0 z#52`q@vU!Z$bEU>64_q7v+4%AEr-hN!nFJif zmy0G0Psm;m;%8x;gzBPtUmNZRtXfpAytc#}*RF3E+|6LuJ5rbT0;WjYKw`geP{UjO|M!j~}XY zqN+mOXc51&8XZsF!4Y&DnDRK=xsB_!xh~I9vnoTdC6pTmreHG|I zS%6L1#B~2E3$L+H;Xc8u1(+Ot4(5=v2$ZNv!e%2!&F54H-HlWO`qo;+Qo-^tG;~=a z@bWncJ_eI&*QHBBvW8mWU3#^&f`Z=)n-UiLYYo&IiMU+G7hqypPc~s8xvHm2cxg!p z{$J1n4G4VP##wRy@DoNT%chra-1FnAT>8f}O#;gfkY=*b(-*f|Xj#oHC^K;^yzPIG zWv|vln`>GaQikl6gZd;?s4U(kB;^?W4M)QOLqpfNX((B7F@^5dTVzrVs33yx`JU2( zAfTe*f1Tf>V(cauc17`(WOQsMNsH^r!_ame0LeC!_wj%zI^#Kb4 zo(bA*$ZxU=4&vtb+a42=b}heg4&6KfoSgIYq0-r2^D(rozNQ2BU`qN-MQB*`F0nb$ z{t#to8$e%4z=m=V+lAY;d}qm=DOuuuPLPpem$!|aL+?3$BXuY~Zj`G3-RfV@hHtX0 zs%k-?f~A#!;$=#=B|wcL`YL#9M@Pr>Gvyt4%hmr|@9RYwf>u#-P1Oj_-Pw7!?}G;T zUPN@~l^RAyp-STmX^!j~jBBVDk6hjuSQ7uyw|3a5oLMvr)X!A5UsUMEyPLO8$}=5j zG`bHfZcE`?`3-PF1STo;b-&{Fm!hWno<+aQ@R?Dqo`hrvcEc9jcCCGZB z`So8Kb#B4nrm)8$cSK)_nyLF^OEensznA6-i;Ln=iKfzd)91y#)dvD&|1EP6?en)8 zFv=0j#r1K!lWcw_29p@*0gVB^^iKc25G5Z$CAQpNMA&aQ@HPsF$7s zYho5nj~$g3WY}2$WE|;V>Qe=8qWhI}*ld`1T949%*Ae{h-ODX0T_b?O^x!B>c8B9| z-pWv~0ogx^&QS{%n+taTdzk4VTKyo1ZzVejo?wia<>zV#dbiukLq>AF^M;9t-^-5*hw~Z~K3L)gX`=q#^u)DLWMa`13;Xl|r?gdEoy6 D3yx3| literal 0 HcmV?d00001 diff --git a/src/app/game/page.tsx b/src/app/game/page.tsx index dea73e5..93328fd 100644 --- a/src/app/game/page.tsx +++ b/src/app/game/page.tsx @@ -6,6 +6,7 @@ import SudokuImage from '/public/SudokuLogo.webp' import SchulteImage from '/public/SchulteImage.webp' import MemoryFlip from '/public/MemoryFlip.png' import BulletMath from '/public/BulletMath.png' +import SimonSaysImage from '/public/SimonSays.png' const SelectGame = () => { return ( @@ -46,7 +47,20 @@ const SelectGame = () => { backgroundColor={'#760172'} textColor={'white'} /> - + + ) diff --git a/src/app/game/simon-says/page.module.css b/src/app/game/simon-says/page.module.css new file mode 100644 index 0000000..2ca1b65 --- /dev/null +++ b/src/app/game/simon-says/page.module.css @@ -0,0 +1,713 @@ +/* Simon Says Game Styles */ +.container { + max-width: 1000px; + margin: 0 auto; + padding: 20px; + text-align: center; + background: white; + min-height: 100vh; + color: #333; +} + +.header { + margin-bottom: 30px; +} + +.title { + font-size: 2.8rem; + font-weight: bold; + margin-bottom: 10px; + background: linear-gradient(135deg, #9333EA, #7C3AED); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + text-shadow: none; +} + +.subtitle { + font-size: 1.3rem; + color: #666; + margin-bottom: 20px; +} + +/* Game Setup */ +.gameSetup { + max-width: 700px; + margin: 0 auto 3rem auto; + padding: 2rem; + background: #f8f9fa; + border-radius: 16px; + border: 2px solid #e2e8f0; +} + +.setupTitle { + font-size: 1.8rem; + margin-bottom: 2rem; + color: #333; + text-align: center; +} + +.setupSection { + margin-bottom: 2rem; + text-align: center; +} + +.setupSection h3 { + font-size: 1.2rem; + margin-bottom: 1rem; + color: #333; +} + +.buttonGroup { + display: flex; + justify-content: center; + gap: 15px; + flex-wrap: wrap; +} + +.setupButton { + padding: 15px 25px; + border: 2px solid #e2e8f0; + border-radius: 12px; + background: white; + color: #333; + cursor: pointer; + font-weight: 500; + transition: all 0.3s ease; + font-size: 1rem; + display: flex; + flex-direction: column; + align-items: center; + gap: 5px; + min-width: 120px; +} + +.setupButton:hover { + border-color: #9333EA; + transform: translateY(-2px); +} + +.setupButton.active { + background: linear-gradient(135deg, #9333EA, #7C3AED); + color: white; + border-color: #9333EA; +} + +.modeDescription { + font-size: 0.8rem; + opacity: 0.8; +} + +.playButton { + background: linear-gradient(135deg, #22C55E, #16A34A); + color: white; + border: none; + padding: 18px 40px; + font-size: 1.3rem; + font-weight: bold; + border-radius: 12px; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 4px 15px rgba(34, 197, 94, 0.3); + margin-top: 1.5rem; +} + +.playButton:hover { + background: linear-gradient(135deg, #16A34A, #15803D); + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(34, 197, 94, 0.4); +} + +.soundControl { + text-align: center; +} + +.soundToggle { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + cursor: pointer; + font-size: 1rem; +} + +.soundToggle input[type="checkbox"] { + transform: scale(1.2); + cursor: pointer; +} + +/* Game Stats */ +.gameStats { + display: flex; + justify-content: center; + gap: 30px; + margin-bottom: 20px; + flex-wrap: wrap; +} + +.stat { + background: #f8f9fa; + padding: 15px 25px; + border-radius: 12px; + border: 2px solid #e2e8f0; + color: #2d3748; + min-width: 100px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); +} + +.statLabel { + display: block; + font-size: 0.9rem; + color: #666; + margin-bottom: 5px; +} + +.statValue { + display: block; + font-size: 1.5rem; + font-weight: bold; + color: #9333EA; +} + +/* Status Message */ +.statusMessage { + margin-bottom: 30px; + padding: 15px; + background: linear-gradient(135deg, #f3f4f6, #e5e7eb); + border-radius: 12px; + border-left: 4px solid #9333EA; +} + +.statusMessage p { + font-size: 1.1rem; + font-weight: 500; + margin: 0; + color: #374151; +} + +/* Game Board */ +.gameBoard { + display: grid; + gap: 20px; + margin: 30px auto; + max-width: 600px; + perspective: 1000px; + padding: 20px; +} + +.grid4 { + grid-template-columns: repeat(2, 1fr); + max-width: 400px; +} + +.grid7 { + grid-template-columns: repeat(3, 1fr); + max-width: 500px; +} + +.grid10 { + grid-template-columns: repeat(4, 1fr); + max-width: 600px; +} + +.gameButton { + aspect-ratio: 1; + border: none; + border-radius: 20px; + cursor: pointer; + font-size: 1rem; + font-weight: bold; + color: white; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); + transition: all 0.3s ease; + position: relative; + overflow: hidden; + min-height: 100px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 5px; + transform-style: preserve-3d; +} + +.gameButton:hover:not(:disabled) { + transform: translateY(-5px) scale(1.05); + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3) !important; +} + +.gameButton:active:not(:disabled) { + transform: translateY(-2px) scale(0.98); +} + +.gameButton:disabled { + cursor: not-allowed; + opacity: 0.8; +} + +.gameButton.active { + transform: translateY(-5px) scale(1.1); + filter: brightness(1.3); + animation: pulse 0.6s ease; +} + +@keyframes pulse { + 0%, 100% { transform: translateY(-5px) scale(1.1); } + 50% { transform: translateY(-8px) scale(1.15); } +} + +.buttonLabel { + font-size: 1rem; + font-weight: bold; +} + + + +/* Action Buttons */ +.actionButtons { + margin: 30px 0; +} + +.startButton { + background: linear-gradient(135deg, #22C55E, #16A34A); + color: white; + border: none; + padding: 15px 40px; + font-size: 1.2rem; + font-weight: bold; + border-radius: 12px; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 4px 15px rgba(34, 197, 94, 0.3); +} + +.startButton:hover:not(:disabled) { + background: linear-gradient(135deg, #16A34A, #15803D); + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(34, 197, 94, 0.4); +} + +.startButton:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; +} + +/* Keyboard Info */ +.keyboardInfo { + margin: 20px 0; + padding: 15px; + background: #f8f9fa; + border-radius: 10px; + border: 1px solid #e2e8f0; +} + +.keyboardInfo h4 { + margin: 0 0 8px 0; + color: #333; + font-size: 1rem; +} + +.keyboardInfo p { + margin: 0; + color: #666; + font-size: 0.9rem; +} + +/* Rules and Benefits */ +.rulesContainer { + max-width: 800px; + margin: 3rem auto; + padding: 0 1rem; + font-family: system-ui, -apple-system, sans-serif; + text-align: left; +} + +.sectionTitle { + font-size: 1.6rem; + margin: 2rem 0 1.2rem 0; + color: #111; + font-weight: 600; + letter-spacing: -0.01em; + text-align: center; +} + +.rulesGrid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 1.5rem; + margin-bottom: 2rem; +} + +.ruleCard { + background: white; + padding: 1.5rem; + border-radius: 12px; + border: 2px solid #e2e8f0; + text-align: center; + transition: all 0.3s ease; +} + +.ruleCard:hover { + border-color: #9333EA; + transform: translateY(-2px); +} + +.ruleNumber { + width: 40px; + height: 40px; + background: linear-gradient(135deg, #9333EA, #7C3AED); + color: white; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + font-size: 1.2rem; + margin: 0 auto 1rem auto; +} + +.ruleCard h4 { + margin: 0 0 0.5rem 0; + color: #333; + font-size: 1.1rem; +} + +.ruleCard p { + margin: 0; + color: #666; + line-height: 1.5; +} + + + +.algorithmText { + line-height: 1.7; + color: #333; + background: #f8f9fa; + padding: 1.5rem; + border-radius: 10px; + border-left: 4px solid #9333EA; +} + +.rulesGrid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 1.5rem; + margin-bottom: 2rem; +} + +.ruleCard { + background: white; + padding: 1.5rem; + border-radius: 12px; + border: 2px solid #e2e8f0; + text-align: center; + transition: all 0.3s ease; +} + +.ruleCard:hover { + border-color: #9333EA; + transform: translateY(-2px); +} + +.ruleNumber { + width: 40px; + height: 40px; + background: linear-gradient(135deg, #9333EA, #7C3AED); + color: white; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + font-size: 1.2rem; + margin: 0 auto 1rem auto; +} + +.ruleCard h4 { + margin: 0 0 0.5rem 0; + color: #333; + font-size: 1.1rem; +} + +.ruleCard p { + margin: 0; + color: #666; + line-height: 1.5; +} + +.controlsInfo { + margin: 2rem 0; + background: white; + padding: 1.5rem; + border-radius: 12px; + border: 2px solid #e2e8f0; +} + +.controlsInfo h3 { + margin: 0 0 1rem 0; + color: #333; + text-align: center; +} + +.controlsList { + display: flex; + flex-direction: column; + gap: 0.8rem; +} + +.controlItem { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.8rem; + background: #f8f9fa; + border-radius: 8px; +} + +.controlKey { + font-weight: bold; + color: #9333EA; + background: #f3f0ff; + padding: 4px 8px; + border-radius: 4px; + font-size: 0.9rem; +} + +.rulesGrid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1rem; + margin: 1rem 0; +} + +.ruleCard { + background: linear-gradient(145deg, #f8fafc, #e2e8f0); + border: 1px solid #cbd5e1; + border-radius: 12px; + padding: 1rem; + transition: all 0.3s ease; + display: flex; + align-items: flex-start; + gap: 0.75rem; +} + +.ruleCard:hover { + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1); + border-color: #9333EA; +} + +.ruleNumber { + background: linear-gradient(135deg, #9333EA, #7c3aed); + color: white; + width: 24px; + height: 24px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + font-size: 0.875rem; + flex-shrink: 0; + margin-top: 2px; +} + +.controlsInfo { + display: flex; + flex-wrap: wrap; + gap: 1rem; + margin: 1rem 0; + padding: 1rem; + background: linear-gradient(145deg, #f1f5f9, #e2e8f0); + border-radius: 12px; + border: 1px solid #cbd5e1; +} + +.controlKey { + background: linear-gradient(145deg, #ffffff, #f1f5f9); + border: 1px solid #cbd5e1; + border-radius: 6px; + padding: 0.5rem 0.75rem; + font-family: 'Courier New', monospace; + font-weight: bold; + color: #475569; + font-size: 0.875rem; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + transition: all 0.2s ease; +} + +.controlKey:hover { + background: linear-gradient(145deg, #f1f5f9, #e2e8f0); + transform: translateY(-1px); +} + +/* Responsive Design */ +@media (max-width: 768px) { + .container { + padding: 15px; + } + + .title { + font-size: 2.2rem; + } + + .subtitle { + font-size: 1.1rem; + } + + .gameControls { + gap: 15px; + } + + .buttonGroup { + gap: 8px; + } + + .gameSetup { + padding: 1.5rem; + } + + .setupTitle { + font-size: 1.5rem; + } + + .setupButton { + padding: 12px 20px; + font-size: 0.9rem; + min-width: 100px; + } + + .playButton { + padding: 15px 30px; + font-size: 1.1rem; + } + + .rulesGrid { + grid-template-columns: 1fr; + gap: 1rem; + } + + .ruleCard { + padding: 1.2rem; + } + + .controlsInfo, + .tipsSection { + padding: 1.2rem; + } + + .gameStats { + gap: 15px; + } + + .stat { + padding: 12px 20px; + min-width: 80px; + } + + .gameBoard { + gap: 15px; + padding: 15px; + max-width: 350px; + } + + .grid4 { + max-width: 280px; + } + + .grid7 { + max-width: 320px; + } + + .grid10 { + max-width: 350px; + } + + .gameButton { + min-height: 80px; + font-size: 0.9rem; + } + + .buttonLabel { + font-size: 0.9rem; + } + + + + .startButton { + padding: 12px 30px; + font-size: 1.1rem; + } + + .sectionTitle { + font-size: 1.4rem; + } + + +} + +@media (max-width: 480px) { + .gameBoard { + gap: 10px; + max-width: 300px; + } + + .grid4 { + max-width: 240px; + } + + .grid7 { + max-width: 280px; + } + + .grid10 { + max-width: 300px; + } + + .gameButton { + min-height: 70px; + border-radius: 15px; + } + + .buttonLabel { + font-size: 0.8rem; + } + + + + .rulesContainer { + padding: 0 0.5rem; + } + + .sectionTitle { + font-size: 1.25rem; + } + + .rulesGrid { + grid-template-columns: 1fr; + gap: 0.75rem; + } + + .ruleCard { + padding: 0.75rem; + } + + .ruleNumber { + width: 20px; + height: 20px; + font-size: 0.75rem; + margin-right: 0.5rem; + } + + .controlsInfo { + gap: 0.75rem; + } + + .controlKey { + padding: 0.25rem 0.5rem; + font-size: 0.75rem; + } + +} \ No newline at end of file diff --git a/src/app/game/simon-says/page.tsx b/src/app/game/simon-says/page.tsx new file mode 100644 index 0000000..ff455a1 --- /dev/null +++ b/src/app/game/simon-says/page.tsx @@ -0,0 +1,129 @@ +'use client'; + +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import styles from './page.module.css'; + +type Difficulty = 'normal' | 'strict'; +type ButtonCount = 4 | 7 | 10; + +const SimonSaysLanding = () => { + const [difficulty, setDifficulty] = useState('normal'); + const [buttonCount, setButtonCount] = useState(4); + const router = useRouter(); + + const startGame = () => { + // Navigate to the game page with selected settings + router.push(`/game/simon-says/play?difficulty=${difficulty}&buttons=${buttonCount}`); + }; + + return ( +
+
+

Simon Says

+

Memory sequence game.

+
+ + {/* Game Setup */} +
+

Game Settings

+ +
+

Game Mode

+
+ + +
+
+ +
+

Difficulty Level

+
+ + + +
+
+ + +
+ + {/* Game Rules */} +
+

How to Play

+
+
+
1
+

Watch

+

Computer shows button sequence.

+
+
+
2
+

Repeat

+

Click buttons in same order.

+
+
+
3
+

Level Up

+

Sequence gets longer each round.

+
+
+
4
+

Game Modes

+

Normal: retry on mistake. Strict: game over.

+
+
+ +
+

Controls

+
+
+ Mouse + Click buttons +
+
+ Keyboard + Number keys or WASD +
+
+
+ + +
+
+ ); +}; + +export default SimonSaysLanding; \ No newline at end of file diff --git a/src/app/game/simon-says/play/page.module.css b/src/app/game/simon-says/play/page.module.css new file mode 100644 index 0000000..4b0ea85 --- /dev/null +++ b/src/app/game/simon-says/play/page.module.css @@ -0,0 +1,349 @@ +/* Simon Says Game Play Page Styles */ +.container { + max-width: 1000px; + margin: 0 auto; + padding: 20px; + text-align: center; + background: white; + min-height: 100vh; + color: #333; +} + +.header { + margin-bottom: 30px; + display: flex; + flex-direction: column; + align-items: center; + gap: 15px; +} + +.backButton { + background: linear-gradient(135deg, #6B7280, #4B5563); + color: white; + border: none; + padding: 10px 20px; + font-size: 0.9rem; + font-weight: 500; + border-radius: 8px; + cursor: pointer; + transition: all 0.3s ease; + align-self: flex-start; +} + +.backButton:hover { + background: linear-gradient(135deg, #4B5563, #374151); + transform: translateY(-2px); +} + +.title { + font-size: 2.5rem; + font-weight: bold; + margin: 0; + background: linear-gradient(135deg, #9333EA, #7C3AED); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.gameMode { + font-size: 1.1rem; + color: #666; + font-weight: 500; +} + +/* Game Stats */ +.gameStats { + display: flex; + justify-content: center; + gap: 30px; + margin-bottom: 20px; + flex-wrap: wrap; +} + +.stat { + background: #f8f9fa; + padding: 15px 25px; + border-radius: 12px; + border: 2px solid #e2e8f0; + color: #2d3748; + min-width: 100px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); +} + +.statLabel { + display: block; + font-size: 0.9rem; + color: #666; + margin-bottom: 5px; +} + +.statValue { + display: block; + font-size: 1.5rem; + font-weight: bold; + color: #9333EA; +} + +/* Sound Control */ +.soundControl { + text-align: center; + margin-bottom: 20px; +} + +.soundToggle { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + cursor: pointer; + font-size: 1rem; +} + +.soundToggle input[type="checkbox"] { + transform: scale(1.2); + cursor: pointer; +} + +/* Status Message */ +.statusMessage { + margin-bottom: 30px; + padding: 15px; + background: linear-gradient(135deg, #f3f4f6, #e5e7eb); + border-radius: 12px; + border-left: 4px solid #9333EA; +} + +.statusMessage p { + font-size: 1.1rem; + font-weight: 500; + margin: 0; + color: #374151; +} + +/* Game Board */ +.gameBoard { + display: grid; + gap: 25px; + margin: 30px auto; + max-width: 700px; + perspective: 1000px; + padding: 20px; +} + +.grid4 { + grid-template-columns: repeat(2, 1fr); + max-width: 500px; +} + +.grid7 { + grid-template-columns: repeat(3, 1fr); + max-width: 600px; +} + +.grid10 { + grid-template-columns: repeat(4, 1fr); + max-width: 700px; +} + +.gameButton { + aspect-ratio: 1; + border: none; + border-radius: 20px; + cursor: pointer; + font-size: 1rem; + font-weight: bold; + color: white; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); + transition: all 0.3s ease; + position: relative; + overflow: hidden; + min-height: 120px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 5px; + transform-style: preserve-3d; +} + +.gameButton:hover:not(:disabled) { + transform: translateY(-5px) scale(1.05); + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3) !important; +} + +.gameButton:active:not(:disabled) { + transform: translateY(-2px) scale(0.98); +} + +.gameButton:disabled { + cursor: not-allowed; + opacity: 0.8; +} + +.gameButton.active { + transform: translateY(-5px) scale(1.1); + filter: brightness(1.3); + animation: pulse 0.4s ease; +} + +@keyframes pulse { + 0%, 100% { transform: translateY(-5px) scale(1.1); } + 50% { transform: translateY(-8px) scale(1.15); } +} + +.buttonLabel { + font-size: 1rem; + font-weight: bold; +} + +/* Action Buttons */ +.actionButtons { + margin: 30px 0; +} + +.startButton { + background: linear-gradient(135deg, #22C55E, #16A34A); + color: white; + border: none; + padding: 15px 40px; + font-size: 1.2rem; + font-weight: bold; + border-radius: 12px; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 4px 15px rgba(34, 197, 94, 0.3); +} + +.startButton:hover:not(:disabled) { + background: linear-gradient(135deg, #16A34A, #15803D); + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(34, 197, 94, 0.4); +} + +.startButton:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; +} + +/* Keyboard Info */ +.keyboardInfo { + margin: 20px 0; + padding: 15px; + background: #f8f9fa; + border-radius: 10px; + border: 1px solid #e2e8f0; +} + +.keyboardInfo h4 { + margin: 0 0 8px 0; + color: #333; + font-size: 1rem; +} + +.keyboardInfo p { + margin: 0; + color: #666; + font-size: 0.9rem; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .container { + padding: 15px; + } + + .header { + gap: 10px; + } + + .backButton { + padding: 8px 16px; + font-size: 0.8rem; + } + + .title { + font-size: 2rem; + } + + .gameMode { + font-size: 1rem; + } + + .gameStats { + gap: 15px; + } + + .stat { + padding: 12px 20px; + min-width: 80px; + } + + .gameBoard { + gap: 18px; + padding: 15px; + max-width: 400px; + } + + .grid4 { + max-width: 320px; + } + + .grid7 { + max-width: 380px; + } + + .grid10 { + max-width: 400px; + } + + .gameButton { + min-height: 90px; + font-size: 0.9rem; + } + + .buttonLabel { + font-size: 0.9rem; + } + + .startButton { + padding: 12px 30px; + font-size: 1.1rem; + } +} + +@media (max-width: 480px) { + .gameBoard { + gap: 12px; + max-width: 340px; + } + + .grid4 { + max-width: 280px; + } + + .grid7 { + max-width: 320px; + } + + .grid10 { + max-width: 340px; + } + + .gameButton { + min-height: 80px; + border-radius: 15px; + } + + .buttonLabel { + font-size: 0.8rem; + } + + .header { + margin-bottom: 20px; + } + + .title { + font-size: 1.8rem; + } +} \ No newline at end of file diff --git a/src/app/game/simon-says/play/page.tsx b/src/app/game/simon-says/play/page.tsx new file mode 100644 index 0000000..147ccde --- /dev/null +++ b/src/app/game/simon-says/play/page.tsx @@ -0,0 +1,503 @@ +'use client'; + +import { useState, useEffect, useCallback, useRef, Suspense } from 'react'; +import { useRouter, useSearchParams } from 'next/navigation'; +import styles from './page.module.css'; + +interface GameState { + sequence: number[]; + playerSequence: number[]; + currentRound: number; + isShowingSequence: boolean; + isPlayerTurn: boolean; + gameStatus: 'idle' | 'showing' | 'waiting' | 'success' | 'failure'; + score: number; +} + +type Difficulty = 'normal' | 'strict'; +type ButtonCount = 4 | 7 | 10; + +interface Button { + id: number; + color: string; + tone: number; + label: string; + keyBinding?: string; +} + +const BUTTON_CONFIGS: Record = { + 4: [ + { id: 0, color: '#FF4444', tone: 261.63, label: 'Red', keyBinding: '1' }, + { id: 1, color: '#4444FF', tone: 329.63, label: 'Blue', keyBinding: '2' }, + { id: 2, color: '#44FF44', tone: 392.00, label: 'Green', keyBinding: '3' }, + { id: 3, color: '#FFFF44', tone: 523.25, label: 'Yellow', keyBinding: '4' } + ], + 7: [ + { id: 0, color: '#FF4444', tone: 261.63, label: 'Red', keyBinding: '1' }, + { id: 1, color: '#4444FF', tone: 329.63, label: 'Blue', keyBinding: '2' }, + { id: 2, color: '#44FF44', tone: 392.00, label: 'Green', keyBinding: '3' }, + { id: 3, color: '#FFFF44', tone: 523.25, label: 'Yellow', keyBinding: '4' }, + { id: 4, color: '#FF8800', tone: 349.23, label: 'Orange', keyBinding: '5' }, + { id: 5, color: '#8800FF', tone: 440.00, label: 'Purple', keyBinding: '6' }, + { id: 6, color: '#00FFFF', tone: 493.88, label: 'Cyan', keyBinding: '7' } + ], + 10: [ + { id: 0, color: '#FF4444', tone: 261.63, label: 'Red', keyBinding: '1' }, + { id: 1, color: '#4444FF', tone: 329.63, label: 'Blue', keyBinding: '2' }, + { id: 2, color: '#44FF44', tone: 392.00, label: 'Green', keyBinding: '3' }, + { id: 3, color: '#FFFF44', tone: 523.25, label: 'Yellow', keyBinding: '4' }, + { id: 4, color: '#FF8800', tone: 349.23, label: 'Orange', keyBinding: '5' }, + { id: 5, color: '#8800FF', tone: 440.00, label: 'Purple', keyBinding: '6' }, + { id: 6, color: '#00FFFF', tone: 493.88, label: 'Cyan', keyBinding: '7' }, + { id: 7, color: '#FF00FF', tone: 587.33, label: 'Magenta', keyBinding: '8' }, + { id: 8, color: '#888888', tone: 659.25, label: 'Gray', keyBinding: '9' }, + { id: 9, color: '#8B4513', tone: 698.46, label: 'Brown', keyBinding: '0' } + ] +}; + +const SimonSaysGame = () => { + const searchParams = useSearchParams(); + const router = useRouter(); + + const [gameState, setGameState] = useState({ + sequence: [], + playerSequence: [], + currentRound: 0, + isShowingSequence: false, + isPlayerTurn: false, + gameStatus: 'idle', + score: 0 + }); + + const [difficulty, setDifficulty] = useState('normal'); + const [buttonCount, setButtonCount] = useState(4); + const [bestScore, setBestScore] = useState(null); + const [activeButton, setActiveButton] = useState(null); + const [soundEnabled, setSoundEnabled] = useState(true); + const [mounted, setMounted] = useState(false); + + const audioContextRef = useRef(null); + const timeoutRef = useRef | null>(null); + + const buttons = BUTTON_CONFIGS[buttonCount]; + + // Handle client-side mounting + useEffect(() => { + setMounted(true); + }, []); + + // Get URL parameters on component mount + useEffect(() => { + if (!mounted) return; + + try { + const difficultyParam = searchParams.get('difficulty') as Difficulty; + const buttonsParam = searchParams.get('buttons'); + + if (difficultyParam && ['normal', 'strict'].includes(difficultyParam)) { + setDifficulty(difficultyParam); + } + + if (buttonsParam && ['4', '7', '10'].includes(buttonsParam)) { + setButtonCount(parseInt(buttonsParam) as ButtonCount); + } + } catch (error) { + console.log('Error reading search params:', error); + } + }, [searchParams, mounted]); + + // Initialize audio context + const initAudioContext = useCallback(() => { + if (!audioContextRef.current && soundEnabled && typeof window !== 'undefined') { + const AudioContextClass = window.AudioContext || (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext; + audioContextRef.current = new AudioContextClass(); + } + }, [soundEnabled]); + + // Play tone for button + const playTone = useCallback((frequency: number, duration: number = 500) => { + if (!soundEnabled || !audioContextRef.current) return; + + try { + const oscillator = audioContextRef.current.createOscillator(); + const gainNode = audioContextRef.current.createGain(); + + oscillator.frequency.setValueAtTime(frequency, audioContextRef.current.currentTime); + oscillator.type = 'sine'; + + gainNode.gain.setValueAtTime(0.3, audioContextRef.current.currentTime); + gainNode.gain.exponentialRampToValueAtTime(0.01, audioContextRef.current.currentTime + duration / 1000); + + oscillator.connect(gainNode); + gainNode.connect(audioContextRef.current.destination); + + oscillator.start(); + oscillator.stop(audioContextRef.current.currentTime + duration / 1000); + } catch (error) { + console.warn('Audio playback failed:', error); + } + }, [soundEnabled]); + + // Load best score from localStorage + useEffect(() => { + if (!mounted || typeof window === 'undefined') return; + + try { + const savedScore = localStorage.getItem(`simonSays-${buttonCount}-${difficulty}`); + if (savedScore) { + setBestScore(parseInt(savedScore)); + } else { + setBestScore(null); + } + } catch (error) { + console.warn('Unable to load best score:', error); + } + }, [buttonCount, difficulty, mounted]); + + // Save best score to localStorage + const saveBestScore = useCallback((score: number) => { + if (!mounted || typeof window === 'undefined') return; + + try { + if (!bestScore || score > bestScore) { + setBestScore(score); + localStorage.setItem(`simonSays-${buttonCount}-${difficulty}`, score.toString()); + } + } catch (error) { + console.warn('Unable to save best score:', error); + } + }, [bestScore, buttonCount, difficulty, mounted]); + + // Generate next sequence step + const generateNextStep = useCallback(() => { + const nextStep = Math.floor(Math.random() * buttonCount); + return nextStep; + }, [buttonCount]); + + // Start new game + const startNewGame = useCallback(() => { + const firstStep = generateNextStep(); + setGameState({ + sequence: [firstStep], + playerSequence: [], + currentRound: 1, + isShowingSequence: true, + isPlayerTurn: false, + gameStatus: 'showing', + score: 0 + }); + initAudioContext(); + }, [generateNextStep, initAudioContext]); + + // Show sequence to player + const showSequence = useCallback(async () => { + if (gameState.gameStatus !== 'showing') return; + + setActiveButton(null); + + for (let i = 0; i < gameState.sequence.length; i++) { + await new Promise(resolve => { + timeoutRef.current = setTimeout(() => { + const buttonId = gameState.sequence[i]; + const button = buttons[buttonId]; + + setActiveButton(buttonId); + playTone(button.tone, 400); + + setTimeout(() => { + setActiveButton(null); + resolve(void 0); + }, 400); + }, i * 500 + 800); + }); + } + + // Start player turn + setTimeout(() => { + setGameState((prev: GameState) => ({ + ...prev, + isShowingSequence: false, + isPlayerTurn: true, + gameStatus: 'waiting', + playerSequence: [] + })); + }, 500); + }, [gameState.sequence, gameState.gameStatus, buttons, playTone]); + + // Handle player button click + const handleButtonClick = useCallback((buttonId: number) => { + if (!gameState.isPlayerTurn || gameState.gameStatus !== 'waiting') return; + + const button = buttons[buttonId]; + playTone(button.tone, 300); + setActiveButton(buttonId); + setTimeout(() => setActiveButton(null), 300); + + const newPlayerSequence = [...gameState.playerSequence, buttonId]; + const currentIndex = gameState.playerSequence.length; + const expectedButton = gameState.sequence[currentIndex]; + + if (buttonId !== expectedButton) { + // Wrong button pressed + if (difficulty === 'strict') { + // Game over in strict mode + setGameState((prev: GameState) => ({ + ...prev, + gameStatus: 'failure', + isPlayerTurn: false + })); + saveBestScore(gameState.currentRound - 1); + } else { + // Retry current round in normal mode + setGameState((prev: GameState) => ({ + ...prev, + playerSequence: [], + isShowingSequence: true, + isPlayerTurn: false, + gameStatus: 'showing' + })); + } + return; + } + + // Correct button pressed + if (newPlayerSequence.length === gameState.sequence.length) { + // Round completed successfully + const newRound = gameState.currentRound + 1; + const newScore = gameState.currentRound; + const nextStep = generateNextStep(); + + setGameState((prev: GameState) => ({ + ...prev, + sequence: [...prev.sequence, nextStep], + playerSequence: [], + currentRound: newRound, + score: newScore, + isShowingSequence: true, + isPlayerTurn: false, + gameStatus: 'showing' + })); + } else { + // Continue with current sequence + setGameState((prev: GameState) => ({ + ...prev, + playerSequence: newPlayerSequence + })); + } + }, [gameState, buttons, playTone, difficulty, generateNextStep, saveBestScore]); + + // Keyboard controls + useEffect(() => { + if (!mounted || typeof window === 'undefined') return; + + const handleKeyPress = (event: KeyboardEvent) => { + if (!gameState.isPlayerTurn) return; + + const key = event.key.toLowerCase(); + let buttonId = -1; + + // Number key bindings + if (key >= '1' && key <= '9') { + buttonId = parseInt(key) - 1; + } else if (key === '0') { + buttonId = 9; + } + // Arrow key and WASD bindings for 4-button mode + else if (buttonCount === 4) { + switch (key) { + case 'arrowup': + case 'w': + buttonId = 0; + break; + case 'arrowright': + case 'd': + buttonId = 1; + break; + case 'arrowdown': + case 's': + buttonId = 2; + break; + case 'arrowleft': + case 'a': + buttonId = 3; + break; + } + } + + if (buttonId >= 0 && buttonId < buttonCount) { + handleButtonClick(buttonId); + } + }; + + window.addEventListener('keydown', handleKeyPress); + return () => window.removeEventListener('keydown', handleKeyPress); + }, [gameState.isPlayerTurn, buttonCount, handleButtonClick, mounted]); + + // Show sequence when status changes to 'showing' + useEffect(() => { + if (gameState.gameStatus === 'showing') { + showSequence(); + } + }, [gameState.gameStatus, showSequence]); + + // Cleanup timeouts + useEffect(() => { + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + }; + }, []); + + const getStatusMessage = () => { + switch (gameState.gameStatus) { + case 'idle': + return 'Press Start to begin.'; + case 'showing': + return `Round ${gameState.currentRound} - Watch`; + case 'waiting': + return `Your turn (${gameState.playerSequence.length + 1}/${gameState.sequence.length})`; + case 'failure': + return difficulty === 'strict' + ? `Game Over. Score: ${gameState.currentRound - 1}` + : 'Wrong. Watch again.'; + default: + return ''; + } + }; + + const goBack = () => { + router.push('/game/simon-says'); + }; + + // Show loading state until component is mounted + if (!mounted) { + return ( +
+
+

Simon Says

+
+
+

Loading game...

+
+
+ ); + } + + return ( +
+ {/* Header with Back Button */} +
+ +

Simon Says

+
+ {difficulty === 'strict' ? 'Strict Mode' : 'Normal Mode'} • {buttonCount} Buttons +
+
+ + {/* Game Stats */} +
+
+ Round: + {gameState.currentRound} +
+
+ Score: + {gameState.score} +
+ {bestScore !== null && ( +
+ Best: + {bestScore} +
+ )} +
+ + {/* Sound Control */} +
+ +
+ + {/* Status Message */} +
+

{getStatusMessage()}

+
+ + {/* Game Board */} +
+ {buttons.map((button) => ( + + ))} +
+ + {/* Control Buttons */} +
+ +
+ + {/* Keyboard Controls Info */} +
+

Controls

+

+ {buttonCount === 4 + ? 'Arrow Keys, WASD, or 1-4' + : `Number Keys 1-${buttonCount}` + } +

+
+
+ ); +}; + +const SimonSaysGamePage = () => { + return ( + +

Simon Says

+

Loading game...

+ + }> + +
+ ); +}; + +export default SimonSaysGamePage; \ No newline at end of file From 9bb00b17d83ab23bc066ba10c8fb7a3fc1d5d543 Mon Sep 17 00:00:00 2001 From: Sainava Date: Fri, 3 Oct 2025 21:20:32 +0530 Subject: [PATCH 2/6] Fix mobile alignment issues for rules cards - Improve centering of rules grid on mobile screens - Remove duplicate CSS rules in media queries - Enhance responsive layout for better card alignment - Ensure proper width and spacing on small screens --- src/app/game/simon-says/page.module.css | 77 ++++++++++++++++++-- src/app/game/simon-says/play/page.module.css | 39 +++++++--- src/app/game/simon-says/play/page.tsx | 68 +++++++++++------ 3 files changed, 145 insertions(+), 39 deletions(-) diff --git a/src/app/game/simon-says/page.module.css b/src/app/game/simon-says/page.module.css index 2ca1b65..0df6441 100644 --- a/src/app/game/simon-says/page.module.css +++ b/src/app/game/simon-says/page.module.css @@ -477,9 +477,14 @@ .rulesGrid { display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem; - margin: 1rem 0; + margin: 1rem auto; + align-items: stretch; + justify-items: center; + width: 100%; + max-width: 1000px; + padding: 0; } .ruleCard { @@ -491,6 +496,7 @@ display: flex; align-items: flex-start; gap: 0.75rem; + height: 100%; } .ruleCard:hover { @@ -584,9 +590,40 @@ font-size: 1.1rem; } + .rulesContainer { + max-width: 500px; + margin: 2rem auto; + padding: 0 1rem; + } + .rulesGrid { + display: grid; grid-template-columns: 1fr; gap: 1rem; + justify-items: center; + align-items: stretch; + width: 100%; + } + + .ruleCard { + width: 100%; + max-width: 100%; + padding: 1rem; + margin: 0; + box-sizing: border-box; + text-align: left; + } + + .controlsInfo { + display: flex; + flex-direction: column; + gap: 0.75rem; + align-items: center; + width: 100%; + max-width: 100%; + margin: 1rem 0; + padding: 1rem; + box-sizing: border-box; } .ruleCard { @@ -677,37 +714,63 @@ + .container { + padding: 10px; + } + .rulesContainer { - padding: 0 0.5rem; + padding: 0 0.25rem; } .sectionTitle { - font-size: 1.25rem; + font-size: 1.1rem; + margin-bottom: 0.75rem; } .rulesGrid { grid-template-columns: 1fr; gap: 0.75rem; + justify-items: center; + max-width: 100%; + margin: 0 auto; } .ruleCard { padding: 0.75rem; + gap: 0.5rem; + width: 100%; + max-width: 320px; + margin: 0 auto; } .ruleNumber { width: 20px; height: 20px; font-size: 0.75rem; - margin-right: 0.5rem; + margin-top: 0; + flex-shrink: 0; } .controlsInfo { - gap: 0.75rem; + gap: 0.5rem; + padding: 0.75rem; + flex-direction: column; + align-items: center; + max-width: 320px; + margin: 1rem auto; } .controlKey { - padding: 0.25rem 0.5rem; + padding: 0.4rem 0.6rem; font-size: 0.75rem; } + + .gameSetup { + padding: 1rem 0.75rem; + } + + .setupSection { + margin-bottom: 1rem; + } } \ No newline at end of file diff --git a/src/app/game/simon-says/play/page.module.css b/src/app/game/simon-says/play/page.module.css index 4b0ea85..286ccfb 100644 --- a/src/app/game/simon-says/play/page.module.css +++ b/src/app/game/simon-says/play/page.module.css @@ -153,7 +153,7 @@ font-size: 1rem; font-weight: bold; color: white; - text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); + text-shadow: 0 2px 8px rgba(0, 0, 0, 0.8), 0 1px 3px rgba(0, 0, 0, 0.9); transition: all 0.3s ease; position: relative; overflow: hidden; @@ -313,37 +313,56 @@ } @media (max-width: 480px) { + .container { + padding: 10px; + } + .gameBoard { - gap: 12px; - max-width: 340px; + gap: 8px; + max-width: 320px; + padding: 10px; } .grid4 { - max-width: 280px; + max-width: 260px; } .grid7 { - max-width: 320px; + max-width: 300px; } .grid10 { - max-width: 340px; + max-width: 320px; } .gameButton { - min-height: 80px; + min-height: 70px; border-radius: 15px; + font-size: 0.85rem; } .buttonLabel { - font-size: 0.8rem; + font-size: 0.75rem; + font-weight: 900; + text-shadow: 0 2px 6px rgba(0, 0, 0, 0.9), 0 1px 2px rgba(0, 0, 0, 1); } .header { - margin-bottom: 20px; + margin-bottom: 15px; + gap: 8px; } .title { - font-size: 1.8rem; + font-size: 1.6rem; + } + + .gameStats { + gap: 10px; + margin-bottom: 15px; + } + + .stat { + padding: 8px 12px; + min-width: 70px; } } \ No newline at end of file diff --git a/src/app/game/simon-says/play/page.tsx b/src/app/game/simon-says/play/page.tsx index 147ccde..c95212b 100644 --- a/src/app/game/simon-says/play/page.tsx +++ b/src/app/game/simon-says/play/page.tsx @@ -2,6 +2,7 @@ import { useState, useEffect, useCallback, useRef, Suspense } from 'react'; import { useRouter, useSearchParams } from 'next/navigation'; +import dynamic from 'next/dynamic'; import styles from './page.module.css'; interface GameState { @@ -27,35 +28,40 @@ interface Button { const BUTTON_CONFIGS: Record = { 4: [ - { id: 0, color: '#FF4444', tone: 261.63, label: 'Red', keyBinding: '1' }, - { id: 1, color: '#4444FF', tone: 329.63, label: 'Blue', keyBinding: '2' }, - { id: 2, color: '#44FF44', tone: 392.00, label: 'Green', keyBinding: '3' }, - { id: 3, color: '#FFFF44', tone: 523.25, label: 'Yellow', keyBinding: '4' } + { id: 0, color: '#DC2626', tone: 261.63, label: 'Red', keyBinding: '1' }, + { id: 1, color: '#2563EB', tone: 329.63, label: 'Blue', keyBinding: '2' }, + { id: 2, color: '#16A34A', tone: 392.00, label: 'Green', keyBinding: '3' }, + { id: 3, color: '#CA8A04', tone: 523.25, label: 'Yellow', keyBinding: '4' } ], 7: [ - { id: 0, color: '#FF4444', tone: 261.63, label: 'Red', keyBinding: '1' }, - { id: 1, color: '#4444FF', tone: 329.63, label: 'Blue', keyBinding: '2' }, - { id: 2, color: '#44FF44', tone: 392.00, label: 'Green', keyBinding: '3' }, - { id: 3, color: '#FFFF44', tone: 523.25, label: 'Yellow', keyBinding: '4' }, - { id: 4, color: '#FF8800', tone: 349.23, label: 'Orange', keyBinding: '5' }, - { id: 5, color: '#8800FF', tone: 440.00, label: 'Purple', keyBinding: '6' }, - { id: 6, color: '#00FFFF', tone: 493.88, label: 'Cyan', keyBinding: '7' } + { id: 0, color: '#DC2626', tone: 261.63, label: 'Red', keyBinding: '1' }, + { id: 1, color: '#2563EB', tone: 329.63, label: 'Blue', keyBinding: '2' }, + { id: 2, color: '#16A34A', tone: 392.00, label: 'Green', keyBinding: '3' }, + { id: 3, color: '#CA8A04', tone: 523.25, label: 'Yellow', keyBinding: '4' }, + { id: 4, color: '#EA580C', tone: 349.23, label: 'Orange', keyBinding: '5' }, + { id: 5, color: '#7C3AED', tone: 440.00, label: 'Purple', keyBinding: '6' }, + { id: 6, color: '#0891B2', tone: 493.88, label: 'Cyan', keyBinding: '7' } ], 10: [ - { id: 0, color: '#FF4444', tone: 261.63, label: 'Red', keyBinding: '1' }, - { id: 1, color: '#4444FF', tone: 329.63, label: 'Blue', keyBinding: '2' }, - { id: 2, color: '#44FF44', tone: 392.00, label: 'Green', keyBinding: '3' }, - { id: 3, color: '#FFFF44', tone: 523.25, label: 'Yellow', keyBinding: '4' }, - { id: 4, color: '#FF8800', tone: 349.23, label: 'Orange', keyBinding: '5' }, - { id: 5, color: '#8800FF', tone: 440.00, label: 'Purple', keyBinding: '6' }, - { id: 6, color: '#00FFFF', tone: 493.88, label: 'Cyan', keyBinding: '7' }, - { id: 7, color: '#FF00FF', tone: 587.33, label: 'Magenta', keyBinding: '8' }, - { id: 8, color: '#888888', tone: 659.25, label: 'Gray', keyBinding: '9' }, - { id: 9, color: '#8B4513', tone: 698.46, label: 'Brown', keyBinding: '0' } + { id: 0, color: '#DC2626', tone: 261.63, label: 'Red', keyBinding: '1' }, + { id: 1, color: '#2563EB', tone: 329.63, label: 'Blue', keyBinding: '2' }, + { id: 2, color: '#16A34A', tone: 392.00, label: 'Green', keyBinding: '3' }, + { id: 3, color: '#CA8A04', tone: 523.25, label: 'Yellow', keyBinding: '4' }, + { id: 4, color: '#EA580C', tone: 349.23, label: 'Orange', keyBinding: '5' }, + { id: 5, color: '#7C3AED', tone: 440.00, label: 'Purple', keyBinding: '6' }, + { id: 6, color: '#0891B2', tone: 493.88, label: 'Cyan', keyBinding: '7' }, + { id: 7, color: '#BE185D', tone: 587.33, label: 'Magenta', keyBinding: '8' }, + { id: 8, color: '#4B5563', tone: 659.25, label: 'Gray', keyBinding: '9' }, + { id: 9, color: '#92400E', tone: 698.46, label: 'Brown', keyBinding: '0' } ] }; const SimonSaysGame = () => { + // Early return for SSR + if (typeof window === 'undefined') { + return null; + } + const searchParams = useSearchParams(); const router = useRouter(); @@ -480,6 +486,24 @@ const SimonSaysGame = () => { ); }; +// Create a dynamic component to prevent SSR issues +const DynamicSimonSaysGame = dynamic(() => Promise.resolve(SimonSaysGame), { + ssr: false, + loading: () => ( +
+

Simon Says

+

Loading game...

+
+ ) +}); + const SimonSaysGamePage = () => { return ( {

Loading game...

}> - +
); }; From a32893268b07d89b90881346941d43468cd25f86 Mon Sep 17 00:00:00 2001 From: Sainava Date: Fri, 3 Oct 2025 21:27:51 +0530 Subject: [PATCH 3/6] Fix mobile alignment issue: Center rules container on mobile - Add text-align: center to rulesContainer on mobile screens - Keep individual rule card text left-aligned within centered cards - This fixes the alignment issue where cards appeared left-aligned on small screens --- src/app/game/simon-says/page.module.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/app/game/simon-says/page.module.css b/src/app/game/simon-says/page.module.css index 0df6441..cf00cc4 100644 --- a/src/app/game/simon-says/page.module.css +++ b/src/app/game/simon-says/page.module.css @@ -594,6 +594,7 @@ max-width: 500px; margin: 2rem auto; padding: 0 1rem; + text-align: center; } .rulesGrid { @@ -608,6 +609,7 @@ .ruleCard { width: 100%; max-width: 100%; + text-align: left; padding: 1rem; margin: 0; box-sizing: border-box; @@ -720,6 +722,7 @@ .rulesContainer { padding: 0 0.25rem; + text-align: center; } .sectionTitle { @@ -741,6 +744,7 @@ width: 100%; max-width: 320px; margin: 0 auto; + text-align: left; } .ruleNumber { From ffd9e7d8a706ca12931b17ccc463d52b93761c1f Mon Sep 17 00:00:00 2001 From: Sainava Date: Fri, 3 Oct 2025 21:32:23 +0530 Subject: [PATCH 4/6] refactor(simon-says): unify rules layout & fix mobile alignment - Replace grid with flex column for deterministic alignment - Introduce .ruleText wrapper for consistent spacing - Remove duplicate .rulesGrid definitions & conflicts - Add single-source responsive overrides (<=480px) - Ensure left text alignment inside centered container --- src/app/game/simon-says/page.module.css | 63 ++++++++++++++++++++++++- src/app/game/simon-says/page.tsx | 24 ++++++---- 2 files changed, 78 insertions(+), 9 deletions(-) diff --git a/src/app/game/simon-says/page.module.css b/src/app/game/simon-says/page.module.css index cf00cc4..d367197 100644 --- a/src/app/game/simon-says/page.module.css +++ b/src/app/game/simon-says/page.module.css @@ -777,4 +777,65 @@ margin-bottom: 1rem; } -} \ No newline at end of file +} + + /* ==== Alignment Overrides (Single Source of Truth) ==== */ + .rulesGrid { + display: flex; + flex-direction: column; + gap: 1rem; + width: 100%; + max-width: 760px; + margin: 0 auto 2rem auto; + padding: 0 0.5rem; + } + + .ruleCard { + display: flex; + align-items: center; + gap: 1.25rem; + width: 100%; + max-width: 100%; + box-sizing: border-box; + } + + .ruleNumber { + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + } + + .ruleText { + display: flex; + flex-direction: column; + gap: 0.25rem; + text-align: left; + flex: 1; + min-width: 0; + } + + .ruleText h4 { + margin: 0; + font-size: 1.15rem; + } + + .ruleText p { + margin: 0; + font-size: 0.95rem; + line-height: 1.3; + } + + @media (max-width: 480px) { + .rulesGrid { + gap: 0.85rem; + padding: 0 0.25rem; + } + .ruleCard { + padding: 0.85rem 0.9rem; + border-radius: 14px; + } + .ruleText h4 { font-size: 1.05rem; } + .ruleText p { font-size: 0.85rem; } + .ruleNumber { width: 32px; height: 32px; font-size: 0.85rem; } + } \ No newline at end of file diff --git a/src/app/game/simon-says/page.tsx b/src/app/game/simon-says/page.tsx index ff455a1..24c3f15 100644 --- a/src/app/game/simon-says/page.tsx +++ b/src/app/game/simon-says/page.tsx @@ -86,23 +86,31 @@ const SimonSaysLanding = () => {
1
-

Watch

-

Computer shows button sequence.

+
+

Watch

+

Computer shows button sequence.

+
2
-

Repeat

-

Click buttons in same order.

+
+

Repeat

+

Click buttons in same order.

+
3
-

Level Up

-

Sequence gets longer each round.

+
+

Level Up

+

Sequence gets longer each round.

+
4
-

Game Modes

-

Normal: retry on mistake. Strict: game over.

+
+

Game Modes

+

Normal: retry on mistake. Strict: game over.

+
From 552a5e168eeb6b2ccccfe13143f76818bb6b3348 Mon Sep 17 00:00:00 2001 From: Sainava Date: Fri, 3 Oct 2025 21:49:09 +0530 Subject: [PATCH 5/6] fix(simon-says): clear all queued timeouts & persist best score per round - Introduce timeoutsRef array + clearScheduledTimeouts helper - Clear scheduled timeouts on new game, new sequence, unmount - Track both highlight and reset timeouts plus player turn transition - Save best score immediately after each successful round - Prevent potential state updates after unmount --- src/app/game/simon-says/play/page.tsx | 51 +++++++++++++++++---------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/src/app/game/simon-says/play/page.tsx b/src/app/game/simon-says/play/page.tsx index c95212b..1de530c 100644 --- a/src/app/game/simon-says/play/page.tsx +++ b/src/app/game/simon-says/play/page.tsx @@ -83,7 +83,15 @@ const SimonSaysGame = () => { const [mounted, setMounted] = useState(false); const audioContextRef = useRef(null); - const timeoutRef = useRef | null>(null); + // Track ALL scheduled timeouts so we can clear them on unmount / restart + const timeoutsRef = useRef>>([]); + + const clearScheduledTimeouts = useCallback(() => { + if (timeoutsRef.current.length) { + timeoutsRef.current.forEach(id => clearTimeout(id)); + timeoutsRef.current = []; + } + }, []); const buttons = BUTTON_CONFIGS[buttonCount]; @@ -182,6 +190,7 @@ const SimonSaysGame = () => { // Start new game const startNewGame = useCallback(() => { + clearScheduledTimeouts(); const firstStep = generateNextStep(); setGameState({ sequence: [firstStep], @@ -193,33 +202,38 @@ const SimonSaysGame = () => { score: 0 }); initAudioContext(); - }, [generateNextStep, initAudioContext]); + }, [generateNextStep, initAudioContext, clearScheduledTimeouts]); // Show sequence to player const showSequence = useCallback(async () => { if (gameState.gameStatus !== 'showing') return; + // Clear any previously scheduled flashes before starting a new batch + clearScheduledTimeouts(); setActiveButton(null); for (let i = 0; i < gameState.sequence.length; i++) { - await new Promise(resolve => { - timeoutRef.current = setTimeout(() => { + // Wrap each step in a promise so we can await sequentially + await new Promise(resolve => { + const delay = i * 500 + 800; + const highlightTimeout = setTimeout(() => { const buttonId = gameState.sequence[i]; const button = buttons[buttonId]; - + setActiveButton(buttonId); playTone(button.tone, 400); - - setTimeout(() => { + + const resetTimeout = setTimeout(() => { setActiveButton(null); - resolve(void 0); + resolve(); }, 400); - }, i * 500 + 800); + timeoutsRef.current.push(resetTimeout); + }, delay); + timeoutsRef.current.push(highlightTimeout); }); } - // Start player turn - setTimeout(() => { + const playerTurnTimeout = setTimeout(() => { setGameState((prev: GameState) => ({ ...prev, isShowingSequence: false, @@ -228,7 +242,8 @@ const SimonSaysGame = () => { playerSequence: [] })); }, 500); - }, [gameState.sequence, gameState.gameStatus, buttons, playTone]); + timeoutsRef.current.push(playerTurnTimeout); + }, [gameState.sequence, gameState.gameStatus, buttons, playTone, clearScheduledTimeouts]); // Handle player button click const handleButtonClick = useCallback((buttonId: number) => { @@ -270,7 +285,7 @@ const SimonSaysGame = () => { if (newPlayerSequence.length === gameState.sequence.length) { // Round completed successfully const newRound = gameState.currentRound + 1; - const newScore = gameState.currentRound; + const newScore = gameState.currentRound; // Score equals rounds completed const nextStep = generateNextStep(); setGameState((prev: GameState) => ({ @@ -283,6 +298,8 @@ const SimonSaysGame = () => { isPlayerTurn: false, gameStatus: 'showing' })); + // Persist potential new best score immediately + saveBestScore(newScore); } else { // Continue with current sequence setGameState((prev: GameState) => ({ @@ -346,14 +363,12 @@ const SimonSaysGame = () => { } }, [gameState.gameStatus, showSequence]); - // Cleanup timeouts + // Cleanup all scheduled timeouts on unmount useEffect(() => { return () => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - } + clearScheduledTimeouts(); }; - }, []); + }, [clearScheduledTimeouts]); const getStatusMessage = () => { switch (gameState.gameStatus) { From 866ec69267bef828df72059636d7ea6d17e00f3f Mon Sep 17 00:00:00 2001 From: Sainava Date: Fri, 3 Oct 2025 21:58:46 +0530 Subject: [PATCH 6/6] fix(simon-says): keep hook order stable & track release timeout - Remove early SSR return so hooks always execute in consistent order - Rely on mounted flag for gating UI instead of skipping hooks - Track 300ms activeButton release timeout in timeoutsRef for full cleanup --- src/app/game/simon-says/play/page.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/app/game/simon-says/play/page.tsx b/src/app/game/simon-says/play/page.tsx index 1de530c..cea5f16 100644 --- a/src/app/game/simon-says/play/page.tsx +++ b/src/app/game/simon-says/play/page.tsx @@ -57,10 +57,6 @@ const BUTTON_CONFIGS: Record = { }; const SimonSaysGame = () => { - // Early return for SSR - if (typeof window === 'undefined') { - return null; - } const searchParams = useSearchParams(); const router = useRouter(); @@ -252,7 +248,10 @@ const SimonSaysGame = () => { const button = buttons[buttonId]; playTone(button.tone, 300); setActiveButton(buttonId); - setTimeout(() => setActiveButton(null), 300); + const releaseTimeout = setTimeout(() => { + setActiveButton(null); + }, 300); + timeoutsRef.current.push(releaseTimeout); const newPlayerSequence = [...gameState.playerSequence, buttonId]; const currentIndex = gameState.playerSequence.length;