From bf47c50dfa3ace922b848f9d8866b58ea5a98cda Mon Sep 17 00:00:00 2001 From: Brian Fertig Date: Thu, 11 Jun 2026 10:03:22 -0600 Subject: [PATCH] Added Block Fighter game --- public/assets/images/game-icons.png | Bin 210572 -> 214472 bytes public/assets/images/game-icons.psd | Bin 540598 -> 574441 bytes public/data/blockfighter.json | 44 + .../src/games/blockfighter/BlockFighterAI.js | 185 ++++ .../games/blockfighter/BlockFighterGame.js | 818 ++++++++++++++++++ .../games/blockfighter/BlockFighterLogic.js | 634 ++++++++++++++ public/src/games/blockfighter/tutorial.md | 51 ++ public/src/main.js | 2 + public/src/scenes/GameRoomScene.js | 2 +- public/src/scenes/PreloadScene.js | 1 + server/games/registry.js | 1 + server/scripts/verifyBlockFighter.js | 334 +++++++ 12 files changed, 2071 insertions(+), 1 deletion(-) create mode 100644 public/data/blockfighter.json create mode 100644 public/src/games/blockfighter/BlockFighterAI.js create mode 100644 public/src/games/blockfighter/BlockFighterGame.js create mode 100644 public/src/games/blockfighter/BlockFighterLogic.js create mode 100644 public/src/games/blockfighter/tutorial.md create mode 100644 server/scripts/verifyBlockFighter.js diff --git a/public/assets/images/game-icons.png b/public/assets/images/game-icons.png index e288b62574790bbdb276033d3859ac6169ce7c5a..bb56cf1c603f3db8e6a0499b9f30fb636edbcb93 100644 GIT binary patch delta 54194 zcmV(=K-s^H?F`6^4Uk0wMQwpcg+~FkM*@s0b9Q4q0UK?MXssx(2GgceAE^j>B%z0cfUPyg3Gcd*cYf(Zy_@mwY| zx14?V*?X;bt-aQ}?A#ENnuC>-VF*62+sD{8s}-%RFhCSNF2Lkv0>hD7w z#D$UGp+ZZkq6srveMJwiX@)*za*kdM$ZB3gyXE-z;tJX4=56fKq~1w51QTEulU%{} z!Egiu4jLZ~`>jaK^Vr-OzLnXsKJwW5q0a4nqvv_Oo+X#xx8mW>miW{M7Pr4*eYD<6I(V_jy;JGjbHZTGc(>LXjPPzZmsp zN8C0$^d`g1vtlauc%1GXfkmF2+QeCBV(7BVk$ZMIVyB%3AsC`}nQkz&@G7yNd+vEa zZES3q>GcIdk{T=yPh^J1CsS8^fBkpe32DLM=Q?5zN~F%^C64wFqDG!X{J1$z_Q)@D z5C8IJObHlJmQPqhtck*5G+>;U;M-cm(y6sNG(=)-dmy7q&ckImkkJ<$?=LM zs_Kip(QaH;lI7jP(z1F@x4n7|$h%+3ie;WW>c9od8dd(l;#lcozA*7#e{M{bD-E0J z9_$Tm#WFOsCNb)W7+*GmVQm7nTnqu$V8mi4Hx-oX;uuVYA&zCiB`1*2P7KGQHQ$-j zI_nR=e*A`=in9H)jBc$=B=gGI*-B~O1NZkXZ0JDe=!W29b^N}aZMDI=Xi%M@D4s>x zY<5XqxYew&T0&aA@kFJZe~&JIcH6Cgdg$rDb8LIPUanMn<@UR~LO%J4&-#|FA6VHi zSD76Q#=zMIIf52;3~$0<&lb=)-40uhr9<%e+!H$%Q7>g^$dI^ zF1)`i{o93|%?;nq?HGCAaFP31#nU`@VtkhGz{*A;8lFTXW@B*kW^8*(LwxU$J>j zdMcH!qP?A$e|gW<-}=&{*WP&Dw}K6`zLOd4mq%A`M3I1>!IZbCxU@YCNSS&C4lh=7 z$LK$U0ANWoAQ8Co%TsQ?qwLygo(!?&Mlg!H300L`$`zJpeBM|kXSS~RRM(3!Lz8Dxw8XqTxv+q;O)*mxDm1!}^ zDc~d@lxV$Mc~_k51uv0NC}+WY5>TTJuuB72*d9S?#6qQ5aSx%Inxd#ANrAfUQQ_u_ zdI(TDmIkL-e}qAvXb*fCN;!xGL>7T`kKTvRX3p&)xgjrYL69NKFhPIW~j>0mgD! zBco!YOhA%#0n@b?*DdGJ-$yLVdd@J+)%5O-vMi6Vf9{Eel9}DOZ8ZPzvdu3uB9~u& zx&5nOew*vw_FJEZG5kUWMU@E)HM^k;L$$z-t{n3ah2H`PQ1i{ouo z5=*9~=V`}eWoy#5OM}hnV$yG!v0P`X5)0Qh!BT@D?lUY_3l*zaF`WBe*v`HGfXVnox`^J&_@65|)H)>3lStVoxjn*HSMDginx*fAN!&~#oUH_gRIZUErw15`WW zaH5;1#=3KPJ7+?wWoc3h;-`MV)hUj{Ky1Zre~+wDd-TehpkNn@W-1mCb=IrIaoe6SrWRNXO4ERu7I4T>e$%P`o_u}- zf3sgpXSTI0d)M3r`&#QKPWGi6FEnN$ocGWGd!B@sbXu(^jKnefp{%QzGDlIM^-UTN zW}u9SXB-2)y;de)ytgJX_Y2E++&BEfj%WX^aV>v(fXxL{gHO&CKKk^xF1gta7@cx_ zP~aA{$d;Hr)-HS5vDbmLc9L}8e)XkGf36yRrO(=(V3Nti)KpaFijDW&a5Z9$9awna zA@By{h}ATsaqeN59NZ3xwSx=>P0z8`Xf0BpX!;Z zssS~xYb<&5|66Rl2T=a2OlQCRJp$(Z)8D!Ap(pI+72o z7xxtAAIcXtsa|y)T80c2DXv_WSFMiLEBexoXmdfGuh`x8_|VhE%<#j-Lh8=#-D{@0 z`}?elVeD3N`*b>EU-X%aetOT{e|H5!wJn!y-<-ziK#>5{7#RQw9v>se)xj9}E!Itl z42!B(4ThmJM8znC1edH;ky+#p(8yzRDMeVml;lnf%!&z}jI(C4gnCxtXSq=foAbC; z+#WI@!EillYHA5gvRqRRvHCD#iG(wm8o!^7b*dmdF*cdnhf%q)V`-D~e-je)K=KA$ z&r#2f6S(xlEM@5064YR=yP6JL+=N39TZsONt*EUJp^zyd8fQ^e0)Tp&#->B^kl74` zVONp_oF!-;FCKe(%I&UV+g!E6mGx&1Mae_;!>Hs5Z>tbxuWK-Q0=4c5pd5A!1{A@@ zlG*nZ*5iGX*czJ^@NiV+sLiju4FMykRy!<=+re@%MCXr?8u2<6E& z?<#S8Snx?s+^2JJ4VT5;lCWs_hg}aHEU1+aA&RdwwMAYQ^J40B=q^ zR7w?Ia@Ax4$0aVWe}YSwijtu)n{&&=Ibr&g*!?`dN;b_1jRIbH;xXuhLl7e&H!qWY zcrlx@bGq{~y?Dey@0Ldf3SZ6XIi~_k)&+aGWaz#fNqe87AssfeBfL*jebDkn!{|$j zt{%Gf`t4}-mC%z>vEcj*NjDUsmySRwM>5OYTdWCD`aq)<;KtxsABBU$Hp|;cT(@=Xx z(nu4?H02`XsT4L140R7@QfGTPdqr2@)?FUdx#iC{|NVa5`|uOiy$|D`^RSof>g(;S z-2az52M;^ue}w((BK$Y{x2-;7Xv=Cjl&C@5jCQm%H;`?LKqgz*($auo*U+C1my^BO}I&XCMFVX=i@u ze-GKOQS6Q9AZV;`h9fVQgXHLL-))l-yb}16Wx=iX6kWns!OexN7?yh^#`w5c6(*yk z;Kk~(e-=A>HZ9&IHa1Mn?SsqwdltomOG_fq-wb%Y61*IvVzE$L6GYe_rT0`e{p=S% zyN``?JgF~s1TQnX?s2mq^K_j6!C4d8u^j~XShBwYQ`5+35l9Wj2r!o6WX6!}-G-Sn z8{Oz>DU(M{YaM~o5;!k`!+{<2T0a3q4-A5{e-nlTe%3rZ{*9jyK%7b7T4zWRo&ZxS zVPKjN7t;^cc|nn@3!H!fydEBf;yaEZ>_1-CuHTZL)7fBJmL=LowN+cCTy}FCCkZc2 zj0~ZW&$>}@P17wytCUQ$k|w~Iby#y5LEd0F!uDiFTh=!;)pyrpYhSy+-y4*fb1oq8 ze}>_~fi)uS4I!5`$e&UKUiOblreF$!mGN-inAgXLXnm$i&)7+_V-bGl z14w-KeA>gNOB(G)F6kN*ql4YUV{U_ue-Xv!2@LHh6~~Gt*dphoDtZ5KUi-yVe@Q)g zMrX5dz`{BN0unjGmh=3k9QW9hJ7A~Buzt87#s1Y8Th5L%Y5xY|PNH#!z0dwLpWm;u zc16pa#B+gwxKiM)vTiuGM-mv2bQF*BAJfV1CF1<<{(A?$e%|>P3bu8elXlKCbH@CF zXi3?afLb*VZ7PO>J`5+l&B^C-f6GV9l`rR!eJ=mDHoyO#JoETN+c-h^cwJM+H#jNq zX`UBP%1n*#m6{rt7`LRt0tn?a@&qg^WMjrhMld-shGH)3D3X*@Jl;(nU-)6k8@zK{ z*NSaBd;L9t@|ETPyCfNz`?r4ZgGb+W)Dhi@NU(i;bYwdL3X2rc>*ncte>;dxZh3lY zGEm1VvR-TAWCrfY9DmlZBZ{(bEZh!x;;g!Ej8TZ zaf1xJK_HLyPBqB0;|aQfe^qyCdG|P6;(hKBGAjV``Dzdf$KZ?jdN*%a{gFHFy35U_ zag+T8Wtxtp+TWLf*!)-zI5`B_A0{MUc8{UuK-@i%GfG+Pb6`6f=k5cnS%MmtfG=g; zXro=yVT^S{%N7utwaC48v0)yxjn81sjcYJi@*z^2a1EJDWx^_zf7p>YeJ%ljYS#`j zpk7r%u~Z?TO*UDPm?0)0{8r!6|2|o_q1)ur=ir;B>#(WRR+Yqk%+sW+{;Wig8cv4?i|-S-p{wKY>M&CS_> zKQPV-e1IoJ#dD6Af7ZAyaZZk4Qc4n)l&l8w!oK_KPIAU-?eWL<@!Q<^JO{%*zUCPz z9^PhqYAY5xbb<6~UC95c-^mj=o>9WE2LreaWOMi@_50zoKU=x&rt7~Yit^3vGipP! zR3xA$RgbV)uCg&Q)R4NF#^3}!tY~rOggl5(PS>=l-bQjBr2PU(i>eCs#A>}jrVxaQmw-gj?63>;q>ojT6! z?>nj#G2@k##_LJ5ubfL>9YgrWtUbI_o$21GLe7yh+P|Gq2Yr3P+{NFFCgSzu6?s;A zv`^6U`P%fvXqXuxkvuGc=cc@hlnXZ0kNQG^u5D|cf6e7`<8OFf>;aTJC;u|hl}g24 z_QDH2m9jQaE|=MJ|53fmccD1Ct{EG;wlbRhO{9CjZ+Ed^R`SKPfd2@^G&wot%-{cz zU*3D|k8fyfnRQ;kAD)RoP!c7Xmn7LR^$PjsEL+eiS(cTh{}lv|uP8hx1U!U2IWo<> z9;90sf7DIKF&*9`@tj1!QgX2+I7v4(Mtsb|dc0xsLRsT0211r7NVOhW^bktpXeFp3 z$MusndHT6!OKBDMYmoHxR=RX6ee(1Xv>brwjX=+h zpguE(sZ~#*sdGM@@jgUq5^lA(NoZM1m!Z_Re?nIT zMXlFKKI!p85f6$&w`{t)h*4Jf4(9 ze=Ch#F_oj0$)UnYPFfG}V&37KiUhw{Zji-CqMUm2`(~AKlHdL2-d7s`p@%NoblDGoAuL$1Knlru?T|&y-!AITPy2_{b2N=R z$g&K=3!EvF+fk4(>a;|XoXGQ>M;@6qCoz8D{7ClTISG4H-xRM1HphBR6JUT(f8`y8 z&?KX`VcLN9_kV=WemyccI^uly`fEl-LAYUlXXl>|X`8!uQrF&}sO9E`V#)6-#nPIk z`u4ZY_jxmA!{#+MHm`9{6W)yL?E#ekrleCYA@VLMh zZ)=Iv#P0D(Vm*u6u&9&BiY<5#Y{BL3;oSbWZks+gTJI)U%hP#xmMFU6Yqtk2YawFU zS~i_I>r)3F^ZdUvuDjOuUq(MJ+XY{7x5}fB{E9RJRf76k?idLe> zKhdUhEYUDgO$y>lEQ_$({BXLsh&yn9x0j8mrfwU0xj5WE_S1Ktc=FIo$NHsjefcID@3KU~_lo14E`Iu_@lFqp z#;aDZ{4e7Taj5p*;T+dne`6!F40C$C;a41QxW;yxJ7;qCNyjvt`IQT=dA0Fg^0m*i zz{HCMQ7rF-MKW32+6Vvnb$4Do&oi?=eo9F2UR0S(UA<;Q%^e4ye!mOC=e5P(ae)nT zo}IN@w&?U&Nx!aqtH-lPUM_n8i+AkS-PXF%A6;!_XH`f2A^9zEGG@eQH*# zRFZ{UZWAfx<$-{AaP6A)|Ixs7kGyl`ClB7z9IlDpBFo}pw-bunHHMwtif#%7E2wb6 zgUt>pt8hY|?rdT)jzONEE|_soNnL_$fgna86!_yWmp^jW=MFw@&1+o#9U+n0#-sC= z`OV(3Nlu)m&=N(df7&qy)z)-8D;{Pi%C_$280+i^d3vsS_j&gK89v^Chi3&Vw%c)} zqUo5J$e~oyP}|z<@<&>3^>j6YXc`riwF!AibzM)<9lEG3X3e*vMjAwv}peS zu3fuv>03SSe?9WwC09OpOG~uA=2l4(4`waWY>uyjN!GvFF$qLkFgbp@7=uM6c^6Eu zV+h6t$-C9P<}_&(+|&cc2EZPU$ju6bg17v7`6C~`U^jxwpMU7X+`dy&rxnL1&&%0d zy$}eE20Qk8tPu7;KJJ*C&;Qj`yR2K+tI1ft#2KQ@e|3$G(^QF&Xodtqf$H(NdAO=9 zz#|fBNYq@6z@|S{8dk z?Ho(vf99+ll!Z~BcfNdfV4d^+psD}%zK?zIyYIj0_B~Z~d*n@({|Z2P#g)I~T4&Cw zrOkX)rI0(gG(K^_U~V8R3C5E@yW+>zR4U1Lbj&U%CkK}0GCe1AQU80_uH70nNr@!M zq_AMV+- ze{JOt&$)1yMvInyuA;cai4HFQ#3{ZaIEvCW{c;f5_)0?6-d#1~YzmLKA3;D)iYl8LP6JuV_Jr zLV(Th2rw%pH}a>K3-EX%>|VVp0rT5!yf4c4zV<=kra0Fhd+DGuYUcR&s=!H+PC_<{ijHxsbz6A(fmJC6XUKy ztx|4Od*X_JZzBhn15yyG)sD=7e=%l zOYWHE3kI$iBynF%J3G@1gKl>Sd%C}~?RKfDQbO^3wQG$r%}dJO!$4-0e-^kJ!DQgl zjZV_@f=f_HpC}0(ve)y+mc3?voL;nISB(FGi@zKR^lrRd7+iM_=M9Q!UjY0>2~vA2 zD#0*>Y?gqffQI@;Kj-tkSI=eNTS!i!*5LAMKX$=(ZFFE=D9G36rl#74ySArCb!%($ zyjhR`)!TC0H4p!Jr*rNIe|ocq*RV=iD0UDsr$TUaNbLBZ^*In_2v&h!r_sDr$dOd4 zU8xktq`bo9)b^Lokx~HTx~cAuUEtDmIMMG2dd1w{pl^U8sW1o&Am5@ z%s3%S@NkC9GfDg4RoDC5ZM+ZM^eJ&}%s56YmA>x{H0~E%!~%|Cf7rPogc=2$q`G5) z)Ck9<=gK*^$F$%dL*xj(!8@j3|CxCF^XKe;%0++OVl4ZgEC2c*r_1)pJ4@d2fU>uL z#|6FHpFe-@e#c|RzWYP@b{Byj4(3dRBEKH^Hznz@SZ#?1T6RmKfEdk$6BIB$LRMApR6D1zBW#ynlzy@{eTL3@p3@wQ^4}?BBo?va*-~ZP5DWXY)btjn}St^y4Fa!>cYmo7(62o4z4Bi9(r3NPmwwGV ztD&Z8&TL4DR%H9eF}QIvjC2wbXQ3F1K$0NPJr*5mOI=%$yNX^1SsC(9_N@~{}2fmj1AI}Q&4AW>ARG#Ip0y$U1SCtzl& zuq8juf29nIwSp`TVg0&cs8$;Fp=JVnC6o%Bo6Bh0jFCWg+s4@rYa5wHfSduZ#JYg9 zd`9zCPmkVC<30KP$8hub-KS&nDHDQ&bo8^K451ST2BWr998=B8*koEMHr zkIXm>wH~~dg&^mVXfqd;RON4nWz(0krSwMDf8QS2Bd=H9v4HZX+x{Zf*VeUPcFB3C zH!VH}ZS(hqPa@?|NukJi$n+8fy$_?CdLVj3IP}OfP*_wzffR|MWdOs66W=o*p_+M6 zLiO+keFQc`F8xEvF}#*fyyK5|K6Gw&Z1VAoF8cT`tEl_ijeic-gahw2QzK_ta{ds< zf18O$8+dsAaT>S2k3Pk9(8~^Huf6M*pI<(sqyGGgQ}mRxR_?AluUS^Kt(%R$@`IoJ zzc23=LgQDGzub4fbJTm@dyg*|Sx_ouUBY0?G{ExH1lDGTcMy${5*MfIC|M+kjB&nL zei^avTP>>I|w zBhMjFR|`q?5O5)|lgc9b)Z-|xe|QeD1@qjte~Ql!$)iAQsYhMwX@s3=KUT^p@905g z!+Q5QxsaS~j*iYY?B35>TUMY|jya+&T5Xxw#GP{w`U{urzxMR;(U%cOv7O9{S~w}_ zR>m8e0mWlMQV0eyN*<{MgZGj?Ah4~MG19#iajh5fyklvy2*4FZ$Rf=re;c2`LDwy` z)RBgbOr*Z82OMgqn_h~0lNN1x9_sU0HiezM`6tg!r+SZZ>J1L4uE5{M*cj6@)WJ- zalq4z)KCCx9v<GubdSJ&duT&EU-djy&QxXcYtg zrVfO{0gO$iFg`YlVp+q)l-3vvhwqOzB<}w6-49-H`l%;o-{_dHf4=f(O2i`_Q6G}N z5)Doq77R~%ymLcvSdNX@LAoqJKsF1@C?gOo9B}#fe>uNa&I#eD^W~9}#kDrfKy!l~ z7cK358O!;@XtSdax5o2m^b`O%}J~0f4*_iZq1{KMIz6!=JT#N)n-Gi)t@9Q z@cYB)>*iwjV3%#oalkot1n4eC=rs4Y9LSU%(ASH8wV!bl2^_JA2jY z&40Y?@}KSYhXKEE@749;hQjwio-z^$LQ1 z0l`qMO9svyGM@InMW5;INkiD$52@6Hk&ucR3d6J}0dvs>at4jn;S7`uMfemQo4ZDl zDzZHoLLeN2ZaMB;GVaKcw2G0zKExXw)YsL~^`g+TNxCNzBv!Hg8d~n{G~QF2bbma) zjx&r=e{kj*aMFt)Mswh`7sz2DyI|3v)UB}9neKIA<>)(2kb~3(Mq``V-hk&H8bi9M z((6<3hWxcVwQ=^y9(e=ujsleTJ^1KeK7a7?jq5j^_|RYe%rD-5E*2g19>l_=I8>9A zJdfM%xEoLR7zh#w3(wpK`FsHrV}n?*@KAXCe_5wn$ti*SJ@LhDyTZA$_-_+8G zsZ0i$^cb4ze8}WDY*@J*0~^yyb8YKcEi?D8yW#gYeB_)@oip}&$NKZDFZa|Vw@9p1 zKI)65PO6L7wbXUY;N&3LGRcH#*ih6sDWz)X29EO}z^Bnd4(d9sa(F|?Y_J8LyZ`Rh ze|T=$Z_(URhl|cT4f`&fhj26-Zqxcs(Q}ihWHYuk6|~3h`TZ}K37+sns_K8}*warL z-_>((nWa8@KvjLNF=mk)m4KuyiO3}3A!zRaUa{K4<~eXzi;hjmt)=Ty7tN! z9t_mf)Lg76{!j5zuz9q90FynNVCQ;Kx7UHliOmT6e5?iEf|+ftpO_dQ*Y3OjQ87$T z((^B@K)GDOl4VP9+WQwFl{%bWD`NBJjR;6XXs8)PsP}0&dmm4r{{+nL+z%Daf3(s$ z;~k${NnmoOFBJT}*2-C$nZcZd2 zQ05F(VM(>eHj7*etS39`hTyUbai9&;lDu|9K`IpMnrrnlsJK(NFBsv2C>e1 zgb&yc{H$h}aX&I6K-id-iYe}C8e^0Omk}pG!LqDyB9YF9wvKQ~7H7Sx4F@f}25#1{c^bhWU z5|N+=Gw8_&Fw@q2JGXoG$R62w*`)Nk z;IHO(<+WZa*WY-<-1>%whbv{R>7je?#C`|Nf$3NnA6SFYo(gM6e}YOOk7qYb;e^HY zm^W{KJhdZ(vS}b5Ye2D(!RB>qaoa7oVDaL;VVB3zy>TT}UleIlw#n=yrY0s((>#|9 zk%htDQTRL(Hm_OFRk}xyRyt;1MZ@^;YmITqLw`y{gNZZD$?QpruXLm=`@9l)#-30O zeDNl5e5M*5u>$Z0f0@#9+@xzk6y4)Do;=?K{XH5ee_HnT^KI=P##g`Egi}vhfv;bB z7M3sV#sLQ|h9rc*^KFo$MR-d&p{^y}2%~UDa$@3)bTQ>TamUa5w6e3rFD0JRE%Bja zKK8*koKJV~p@+5>OU5agqCKO&X@=VttW?Z{C!^~RsAQDMe-r09`1~QnYHBdtx0MvG z==RGkclfYynN~7Ck15tcKWz;-{?60UGaOpXT}qOj#8z-{*5CJTZoUGc@hLk ztu2+xpZVeUzH!mlzWM!K3c^;ed|Zjf0!PV`@*zP~KTs(a0=?^Q#pdT$fJYSm;5dYU ziGb&58dDqme|`^AnaU^p{_u{|PCc2{hKcqD4kgorS+j%a>0X6db2}0ADae)ttXZ`l zjWfeQfYwks4I?rE78$lIOUo9@Z%MxGnm?a(N zU^lY4JUq!FWO7su$>#!CN0jzbyviSbzU1!rT>P$6|Hb({X?XAp{^q8igj?F+q_R*H zAKFei6~&P;L>n96jYUw{v=JPQU9E}H^>h>~!$8jtc!u*(qGbqPi$EcLL!Ul8nC8 zQ)sD~3xxo>$a@$p$MBSbXEx*scx|UOoxt4oe>Mm*1L+cqW`uy62lZAEZMh`v3l(7k zWLAO3PS@Z_$1um0wHDQ<^H^U=-cI997tC^-5CPN<0+4M87DrGR+=f(cF#cF-;KBfF`Hu6z5@H$)DSR78xN1G!v8hHs_quMz~7{EDJ*nx^e3%-JKmO?DZe zf84RQP4$PoU$w04mp1O$O!$HEaE)U!Y3uE?wDIJ?d3X129sACH?|C4ztFHN?{N$=D zqaJ_o;i;+7rn_#vhd`i;+PVN%tmwzoP!YPtVPdq4jDwDg&pZVmJ^SPM+d~gvmS+&h zo$ww~C|*4I$e;1_Q)`jQW)X;nFg{U$e_1AQA&-F9MxZ%)IPrPdxNu^Ivzw4o

!02jFec!hyYKSV~wV`mUixxH)J+L5T~z_#tZShRNw+L~sf<0G9ee^_I5 z5{%A7P@|-D$dCp@V9k7L>l#pF6*;Ro+Fs6P&l?>aIIonB6t27Gw@U^~_KnG|p8Kx; z&5vKV!a3rYV-D@>8+&kUs3%~qTS>rILMR-C*XM^qN;(paBTA2^<~A~3WX#C2&zQSE zvY9lJlcO+nCL<9n%&!0E%5q$UzHX06vyG!?JIA`_O-XtIvv5la0n-z zx;Fuk2)1;sM-BY&s1elBTq0fEpTOs$#S7;nnI6DE?L zzNiES-C|vW2BzuK{Lnt5Wp7(<>q(Il2qfBWu?^1}ytvd?)l^H?|3q-3lmy-~W3cry zIdB{-fq;ftL7qUw*0n<@f9H#sySIdx#*UHm^d_HsbS_BJ{BUjbZ@*sp;D;_a{KSnf zj=AyV_njZDOQ3T7uTYqrg4oysy{QTE{)ZDd$ztrUOHmpgL1wrYg<=keoqIH5ht<(% zY9Ne5``lO6=Hg5Oi{9!{YAt=+P5m&IZbW5NgC>!#S-r;15fma}fAF*i#ZU=O9lg=x z2RG|T6gLfG^noQP&;)U{QD~Ji@yMOBqad4_L^w9X1-&Cl``KH#R3l7x0b*{9}1d@XPzs|#~!x4S+sDm$`7$~luKXf{Wo3z zW%;Nh8u#&pf0MN^4SJ1828-ulR!oGvWiAvC zyWrriJ0HZm-?cY<9+M?@!PXcBiid?{+@n2CV1Wg}%@cy?hhr7s$Z@FQ5@s~^p)TL& zpIw;${;o0geT8`Rn^*tl^|yS>XC^;8+`lcra>p$WC{Ek2ainvauZQPxch znxP{#IgY9FHmqJg3~$B4;pCjymWi!B11@FBz`zI+;dTg493C=|EJCSOD!HQO?A8;5 zw@Q9^-}NoA`r6w#kw3@{CvvPXOa{+#92vS~6fSOISe3GzFrM49Ste&lW`K_#3`pvF zf5iojtODGv3TF{bry8@gC;Cv{@C3%XdPv6+03~Nl@`q5#Wl_jw+}yZmNQFT^3*;~c zE2BtzBCu#Lvpp#)$~=Emxb@f1{`Dgl9D36Fn=kyNXY}!hYBQ5b)YkeC^wGYbm_&Z- zX5^Os4S8Fld#lmCk0BcLLD_dUI9);u6j-19=6v)u+v`L3(S<(X;IJq*Myhe`- zxaI~Z5dxPb`rh6kIjst`u`!oNQ8$9XFaadnhtz$40@t-2oGL@@h|~8nT?i%zLmXGz zpuBXh-SktMlZwt>M-yr5sFVdaf+u@uoTP8dmO&s(fZ~s!n43g$xDPz7`Ha<#fAXXN zwA6z)4K(?RST&SHK39RzN?(ET)fx`^Cspj|9mPCP1tK}3xv32N`!~bGiB;AJu1q@8 zaY3Of`CoHwyq(6IG&oxbddc@mkrUJIIwJ_QLvfO1?+65ER-lwtvG(B+JUduHa~(ND zicV`?hextqGWKlRhq6=)DL0OaeA;EmMdxZ}iu{KRJ;81C1*8`=WJSNnkj4p{i@7hYKR_guNn%g%3WXh?li_Ku$-e|l08LoY#* z3J3-RrToaJ|2jJE`(EebpKg5VHD8pU{`%*UN~w6EUfh8tk3ND}A`aQBAXhSx&1BuE zfme+nnX}O}(+8Op1v~8v`30l~)?n+ZBr4e?vJCvBrjRqqBljo-_+q4NQlznM)HH^m ziZ$5Y-v`CAF*Y_1J3oe1e_IO(5g@IPG;EIq#H(IueC1^S`&nJ+;O&3Kyia@x=}oJkDjh~i5+uN=(!W{Ke*nDA=6?hVDK6RXccY9<>1k#d6|R?TT7?W&1IEY4 z@$}Ox@R`s2?{wZGsY%6gBPy)Y+F`jtZY$VAwP=8pp2?U|$N)0Ze+q}Mh3V4}3eP|@ zKg^M{djxi(hcM?jkNCXa59zwW#cSet-$#$b{;?Qh0~;_*265w99v!WKpYjf6di2mBs4 zwcz-7ABf(;DV%h|e^Ka|(d6D2Rprr88=aNS4S(T|+phcBnP+|S4TH*>y2gjaRBkrw z#8G^11uDP%IYRUIg4}R4jy^7dNc>H&Nr)5U7~i}R!a1kH z-`Iqqf4+X~=vj|4`8hp(qd5G4Sx~qN=J_3X;w6lar4dXG!w!qk!~g<5F9GyCikgO0 zItA71VFmfqR8X%XZ=>;=q|14QwSky;6b%TP-dT4x@ZafKXG8BIOoaONbqg|pgxe>aosu`~N=kL;115pmfA7t1p`27jdb zN*6jptLQjBJos3KjH-Z-oRUI3!MdK*wTO250zbE+bXJWJBxMfA ze`oQ)LpsKC^*HUg93tV6*W=~RsEhO-_`*HM|L@(82k!jQ)px#Z_X)7t17VIsZo^aExN9w zOx~-hYX}gCh=&Q#h1zKY>oAhrP*->ge@mZF;rUGy(8!21G^!9hVc3eg>^C>xwf>bR z*vJ)AjYk}WiQnIe)C;T8dD^=n$m68ISadQAL2Y!694rEUoZvb zC+;d0k&_8jv_ldYmsumynG(+W(0ifjOdWdZbIIGb38dOnRlw+I?oUpLyEGXbe@c*e z)1xUL=pPu+#1I-6j(%h(+Hf@gfj|&@&yTrje*XA!uOe+XO{1V*(& zGCB697v2|`g46qj54p${H=Uu9wYVM@W^JJf8_c)yK zO|txDk?lo+bBi2o#_2*je_?9F79@N6F|eZ>3mQ!f<$YLi=4nvLd1YNzR6&Hn=z_AO z77GMr!*K)?Q;>rZ=%IQ75(E$g#Rc=?bm0%9B@#q_F8jBnL?yxG`D~fxZ^ogO2;itP zTD<|(6${v8X{h)j;DQ8%hDO0yWJGc%>_2Ek6>4Iz@MB0z?qNB0#o;#rD(r676aHVyw%<%#PW8k6zBofW2 zq)bk=N-sN_Wi3<%H`Hso<(OkwrAig&amlN8k(4di00fzSgCZj)W%$A^a?2GyNDJ z3V`r03afh2wBNhIe~lQZ@vTFJ9JtetKa`vw8Uq2`m`pml#gEPF*J3DJKx5}j%=7t4 zt5q;nDx+_p51lhQUHvJraR{$ol=F5PZ$m60I1a<`c^V-jEccTgVx2r0n*hs`rL`gz z-TiE5D$qu0|F?w^FxElx3K-8NASXn4`4X%|6rw?ET(aKre}#B^O-P z7e9GVP-dW*B=642V9snW!htE?SF5@`%$|Gn3-vXTmg^MHf*J^8^dUvO`zQi;S{8m% zZ1gYsoId16f4W*!-MN16IWxD@+g{e4Mv@7^I9#rjNrsB~9JX&>4QK@f5_Kq6OgHbz z>rt>FlSN@_0F`2rl!=b`jQ!C%vlEr6A$WrgNINVdyAe~xBF08Wz?aF8=gA}FMvx!d zf#K~N@Wk_dNRg88lZPuwDr(~qgd<+Gbk3Rgqr)y!f4+Lj%Ud#5Jjx@yiubAwbpTO7 zuD|HI_6{6))g=&x7Sd@Y2;N$qE=XvwSs9zxr#4 z)v-dB0P-aRbp%2c0-2qiVRX=UaBRkTHb!Go;GWF!3K>lTlJpa<0jsz)6}+I50#YF= zUYIOLiacgTsDGh*R)pSLfnSrpD5;L(8s(Y7~h?8`DNeWJRa%7Xhc0j zRz#QR+HGaViZ@sa0{h91EsS zz7zICesmoA^AF(ElN+$A--rCR0t%S{)W^!uX)kWtYGHkU1hafQFgNCbp6@5X$%vE< zRxmR5=YN}Sc+)ECe}CqO_}Kc*A5sQKE((Pk_Omk?-pNn71tA;(O4gN1@C6|Dk6}Ny z^Zg3e)V8auf`P@I%b)vIr|SJ~*XO=;Yp$W;%6%{S@@sOJ5>OOGmj7s0DSxU4yL!~k z3c*~eAlq{H9SadLIv~a^%Y?6Tu1^CQ1z6UzO4@b3R^cUn@v{xQ7cn&`X#T^^K zTPa8}4N{^3-j*5YS-RaRv2)s&O}5ME?l_jtu~QMYZ%oM8xOF|vwTuM>rg^J`Oj$*_ zY{R#;7gBOIMz(Lk;{$6EY?z0W&v`$6e}4sg?>&of{5J5TDfoOr2wDk=Tm)vW01sWJ zV2r}k7C?P-J7Vz$n)h)Gc5g##OACT3K`wHvOj|}ZPvO-T$J=MTK95)8Oj;`@IRdui z+EXXVxFt*UWC!h?JS`j*7Ar8bHCiV{aGqKiaucSUJSvm)966|-ayRm13j=}!U4J+B z6y)rY-72ET|G)<`c}rpm!tlkmAzI%1KGz`3Ipux@Q3!+?MPWfuo z#I<9*p5-+hm!{uOq|F_LM_B_!9e*K3OG-Ue!bJBL$h5&@vhHn(iHu>$xpkA(*=g6fNr*BvG=E`p8g)?)kU zDs=Y@VLV%Qb8DDPq%v6g%z7Gs3T0yjo?F$8ZT(YFB-Y`DgHGd{H>Z;Twtt4?L@y~6 z|9*3397SIO_q=p`-hi+B%73kt;5y^Vbk|_-~&$p zW?;(%?)v&KVQlHb2Y&rccx%Eg7_>}MR2d!ZfvDR8mS@B#oB^FY+28&0UKqBY6kRj+ zUYI~I%(^`IV3QG*c-hSbbbsueTRHA<0s1^f#!SY?W_gzxqznl)LEqzVWDyOj{PPVm zkJcj1VM&D57DIC)0U!AeN7s>eEJl!j`YB*w2(_JaapvqatUMXXoUR$&n_oB8(yo*l z?R7JJKF=9Jzr-<_h4cc?~c!8brgp##`5hpwv7&>CNzqHQ5jBQfSj@%2B$jFQtPlj_Au%GPEPcL zEEGLgVtJQ+yK*jHOMkAo=yQt_qobG2SbE`Q>RCV0TAHpo;AdC9VtzI2cO$uL zUa0ojcGD$oQUSbs)a*>RSCm+BjRP0h@$Lhd?CnF}n)S$zPJhrX7uBVHvFvhwD850iFN1i~0fNg=sBYM|ja%d7#Lo+t7Uyt1C zXOQqaXj4c(L|b97A`zM=V|f-eXVpW>=HaXPf9$;nm}FJ8HvFDYImhnm>YTeL^du%R zFfimK5|kudK~ONDC}#93#;bTyF-s7XBp@JBh9EOc9)BkGgwDCEtE;LjSDmVJs?N9e zsTuDlLBV_f?|*pi?B{8^si~^7&pvCf^{%zodY2W}dJmVi*t~f54U0|6g!PbSr6|Rc zj`q79+qU;(e{Y0zxrD`Cb=rE%a-N>8h{^hDjL5-($!Q#je@Q=Y~Hy|OG z;PKJg*?(Qo<&EGHqflZ=#3EtRfjm5ZA3EAP$f+Jfbap%Zu2wiqw5D>FbazS1-zwuD z0m}Eze8ba2IVGGa=$iBfRY}mCCqSqlD(|7qQA8o+gy2e%Jc-D$Zaf7U9+EXFoI^fA z0MlbeUZr_uXJ9JEkO{UWw(TE?zuWzLY#bZ!W`BrEN$~yrcOG1EUN73)V^H%iK^5Ll zzYU~t?!}Y@xE$okd?}#3r=$ug{%NUpQnu^BAoVW;Q+g1(k+-+bu;5kdz1jx#bjizqw8)(eDDFxg?GWM z^MArbTE9SG$n8s2zuirQrpKNMCDXGyQEv?dTWQmKkX7cO7V;>YoUq6-G_@~+!C*rw znZh3O?ze2+f&F`SBbmv-?Xhc>o)#mIY=+nR@^0VThtc6NB$)TFRw5Xz8-cs7jR{NZ_&NWnSi-kCvNys#Au=ha}v$^~d`ts>8%NOQG8BOGU(W!y9wkAEeUi{@%w+}vj44Nw_9)VdMxca9bbt?a)Jgp*mA` zCbmo4mKU6V9)C(p8>TbyQ@N(#6TZu?Fm8V4FX+k5qSr`Ll8J)Hz7 z38?o^fOUR4e0UBs;V24uh2+LbU_gS6(Rwj?VbMm`j+w8tYb>)PNe!T~Lr%ID$(BOb zD9V*y;L4FXuz9iLcs9!WcIiyW@w}bD#=Nso^_^eQJgkVu2-x`A(SN*n4Gv#-G0YZ; zfFIpelrHBm!AN*CpA;*goB*(q5NFxyfS!P6ESW`!fbUSDjH<#kEP@{U;{r^2GlJeA z4!phrtk;S@BVd8K7Lw7dffBvJ2sujnW&`cvc?+RR5=>?e1*pC^Ew)-xSxdsQoI^n; zXdI1+EOX9mFq-X1OMe1vW)dsj0;>sd1{NccE@LV^4M$H$C3>u@(ZF3SP7XvUj_%Gj zEj?i_pMjkmIMHswc;XQBP8~XfofsJJMI=6g8gD!4zc}VnDdaOHD5@7Wljc3VeH89J zG~dtu@S)sEi$8czy-oR;-nX4o)y4F%(xj(CWM-{UtgMnhr+-D~Z2V7e?U~BZ$fiQQs6e!E6biz>zY}XJ5d*fAXNStp+Ep z9HWgm54>|RTz>`uA2|Ir*nN5^VLe{jqepv}9!9Y^kX4$s{K=wPBdB~79Bpu^z$)W@ zkIs1+WG5cQ&=?ulNh?n5WE+MQwBJRaXr!G!Rq2`bhEXE*J+Cb&JimT39)Iuw*iAg5 zEa{qnDAU>5WP&`o=a4U|$VmlK2I)$E9Qp7pVh~T{$$vxTDv=6i6w1)5F(cU$3`QNA z8iE)Znn0mgKwXs$HhRs-l}YcI2)yOdzjr^TM@E-_`V${r?6%>0GLTPx^Lw{x{`~v? z?Kb)4J3saW7D_i6<7Cid1vGr+qtKJ*|IFPlBlYXw!%MJES+Ndv3mi0u0)$cweH(XU z(HU#VP=CnGdVpn%#^VJz6at10vC@sE#PYrhc=@lpVbEFetGmAmi^Yvs|GEi3{_$^c z|HEPEbVe*+Iv*>S&&Nq8uSR!w8%)eWfE9XPuZ6mD(?&=tw{!8O=ln~2lwW@6Vd;dg zTyM ze_e#%{aF9(eV5e--B;Nxdcu;1Be{}-R=89lhk^DQ&l^aFDqx|KL0^W6j6hv&&@LbzR@EW-$_{XW z8tA+(0tn7py$;v&C|%mxWbA9I3+z2}sP{QpQTjtuQ^RZ4o%7$ywaO&3pB&FCwT(SX zuI*l4ZQOov0)IIe!gd#rE?&m*Ha%+dX@4l09F(FF38w>p9vR2xXd2Gec4U-1Yy@8d zf%$lRBZp5pO*qf)Lu@XB(XlD00Z;SWUAtT^b4>1dt?;I-vd_cpHq-llN4s;76ju@#pDhsRBGt^XrYAEf{er@i=e;6=#kS$`ZD)1!CThT}V@ zY2%g&h>Dm=I60Ha#OL$*Hysh&_M_ViS6uz!VYAKAFdd1))!u+PQvBI?95N&IU`m8M zQk~h#j;A$6nN6m&gnL12mq9>}j4s>WEa;$vM21199e*}8I#BJec~(iM7v}Q0TA9{p zYA%Ht`VQN0KlEg9`hSOK;dT~|H}wy`8jpmn3%Z)Fxb@4QU-$Lz{ct;*U$$VqBOZx; z>eNT>77N7)Tspe*R1Rl;so&ihpJ=AIUrAD#cRlMOH!2^3}?DO)i{vBV2aNrN8>|*S>ZAXTJ5W`;jkR zwXQTddiZy|$e$XlZti*ViGHNU`{8xh6PU1IV}CG+qWb2%b$SrRzbez zI><}wU@#R0tE0)|an-N3n2f9S2Km}@S%{j=mMz15yB{AP>3{sBGcWm<`oRYTeZ9eF z{j9&uia$KK0Z%>jBC6dMEL*!2JHsiwp3S3OFyeHV6)%hx@%-K>@&vkKGo$2;il_~m z;kD|KnSYk>u?1Z)jErH|{y{CjRo3Y-Q&+d>##P68fBNgYqi?#nAh1U9tRxKrr!huW z@)bEwiyUgtnPUgz%psGp{m<)R<8`!lwWQ~=h(;uMtE%C38qi1J%j@?b7E57bXc9S= zYb?vql~UNfVGG>9_Tu7CTy>-wGpAjXVOI@QvVT3RRw9#ZpRym;QHNcdjS|a?MCETe z5jT}-J#1}7mej6Qm_PO8Q~1Ttf2g@P_uX?Be)jb*p%9gzX8l^43~$ksBVmS)z=W!B zkc?K8zxZQ#mi3@mEW^nHg%{ zHh5ef0>ITsW_ZlxtkB760_YNQVg^tA^*J1{M`1M^vEsy&amw+hk?tihpBO`^n1(E; zP)M>~^iHHR8Ej1KBXHM^nb}FQ2XW*}Cx5}AZ$(a?qdmb2RLqPclz%(9d@s%S{)>9d zOU^svZj&KXHM)K~UK<=k>lz;3hC1-_5Y%)DoGngH4cQWt9);{I^07Q}=>lF)7|^c< zA=J8{k`3ooJ(4kUCt8`ljxBedb>=9PL`&TO-X1Bl0q>BDn$SsyS5%zUwr`qVLP;JF}lw^yLTcvmq*vU z2C zsFskh2?Qv{QSw*eH@ica+)~6V+xu}5dHLtK8Uf(;T+W3^f5ZavVTxHE+ttJxDo=@ zUQ749&R;+Bz|a5f1DAhd_TA29_v_maEje!GkE}N9eM?(gu(hB=iYycE^lytZll}(k(@3^Kx`JNa9Ar~)5#o0q6yfYWsFTL z7|NPaw8rrYfgObvMP(9@GBTE}F2Wq8`@cbcNPoKOYcVAy@EidFXEERS zzUAH9?_TngyOa`l*GE5oQzT>xiRP7|AtI$1o(zo#a$4zi@VR3d!*Z z8e8fRkIcYH;G6YewlFXy@GzQ85Xcb?1OhOb?3fxILn@KdzF(2@cdk> z?8Ju2j(j8<$$yB^i;O^>Ieo0Uo8{s%$Asl|(rbFggvjeEEyZhHIyp?vUB(W9aouKH zuxiy3{QBN5nCy0}SbYle73#xs{7i7#zdI(YqL4vca`G2H#=g!=q<1kxDmSIA8 z60j0DHCr6WN*2t{jKN-tfHV4O%?(()Vl6fgM#(b3Gw=ju6mvW)z=T<6 z#eva1w119XxLoaslhfp`kyypNw$40HWOCFj4Hh^&B{-`rFxbg)4}UtL9~;NUKOdx3tbIFn1y8f;XK~6} zR;fKrO4$s(QGi2N#91rJD>v{k8%)@=dk1-2C0ugB>ny|u7}i1M&xKmdAklj-gvW9;Q$<7SZ0L5Wvb1G?I;;P>#V&_?mbF!>rG0Q(BU*GkVea-5X4j>Azis?>qb&7 zn5?bwo_F@r?hhHv;j0b|&vZrO=^CbL8GmnOQ*$G_T51uR9*4mQOif2GIOXJZ;!Cf* zTnS(llP^j6SdYbQ)c`mv0^k|QwRzET>I!WC?PKWQx&xjImSgV|uVVgXmufkDDw&?L znm~~}U_K+jFuy?qJDLF#9c1`SU@=uzR8qc$|9b3Gh3}fbb>sG=Er8 zQ|-s`D`{OAEfGzCD~TxNXOWDHqhI{)*WYq||JnHHb=O;aheq$796jJ4aJFKBRK(m| z78U}y=e9a9FqgnbKf4SbnrkAf;^rGJ#N`tQ;A^Wx)YN+Dgv-D5|BM$+FRBZzCh>WBWF3K+1u*Lk_1S`Y6HWD>0?MaX&DWW8 znM*8V6W=f++ME}MuxZf}gdNpbcIqkUYHPu+U4zh(6UB(R8IwbuNk_UQh0X>O#>XeX zx&266;#e`f9DQt`_m?@Lp7_0#+|sj0-<`9pcGcBRJ(-4!h~A zkKKRQpR5k|S6|zQ}sxzajc>+19IkAEDI*%5?i6KHI1!WYgv4O8F$ zIx?YIEs|={8R6g+ZGZoZWnODGK*}9rDg~>NfQXsoDBuHU&X6Nwp?Q;z(Xr$p-J5zr z^PI{vviMjkO0wz4&{P&8ef6>>CnI38z~(Ze5Z+AAL=>)qhWpKqz7$oI|zE zqj9`?+*P#RC5+F6VWzz$k?!bfCOI@UBf9@3h>omw9KEQyL4V^{<;l_aI*%h6;9(;t z%xWWBkz{wL^;&;>K=~e;@7c$#^4Kw?7jnH2j9pMkZ!cTA7PC)2gY`c?4F3Wb+PVaI zb$NK@9R3^! zKWBczZc9CJ%YUt(y!zW;|Jna<{Iti$dw+=WaOschmYc3r_4N+$6I!LKiVP_yGl4}F zPi=CdF<6ErAfc_z3cZ=-^JS4&Ot}00Ui{k!#vqfk#HV+mD6*(e1oN89s9CTG-YOkf z9y5m-jEJZ6z!W%vzr0AsRKg|4 ziwGBe@@UBT{2M2K;rlN(F6imGdCTT4m#U$#mLDle74F5?_x8fbB9e@gPGrtEheRp~ zlfFobokixzt0zFkT5TE0xXE$}seAzevsrOkO}otw_pcM_!k-CD{_R+8ayqL1W_kmw zAo;!5>3_V{Y&6|cUFFA$#oee524VBNQQhFh*x@16H@6L+aMJsN1SrSYXJZrbdvgA5 zpD}lwOgP3#_nL&RoFdy|K=ZoAICS?z7}>fP-hdg&p|FoS%fC%!)f3Y3NgRj;&{ge3Ns%#+ei#Q^JvlOAp1b+M6X8YQ+ufBojmFEv^dwz5_68Zm(*5dyAenA+Jzp=(| zcYo{1crXCYh~z7|bvliHMJv8xYpK!690eZ)-&Cgj=`~KTy+?~IkX-rvew|cQZ~F6- z_wRh)`B%JaV)iSpzFwJ{ICPuQVEEP=DJd?vvR}fv3 zNf>Pdn#046=cY!d;rDR3<(ubW&yGQCet&%gE9ZHzV15wAyovxhPO6UNx=3?Tk%Q165%Xvg{I)sdu^FcA|_PD$|0#qpYyM1PB` z0d1D~sG5-R0lHuNE1NJSf$;DIJdJgzun$#Q zII1{u-3lYDCTC2Z7%dS4sRfcF9aj96D(I(&QGED5h{vCX;~Se%92>^K z4}J)K<5q}0HRLqJNJQm|JTfQ}Wx;U*mASlzm+f{p-N!T<>wN?cO>8ybG=B&hw`q!W zoUAu!or6ft5>Vn`wi-0fPKnmcwXg*iKjB7_56;Od@fSj@^zd0xP%cM6 zDF$1$1&N$Y`kQp`?!8zuPo!si2*r{c6Jrw)oeRm~RZ;8VQC-^%B^O5CP(Y6KPa@Qd zyAJ#n3g3f;OV$wJtw+%9L4QsqS#%mO9-4%ir8-2WF%&z9HSzYM*9dRz zJAr5ZW9$|0gxvGMRM9$&wq%~RF>bAP~6sDEv39ck|F+1F6l`ncJkd+eUO z?)V=i;UysXv4GEZrp;l~T2pGrU)nJ@D!*0!$p33ela&u?)fQ##M+1_TG7gQhiA9ga zvI39W(UHyOzV_{Jf8&F<-13cgT`4_1HS@c=`l>bE^Xf0Xf{aD)UtWMbIE!jAiHwwn zRWD#RDt}=z;Dg`EfL%fm87U~XPL40;wOQ!q*Q*wVHFRc%F#Ogx3!&a(-81Npz z)_TYO5wtaSpoN^#ru_#HPmz;aF2ZF~AzFDHcGYS;kh*1yaO%3H*z}qK$=oDNf=mXn zjOp=Fw03l%rM(N|BSTZ+nb4Q%kvv0?>WWIFxl#xU?9V9glM7XuK~#lU}a*#e(UIcqx<{dY^VWi z5U!R>f0Li`kpi4bn@3KIkTQ^Z6l&&=K+s%SE?RX_RZcoN3+YZAI)u!|O-QC>{tt>w9J<24_u?=0wkh9r^mZuyOAQ_78T#Kn`ww%@UGl0rP@3xN1B|41Z4I zjAds*v;dA(OENv9aNB|U4Jov65okx z_ly=vOU+F{Awf10un}YdR`-~ep9iiEs|8`AFi4{ob0&QY23_AN<1PRQS?BZ2*&F zhiT*GD#ySC2=e(HTuvX{E`Qe@R*&hLsnN+BQ&Qnegi}qdtyaFkdiCaLqp{X&%s^E3 zG&2x~2K%olW>Z(RceJOjy7D7i^%h&MrM7m~TUGbOb=O^@MM-bD`Qz@vv8l^~Ed<&Y zbinWPf)D30kdiPn8bd={BPl5Y-F&J0=zMSe&xcBj=6&L+rg@nTQGaLDy5Vpohi33B zb^_tYFU81Xk6|Be@D-oE0g(qDK;O5%jnUUHz&YQz404PNc7yLoTSZ>0K-Pc`e5-t%F#@LCj3CGfA5CGF@6EaJ({X}xUabchrSAE6cdr;#W&=Jzb- zGMUnaBg5lgRpk64i+@!Qp6sVWpqNF@WHL^F-S8ql^Tm%M{_-=hWzwkcbfc{IV9(wG zT3`{@`Zg5Giu!ser`nxfetL92qB9NyFL0e?uRG_W(GSXn{M6c0-uLI)+P3f9fA5c` z|GPr|8o$ZkN#|=uMSk8?ExUQq_Irpu6%+Sd>z$=zrF0)KF}2+|h0pWgI=j zWZxms`>iIO;C4ID?rf`Tr#HOoX5`1MJUKTyxclc;yXCCqE4m$1`**^;)(>?ugFual z@VJ0_hYRyobV0|8oB2w`vd&0=B;iEjz!3OZ0s!$0W@R!asSN7dnzSw;v9W#Fefu3K zHh8dczX73G7Juh2swUu9Kq?wXhGfFv)03=pkP~OZz}O~qEGJ+@&Ppbi9}&v=wH<>JG1#m=SZV^$6$+3eVe*#@&^OG7zL>&!b3<5Y4PfK?Lm26a zz!E8Ae4rop;XXvN1vI0GI-e6IryaXf9MS~^_GAh>GJkOd$QozTNeqq-U~#J#)!r&J zwD+K)wNr~CC+6m!Os2CpU3BS{qik+>?OI>KYe&tr<~OKEpRo24Tf$r_&FTb6X35bZ z2O%6wkUk%^&DndTgD^Yr7R+- zYQ*K0V}I_lcGO-0l-jX-xpMR^Fe)RiB44Ef&zEg1xl_>gDa&y#S*)0uo}^tW!0oX? zH0Kyg1~^|k0bn~#f%I^a6s5^PU?2`-atN89eH(Q*d=|Ied=bhsQ%FDaDELPogq=Pw zkh7t)+ECuvkG!OZlRn3e_&J*%vEBWU4I*?M4SxhA3Fx@00CE68?-b?chOJcrCnt`Mv=T(#-GJq-AVJJi)80(RZJPD60 zjm*>@@W-VvWyqu6bP{}I&+@bf6=y9BaSy}*x>)Q1=DeI31f z?Ktt2>(RMnB}|T5M3ZHtGX;-g)PJpHc7ML_q}o-tmZuK?dBX#@{y#0AJT~5&Lp=J& z+6(U+reELv)orV8Z}seJvl*57v01;Zl=cq3vNzNp?DoDdo5mf}Aw3qiSCbbX!T3xS zB4iv4DwolYH)W-uVFOlBBZ!rJ zb5?oDq6(304$1x@yWBr{eRwY!aDRoO8``h|5@;M%|HIFj0$CX*pX2h<_CZs7nQG zIWUf5Ou@2ctmiZ9*_|ZgRv_q<)eJBrc-EJUD05b1(_|#Gx)(lp&DH;~nzx~)OV4xm zGXzopfkJWi3Nl25-9>HfiiO0@Ynd^7&Rg;?S-=cAa;pb96ha$wiqLztUOXAFFWHiG5OvFMZ2FZcELa_MacNNEXWYlE%0>(gH{hr&_8=5!N^GjMjg?{p%r)xEH_V7=5? zb@ieBTir?_{c_j5<)1rzXy=`3x%{q`nZ;sS>~>kMGMe<2mUvq9iVSR7LM3!`!WhWQ+tPcZ`9+r1zm`(Q>2? z#$oF!z-$@AOf-Rs@BkXEm(uST4keRlr2F1=D2fTxYppw#KoqInducBysDHC5*qJck zwGDevR|_myvjjR(Mk-Y*X7j}t-fFIoKl1y=T^lx^U)Neo&eIG|3W}(56d}cB0$dgt zjb#+(!kF!!CXk|oMHHZ`vOr&L#%QdJf$?o9B!Z(> z9W1jWZ}uz=+A+2Ovz7K3fl5}jZ4gSdcXC=nL*=paeOA^g$t%)P85Ky&D!!RqaavVxg>`~41_)dwiGR{K?|qG>fk9Of zW;n%If-EX16$&WOv(~YbdL;wBm2{WScf=`FD`fme+S3AE+2{c1*>kiHEzLe4GDFv} zq3o&C0J2JJ%V?6gG_89T0n64psN4F$`)S@59bD-mlC0smmA=Pf12;#0e7X{eRmr(l zjjSiQ)A2@GD3ywQMt_nx*3cqL&xXxQ;7yp>Js;~}KCD>sa+= zsR+H%tWhL+eSSoyhqR6uOL`V+y;1Ahx?yoxVKQ6Dm=B}B{}9sY47L_?`ZLeF=!-U2 z-DBmxQA~#8$jqj2$Y4aF#A9k^mhN8%r|LvDm4qbd@^74bd^+@OZF9I?DcKwE*hYotT>p!Pi~`M}r=x-2D~k?zk6$PA3^G@@UD(h$5pP$5MtW=I48%cU3$CB0T?Qj8BXDvac0badO>t@BxD zozprz7J7`p_B>WuS`?YMRC-=(RplC$4xl%fpnoTXkWD8@S0@mjn8e1XALS-TW-!q0 z;`Z;Ig`GgC*=!XuLmSQPcs!Sy!@1{Qfajlm8a4H;m@Exxm9=`-<)x`hWd?k9dWstv zI%M_v{r_>zH8;%Mb?49D6}8drKllc3Hu2ZFUHT?Q;lUr7KjZGKL>qM#FiC)c zE`M2pQzd%MfTzmZQ?gHqcKRH9oJZ+|jsQ+A&7#EQ&L94!#UV^im2%L~Jm7L0< z6eFW#^FuU?Fjd(RqkTEDZvgZl5fE~iBQXDp>VSo}Aefjzh!EJrd^Lg2Ds(iP$$wGx zqRL}KbNdo>bS+iqW@or;ih!Tl+FvZ?-~ZxEf9?C+=fCjQ22E9u#wZEAl{F5^qiL}k z)t0Uj7p$%iYLqLPToE}=)*`w(@@TBICrc`?@%u60$37~PTqfgbf# zLNYRmD6aJN>)aqO@ChA8%9AX zicxZ;%ejjCC8cxF6$jyQw4$M|4jzLKMwgFtO`L>Jk9dCqiX(x6J%`9I=22f&2UDbs zq$h`|p&2C7laR6m+{#V@@B;j9JNk#Sn2QmhHk2?rF^r|ni_jiuLwjQhg~&c^KM=$I zNB}aIh07dAjbDA+Jm&Y(e1CTz9F)5Ih>qo}2tb;V%azFa5#e>1 zFp)07rYplt9!yD+ppcxW^US`IQ|K@ds5BdjUdt7RVt!4a=elnapnv?`KhkH%#{bUo zjsYbbeZ!f%niOutoQ-|j?#P~a(Yb>bGwC5^T1CL7z-E>(8V*M7oY9&J8I3+adHBU$ z2xm=JX|p8bqi-V3{YPUAomV{DT^NAB4jr_#6h%>c z(^AylGd8hDtM*1EZ`8fD_ANAOob_A)(4k}ETn+^g8aQ2qG|ywRj|x2quoGrR2FI zavQL^zhDn~yiolxI0wq;7*;~=ub+^m5p`CIO}gL<>@`QoHHUBmQP5+&pj?^%SQ$&yV?(&35`a0?Ps}k<+^`E=H$! zb}8ik4vYykUq%LWZB3xx#I?F!oTG3It{#n2?jIs&m{toAfBv+it5MlsrM)|kgx(BC zKD#|hU-nUZ`(x1xxXW#n$JjH$=|qS!4!Cnu*`6utWbZ%mhJOr3Xka+uGZ)#t3Ej70 zq7t+cwB+P8HI7A}M0EWg>Cs0FLT^^2ncpfGV(7h~KIbDpFk0QRS@>^}ntA#(jkxr)l&}!4XT3z6={C7I$b<2hC zUx%E|MmkqJWw^3K`sH@iDwxUsEr()VT84 z&G~u={O8}Le0By6gn!+36Ts6CrmGm4mF0?Xs+WGAaecP+sA!XE8NY9Ko~`a&30NrP z*>GI-Slq83wHEBspy0&iYu;`RI%ndDEHMAi^O0UoW^l5!nytQUs^;_LH-9=5!jWt%-_j<1K9OATM{cER8F>fTQn?&#%=?ynvSw;GM4)hJ|b5D%h zbhJVVT}}>U^S&{9>AdSnBB&i%@E>3`^azr&rOFUH92tSh>y6Ambg~AL&O(=lT$S^&gK-`N>`;DcvZj(Zc118#0II z;zN5!KW60>Uc|m@{3|Rb4Y}8+C)IY8t*co3>H{C0I%I@?QzJdul3J701rJc#mEWU~ z_(0eEIWSE^?2c2P!R^-V5E>(k2UF+<%#v5&y40(#MKl%K-|cdrexFn?=W1#3<==y! zur=*%&x>fp>=sWbtC4Vsy55GFZQZmf>sHest_JfQ(!%D!?>jr@TvP6AhHf}g%4-T5QRx5qu{pWXkn3m4NaC_AZ z0nu;Bz*M8%nt30uJWnev;hc7n@PV0>#2bml&qn*$-Tj5RBc9-)kDmoyhmWPM1o=Pd zKhMDhG8l*`eSODbuu0PClw2$fo2b%yu9U6K{)}ArD(Sb;rWdEprVkF#rr&t?Ds{9? zExk{%wEvrHSvaz8o5C{-<1+59(Md_AiZr^Lo&3P++j`1>c*$2etxwT%-p{YbL8gTK zx-GOkCq2g_rVmbDPHK*l?L{~AjJGP&>Uu@;)O9(ORL`-K{)Nx&YgU`cKifU(e*`3P%RrD}L<*_&PkXZ#{1O$O040RGHlI3A!&5pX5FG4tn`$W}R6#vRPe6 z{u$@Z^&Rf!ubCa4Z*FtKlYbP)t;@@snM|-M3J3^Z?rba?61tz!Z`0ZyEZgH+#)j?+ z_2{h&JPIL9{q5&_VV>37>1!V0RipPFIioVEo-b$`7s&cBeXQZf3UH9CY8ZT;-P<=X zJm<=4e?f&BRBxIyEpLD8TB83SBTwsFefQ(*f(YWg-WSuf6>8XXfzl)dduNT(FM}$@ z@?w?LB29brv3Sh0&-A|=X9vC5n;JBK1RTf&%pd&4GM|m3VCUAgm-XYn#);SC+k{20 z<`({P%TLe3Up`5q@&X=lSLFg?Gc?w!Z9mEu{F${FAsO(Dftf-1d6^PWa%&*H@r|49(>pN_M=I`$ z6*3qu#eN@Nzdk%*kSnEY5eg%&lYDG%Eiy7#v(;a}_r|;4A*&QXKel}S8HA9fvdCTh zd<$`6YpXz^$#j2R|GvKQ`)XI2d)Bt7q4yuURbPpV|4B%YA$QD=byVRR#ma2Dh`uw6 zVjh&GaKX)BNkD03U5b&Jbc`tyI#X^(x5MVdUtcbt(OTk%T8_)I%x@%9lWPj__C-&< zR%IXZiHqS*%(n{y7LRmIqm^clodl@H${&2W?Fw@G*jF~G{=+xcJodk0Ed~J4~Z0l}`~*G#|_J6lAH(C#riUx@CmwjAAvujorVQT1f2( z()qAmOZghS8SD$X_M3hPrj+o@{O<|Q+T2JrzTuF*0boq;j0Cazhj zs~+b!qSKz4lJ%7{S~}g-twa`j8dF5bgTbK>W$w}r3X!m_j8}MlK(7S~4T|&i;Z}*| z@U^4^i(M|UEKkJpZBY>rFD%vWy9wT0Qd@6)wl`1@>H%Bm|!UqSKT z_Lw`9hucWoZ56;jTtn4@52j9S#22XZq$L~OnDrBY>)spZM=QFr_(X(2HZ$R=iiF=G z$8ZUA8*oxr@{w|Se5|H4f>yG9tmLhmzmd zzG-!{4@l0YKyE{gp1i6(>OZMn;h}Not6h+-pC$u}-td2{xbfP0u$J{dBT>=B4deg1 z)!G`ph#$v0?bO%&q>(;nl#sSIV-^yyMc%>wTUW$zAIKtexG0Qv^|EBcZ*1A znsoLLKomkF)Y?RmVZ5sv#oqVNc`O~wb z*-Br9Bwy4?9G!bSZs%WD6O9+l4x-{(%c!mpe>t@Q5^ZR0(;IT3DFc=`5Ol6hyeN}%ai zLs`l}+a?X0Gag4?%k|iNd6|Ct&S5y&9ah7eZOP`k?yJ3)#l&Z*7xhjIM;NQ^I*3e;P7wSrHv}@M${Y~ z9GNfijvT!)`Eu66R83A>J+k}i=S;mK-mw(%1?@Vj+%7+tAz`sa{P*m> z*T>7nSX@2Yl|7Y_jgxtvJ@k;bcr?Cn@gKIrqHQ7gA|cExeaxeZftj)5z~rvBwm>#c zxu_Q%_WD!c=5rm%Fgl-)H-eFSDs5PWAJ|w^+zZ7!+*Yf1Q9T5`$P~0wVGZOFT zC8!78Z)A=C_+cjnyA*W+P2NnE2ukjV8ZeMW>3hDol21w%fYS9A&s#2MFY4|*v zpUbd5EqDF8KB0=dWe(0B??{j_=C#hXm>o5~j*=~v!6{bB(L&qcvUM(m7xoU0PZFel zmV3r*ICcxl7-*)UaU1w%8tw97KLvW!b}_nOri=$77Fj^~`Jz+*MxLc+j0o_GKP+I) zzn10=wj-s73ma^m3KN__%t+XPb$4qZ9Bc=;TBjg1m5vIpSt#!|DG9D7(nzY-E9X;| zevRW6<0#(m5o9llrc8*iA2VF$t3)!PjOyG_{>h?iCk|;sdIFVb>s+}Uw^Sn!OT|!` zBy}-Rt#i5C6uwBAfy znWPj_xEZ^Ti(%iI?6?1cO1OoErx=6$AYjhO#{MN0Y?~V{eJ@^+&bqBs z%OS*L_8Fd9C1Gf?zZ+AVy&I@Y>=N0Q^_u%QcCtB`+qyB@d13-QTd>?8KzN_zB0ibG z#cOp)7b;M%Me;sYBP);>x*o!Sk=UZ)RG+@qv?zD z*;ubu==vg!qD1JuXjwd%^s~8Rr*tFXHo%dIP-mqvVt$4Fd;X5sj>a%m zv#(GxvZer6vwg>8mDeFTkkkovxDWINpxan{9c!wG$cf$jE@v=E=u?twF*bJ;O18QtqJ>Ys zt{KEe0!Ft^>OYN>*cjd8Gag7XiBm{!2@ICe$DYy}t>hEu8sM)QR*RdPc?)rbsg^36 z^~S-+p4B_?>XD%j4f@Kq!%JCT?uV3{vL(SH#p2`Kt&oM2(<}Guk&X4OPhwQ|_`o~I zPOtpkB(ocRGE%hSJbX{Uf}3ZS@e87=GIvkXfCgRko%;dGrnJeEQUR8MOK}rYnowcqO*V zZmmXc?>F8g{+RvP{->eK_CQGIo~Kip4eaKcS_cav7(P3nXQlgCFCSriqiprg%#6FM z3_#nUF_%|1a9d)68Fp*yr0YuQ`Lf`u53GEqx5zCQ!`Wc5V!#(t)!`za&DUoaGOWcJ z`mr@$zBzE3NOdc8*qVkkDqf>|_U6)WeVnv5ELIOUTx5VjZ!BD3eA{)yL|fOlN{O4I zffxCAmDvu?(U8+Y9g1=?4dXj^Rf~<7fV7CX!cj}+WUXngjD^p5BPC%+g>btPuA*nz z#w@i;#*hgV*<9(^^j*HLapuO3Uv|RB63oPl((ARDT)Ch%!I+cmr*y5o2Z8I1)x*N2 zml+lC!{`0RA|4)wn58s5+b!gaJ}#LWjKv|%O3 zo5^gR1wixnv~V~oNq*eC%bbTYH+xi;-=|oL*gi5{gm0?=yL&ZtJjKMqM z4MVOfE|73utX48sFDXcEt@y=mqV03REjMsM{^w3gV|Q^xmOy@Q-t9Een1&{BQQnXQ z)~u0?BjBxdxf{sY%^dGL89@PRd8Hp_j(`3wTN?#p22&L~;aaR1wx*G}~(>_*6}I%f5d z$!zJNhT|)ij*05?OI*C2p6x^M{qjb)zFM*JZI9%G%C{PDUtd>$b0+Be+q2;W!IY1Z z{=rarXikuhL#4VpP%Fp}X;awRCTJGA%tmi%m{nPaPfvT7XHtE=OKLd!E%On=MaXBV zRm-RjcC_!2(9+TP)=|oOcbTDz*OK37>n^#(&R$3E!tojPU4gSdJM;Z-`#cjzQ*?QQ zLWj7(rE0l$WaFY@MMDAevl^fn(o8nv>{C{KNF{H7{U?qmK<&7wuX$CIqvbmCuHE8V zi?31Q4Z=UmDU0{GW06SSP3Iuxz2;+{pq>2EqHlvBpCN;qOp8^Ofi#bvnYYfImx~V1 zxNZ6}m{E|+A6LUfqm7;Z6PYfeBq+P6?Pn9}BCajepysAQ{{`tBs z9*#MZNY$|dT3f$qfHbWBZZmA}q?Me5#B{VgEsQ1uYj!EPKC)mQlo}zlBS<_8tT{<< zQT{&qlJ%8T^ks%9@`5zChne)ea^7)+?BGD_?29u0!Iq}C+qL$sLOB7NkkQAapU<6n zOx(V}nDWjilY}Qi(=%DHY730cjEAgv3SM_v zNkPdC*A5{yBkpn!0tOR|$;=K;-OB^{r#G^$=I{SrP)FJMQuh@JQxeuWXcpB(rM)5O zepGOwNp_$4*4O?|oi{Zk_IjhA9mIEVSV)6&EJU8qo;NnJA($*YIWiuV8qmzzJhEwC zV9d@Q0(AOUKM{UJ$(|4x7nyj|BY9JQ>P}V!d25lh>L$d+LGfv+P*Mdbw9{$}wv7wA zbsyqBhK#A><-P+heD+wXg8l_Tva939)-GdeF7fitHkr+7ElwSNQqeR>N@70W{KmMt zedGo(*)=%P1~(bl6`!jRJZIra?WZ)Ry*O?JjDt2<`0Ncx5|<9VUE6o|iYqHC!yw*O zcme^vTw@b}RasDGB~m=>`&0Yog{8$q3sBVFa>k6Jb*S@X;Vlwmx(%PTY#j`oe!JEc z*)gBtB`bse;bUUjBDL8>=Ht-lJnl>BaO~3PU>vzU3uCNXH!C(++kuDq5V$t3R^*OJdB$IW+7tJ|L^H<1G9i^g;08?q$!K569NN1>1n} zP?G#y+LybziF>g)(^zs4`;NL@`Zv61hCq&9xE4iaB5I-{y)nYq&fg?iOA8(|G(OHz znhWjEy^>qJJn{0#G; zlh^7pm9C`yDRs^Q+*lxmDB?mAX6)0iKf3`@yIr{(x zKbj%Ff@^2GHl5_MMAc$u7LS zse^_F(~{+iMYsp$C8=){?(QR+fE>Aw=BB@ePY!Lf5_0MJhnYqJ~<&ClzN3@qE zyeMiojnu#pKSryWEn=KO&~ zlHzpwQ%DVW`@Z3x!hDA}k~JXK)`4BAqa)+InaJ3IA3r{8!N|We9n8PLB{$j@z7kcj z8%cy08$5jiKWtStn#CqPHMx=?z)CJ_6(FQ00>_}&#WN^}gjYHXyvb{MmYGu^*0idy zKVtW#EH}|W9pnChRme&9EfYDK^G@$!u%LuJ^F`|#S5A{n{bB2(!G;=(h2AcV^k=>X zb|Rr%3|h6?4<+rCK3L}jJE}vhjsEb%j$T&CCKFyFwBn8JS4uus%+K*%Ahdc%n{Rv} zcTh3=U^tVKyJ1BKwNlFm zgn5c==c+ek8F24N)0vJZ@E70znY#rY?{WeHj?2b_1ii^otb7O2+wmDY_rl>0~S`tx!dkBkU%k)8-#CGB4s1pVB6Zmt- zfYheV83tXWI27aGi%P{B(~*Z)U!tmH-$`bB(|VZd=l&bNxPSK1;IM0MCXM*jI^^-A zS*oP5j&d5!I54dcB;Qb>htx0H#Of)l zaroPOWbHy`e(f!(7-2vyX^pMNpXyOJ#a{#$aST{e?3?Vt2hGZV8W#JJLtAT53V zBb;KcM6sx$hY1B7>_#z#^v=*uOzP?sUnV*!mvEM-&^jT(O_RP4$*2>iGG00ZMi0HJ z^wRQ3Q=w~UK@Pt&>VN_Lo*Z7jfQ)^byxgp(zKTmt%I`FK1gCL5d_(uSp4rQUoLC9# z^EZ=>zN`8AD_71YS966EJT{xQYQTEtmu06j^HcVccCeZQqYWaML zU!n5WHEsF?Te%3biZS5YgPMOH2s*%KkVrIIJefPZ?Lf2YVI%q2i`KkJXA=2y zcLr}0K+70Vk&pAY{{G#o|s19K7X2<(XNiq&jKW6z?Po6fV;+5JWaF~NV1 zg~GPvh`47f@-OOnpOX6>d+a8pV={l3l<9gRgng%7dKUGtUXe{f8dxGEy129At zyJ5aaj4%812L66p7zxt3URGOe&)iFi@(5BV9di!oVfK87HZmB4dFIOsO*`{!loq)w zAfJWIjR<>Aax8-xX>x}PhXe`W%8CA4Tf-RpX4FyHPKoR-0F1mc7wR{<@`8q*Qm6nP$R2D(v{5Xj*&V)Og>XcqukM)gqivcl+!gv z03UxFHf9chvz0wfCnt@r#iscEp>?KKup?ex_G0i@NGB3{&IllCT386%wQIcmvwa9ZW$;0ZTMt~)UM4NS1 zv$nA(F^&;Z8+7y@nq#fuIG4~zKAivW3x91SY2hUpd|J*GO+=F~s!TAyo@H|^QWcKU zd1x%lZM;Ibn=~$ z+@Iy#`O5*339ZuTySlV9;uN}$Os6=Ej2L$T?av3TRXtWsfs+@Jkz!eUzK6W6uheUB ze4e{o4YWcw$9o^#Ly4Il&ES^xvKYtL`CPzT(DmOY1(~=n?1?-}>NEBkmPUF+&ElM~Gus8AqSp!1ho-}cYR@3} zu#4SmG_8###_cFA2TDD`A8LUB%mp+YY?T$iaNbu<#?^p-8?m&u)tG6fwyt7xL0zpO z+jGpOv^J-qYC=w5IzuQ))#hHxSpA;cju}{pL1inETf18USG&;AqJc{-D>H>}Zw0yp7kUbL>}8(?HZ3Arcf4C0Qd@KM+~KOTIcC*{*=C)F ziKfzTMJLn}z#GIioX{SST6)5~gE?oHk@Jc2wY1sW%M>~(mk-jbY+NeZX$yu+22cqv zEC><CGmN47YkMoo!^-cB+ZtJfy+d)&XmzSRm-KyvKt$UP|10IKQx42t;8nhV~brXZQQ;E@xmp zREUT6QXY%XHz_tF-X~3hz3@8XVF;9rvn8aFJajU#7RLxL3b=BZ#?QqF%f4n!Ey*Lb zeC(V%D;Q%Um|QYM&bZKZP#zB_cYwu>tV}=`O0J?Hv#{WNNk3}pR~5TRl#EDl5S)r9 zw?FJM86FS#Z({^A6>1cv2RLlvL zK7by?0DDj}N|{4#Hz=>O>-b&tBn0=8gPT#{G4-El-d45Z{2{g#oVR$n7meq>adulYv^8vN zdpA9;Dgq|pYv-m(l4Dx}DUBYqiu5J+f3{!d*?!k$BPTF{xS|poyvk#iBRO>_o z1WYAe7{D^EXwy}{^uK}HUc8E6On*4j7ayQj)w2I3>Ab}kV`Z_wnjYva~3l?#oP)Y$RkZU>UgQ52HAq2b{Zw6{S6~p5D(ci z{MQE_bg7#aI@miV9CBO9J58kNeR;P-0m9yR^jBzw;+oK6Nzb*Fr+MDoUiLmlIU(?} zOFvd~|pKSnXTk+u}F=Y%0GYHZoult=?=-WN-r zGi{k+G+@*g^d)x+;U);&T+7?tSw9U*8Oq0TpNed(-qQ@@h~95o_H+KlF)@6+)xT%J z=4{;Vd}WwSqWL7Ij?Vy0oAAEVzBhirE1#Qed3QObWZr$cb?;ZIGqqI`?(@-Jv<}EG zw5v?2R4C6xJDAeoKu1GVoDvsH1OjMM!kc!T%#j8=$#wfVb z#2<=}KFN?tgE01I(1)c3GZ;9y=-@(3Ve|9>|6(qFzyhj@ir9WIs_mdk|TX6kL<4iruUU)s*)tL^o%RM+~AdS~`8 zvQGbGNyJ^#-Ba5aO5@$QJV^ioH$*2bk4ko_F3Z_;(c7)4s#En|(reRD?&S%sd|w2q zRm(!rp5PXVltjSGR8p;$ufFn-wIq!UWRFiq-Gy-Z?k8|+tu=pPfC{PVoW4$||0ap) z`351vG^!t{U2t|NDax}6)t%(RUZEVklr~&Tbxv9XRKDp|9z1+yK>w9U9pZwuw@Y(lzn*i@qt!?rl*U+VI!&q zhSe)*#zu=MXBgZS?)nVpbi_s&)*0o z$1QJFOrb4|_c5iAfqmwRK{>v3yFtSQ^?(!BtsS@dzvIai|Efj9(i7uTct`jHA>tNL zFFt-YG+y{=a`CcSOKWYFcsDl{BX@C|`X-(OEoR`nZJq>W*I#T)oN0`10KcXVU{P_8 zZ$;9`K+c}aymyXps0GT96GW5lIor(!@DCPbSTWPp_bmE!o2zq&tu4kR856y36pX*LcgkE?!s+BbrqowmC9FP*vHYm zztxqy4;y2(wELEo($xo=NF^s_xCy!Qx|iMvl$SlPVc=U~QNa7k^$LSI*k2sY|9c{L zxqxK0u60_P6DBjd_%? z<79EhBkJcoK+)2eTDb7I$J=3D3&(;eu!u6at1~|cH|F)w_qC~{Eh{xl0X6?D?LNN~ zc7FJ-!lDUZ^m7FXvfIBZKKwgoaG>6Ei!D_ z-XdD_VBtdnECR;8D>PZi3dY5_`RVu*>`c?1gySSQ%93sfGB5u30w<>}EeAE;s5*#L z@l|#ZZ|qY#tHg!)&55+_=nn_9K3{TDo>(Vh3yJ^4MU zL!7`MOvE(h;LFPlDaSB}$y7efOKC}b9Ur1aQItX?IfIx5X-6P{z1RT8izW3D;5@8q zH50Mj<<3B#7$V5VIH-{^HvK$6y4AR4Yy}=Lz0pf{Ec0dr&Gzn!7JqV#xAw+rBcR@i zX+u!Y-iIDC0>ZA%Q^nPt!X}Cn%K!$@Jup(~Vgm(E1GI=V$d z!Dz(AKmSc?Le~m&Hf^kj5BuLr#jxL2ag4gkMS_og)Li^Fzl#7>u@B7N_C=9?Lxf;>?FkdvTI&%aF_kZELN#tq;DA)b zv#Hb{KJ)NX#LVt)Mk1(GvTn8(Y?1XYsBlkUXEy@IXtg;!QJAK3a`qV&k#lit@7*SL zuQcaeuz6>4vM)RVkcQt}(>GY0T6U&qJ`E|RY&FD$AWk6HAXsB|E#t7=k9}A7O_y}9 zj-^^&TsmzoRMmJVH8nryZS$Xk$7ZcQIj!?!&p#XqJ;E%gtQ~|F;R|hITCd*FSv2nN zGGF(aS=j8q47p6s9m|Eg>PivN0f@O!mC4$!qhr0Oqgx1I_nb zy#%h(d~p){82enq#UOKTrluCR*~htUDwS=Q#5^Mzrhxid%YV~}M1m0|V#S-buDPon z@;KOgTYZ?n3`4Pu6@{%6aD; zILA^AUjl``0ow^-hn8bA8?efS`v{R20t@Ss2d~&Wkxn^*io8iU{jMc<)|gqNdoI+D z?1hReWv>MqqIY%jKn*0@;}Nm~I`H!aOG2Im0d1tU({Ec&W6>p7G;~ykdvV4~Dectw zBn+wGMor3z-5BdzE3!}5K?XL*ihp@fhS=yX26h$$TuZRQ%N4_6!BHq#3ErI34lpu4 z--;YGSpoZH(dFTrFJv%%D2kqc2G3s}53`ZBl7cRmppQhVCiVwk>9hlKf?69PgE zumR!y3it0mqtl?qO62+m%)xL^^=iS6-4?+fusFh&iY@G2@&4$Pjw#gU=@WpPhtQYT z$#uT)S*hFNw+;h0k}q|xi#pjt+zm+jQlkx1_=`IWN*b`^Fzf|!7i??>k-bZPCvF*p zMG%Zd(so|!LGckQ=_ZR_8yY=K!OQZ)X7w?kdXch zXCiRBGgcNxF+w-HPeP98%khvTMw~TX4G{IC2L1Hc zi{8p&=cSt-WKLB^=}zc6a(RT=Z7aFvESz30$DjTzIqqyD5c($cS_k(7KkoUv5S6tE zNj>v1?-qPkm*F1z?a;Odv9X#K!SDxhgg!eM{?dv<<}j(xHAkb=ols!eV5iZO&KzuK z(Yb*odk>pPA|Z^9POM1-_})6>HYW}48TR6zpHO=F1_!Y2UrABSs7dH%ew6(_nnq&H zcA`dFf$Nd_Yigv#-5SUSB`@sA$Ohcctz4M;sr7}qwDGqp zejnZ-gL3Ui#J#>{D*;V7$pBWWaA`A|d%F=l^XH!w&N|f8_EXrb1MG|!6)}f6274c2 zzTC^H;Q84lUOkQAo{IGl=RZf7X8cDA%mzN8MBDnpWONa=C4n-R6?__9K^oD~L-O;W z8D#uS#C2%70|-f8DHN8KX33mzPkK}IobV3U;H2MPgfz)&d-)GHxNw*bOU21}cGg(> zpCL-EL$jS5+=siCdi__I$z0ho2J}-LWf!5-dzcQn1 z+h;*QN=CN)qrGWs`)5;qE08efL`qIfhB4z_))QP3rJMe+l%AfuD#qer$9FfXW{8pI zzzkFGt0CFqqk7;nhHtNK`lF+lg+@t<>fG!lHJ@Mrk?8!TX9A>T0;ciXEtmW>tjU|1 z-c~8pr)v|qV0E=H{y1q3<^S(6@ZYjf<}3lCM<4NMvgR+ul%0oAhfv0<+S|V1t4q6W zgwjuoV(tgE4hh8_t!oT_6=1_%K9mY_b|D1`yYzK>DNyFiyI&k5dk@-}7;=K<5*?WU zgRA2Qvl2_`|JHj)hAUm+5CI-+_Sfiz$FMwFDhDjjI7|6Qm-s`TJ`;$$mYU|+*uVQv ztrFcZLKw9(7lzLM4m4o*=)<3aI-1?q7Z-ZpbzAi4Fmn&A)l!k(os9TN49_eKbCqQN zYAVf(w&*Ep<`JZ}svFsk;bKh$&N_QAe`go1A$^SD;j=Cmwb*f&PuChh+?;lQ37O+6 zSNpN=7ho?lKC9<{1v9sN_6vyO0J6U)B`)324FzSc{wt)l>7D=DnVzqm%n82UrqY!q z!uIZG5&HIvJue6*d%Sj~i)$d*f(MUa@y?NL0z7BMOTL@5&+l6KAm@W64!1Bef+AjA zY!KgKjzn~T0NaG=kszy)zqquCiIvRtfIS;#5H4fQ79JBN@UGUci@zB-$|>!nI_@=T zJC~7IFAL?!ik-X8Wbk;kM_oEb6%I-l0N%|0aKpS_2!&(&xIC_}I_?hUVg~yiG!0;Z z^ZISdt@_asJmR^-JPs-D+vsxE(MZ8h|AIkm6p>_6H4L3|SscUau_1M2a_T;78W9{QM=I9h= zm2tR|0b{jq(#xKmS5c3ij8@x~fM@ApEm1BUHfB|GLfaj5sr&E5_Ao@OZfY^^3C9k_ z-BPYd-J|K^WD2X$DerU}$X2KbwI6&#X=_t**m zHl|68s`Wmo6%cEsekS%mk09uao}Z;_MBb6w62M_4KRjg zDYCpsW71g=li6!V>!~jW^i&4miN}$d#0uKrzG95F6>EW~nvCzC55d-6;|do7mgDmw ziiYF9UcyhN8w_%t%&RMAF?Q(^%axx1_EJTc;#UTH+kBR;hvuG>vy&z+o{N`pW>$=- z4c~H(WizTo!1jB* z)Ue07?*Pk%Px5Cz%)}{2!Vn<*Pw3f)H6gxkblVa{^{|cB7)xjwysf+ zcFi$i{FY)Do*3+LF<&g>x$b@CS$$lkR%Pbj8@8q{eN08LM&eq&b}8y)m)@WzMK{dO zd5DtqR9bEEm^f&vU}3l1giNc3Nf13!hLM_x_(s=knyzfM@@-8i7?7JG6c%dhwTZHk zB&K$(@?``czHQ+j(-yJuS=<0hF4PA?1g1j=!h)~$-nw0@OQ7?-hxBu*vuXC_!19;g@1XQXxyo{)4v1Vse+C zWpf4l{oPxNoR6n+A`WVfw$~9;Y_9`q*!>`_ElsgBqe&X&zVnEo4NdcCsvA7;A~WrD zj!}mgxf4H@uZS=%ap&s`{O@dpYsZbt#lLCyQm)Y9$Y0TGVQ$kC87#o7nNq>k;%L&4 zgRi*M$YP*QU{%ZUXeba%Yx+Co^pJS56M$1nkgXV~Z4F5|T#GC7-JcKe2tL_zoO_iq z>r)FRH8n#=)EL&&Zzm^x*V5Sdm`eE>D?!4?4ly1%D3dq%RU)(GbXpb}8R@E~s%j0+ zyF}GtPX7vpop*H1pc$i#d_HJFr9+3ynWZ6{4S@9Z#Xe9AYr}6&BJq>X`%(g7I|~>5 zI>s`dNLW3%>n; zeH!%_xwBP66I#;dzJ-Mx$Gw*A9NIK~voHg}^yYxU?~cqDpq@m&l&Yspbf_*z+TyAj zg@_O|Y{Y+2Uq+?fA7;RBVlSPVmN&Ug1M{DYkZ6>eSv{JV@SKgW64@%J{(yKwcy<5a z3{FW3TWHyZY|L$qbQ|<}2k4Fzezyj!q6}#jCuirj6!a3-A`Nm&S^b;Y3&WEQ1j~oi z!yH)j+{4o_<;gl{pL5m^eh6SL2C0rVez^-g89<#~o`ouY_{)NGnRezlj!_>W&Laey zS082!5HQmu*XIIPbeJVI3d_ky(UI3Oe-0Y97{*#|<=-l@u73j`3MY?R$OrDpUnl~tC-nh_iJRc3a}_X|F?{tazShLbFpK$T-X*Cu{JfnP4%~2p5}qX=v0xc z+!zOs(M_Dl)N?jNs-(1v|BE}*s3f;8j#JmV-O9|NtW>nnY_Q?pN=zlwEG#W8$_bSQ z&9s!8g#!prTHex3BMsA3(wx9qQ%V!cAyO-}Qk=k?6%lj50VOVXt@pkkTFY9?m$$y0 zwaghtiwjQ4Lk?@fSr%0K(WSPT*n{WZ$B)+rN4y=ajPtrT zDN4y-zv1yt!U!Y}>yX?}6YsJ!QacUlU=nTUqKXDfhtbJ2;OnF#Sy?H#fJl=VoQnhbp(ROX_ z>Dx>1M|t0TU5`|XF;)r>NO@=U)ImJW|MC{vK1MS*#&6>)1&xN)5@N|P+(ddWhSiR) zNK>7taYOLVYDTA#uz>rQatGhkEXM#KA&PYrNjc%#lYDS&1tMsaHp&bdjWnKY`|yqu zq<}BVPwnucj0})UZVXjZEyqqKZ4zRR4_rDfU$lc~??UdHJeczPTFk5qs)Rus9x1-t zvvzvyZpi_6e1Dh7w@b~pcBBC1OtM8;XSDAdIRxw7dN>_{2SUxAg>>BTbb83L-l@ny zrYSSmzPKAj4j@!S39Gy582O`Y*AEDCGmcdi>$KOCG1aL*Oo|a0k7~e&0-8jssPP0p z0UO+k>nV>^h)`+WH=#g&>Vw6WIBimCtJ?$HBTm2#m!eo@Gq-a)j3Nwspy=K!ftd!p z{>gq~2#~!C1$7}brzZ7S_CZB~^glfVvnF$Ar;s&A`<$n0&+QYN>MGrtEoJqgX_)ms zG(N&%QFW~IU4HD|!d7^;`$R|1kfO_FMo;5&@-)pbx@6Tc%mcj`mgXR50`p<#GYHyb zk~|(9y;RJkhE0M4-i@ z>=O+4+XgV}!>JdEp0r@(W8GoA+3Z$!S?{8ABroQ5dfx7anN*u6uGnO)>65nWkoW%J znrP<-+PYw`TInwqX@9y%^k<$8@F?FAr8XR8QBHJ+rn?TNDGtOb6aBAPh-jx$;wAyh z>?6G;PvY7|*pda>yRSlU%J-{cLHxHYPvxxCc5X%7&%)s9rJ0*WW1JPn zol!A~5{2+1ou>jU<&Jpy`!^9iFDyROW@)ol-81gri1sAFC zm%%gYgU28g(w2KHRSxGbX)^#>%u&H5gq2p1bXQ)krMRVSY#=WaT*xyr;|s%fC@Gh% z$#;5V;rraWXqcq;0A|ewb34X^rB4&vttPpCgCA>PCs6$bTTK)hft02;LY4{@L$o)a zP3-J-1)Mp3en=I%OUPkQxsFq~TDclCQ{j@K&bYK*5-0P1)-L_Tm<*^^z3X-7&F5iY zqdxNuFFkjvWQ1Fv=TvQ*u!@V8Xl|6~y>J2=PWh32`RlExf*Wi0zh0r&>PBawr(tii zj|zOtzPt{mt_rnVYRW2#3h`oh7gFvZ;mnOtKt3YfEa?;Si+(409Xb9gim5sKndxmm zEgteSnJz#BKrh>F-CEx--FE!IVg^|v{g-epFuqH-O{K)Op&o9`3!O?f)Zz`T;b?ih zeG{3`EIfbym#LPPZOqtMyUl{=(3a?%VN(!qd0SlI1^*!vP28)8wt34#pVZe33H96Y~!0^x<;m^Nj)PJt<`mE zihHq_^O${;{xE{`bW$O&GZ0nW@j#;0aoNdEg?O4{73w; zOA(3JA6?u-iAUoa{O~+?Jde472Q291z1+aNIoQC+qN&$H4dPomEC%2CK1UZzFYISf z>H6U3?F3DJTi??I@qyD%1rvWgaMqY=Y}o@XdomCrp+G9$@wO>%TGjX75(r2W3Ex0R zO`+agCsE5JRCD&wzKy>90Dk35)DuJ*ph^(lj1W0WOz`T!GI-!{Ayx$#qsQ)0q&=in zC`IMg5SeuMLXlfxN0Rt0{}OYC=EH(Sp6nsQ1vZSbEF(6xioPUv3)_^9QeC?1QtONKe9zPhbr2e0DSh)XzQ=J!Ux!|8{IehS#d5+oX;J*P7grvy; delta 50263 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�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

XTpF{J|i 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%0DG%+WONpP1&&n%fA3Wx%Xd^QT+=cz5 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 A0ssI2 diff --git a/public/data/blockfighter.json b/public/data/blockfighter.json new file mode 100644 index 0000000..b27f6f2 --- /dev/null +++ b/public/data/blockfighter.json @@ -0,0 +1,44 @@ +{ + "levels": [ + { "level": 1, "opponentId": "ethel", "skill": 1, "speed": 1, "tagline": "A gentle warm-up over tea.", + "dropPattern": ["RRRRRR", "GGGGGG", "BBBBBB", "YYYYYY"] }, + { "level": 2, "opponentId": "kona", "skill": 1, "speed": 1, "tagline": "Good puppy. Surprisingly good at gems.", + "dropPattern": ["YYYYYY", "BBBBBB", "GGGGGG", "RRRRRR"] }, + { "level": 3, "opponentId": "bernie", "skill": 2, "speed": 1, "tagline": "All fun and games... until the gems drop.", + "dropPattern": ["RRRGGG", "RRRGGG", "BBBYYY", "BBBYYY"] }, + { "level": 4, "opponentId": "brad", "skill": 2, "speed": 1, "tagline": "Less salmon, more smashing.", + "dropPattern": ["GGGYYY", "GGGYYY", "RRRBBB", "RRRBBB"] }, + { "level": 5, "opponentId": "jerry", "skill": 3, "speed": 2, "tagline": "Y'all ready for a real scrap?", + "dropPattern": ["RRGGBB", "RRGGBB", "YYRRGG", "YYRRGG"] }, + { "level": 6, "opponentId": "jeff", "skill": 3, "speed": 2, "tagline": "Plays slow. Wins fast. You've been warned.", + "dropPattern": ["BBYYRR", "BBYYRR", "GGBBYY", "GGBBYY"] }, + { "level": 7, "opponentId": "mario", "skill": 4, "speed": 2, "tagline": "Welcome to the maze of falling gems!", + "dropPattern": ["RRGGYY", "BBRRGG", "YYBBRR", "GGYYBB"] }, + { "level": 8, "opponentId": "juliet", "skill": 4, "speed": 2, "tagline": "A summer day, a storm of gems.", + "dropPattern": ["GGBBRR", "YYGGBB", "RRYYGG", "BBRRYY"] }, + { "level": 9, "opponentId": "michael", "skill": 5, "speed": 3, "tagline": "Easy vibes, heavy chains, mon.", + "dropPattern": ["RGGBBY", "YRGGBB", "BYRGGB", "BBYRGG"] }, + { "level": 10, "opponentId": "croc", "skill": 5, "speed": 3, "tagline": "The party's over when your board fills up.", + "dropPattern": ["GBYRGB", "BYRGBY", "YRGBYR", "RGBYRG"] }, + { "level": 11, "opponentId": "gerome", "skill": 6, "speed": 3, "tagline": "Extreme victory or nothing!", + "dropPattern": ["RYRYRY", "GBGBGB", "YRYRYR", "BGBGBG"] }, + { "level": 12, "opponentId": "beth", "skill": 6, "speed": 3, "tagline": "These parts have rules, stranger.", + "dropPattern": ["BRBRBR", "YGYGYG", "RBRBRB", "GYGYGY"] }, + { "level": 13, "opponentId": "steve", "skill": 7, "speed": 4, "tagline": "Stupid Earth gems. Prepare to lose.", + "dropPattern": ["RGBYRG", "BYRGBY", "GBYRGB", "YRGBYR"] }, + { "level": 14, "opponentId": "fireball", "skill": 7, "speed": 4, "tagline": "No x-ray eyes. Just flawless drops.", + "dropPattern": ["RRYYRR", "YYRRYY", "GGBBGG", "BBGGBB"] }, + { "level": 15, "opponentId": "natasha", "skill": 8, "speed": 4, "tagline": "Your secrets fall with your gems.", + "dropPattern": ["RGYBGR", "BYGRYB", "GRBYRG", "YBRGBY"] }, + { "level": 16, "opponentId": "victor", "skill": 8, "speed": 4, "tagline": "Every drop calculated. Centuries ago.", + "dropPattern": ["RGBYBG", "YBGRGB", "GYRBRY", "BRYGYR"] }, + { "level": 17, "opponentId": "balam", "skill": 9, "speed": 5, "tagline": "Mystical powers meet falling blocks.", + "dropPattern": ["GYBRYG", "RBYGBR", "YGRBGY", "BRGYRB"] }, + { "level": 18, "opponentId": "cybro", "skill": 9, "speed": 5, "tagline": "The future has already beaten you.", + "dropPattern": ["RBGYRB", "GYRBGY", "BRYGBR", "YGBRYG"] }, + { "level": 19, "opponentId": "zanthor", "skill": 10, "speed": 5, "tagline": "Alacazam! Your board is doomed!", + "dropPattern": ["RYGBRY", "GBRYGB", "YRBGYR", "BGYRBG"] }, + { "level": 20, "opponentId": "blackwind", "skill": 10, "speed": 5, "tagline": "The final showdown on the high seas!", + "dropPattern": ["RGBYRG", "YBRGYB", "GRYBGR", "BYGRBY"] } + ] +} diff --git a/public/src/games/blockfighter/BlockFighterAI.js b/public/src/games/blockfighter/BlockFighterAI.js new file mode 100644 index 0000000..570829a --- /dev/null +++ b/public/src/games/blockfighter/BlockFighterAI.js @@ -0,0 +1,185 @@ +// Block Fighter AI — placement search over the headless engine. +// Skill 1-10 controls lookahead, blunder rate, evaluation richness, and how +// fast the AI physically executes its plan (one input per movePeriodMs). + +import { + WIDTH, HEIGHT, SPAWN_COL, KIND, + legalPlacements, simulatePlacement, makeRng, peekPiece, +} from './BlockFighterLogic.js'; + +// skill → knobs (interpolated linearly between anchor rows) +const SKILL_ANCHORS = [ + { skill: 1, lookahead: 1, blunder: 0.45, movePeriodMs: 620, profile: 'basic' }, + { skill: 3, lookahead: 1, blunder: 0.25, movePeriodMs: 460, profile: 'cluster' }, + { skill: 5, lookahead: 1, blunder: 0.10, movePeriodMs: 330, profile: 'full' }, + { skill: 7, lookahead: 2, blunder: 0.05, movePeriodMs: 230, profile: 'full' }, + { skill: 9, lookahead: 2, blunder: 0.01, movePeriodMs: 150, profile: 'aggressive' }, + { skill: 10, lookahead: 2, blunder: 0.00, movePeriodMs: 110, profile: 'aggressive' }, +]; + +function knobsFor(skill) { + const s = Math.max(1, Math.min(10, skill)); + let lo = SKILL_ANCHORS[0], hi = SKILL_ANCHORS[SKILL_ANCHORS.length - 1]; + for (let i = 0; i < SKILL_ANCHORS.length - 1; i++) { + if (s >= SKILL_ANCHORS[i].skill && s <= SKILL_ANCHORS[i + 1].skill) { + lo = SKILL_ANCHORS[i]; hi = SKILL_ANCHORS[i + 1]; + break; + } + } + const t = hi.skill === lo.skill ? 0 : (s - lo.skill) / (hi.skill - lo.skill); + return { + lookahead: t < 0.5 ? lo.lookahead : hi.lookahead, + blunder: lo.blunder + (hi.blunder - lo.blunder) * t, + movePeriodMs: Math.round(lo.movePeriodMs + (hi.movePeriodMs - lo.movePeriodMs) * t), + profile: t < 0.5 ? lo.profile : hi.profile, + }; +} + +const EVAL_WEIGHTS = { + basic: { attack: 40, cleared: 6, cluster: 0, crash: 0, power: 0, height: 1.0, spawnCol: 2, counters: 0, bumpy: 0 }, + cluster: { attack: 40, cleared: 6, cluster: 2.5, crash: 4, power: 0, height: 1.0, spawnCol: 2, counters: 0.5, bumpy: 0.5 }, + full: { attack: 45, cleared: 5, cluster: 3, crash: 6, power: 5, height: 1.1, spawnCol: 3, counters: 1, bumpy: 1 }, + aggressive: { attack: 60, cleared: 4, cluster: 3.5, crash: 8, power: 7, height: 1.2, spawnCol: 4, counters: 1.5, bumpy: 1 }, +}; + +export function createAI({ skill = 5, speed = 1, seed = 1 } = {}) { + const knobs = knobsFor(skill); + // higher level speed also quickens the AI's hands a bit + knobs.movePeriodMs = Math.round(knobs.movePeriodMs * (1 - 0.08 * (speed - 1))); + return { + skill, + knobs, + rng: makeRng(seed), + plan: null, // { col, orient } + nextActionAt: 0, + useSoftDrop: skill >= 4, + useHardDrop: skill >= 7, + }; +} + +// ── Board evaluation ───────────────────────────────────────────────────────── +function evaluateBoard(player, w) { + const { board } = player; + let score = 0; + + const heights = new Array(WIDTH).fill(0); + for (let c = 0; c < WIDTH; c++) { + for (let r = 0; r < HEIGHT; r++) { + if (board[r][c]) { heights[c] = HEIGHT - r; break; } + } + } + for (let c = 0; c < WIDTH; c++) { + const weight = c === SPAWN_COL ? w.spawnCol : w.height; + score -= weight * heights[c] * heights[c] * 0.25; + } + for (let c = 0; c < WIDTH - 1; c++) score -= w.bumpy * Math.abs(heights[c] - heights[c + 1]); + + let cluster = 0, counters = 0, crashPotential = 0; + for (let r = 0; r < HEIGHT; r++) { + for (let c = 0; c < WIDTH; c++) { + const cell = board[r][c]; + if (!cell) continue; + if (cell.kind === KIND.COUNTER) { counters += 1; continue; } + const right = c + 1 < WIDTH ? board[r][c + 1] : null; + const down = r + 1 < HEIGHT ? board[r + 1][c] : null; + if (right && right.kind !== KIND.COUNTER && right.color === cell.color) cluster += 1; + if (down && down.kind !== KIND.COUNTER && down.color === cell.color) cluster += 1; + if (cell.kind === KIND.CRASH) { + // size of the same-color group this crash gem touches + let touching = 0; + for (const [dr, dc] of [[-1, 0], [1, 0], [0, -1], [0, 1]]) { + const n = board[r + dr]?.[c + dc]; + if (n && n.color === cell.color && n.kind !== KIND.COUNTER) touching += 1; + } + crashPotential += touching; + } + } + } + score += w.cluster * cluster + w.crash * crashPotential - w.counters * counters; + + let power = 0; + for (const gem of player.powerGems.values()) power += Math.pow(gem.w * gem.h, 1.3); + score += w.power * power; + + return score; +} + +function scorePlacement(result, w) { + if (!result) return -Infinity; + if (result.died) return -1e9; + return w.attack * result.attack + w.cleared * result.cleared + evaluateBoard(result.player, w); +} + +// ── Planning ───────────────────────────────────────────────────────────────── +export function planPlacement(ai, match, pIdx) { + const player = match.players[pIdx]; + if (!player.piece) return null; + const w = EVAL_WEIGHTS[ai.knobs.profile]; + const options = []; + + for (const { col, orient } of legalPlacements(player)) { + const result = simulatePlacement(match, pIdx, col, orient); + if (!result) continue; + let score = scorePlacement(result, w); + + if (ai.knobs.lookahead >= 2 && score > -1e8) { + // probe the next shared piece on the resulting board + const nextProto = peekPiece(match, player.pieceIndex); + if (nextProto) { + const ghost = { + players: pIdx === 0 + ? [result.player, match.players[1]] + : [match.players[0], result.player], + over: false, winner: null, headless: true, + }; + result.player.piece = { a: { ...nextProto.a }, b: { ...nextProto.b }, row: 1, col: SPAWN_COL, orient: 0 }; + let best2 = -Infinity; + for (const p2 of legalPlacements(result.player)) { + const r2 = simulatePlacement(ghost, pIdx, p2.col, p2.orient); + if (!r2) continue; + const s2 = scorePlacement(r2, w); + if (s2 > best2) best2 = s2; + } + result.player.piece = null; + if (best2 > -Infinity) score = score * 0.6 + best2 * 0.4; + } + } + options.push({ col, orient, score }); + } + + if (!options.length) { ai.plan = null; return null; } + options.sort((a, b) => b.score - a.score); + + let pick = options[0]; + if (ai.rng() < ai.knobs.blunder) { + pick = options[Math.floor(ai.rng() * Math.min(options.length, 5))]; + } + ai.plan = { col: pick.col, orient: pick.orient }; + ai.nextActionAt = 0; + return ai.plan; +} + +// ── Execution: one input per movePeriodMs ──────────────────────────────────── +export function nextAction(ai, match, pIdx, nowMs) { + const player = match.players[pIdx]; + if (!player.piece || !ai.plan) return null; + if (nowMs < ai.nextActionAt) return null; + + const piece = player.piece; + let action = null; + if (piece.orient !== ai.plan.orient) { + const cwDist = (ai.plan.orient - piece.orient + 4) % 4; + action = cwDist <= 2 ? 'rotateCW' : 'rotateCCW'; + } else if (piece.col > ai.plan.col) { + action = 'left'; + } else if (piece.col < ai.plan.col) { + action = 'right'; + } else if (ai.useHardDrop) { + action = 'hardDrop'; + } else if (ai.useSoftDrop) { + action = 'softDrop'; + } + + if (action) ai.nextActionAt = nowMs + ai.knobs.movePeriodMs; + return action; +} diff --git a/public/src/games/blockfighter/BlockFighterGame.js b/public/src/games/blockfighter/BlockFighterGame.js new file mode 100644 index 0000000..408ded7 --- /dev/null +++ b/public/src/games/blockfighter/BlockFighterGame.js @@ -0,0 +1,818 @@ +import * as Phaser from 'phaser'; +import { GAME_WIDTH, GAME_HEIGHT, COLORS } from '../../config.js'; +import { Button } from '../../ui/Button.js'; +import { MusicPlayer } from '../../ui/MusicPlayer.js'; +import { playSound, SFX } from '../../ui/Sounds.js'; +import { api } from '../../services/api.js'; +import { createOpponentPortrait, createPlayerPortrait } from '../../ui/Portrait.js'; +import { + WIDTH, VISIBLE_ROWS, HIDDEN_ROWS, KIND, SPEED_GRAVITY_MS, + createMatch, spawnPiece, stepDown, hardDrop, moveLeft, moveRight, + rotateCW, rotateCCW, getGhostCells, pieceCells, peekPiece, +} from './BlockFighterLogic.js'; +import { createAI, planPlacement, nextAction } from './BlockFighterAI.js'; + +const CELL = 52; +const BOARD_W = CELL * WIDTH; // 312 +const BOARD_H = CELL * VISIBLE_ROWS; // 624 +const BOARD_TOP = 240; +const BOARD_LEFT = [360, GAME_WIDTH - 360 - BOARD_W]; // P1 left, AI right + +const FELT = 0x101626; +const FRAME = 0x0a1020; +const CELLBG = 0x182238; +const GRIDLN = 0x243352; +const GEM_COLORS = [0xe04444, 0x2ecc71, 0x3f8efc, 0xf1c40f]; +const GEM_HEX = ['#e04444', '#2ecc71', '#3f8efc', '#f1c40f']; + +const D = { felt: -2, frame: -1, grid: 0, cells: 5, piece: 8, fx: 12, ui: 30, overlay: 60, overlayUI: 62 }; +const REPLAY_DELAY = { place: 70, settle: 150, clear: 230, diamond: 260, counter: 140, garbage: 200, attack: 60, spawn: 0, lose: 0 }; + +export default class BlockFighterGame extends Phaser.Scene { + constructor() { super('BlockFighterGame'); } + + init(data) { + this.gameDef = data.game ?? { slug: 'blockfighter', name: 'Block Fighter' }; + this.bank = []; + this.roster = []; + this.levelsCompleted = 0; + this.canPersist = true; + this.view = 'select'; + this.portraits = []; // active Portrait handles (DOM-backed, not in layer) + this.match = null; + this.overlayUp = false; + } + + async create() { + try { + const music = this.cache.json.get('music'); + if (music?.tracks) new MusicPlayer(this, music.tracks); + } catch (_) { /* optional */ } + + this.add.rectangle(GAME_WIDTH / 2, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT, FELT).setDepth(D.felt); + + const raw = this.cache.json.get('blockfighter'); + this.bank = (raw?.levels ?? []).slice().sort((a, b) => a.level - b.level); + + try { + const res = await fetch('/data/opponents.json'); + const json = await res.json(); + this.roster = json.opponents ?? []; + } catch (_) { this.roster = []; } + + try { + const res = await api.get('/puzzles/blockfighter/progress'); + this.levelsCompleted = res?.levelsCompleted ?? 0; + } catch (_) { + this.canPersist = false; + this.levelsCompleted = 0; + } + + this.makeTextures(); + this.layer = this.add.container(0, 0); + this.showLevelSelect(); + } + + opponentFor(levelDef) { + const opp = this.roster.find((o) => o.id === levelDef.opponentId); + if (opp) return opp; + console.warn(`blockfighter: opponent '${levelDef.opponentId}' not in roster; using stub`); + return { id: levelDef.opponentId, spriteIndex: 0, name: levelDef.opponentId, bio: '', speech: {} }; + } + + // ── Generated gem textures ────────────────────────────────────────────────── + + makeTextures() { + if (this.textures.exists('bf-gem-0')) return; + for (let c = 0; c < 4; c++) { + const color = GEM_COLORS[c]; + let g = this.make.graphics({ add: false }); + g.fillStyle(color, 1); + g.fillRoundedRect(2, 2, CELL - 4, CELL - 4, 10); + g.fillStyle(0xffffff, 0.32); + g.fillRoundedRect(7, 6, CELL - 14, 14, 6); + g.lineStyle(2, 0x000000, 0.35); + g.strokeRoundedRect(2, 2, CELL - 4, CELL - 4, 10); + g.generateTexture(`bf-gem-${c}`, CELL, CELL); + g.destroy(); + + g = this.make.graphics({ add: false }); + g.fillStyle(0x000000, 0.25); + g.fillCircle(CELL / 2, CELL / 2, CELL / 2 - 2); + g.fillStyle(color, 1); + g.fillCircle(CELL / 2, CELL / 2, CELL / 2 - 5); + g.fillStyle(0xffffff, 0.85); + g.fillCircle(CELL / 2, CELL / 2, 7); + g.lineStyle(3, 0xffffff, 0.5); + g.strokeCircle(CELL / 2, CELL / 2, CELL / 2 - 10); + g.generateTexture(`bf-crash-${c}`, CELL, CELL); + g.destroy(); + + g = this.make.graphics({ add: false }); + g.fillStyle(color, 0.45); + g.fillRoundedRect(3, 3, CELL - 6, CELL - 6, 8); + g.lineStyle(3, color, 0.95); + g.strokeRoundedRect(3, 3, CELL - 6, CELL - 6, 8); + g.generateTexture(`bf-counter-${c}`, CELL, CELL); + g.destroy(); + } + const g = this.make.graphics({ add: false }); + g.fillStyle(0xffffff, 1); + const m = CELL / 2; + g.fillPoints([ + { x: m, y: 3 }, { x: CELL - 4, y: m }, { x: m, y: CELL - 3 }, { x: 4, y: m }, + ], true); + g.fillStyle(0xb8e8ff, 0.85); + g.fillPoints([ + { x: m, y: 12 }, { x: CELL - 13, y: m }, { x: m, y: CELL - 12 }, { x: 13, y: m }, + ], true); + g.generateTexture('bf-diamond', CELL, CELL); + g.destroy(); + } + + // ── View management ───────────────────────────────────────────────────────── + + clearLayer() { + for (const p of this.portraits) { try { p.destroy(); } catch (_) {} } + this.portraits = []; + this.input.keyboard.off('keydown', this.onKeyDown, this); + this.layer.removeAll(true); + this.boardObjs = null; + } + + // ── Level select ──────────────────────────────────────────────────────────── + + showLevelSelect() { + this.view = 'select'; + this.overlayUp = false; + this.match = null; + this.clearLayer(); + const cx = GAME_WIDTH / 2; + + const title = this.add.text(cx, 84, 'BLOCK FIGHTER', { + fontFamily: 'Righteous', fontSize: '64px', color: COLORS.goldHex, + }).setOrigin(0.5); + const sub = this.add.text(cx, 138, 'Match gems, drop crash gems, and bury your rival in counter gems. Beat each fighter to unlock the next.', { + fontFamily: '"Julius Sans One"', fontSize: '22px', color: COLORS.mutedHex, + }).setOrigin(0.5); + this.layer.add([title, sub]); + + if (!this.bank.length) { + const msg = this.add.text(cx, 520, 'No levels found in /data/blockfighter.json', { + fontFamily: '"Julius Sans One"', fontSize: '26px', color: COLORS.dangerHex, align: 'center', + }).setOrigin(0.5); + const back = new Button(this, cx, GAME_HEIGHT - 90, 'Back', () => this.scene.start('GameMenu'), { variant: 'ghost' }); + this.layer.add([msg, back]); + return; + } + + const nextLevel = Math.min(this.levelsCompleted + 1, this.bank.length); + const prog = this.add.text(cx, 182, `Defeated ${this.levelsCompleted} / ${this.bank.length}`, { + fontFamily: 'Righteous', fontSize: '24px', color: COLORS.textHex, + }).setOrigin(0.5); + this.layer.add(prog); + + const COLS = 10; + const SIZE = 128; + const GAP = 16; + const gridW = COLS * SIZE + (COLS - 1) * GAP; + const left = cx - gridW / 2 + SIZE / 2; + const top = 300; + + this.bank.forEach((lv, i) => { + const col = i % COLS; + const row = Math.floor(i / COLS); + const x = left + col * (SIZE + GAP); + const y = top + row * (SIZE + GAP + 36); + const level = lv.level; + const cleared = level <= this.levelsCompleted; + const playable = level <= nextLevel; + const opp = this.opponentFor(lv); + + const fill = cleared ? 0x1f5c3a : playable ? 0x1e3a52 : 0x16202b; + const stroke = cleared ? 0x2ecc71 : playable ? COLORS.gold : 0x2a3744; + const tile = this.add.rectangle(x, y, SIZE, SIZE + 28, fill).setStrokeStyle(playable || cleared ? 3 : 2, stroke, 1); + const num = this.add.text(x, y - SIZE / 2 + 22, String(level), { + fontFamily: 'Righteous', fontSize: '26px', + color: playable || cleared ? COLORS.textHex : '#54606b', + }).setOrigin(0.5); + const objs = [tile, num]; + + if (this.textures.exists('opponents')) { + const face = this.add.image(x, y + 6, 'opponents', opp.spriteIndex ?? 0).setDisplaySize(76, 76); + if (!playable && !cleared) { face.setTint(0x333a44); face.setAlpha(0.7); } + objs.push(face); + } + const tag = this.add.text(x, y + SIZE / 2 + 2, cleared ? `✓ ${opp.name}` : playable ? opp.name : 'locked', { + fontFamily: '"Julius Sans One"', fontSize: '15px', + color: cleared ? '#9be7b4' : playable ? COLORS.mutedHex : '#54606b', + }).setOrigin(0.5); + objs.push(tag); + this.layer.add(objs); + + if (playable) { + tile.setInteractive({ useHandCursor: true }); + tile.on('pointerover', () => tile.setStrokeStyle(4, COLORS.gold, 1)); + tile.on('pointerout', () => tile.setStrokeStyle(3, stroke, 1)); + tile.on('pointerup', () => this.showIntro(level)); + } + }); + + const resume = new Button(this, cx - 150, GAME_HEIGHT - 78, `Fight Level ${nextLevel}`, () => this.showIntro(nextLevel), + { width: 280, height: 58, fontSize: 24 }); + const back = new Button(this, cx + 170, GAME_HEIGHT - 78, 'Back', () => this.scene.start('GameMenu'), + { variant: 'ghost', width: 180, height: 58, fontSize: 24 }); + const reset = new Button(this, 210, GAME_HEIGHT - 78, 'Reset Progress', () => this.confirmResetProgress(), + { variant: 'ghost', width: 260, height: 58, fontSize: 22, textColor: COLORS.dangerHex }); + this.layer.add([resume, back, reset]); + + if (!this.canPersist) { + const note = this.add.text(cx, GAME_HEIGHT - 28, 'Sign in to save your progress across devices.', { + fontFamily: '"Julius Sans One"', fontSize: '16px', color: COLORS.mutedHex, + }).setOrigin(0.5); + this.layer.add(note); + } + } + + confirmResetProgress() { + const cx = GAME_WIDTH / 2; + const cy = GAME_HEIGHT / 2; + const dim = this.add.rectangle(cx, cy, GAME_WIDTH, GAME_HEIGHT, 0x000000, 0.62).setDepth(D.overlay).setInteractive(); + const panel = this.add.graphics().setDepth(D.overlay); + panel.fillStyle(COLORS.panel, 0.98); + panel.fillRoundedRect(cx - 320, cy - 160, 640, 320, 20); + panel.lineStyle(3, COLORS.danger, 1); + panel.strokeRoundedRect(cx - 320, cy - 160, 640, 320, 20); + const title = this.add.text(cx, cy - 92, 'Reset Progress?', { + fontFamily: 'Righteous', fontSize: '52px', color: COLORS.dangerHex, + }).setOrigin(0.5).setDepth(D.overlayUI); + const msg = this.add.text(cx, cy - 14, + 'This clears every fighter you have beaten and\nstarts you back at Level 1. This cannot be undone.', { + fontFamily: '"Julius Sans One"', fontSize: '24px', color: COLORS.textHex, align: 'center', lineSpacing: 6, + }).setOrigin(0.5).setDepth(D.overlayUI); + const yes = new Button(this, cx - 150, cy + 88, 'Reset', () => { + api.post('/puzzles/blockfighter/reset').catch(() => {}); + this.levelsCompleted = 0; + this.showLevelSelect(); + }, { width: 250, height: 58, fontSize: 24, textColor: COLORS.dangerHex }).setDepth(D.overlayUI); + const no = new Button(this, cx + 150, cy + 88, 'Cancel', () => this.showLevelSelect(), + { variant: 'ghost', width: 250, height: 58, fontSize: 24 }).setDepth(D.overlayUI); + this.layer.add([dim, panel, title, msg, yes, no]); + } + + // ── Pre-battle intro ──────────────────────────────────────────────────────── + + showIntro(level) { + const lv = this.bank.find((l) => l.level === level); + if (!lv) return; + this.view = 'intro'; + this.clearLayer(); + const cx = GAME_WIDTH / 2; + const opp = this.opponentFor(lv); + + const title = this.add.text(cx, 110, `LEVEL ${level}`, { + fontFamily: 'Righteous', fontSize: '48px', color: COLORS.goldHex, + }).setOrigin(0.5); + const vs = this.add.text(cx, 560, 'VS', { + fontFamily: 'Righteous', fontSize: '40px', color: COLORS.mutedHex, + }).setOrigin(0.5).setAlpha(0.6); + this.layer.add([title, vs]); + + this.portraits.push(createOpponentPortrait(this, opp, cx, 360, 150, D.ui, { playIntro: true })); + + const name = this.add.text(cx, 552 + 80, opp.name, { + fontFamily: 'Righteous', fontSize: '54px', color: COLORS.textHex, + }).setOrigin(0.5); + const bio = this.add.text(cx, 552 + 140, opp.bio ?? '', { + fontFamily: '"Julius Sans One"', fontSize: '24px', color: COLORS.mutedHex, + }).setOrigin(0.5); + const tagline = this.add.text(cx, 552 + 188, lv.tagline ?? '', { + fontFamily: '"Julius Sans One"', fontSize: '26px', color: COLORS.goldHex, fontStyle: 'italic', + }).setOrigin(0.5); + + const stars = '★'.repeat(Math.ceil(lv.skill / 2)) + '☆'.repeat(5 - Math.ceil(lv.skill / 2)); + const bolts = '⚡'.repeat(lv.speed); + const statText = this.add.text(cx, 552 + 248, `Skill ${stars} Speed ${bolts}`, { + fontFamily: '"Julius Sans One"', fontSize: '28px', color: COLORS.textHex, + }).setOrigin(0.5); + this.layer.add([name, bio, tagline, statText]); + + const fight = new Button(this, cx - 130, GAME_HEIGHT - 110, 'FIGHT!', () => this.startBattle(level), + { width: 240, height: 66, fontSize: 30 }); + const back = new Button(this, cx + 140, GAME_HEIGHT - 110, 'Back', () => this.showLevelSelect(), + { variant: 'ghost', width: 200, height: 66, fontSize: 24 }); + this.layer.add([fight, back]); + } + + // ── Battle setup ──────────────────────────────────────────────────────────── + + startBattle(level) { + const lv = this.bank.find((l) => l.level === level); + if (!lv) return; + this.view = 'battle'; + this.overlayUp = false; + this.level = level; + this.levelDef = lv; + this.opponent = this.opponentFor(lv); + this.clearLayer(); + + const seed = (Date.now() ^ (Math.random() * 0xffffffff)) >>> 0; + this.match = createMatch({ seed, dropPatterns: [lv.dropPattern, lv.dropPattern] }); + this.ai = createAI({ skill: lv.skill, speed: lv.speed, seed: seed ^ 0x9e3779b9 }); + this.gravityMs = SPEED_GRAVITY_MS[Math.max(1, Math.min(5, lv.speed))]; + this.gravTimer = [0, 0]; + this.replayQueue = [[], []]; + this.replayTimer = [0, 0]; + this.matchEnded = false; + + this.drawBattleChrome(); + + this.input.keyboard.on('keydown', this.onKeyDown, this); + + for (const i of [0, 1]) this.beginSpawn(i); + } + + drawBattleChrome() { + const cx = GAME_WIDTH / 2; + const hud = this.add.text(cx, 56, `Level ${this.level} — vs ${this.opponent.name}`, { + fontFamily: 'Righteous', fontSize: '38px', color: COLORS.goldHex, + }).setOrigin(0.5).setDepth(D.ui); + this.layer.add(hud); + + this.boardObjs = []; + this.pieceImgs = []; + this.ghostImgs = []; + this.garbageTexts = []; + this.sentTexts = []; + this.previewImgs = []; + + for (const i of [0, 1]) { + const left = BOARD_LEFT[i]; + const g = this.add.graphics().setDepth(D.frame); + g.fillStyle(FRAME, 1); + g.fillRoundedRect(left - 16, BOARD_TOP - 16, BOARD_W + 32, BOARD_H + 32, 14); + g.fillStyle(CELLBG, 1); + g.fillRect(left, BOARD_TOP, BOARD_W, BOARD_H); + this.layer.add(g); + + const grid = this.add.graphics().setDepth(D.grid); + grid.lineStyle(1, GRIDLN, 0.8); + for (let c = 0; c <= WIDTH; c++) grid.lineBetween(left + c * CELL, BOARD_TOP, left + c * CELL, BOARD_TOP + BOARD_H); + for (let r = 0; r <= VISIBLE_ROWS; r++) grid.lineBetween(left, BOARD_TOP + r * CELL, left + BOARD_W, BOARD_TOP + r * CELL); + // danger marker over the spawn column + grid.fillStyle(0xe04444, 0.5); + grid.fillTriangle( + left + 3 * CELL + CELL / 2 - 12, BOARD_TOP - 16, + left + 3 * CELL + CELL / 2 + 12, BOARD_TOP - 16, + left + 3 * CELL + CELL / 2, BOARD_TOP - 4, + ); + this.layer.add(grid); + + const label = this.add.text(left + BOARD_W / 2, BOARD_TOP + BOARD_H + 36, i === 0 ? 'YOU' : this.opponent.name.toUpperCase(), { + fontFamily: 'Righteous', fontSize: '24px', color: i === 0 ? COLORS.accentHex ?? '#5bc0de' : COLORS.dangerHex, + }).setOrigin(0.5).setDepth(D.ui); + this.layer.add(label); + + // pending garbage indicator + sent counter above the board + const gt = this.add.text(left + BOARD_W / 2, BOARD_TOP - 44, '', { + fontFamily: 'Righteous', fontSize: '26px', color: COLORS.dangerHex, + }).setOrigin(0.5).setDepth(D.ui); + this.garbageTexts.push(gt); + const st = this.add.text(left + (i === 0 ? -16 : BOARD_W + 16), BOARD_TOP - 44, 'Sent: 0', { + fontFamily: '"Julius Sans One"', fontSize: '20px', color: COLORS.mutedHex, + }).setOrigin(i === 0 ? 1 : 0, 0.5).setDepth(D.ui); + this.sentTexts.push(st); + this.layer.add([gt, st]); + + // next-piece preview in the centre gutter + const px = i === 0 ? BOARD_LEFT[0] + BOARD_W + 90 : BOARD_LEFT[1] - 90; + const pg = this.add.graphics().setDepth(D.frame); + pg.fillStyle(FRAME, 1); + pg.fillRoundedRect(px - 44, 290 - 70, 88, 150, 10); + const pl = this.add.text(px, 290 - 92, 'NEXT', { + fontFamily: '"Julius Sans One"', fontSize: '18px', color: COLORS.mutedHex, + }).setOrigin(0.5).setDepth(D.ui); + this.layer.add([pg, pl]); + this.previewImgs.push([ + this.add.image(px, 290 - 28, 'bf-gem-0').setDepth(D.ui).setVisible(false), + this.add.image(px, 290 + 28, 'bf-gem-0').setDepth(D.ui).setVisible(false), + ]); + this.layer.add(this.previewImgs[i]); + + this.boardObjs.push([]); // cell images, rebuilt per render + this.pieceImgs.push([ + this.add.image(0, 0, 'bf-gem-0').setDepth(D.piece).setVisible(false), + this.add.image(0, 0, 'bf-gem-0').setDepth(D.piece).setVisible(false), + ]); + this.layer.add(this.pieceImgs[i]); + this.ghostImgs.push([ + this.add.rectangle(0, 0, CELL - 8, CELL - 8).setStrokeStyle(2, 0xffffff, 0.35).setDepth(D.grid + 1).setVisible(false), + this.add.rectangle(0, 0, CELL - 8, CELL - 8).setStrokeStyle(2, 0xffffff, 0.35).setDepth(D.grid + 1).setVisible(false), + ]); + this.layer.add(this.ghostImgs[i]); + } + + // portraits + this.portraits.push(createPlayerPortrait(this, 165, 380, 84, D.ui, 'BlockFighterGame')); + this.oppPortrait = createOpponentPortrait(this, this.opponent, GAME_WIDTH - 165, 380, 84, D.ui, { playIntro: false }); + this.portraits.push(this.oppPortrait); + + // chain callout + this.calloutText = this.add.text(GAME_WIDTH / 2, 620, '', { + fontFamily: 'Righteous', fontSize: '54px', color: COLORS.goldHex, stroke: '#000000', strokeThickness: 6, + }).setOrigin(0.5).setDepth(D.fx).setAlpha(0); + this.layer.add(this.calloutText); + + const quit = new Button(this, 140, GAME_HEIGHT - 60, 'Levels', () => this.showLevelSelect(), + { variant: 'ghost', width: 180, height: 52, fontSize: 22 }); + this.layer.add(quit); + + this.drawTouchControls(); + } + + drawTouchControls() { + const y = GAME_HEIGHT - 110; + const cx = GAME_WIDTH / 2; + const defs = [ + { x: cx - 290, glyph: '◀', action: 'left', repeat: true }, + { x: cx - 174, glyph: '▶', action: 'right', repeat: true }, + { x: cx - 58, glyph: '⟲', action: 'rotateCCW', repeat: false }, + { x: cx + 58, glyph: '⟳', action: 'rotateCW', repeat: false }, + { x: cx + 174, glyph: '▼', action: 'softDrop', repeat: true }, + { x: cx + 290, glyph: '⤓', action: 'hardDrop', repeat: false }, + ]; + for (const def of defs) { + const g = this.add.graphics().setDepth(D.ui); + g.fillStyle(COLORS.panel, 0.9); + g.fillCircle(def.x, y, 46); + g.lineStyle(2, COLORS.gold, 0.7); + g.strokeCircle(def.x, y, 46); + const t = this.add.text(def.x, y, def.glyph, { + fontFamily: 'Righteous', fontSize: '34px', color: COLORS.textHex, + }).setOrigin(0.5).setDepth(D.ui + 1); + const zone = this.add.zone(def.x, y, 96, 96).setInteractive({ useHandCursor: true }).setDepth(D.ui + 2); + let repeatEvt = null; + const stop = () => { if (repeatEvt) { repeatEvt.remove(); repeatEvt = null; } }; + zone.on('pointerdown', () => { + this.playerAction(def.action); + if (def.repeat) { + stop(); + repeatEvt = this.time.addEvent({ delay: 120, loop: true, callback: () => this.playerAction(def.action) }); + } + }); + zone.on('pointerup', stop); + zone.on('pointerout', stop); + this.layer.add([g, t, zone]); + } + } + + // ── Input ─────────────────────────────────────────────────────────────────── + + onKeyDown(event) { + if (this.view !== 'battle') return; + switch (event.code) { + case 'ArrowLeft': case 'KeyA': this.playerAction('left'); break; + case 'ArrowRight': case 'KeyD': this.playerAction('right'); break; + case 'ArrowDown': case 'KeyS': this.playerAction('softDrop'); break; + case 'ArrowUp': case 'KeyX': if (!event.repeat) this.playerAction('rotateCW'); break; + case 'KeyZ': if (!event.repeat) this.playerAction('rotateCCW'); break; + case 'Space': if (!event.repeat) this.playerAction('hardDrop'); event.preventDefault(); break; + default: break; + } + } + + playerAction(action) { this.applyAction(0, action); } + + applyAction(idx, action) { + const m = this.match; + if (!m || m.over || this.overlayUp || this.view !== 'battle') return; + if (this.replayQueue[idx].length) return; // board busy animating + const p = m.players[idx]; + if (!p.piece) return; + + let locked = false; + let events = null; + switch (action) { + case 'left': if (moveLeft(m, idx) && idx === 0) playSound(this, SFX.PIECE_CLICK); break; + case 'right': if (moveRight(m, idx) && idx === 0) playSound(this, SFX.PIECE_CLICK); break; + case 'rotateCW': if (rotateCW(m, idx) && idx === 0) playSound(this, SFX.PIECE_CLICK); break; + case 'rotateCCW': if (rotateCCW(m, idx) && idx === 0) playSound(this, SFX.PIECE_CLICK); break; + case 'softDrop': { + const r = stepDown(m, idx); + locked = r.locked; events = r.events; + this.gravTimer[idx] = 0; + break; + } + case 'hardDrop': { + events = hardDrop(m, idx); + locked = true; + break; + } + default: break; + } + if (locked) this.onLocked(idx, events); + else this.updatePieceSprites(idx); + } + + // ── Spawn / lock / replay flow ────────────────────────────────────────────── + + beginSpawn(idx) { + const events = spawnPiece(this.match, idx); + this.enqueue(idx, events); + if (this.match.players[idx].piece && idx === 1) { + planPlacement(this.ai, this.match, 1); + } + } + + onLocked(idx, events) { + playSound(this, SFX.CARD_PLACE); + this.updatePieceSprites(idx); // hides the piece (now null) + this.enqueue(idx, events); + } + + enqueue(idx, events) { + if (events?.length) this.replayQueue[idx].push(...events); + this.updateMeters(); + } + + // process one queued event; returns the delay before the next one + processEvent(idx, e) { + const left = BOARD_LEFT[idx]; + switch (e.type) { + case 'spawn': + this.updatePieceSprites(idx); + this.updatePreviews(); + break; + case 'place': + case 'settle': + case 'counter': + this.renderBoard(idx, e); + break; + case 'garbage': + this.renderBoard(idx, e); + if (e.cells?.length) playSound(this, SFX.DICE_ROLL); + break; + case 'clear': + case 'diamond': { + this.renderBoard(idx, e); + playSound(this, SFX.MASTERMIND_MATCH ?? SFX.CARD_SHOW); + for (const cell of e.cells ?? []) { + if (cell.r < HIDDEN_ROWS) continue; + const fx = this.add.rectangle( + left + cell.c * CELL + CELL / 2, + BOARD_TOP + (cell.r - HIDDEN_ROWS) * CELL + CELL / 2, + CELL - 4, CELL - 4, 0xffffff, 0.9, + ).setDepth(D.fx); + this.layer.add(fx); + this.tweens.add({ targets: fx, alpha: 0, scale: 1.4, duration: 260, onComplete: () => fx.destroy() }); + } + if (e.type === 'clear' && e.chain >= 2) { + this.showCallout(`${e.chain} CHAIN!`); + if (idx === 0) this.oppPortrait?.playEmotion('upset'); + else this.oppPortrait?.playEmotion('happy'); + } + break; + } + case 'attack': { + if (e.sent > 0) { + this.showCallout(`${idx === 0 ? 'YOU' : this.opponent.name} +${e.sent} ▶`, idx === 0 ? '#9be7b4' : '#ff8a8a'); + if (e.sent >= 6) this.oppPortrait?.playEmotion(idx === 0 ? 'upset' : 'happy'); + } + break; + } + case 'lose': + this.renderBoard(idx, e); + this.endMatch(); + break; + default: + this.renderBoard(idx, e); + break; + } + this.updateMeters(); + return REPLAY_DELAY[e.type] ?? 120; + } + + showCallout(text, color = COLORS.goldHex) { + this.calloutText.setText(text).setColor(color).setAlpha(1).setScale(0.7); + this.tweens.add({ targets: this.calloutText, scale: 1, duration: 140, ease: 'Back.easeOut' }); + this.tweens.add({ targets: this.calloutText, alpha: 0, delay: 850, duration: 300 }); + } + + // ── Rendering ─────────────────────────────────────────────────────────────── + + renderBoard(idx, snap) { + for (const o of this.boardObjs[idx]) o.destroy(); + this.boardObjs[idx] = []; + const left = BOARD_LEFT[idx]; + const inPower = new Set(); + for (const g of snap.powerGems ?? []) { + for (let r = g.y; r < g.y + g.h; r++) for (let c = g.x; c < g.x + g.w; c++) inPower.add(r * WIDTH + c); + const vr = Math.max(g.y, HIDDEN_ROWS); + const vh = g.y + g.h - vr; + if (vh <= 0) continue; + const gx = left + g.x * CELL; + const gy = BOARD_TOP + (vr - HIDDEN_ROWS) * CELL; + const pg = this.add.graphics().setDepth(D.cells); + pg.fillStyle(GEM_COLORS[g.color], 1); + pg.fillRoundedRect(gx + 2, gy + 2, g.w * CELL - 4, vh * CELL - 4, 12); + pg.fillStyle(0xffffff, 0.28); + pg.fillRoundedRect(gx + 8, gy + 6, g.w * CELL - 16, 16, 8); + pg.lineStyle(3, 0xffffff, 0.55); + pg.strokeRoundedRect(gx + 4, gy + 4, g.w * CELL - 8, vh * CELL - 8, 10); + pg.lineStyle(2, 0x000000, 0.35); + pg.strokeRoundedRect(gx + 2, gy + 2, g.w * CELL - 4, vh * CELL - 4, 12); + this.layer.add(pg); + this.boardObjs[idx].push(pg); + } + for (let r = HIDDEN_ROWS; r < snap.board.length; r++) { + for (let c = 0; c < WIDTH; c++) { + const cell = snap.board[r][c]; + if (!cell || inPower.has(r * WIDTH + c)) continue; + const x = left + c * CELL + CELL / 2; + const y = BOARD_TOP + (r - HIDDEN_ROWS) * CELL + CELL / 2; + const img = this.add.image(x, y, this.textureFor(cell)).setDepth(D.cells); + this.layer.add(img); + this.boardObjs[idx].push(img); + if (cell.kind === KIND.COUNTER) { + const t = this.add.text(x, y, String(cell.count), { + fontFamily: 'Righteous', fontSize: '26px', color: '#ffffff', stroke: '#000000', strokeThickness: 4, + }).setOrigin(0.5).setDepth(D.cells + 1); + this.layer.add(t); + this.boardObjs[idx].push(t); + } + } + } + } + + textureFor(cell) { + if (cell.kind === KIND.DIAMOND) return 'bf-diamond'; + if (cell.kind === KIND.CRASH) return `bf-crash-${cell.color}`; + if (cell.kind === KIND.COUNTER) return `bf-counter-${cell.color}`; + return `bf-gem-${cell.color}`; + } + + updatePieceSprites(idx) { + const p = this.match.players[idx]; + const imgs = this.pieceImgs[idx]; + const ghosts = this.ghostImgs[idx]; + if (!p.piece) { + imgs.forEach((img) => img.setVisible(false)); + ghosts.forEach((g) => g.setVisible(false)); + return; + } + const left = BOARD_LEFT[idx]; + const cells = pieceCells(p.piece); + cells.forEach(({ r, c, half }, i) => { + imgs[i] + .setTexture(this.textureFor(p.piece[half])) + .setPosition(left + c * CELL + CELL / 2, BOARD_TOP + (r - HIDDEN_ROWS) * CELL + CELL / 2) + .setVisible(true) + .setAlpha(r < HIDDEN_ROWS ? 0.45 : 1); + }); + getGhostCells(p).forEach(({ r, c }, i) => { + ghosts[i] + .setPosition(left + c * CELL + CELL / 2, BOARD_TOP + (r - HIDDEN_ROWS) * CELL + CELL / 2) + .setVisible(r >= HIDDEN_ROWS); + }); + } + + updatePreviews() { + if (!this.match) return; + for (const i of [0, 1]) { + const p = this.match.players[i]; + const next = peekPiece(this.match, p.pieceIndex); + this.previewImgs[i][0].setTexture(this.textureFor(next.b)).setVisible(true); + this.previewImgs[i][1].setTexture(this.textureFor(next.a)).setVisible(true); + } + } + + updateMeters() { + if (!this.match || !this.garbageTexts) return; + for (const i of [0, 1]) { + const pending = this.match.players[i].pendingGarbage; + this.garbageTexts[i].setText(pending > 0 ? `⚠ ${pending} incoming` : ''); + this.sentTexts[i].setText(`Sent: ${this.match.players[i].garbageSent}`); + } + } + + // ── Main loop ─────────────────────────────────────────────────────────────── + + update(time, delta) { + if (this.view !== 'battle' || !this.match || this.overlayUp) return; + const m = this.match; + + for (const i of [0, 1]) { + // replay queued engine events (board is frozen while animating) + if (this.replayQueue[i].length) { + this.replayTimer[i] -= delta; + if (this.replayTimer[i] <= 0) { + const e = this.replayQueue[i].shift(); + this.replayTimer[i] = this.processEvent(i, e); + } + continue; + } + if (m.over) continue; + + const p = m.players[i]; + if (!p.piece) { + if (!p.lost) this.beginSpawn(i); + continue; + } + + // AI inputs + if (i === 1) { + const act = nextAction(this.ai, m, 1, time); + if (act) { + this.applyAction(1, act); + if (!m.players[1].piece) continue; // locked via soft/hard drop + } + } + + // gravity + this.gravTimer[i] += delta; + if (this.gravTimer[i] >= this.gravityMs) { + this.gravTimer[i] = 0; + const r = stepDown(m, i); + if (r.locked) this.onLocked(i, r.events); + else this.updatePieceSprites(i); + } + } + } + + // ── End of match ──────────────────────────────────────────────────────────── + + endMatch() { + if (this.matchEnded) return; + this.matchEnded = true; + const won = this.match.winner === 0; + const p = this.match.players[0]; + + this.oppPortrait?.playEmotion(won ? 'upset' : 'happy'); + playSound(this, won ? SFX.VICTORY_SHORT : SFX.CASINO_LOSE); + + api.post('/history/single-player', { + slug: 'blockfighter', + score: p.garbageSent, + opponentScores: [this.match.players[1].garbageSent], + result: won ? 'win' : 'loss', + }).catch(() => {}); + + if (won) { + if (this.level > this.levelsCompleted) this.levelsCompleted = this.level; + api.post('/puzzles/blockfighter/complete', { level: this.level }) + .then((res) => { + if (res?.levelsCompleted != null) this.levelsCompleted = Math.max(this.levelsCompleted, res.levelsCompleted); + }) + .catch(() => {}); + } + + this.time.delayedCall(900, () => this.showEndModal(won)); + } + + showEndModal(won) { + if (this.view !== 'battle' || !this.match) return; // user already left the battle + this.overlayUp = true; + const cx = GAME_WIDTH / 2; + const cy = GAME_HEIGHT / 2; + const dim = this.add.rectangle(cx, cy, GAME_WIDTH, GAME_HEIGHT, 0x000000, 0.62).setDepth(D.overlay).setInteractive(); + const panel = this.add.graphics().setDepth(D.overlay); + panel.fillStyle(COLORS.panel, 0.98); + panel.fillRoundedRect(cx - 340, cy - 210, 680, 420, 20); + panel.lineStyle(3, won ? COLORS.accent : COLORS.danger, 1); + panel.strokeRoundedRect(cx - 340, cy - 210, 680, 420, 20); + this.layer.add([dim, panel]); + + const p = this.match.players[0]; + const title = this.add.text(cx, cy - 140, won ? 'VICTORY!' : 'DEFEATED', { + fontFamily: 'Righteous', fontSize: '64px', color: won ? COLORS.goldHex : COLORS.dangerHex, + }).setOrigin(0.5).setDepth(D.overlayUI); + const stat = this.add.text(cx, cy - 55, + won + ? `You beat ${this.opponent.name}!\nGarbage sent: ${p.garbageSent} Best chain: ${p.bestChain}` + : `${this.opponent.name} buried you in gems.\nGarbage sent: ${p.garbageSent} Best chain: ${p.bestChain}`, { + fontFamily: '"Julius Sans One"', fontSize: '26px', color: COLORS.textHex, align: 'center', lineSpacing: 8, + }).setOrigin(0.5).setDepth(D.overlayUI); + this.layer.add([title, stat]); + + const btns = []; + if (won) { + const hasNext = this.level < this.bank.length; + if (hasNext) { + btns.push(new Button(this, cx, cy + 50, `Next Fight (${this.level + 1})`, () => this.showIntro(this.level + 1), + { width: 340, height: 60, fontSize: 26 }).setDepth(D.overlayUI)); + } else { + btns.push(this.add.text(cx, cy + 45, 'You beat every fighter. Champion!', { + fontFamily: '"Julius Sans One"', fontSize: '24px', color: COLORS.goldHex, + }).setOrigin(0.5).setDepth(D.overlayUI)); + } + btns.push(new Button(this, cx - 110, cy + 135, 'Rematch', () => this.startBattle(this.level), + { width: 200, height: 54, fontSize: 22, variant: 'ghost' }).setDepth(D.overlayUI)); + } else { + btns.push(new Button(this, cx - 110, cy + 80, 'Retry', () => this.startBattle(this.level), + { width: 200, height: 60, fontSize: 26 }).setDepth(D.overlayUI)); + } + btns.push(new Button(this, cx + 120, won ? cy + 135 : cy + 80, 'Levels', () => this.showLevelSelect(), + { width: 200, height: 54, fontSize: 22, variant: 'ghost' }).setDepth(D.overlayUI)); + this.layer.add(btns); + } +} diff --git a/public/src/games/blockfighter/BlockFighterLogic.js b/public/src/games/blockfighter/BlockFighterLogic.js new file mode 100644 index 0000000..88c3078 --- /dev/null +++ b/public/src/games/blockfighter/BlockFighterLogic.js @@ -0,0 +1,634 @@ +// Block Fighter — pure game engine (no Phaser, no DOM, no timers). +// A Super Puzzle Fighter II Turbo style versus engine. The scene (or a headless +// script) drives all timing; every transition returns an ordered event list, +// each event carrying a board snapshot, which the renderer replays as animation. +// +// Power-gem fusion here is a deterministic approximation of the arcade's +// folklore rules: greedy largest-rectangle fusion, then full-row/column +// extension, then same-seam merging. Fixtures in verifyBlockFighter.js pin it. + +export const WIDTH = 6; +export const VISIBLE_ROWS = 12; +export const HIDDEN_ROWS = 2; +export const HEIGHT = VISIBLE_ROWS + HIDDEN_ROWS; // row 0 = top (hidden) +export const NUM_COLORS = 4; // 0=red 1=green 2=blue 3=yellow +export const SPAWN_COL = 3; +export const KIND = { GEM: 'gem', CRASH: 'crash', COUNTER: 'counter', DIAMOND: 'diamond' }; + +export const CRASH_RATE = 0.28; +export const DIAMOND_EVERY = 25; +export const COUNTER_START = 5; +export const MAX_GARBAGE_PER_DROP = 24; +export const CHAIN_MULT = [1, 2, 3, 4, 6, 8, 12, 16, 24, 32]; +export const POWER_BONUS = 0.5; // extra attack per cleared power-gem cell +export const ATTACK_DISCOUNT = 2; // cells "free" per clear step before attack +export const DIAMOND_DAMAGE_DIV = 2; + +export const SPEED_GRAVITY_MS = [null, 900, 750, 600, 475, 350]; // index = level speed 1-5 + +export const COLOR_LETTERS = 'RGBY'; +export const DEFAULT_DROP_PATTERN = ['RRGGYY', 'BBRRGG', 'YYBBRR', 'GGYYBB']; + +// ── Seeded RNG (mulberry32, matches genRushHour.js) ───────────────────────── +export function makeRng(seed) { + let a = seed >>> 0; + return () => { + a |= 0; a = (a + 0x6d2b79f5) | 0; + let t = Math.imul(a ^ (a >>> 15), 1 | a); + t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +} + +export function parseDropPattern(rows) { + const src = Array.isArray(rows) && rows.length === 4 ? rows : DEFAULT_DROP_PATTERN; + return src.map((row) => + [...String(row).padEnd(WIDTH, 'R').slice(0, WIDTH)].map((ch) => { + const c = COLOR_LETTERS.indexOf(ch.toUpperCase()); + return c >= 0 ? c : 0; + }) + ); +} + +// ── Match / player construction ────────────────────────────────────────────── +function emptyBoard() { + return Array.from({ length: HEIGHT }, () => Array(WIDTH).fill(null)); +} + +function createPlayer(dropPattern) { + return { + board: emptyBoard(), + powerGems: new Map(), // id -> { id, color, x, y, w, h } + nextPowerId: 1, + piece: null, // { a, b, row, col, orient }; orient: 0 b-above, 1 b-right, 2 b-below, 3 b-left + pieceIndex: 0, // next index into match.pieceSeq + pendingGarbage: 0, + dropPattern: parseDropPattern(dropPattern), + patternRow: 0, + garbageSent: 0, + garbageReceived: 0, + bestChain: 0, + lastResolve: { attack: 0, cleared: 0, chain: 0 }, + lost: false, + }; +} + +export function createMatch({ seed = 1, dropPatterns = [null, null] } = {}) { + return { + rng: makeRng(seed), + pieceSeq: [], // shared by both players for fairness + players: [createPlayer(dropPatterns[0]), createPlayer(dropPatterns[1])], + over: false, + winner: null, + }; +} + +function getSeqPiece(match, index) { + while (match.pieceSeq.length <= index) { + const i = match.pieceSeq.length; + const half = () => ({ + color: Math.floor(match.rng() * NUM_COLORS), + kind: match.rng() < CRASH_RATE ? KIND.CRASH : KIND.GEM, + }); + const a = half(); + const b = half(); + if ((i + 1) % DIAMOND_EVERY === 0) { a.kind = KIND.DIAMOND; a.color = null; } + match.pieceSeq.push({ a, b }); + } + return match.pieceSeq[index]; +} + +// Peek at (and lazily generate) the shared sequence — used for next-piece +// previews and AI lookahead. +export function peekPiece(match, index) { + return getSeqPiece(match, index); +} + +// ── Snapshots (for renderer playback) ──────────────────────────────────────── +function cloneCell(cell) { return cell ? { ...cell } : null; } +function cloneBoard(board) { return board.map((row) => row.map(cloneCell)); } + +export function snapshot(player) { + return { + board: cloneBoard(player.board), + powerGems: [...player.powerGems.values()].map((g) => ({ ...g })), + }; +} + +function evt(player, type, extra = {}) { + return { type, ...extra, ...snapshot(player) }; +} + +// ── Piece geometry ─────────────────────────────────────────────────────────── +const ORIENT_DELTA = [[-1, 0], [0, 1], [1, 0], [0, -1]]; // b relative to a + +export function pieceCells(piece) { + const [dr, dc] = ORIENT_DELTA[piece.orient]; + return [ + { r: piece.row, c: piece.col, half: 'a' }, + { r: piece.row + dr, c: piece.col + dc, half: 'b' }, + ]; +} + +function cellFree(board, r, c) { + return r >= 0 && r < HEIGHT && c >= 0 && c < WIDTH && !board[r][c]; +} + +function pieceFits(board, piece) { + return pieceCells(piece).every(({ r, c }) => cellFree(board, r, c)); +} + +// ── Spawning & garbage drops ───────────────────────────────────────────────── +export function spawnPiece(match, pIdx) { + const player = match.players[pIdx]; + const events = []; + if (match.over || player.lost) return events; + + if (player.pendingGarbage > 0) { + const dropped = dropGarbage(player); + events.push(evt(player, 'garbage', { cells: dropped, remaining: player.pendingGarbage })); + } + + const proto = getSeqPiece(match, player.pieceIndex); + player.pieceIndex += 1; + const piece = { a: { ...proto.a }, b: { ...proto.b }, row: 1, col: SPAWN_COL, orient: 0 }; + + if (!pieceFits(player.board, piece)) { + player.lost = true; + match.over = true; + match.winner = 1 - pIdx; + events.push(evt(player, 'lose')); + return events; + } + player.piece = piece; + events.push(evt(player, 'spawn', { piece: { ...piece, a: { ...piece.a }, b: { ...piece.b } } })); + return events; +} + +function topOfStack(board, col) { + for (let r = 0; r < HEIGHT; r++) if (board[r][col]) return r; + return HEIGHT; +} + +function dropGarbage(player) { + const n = Math.min(player.pendingGarbage, MAX_GARBAGE_PER_DROP); + player.pendingGarbage -= n; + player.garbageReceived += n; + const placed = []; + for (let i = 0; i < n; i++) { + const col = i % WIDTH; + const rowOffset = Math.floor(i / WIDTH); + const color = player.dropPattern[(player.patternRow + rowOffset) % 4][col]; + const r = topOfStack(player.board, col) - 1; + if (r < 0) continue; // column already full; the loss check happens at spawn + player.board[r][col] = { color, kind: KIND.COUNTER, count: COUNTER_START }; + placed.push({ r, c: col, color }); + } + player.patternRow = (player.patternRow + Math.ceil(n / WIDTH)) % 4; + return placed; +} + +// ── Player input ───────────────────────────────────────────────────────────── +function tryShift(player, dc) { + if (!player.piece) return false; + const moved = { ...player.piece, col: player.piece.col + dc }; + if (!pieceFits(player.board, moved)) return false; + player.piece = moved; + return true; +} + +export function moveLeft(match, pIdx) { return tryShift(match.players[pIdx], -1); } +export function moveRight(match, pIdx) { return tryShift(match.players[pIdx], 1); } + +const ROTATE_KICKS = [[0, 0], [0, -1], [0, 1], [-1, 0]]; + +function tryRotate(player, dir) { + if (!player.piece) return false; + const orient = (player.piece.orient + dir + 4) % 4; + for (const [dr, dc] of ROTATE_KICKS) { + const rotated = { ...player.piece, orient, row: player.piece.row + dr, col: player.piece.col + dc }; + if (pieceFits(player.board, rotated)) { player.piece = rotated; return true; } + } + return false; +} + +export function rotateCW(match, pIdx) { return tryRotate(match.players[pIdx], 1); } +export function rotateCCW(match, pIdx) { return tryRotate(match.players[pIdx], -1); } + +export function stepDown(match, pIdx) { + const player = match.players[pIdx]; + if (!player.piece || player.lost) return { locked: false, events: [] }; + const moved = { ...player.piece, row: player.piece.row + 1 }; + if (pieceFits(player.board, moved)) { + player.piece = moved; + return { locked: false, events: [] }; + } + return { locked: true, events: lockPiece(match, pIdx) }; +} + +export function hardDrop(match, pIdx) { + const player = match.players[pIdx]; + if (!player.piece || player.lost) return []; + let step = stepDown(match, pIdx); + while (!step.locked) step = stepDown(match, pIdx); + return step.events; +} + +export function getGhostCells(player) { + if (!player.piece) return []; + let probe = { ...player.piece }; + while (true) { + const next = { ...probe, row: probe.row + 1 }; + if (!pieceFits(player.board, next)) break; + probe = next; + } + return pieceCells(probe); +} + +// ── Lock + resolution pipeline ─────────────────────────────────────────────── +function restingRow(board, col) { + return topOfStack(board, col) - 1; // -1 means column full +} + +function lockPiece(match, pIdx) { + const player = match.players[pIdx]; + const opponent = match.players[1 - pIdx]; + const piece = player.piece; + player.piece = null; + const events = []; + const live = !match.headless; // headless sims (AI search) skip snapshot events + const push = (type, extra) => { if (live) events.push(evt(player, type, extra)); }; + player.lastResolve = { attack: 0, cleared: 0, chain: 0 }; + + // Place: vertical pairs stack in-column; horizontal halves settle independently. + const placedCells = []; + const place = (half, r, c) => { + if (r < 0) return; + player.board[r][c] = { ...half }; + placedCells.push({ r, c }); + }; + let diamondPos = null; + if (piece.orient === 0 || piece.orient === 2) { + const bottomHalf = piece.orient === 0 ? piece.a : piece.b; + const topHalf = piece.orient === 0 ? piece.b : piece.a; + const rBottom = restingRow(player.board, piece.col); + place(bottomHalf, rBottom, piece.col); + place(topHalf, rBottom - 1, piece.col); + if (bottomHalf.kind === KIND.DIAMOND) diamondPos = { r: rBottom, c: piece.col }; + if (topHalf.kind === KIND.DIAMOND) diamondPos = { r: rBottom - 1, c: piece.col }; + } else { + const cells = pieceCells(piece); + for (const { c, half } of cells) { + const r = restingRow(player.board, c); + place(piece[half], r, c); + if (piece[half].kind === KIND.DIAMOND) diamondPos = { r, c }; + } + } + push('place', { cells: placedCells }); + + let attack = 0; + + // Diamond: wipe every cell of the color directly beneath it. + if (diamondPos && player.board[diamondPos.r]?.[diamondPos.c]?.kind === KIND.DIAMOND) { + const below = player.board[diamondPos.r + 1]?.[diamondPos.c]; + const wiped = []; + if (below && below.color != null) { + for (let r = 0; r < HEIGHT; r++) { + for (let c = 0; c < WIDTH; c++) { + const cell = player.board[r][c]; + if (cell && cell.color === below.color) { + if (cell.powerId != null) player.powerGems.delete(cell.powerId); + player.board[r][c] = null; + wiped.push({ r, c, color: cell.color, kind: cell.kind }); + } + } + } + attack += Math.floor(wiped.length / DIAMOND_DAMAGE_DIV); + player.lastResolve.cleared += wiped.length; + } + player.board[diamondPos.r][diamondPos.c] = null; + push('diamond', { cells: wiped }); + } + + attack += runCascade(player, live ? events : null); + + // Counter tick: one decrement per lock; matured counters become plain gems + // and may themselves enable fusions/crashes, so re-resolve if any matured. + const matured = []; + for (let r = 0; r < HEIGHT; r++) { + for (let c = 0; c < WIDTH; c++) { + const cell = player.board[r][c]; + if (cell?.kind === KIND.COUNTER) { + cell.count -= 1; + if (cell.count <= 0) { + player.board[r][c] = { color: cell.color, kind: KIND.GEM }; + matured.push({ r, c, color: cell.color }); + } + } + } + } + if (matured.length) { + push('counter', { matured }); + attack += runCascade(player, live ? events : null); + } + + // Damage: offset our own pending garbage first, remainder goes to opponent. + player.lastResolve.attack = attack; + if (attack > 0) { + const offset = Math.min(attack, player.pendingGarbage); + player.pendingGarbage -= offset; + const sent = attack - offset; + opponent.pendingGarbage += sent; + player.garbageSent += sent; + push('attack', { amount: attack, offset, sent }); + } + return events; +} + +function runCascade(player, events) { + let attack = 0; + let chain = 0; + while (true) { + const fell = settle(player); + const fused = fuseAll(player); + if ((fell || fused) && events) events.push(evt(player, 'settle')); + const { cleared, powerCells } = crashClear(player); + if (!cleared.length) break; + chain += 1; + player.bestChain = Math.max(player.bestChain, chain); + player.lastResolve.cleared += cleared.length; + player.lastResolve.chain = Math.max(player.lastResolve.chain, chain); + const mult = CHAIN_MULT[Math.min(chain - 1, CHAIN_MULT.length - 1)]; + attack += Math.max(0, Math.ceil((cleared.length - ATTACK_DISCOUNT + powerCells * POWER_BONUS) * mult)); + if (events) events.push(evt(player, 'clear', { cells: cleared, chain })); + } + return attack; +} + +// ── Gravity (power gems fall rigidly) ──────────────────────────────────────── +function settle(player) { + const { board, powerGems } = player; + let movedAny = false; + let movedThisPass = true; + while (movedThisPass) { + movedThisPass = false; + for (let r = HEIGHT - 2; r >= 0; r--) { + for (let c = 0; c < WIDTH; c++) { + const cell = board[r][c]; + if (!cell || cell.powerId != null) continue; + if (!board[r + 1][c]) { + board[r + 1][c] = cell; + board[r][c] = null; + movedThisPass = true; + } + } + } + for (const gem of powerGems.values()) { + let canFall = gem.y + gem.h < HEIGHT; + for (let c = gem.x; canFall && c < gem.x + gem.w; c++) { + if (board[gem.y + gem.h][c]) canFall = false; + } + if (canFall) { + for (let c = gem.x; c < gem.x + gem.w; c++) { + board[gem.y + gem.h][c] = board[gem.y + gem.h - 1][c]; + for (let r = gem.y + gem.h - 1; r > gem.y; r--) board[r][c] = board[r - 1][c]; + board[gem.y][c] = null; + } + gem.y += 1; + movedThisPass = true; + } + } + if (movedThisPass) movedAny = true; + } + return movedAny; +} + +// ── Power-gem fusion ───────────────────────────────────────────────────────── +function isFusable(cell, color) { + return cell && cell.kind === KIND.GEM && cell.powerId == null && cell.color === color; +} + +function hasAnyTwoByTwo(board) { + for (let r = 0; r < HEIGHT - 1; r++) { + for (let c = 0; c < WIDTH - 1; c++) { + const cell = board[r][c]; + if (!cell || cell.kind !== KIND.GEM || cell.powerId != null) continue; + if (isFusable(board[r][c + 1], cell.color) && + isFusable(board[r + 1][c], cell.color) && + isFusable(board[r + 1][c + 1], cell.color)) return true; + } + } + return false; +} + +function findBestRect(board) { + let best = null; + for (let r = 0; r < HEIGHT - 1; r++) { + for (let c = 0; c < WIDTH - 1; c++) { + const cell = board[r][c]; + if (!cell || cell.kind !== KIND.GEM || cell.powerId != null) continue; + const color = cell.color; + let maxW = 0; + while (c + maxW < WIDTH && isFusable(board[r][c + maxW], color)) maxW++; + for (let w = 2; w <= maxW; w++) { + let h = 1; + outer: while (r + h < HEIGHT) { + for (let cc = c; cc < c + w; cc++) { + if (!isFusable(board[r + h][cc], color)) break outer; + } + h++; + } + if (h >= 2 && (!best || w * h > best.w * best.h)) best = { x: c, y: r, w, h, color }; + } + } + } + return best; +} + +function applyPowerId(board, gem) { + for (let r = gem.y; r < gem.y + gem.h; r++) { + for (let c = gem.x; c < gem.x + gem.w; c++) board[r][c].powerId = gem.id; + } +} + +function fuseAll(player) { + const { board, powerGems } = player; + let changed = false; + // New 2x2+ rectangles, largest-first. + if (hasAnyTwoByTwo(board)) { + let rect; + while ((rect = findBestRect(board))) { + const gem = { id: player.nextPowerId++, color: rect.color, x: rect.x, y: rect.y, w: rect.w, h: rect.h }; + powerGems.set(gem.id, gem); + applyPowerId(board, gem); + changed = true; + } + } + // Row/column extension and same-seam merging. + let again = true; + while (again) { + again = false; + for (const gem of powerGems.values()) { + for (const dy of [-1, gem.h]) { // row above / below + const r = gem.y + (dy === -1 ? -1 : gem.h); + if (r < 0 || r >= HEIGHT) continue; + let full = true; + for (let c = gem.x; c < gem.x + gem.w; c++) if (!isFusable(board[r][c], gem.color)) { full = false; break; } + if (full) { + if (dy === -1) gem.y -= 1; + gem.h += 1; + applyPowerId(board, gem); + again = true; changed = true; + } + } + for (const dx of [-1, gem.w]) { // column left / right + const c = gem.x + (dx === -1 ? -1 : gem.w); + if (c < 0 || c >= WIDTH) continue; + let full = true; + for (let r = gem.y; r < gem.y + gem.h; r++) if (!isFusable(board[r][c], gem.color)) { full = false; break; } + if (full) { + if (dx === -1) gem.x -= 1; + gem.w += 1; + applyPowerId(board, gem); + again = true; changed = true; + } + } + } + // Merge pairs sharing a full seam. + const gems = [...powerGems.values()]; + for (let i = 0; i < gems.length && !again; i++) { + for (let j = i + 1; j < gems.length && !again; j++) { + const a = gems[i], b = gems[j]; + if (a.color !== b.color) continue; + const vStack = a.x === b.x && a.w === b.w && (a.y + a.h === b.y || b.y + b.h === a.y); + const hStack = a.y === b.y && a.h === b.h && (a.x + a.w === b.x || b.x + b.w === a.x); + if (vStack || hStack) { + a.x = Math.min(a.x, b.x); a.y = Math.min(a.y, b.y); + if (vStack) a.h += b.h; else a.w += b.w; + powerGems.delete(b.id); + applyPowerId(board, a); + again = true; changed = true; + } + } + } + } + return changed; +} + +// ── Crash clearing ─────────────────────────────────────────────────────────── +const ORTH = [[-1, 0], [1, 0], [0, -1], [0, 1]]; + +function crashClear(player) { + const { board, powerGems } = player; + const toClear = new Set(); + const visited = new Set(); + + for (let r = 0; r < HEIGHT; r++) { + for (let c = 0; c < WIDTH; c++) { + const cell = board[r][c]; + if (!cell || cell.kind !== KIND.CRASH) continue; + const startKey = r * WIDTH + c; + if (visited.has(startKey)) continue; + const group = []; + const stack = [[r, c]]; + visited.add(startKey); + while (stack.length) { + const [gr, gc] = stack.pop(); + group.push([gr, gc]); + for (const [dr, dc] of ORTH) { + const nr = gr + dr, nc = gc + dc; + if (nr < 0 || nr >= HEIGHT || nc < 0 || nc >= WIDTH) continue; + const key = nr * WIDTH + nc; + if (visited.has(key)) continue; + const ncell = board[nr][nc]; + if (ncell && ncell.color === cell.color && (ncell.kind === KIND.GEM || ncell.kind === KIND.CRASH)) { + visited.add(key); + stack.push([nr, nc]); + } + } + } + if (group.length >= 2) group.forEach(([gr, gc]) => toClear.add(gr * WIDTH + gc)); + } + } + if (!toClear.size) return { cleared: [], powerCells: 0 }; + + // Counter gems adjacent to a cleared cell of their own color also clear. + for (const key of [...toClear]) { + const r = Math.floor(key / WIDTH), c = key % WIDTH; + const color = board[r][c].color; + for (const [dr, dc] of ORTH) { + const nr = r + dr, nc = c + dc; + if (nr < 0 || nr >= HEIGHT || nc < 0 || nc >= WIDTH) continue; + const ncell = board[nr][nc]; + if (ncell?.kind === KIND.COUNTER && ncell.color === color) toClear.add(nr * WIDTH + nc); + } + } + + const cleared = []; + let powerCells = 0; + for (const key of toClear) { + const r = Math.floor(key / WIDTH), c = key % WIDTH; + const cell = board[r][c]; + if (cell.powerId != null) { powerCells += 1; powerGems.delete(cell.powerId); } + cleared.push({ r, c, color: cell.color, kind: cell.kind }); + board[r][c] = null; + } + // Drop powerId from any cells whose gem was destroyed (partial overlap can't + // happen — a power gem is monochrome and connected, so it clears whole — but + // guard against stale ids anyway). + for (let r = 0; r < HEIGHT; r++) { + for (let c = 0; c < WIDTH; c++) { + const cell = board[r][c]; + if (cell?.powerId != null && !powerGems.has(cell.powerId)) delete cell.powerId; + } + } + return { cleared, powerCells }; +} + +// ── AI support ─────────────────────────────────────────────────────────────── +export function legalPlacements(player) { + const placements = []; + for (let orient = 0; orient < 4; orient++) { + const [, dc] = ORIENT_DELTA[orient]; + for (let col = 0; col < WIDTH; col++) { + const bCol = col + dc; + if (bCol < 0 || bCol >= WIDTH) continue; + placements.push({ col, orient }); + } + } + return placements; +} + +function clonePlayer(player) { + return { + ...player, + board: cloneBoard(player.board), + powerGems: new Map([...player.powerGems.values()].map((g) => [g.id, { ...g }])), + piece: player.piece ? { ...player.piece, a: { ...player.piece.a }, b: { ...player.piece.b } } : null, + dropPattern: player.dropPattern, + }; +} + +// Simulate hard-dropping the current piece at (col, orient). Returns metrics +// without mutating the real match, or null if the placement is impossible. +export function simulatePlacement(match, pIdx, col, orient) { + const real = match.players[pIdx]; + if (!real.piece) return null; + const player = clonePlayer(real); + const opponent = clonePlayer(match.players[1 - pIdx]); + const sim = { players: pIdx === 0 ? [player, opponent] : [opponent, player], over: false, winner: null, headless: true }; + + const target = { ...player.piece, row: 1, col, orient }; + if (!pieceFits(player.board, target)) return null; + player.piece = target; + hardDrop(sim, pIdx); + + const spawnBlocked = !cellFree(player.board, 1, SPAWN_COL) || !cellFree(player.board, 0, SPAWN_COL); + return { + player, + attack: player.lastResolve.attack, + cleared: player.lastResolve.cleared, + chain: player.lastResolve.chain, + died: spawnBlocked, + }; +} diff --git a/public/src/games/blockfighter/tutorial.md b/public/src/games/blockfighter/tutorial.md new file mode 100644 index 0000000..358d48e --- /dev/null +++ b/public/src/games/blockfighter/tutorial.md @@ -0,0 +1,51 @@ +# Block Fighter + +Go head-to-head against a rival in a falling-gem battle. Win by making your +opponent's board overflow — the fight is lost when a board's **spawn column** +(marked by the red arrow) fills to the top. + +## The Basics + +- Pairs of gems fall onto your board. Move them with **← →** (or A/D), rotate + with **↑ / X / Z**, drop faster with **↓** (or S), and slam them down with + **Space**. On a touchscreen, use the buttons along the bottom. +- Gems come in four colors. They stack where they land — horizontal pairs split + and each half falls to its own column. + +## Crash Gems + +The round, glowing orbs are **Crash Gems**. When a Crash Gem touches any gem of +its own color, it destroys the entire connected group of that color. This is +your only way to clear gems — plan your stacks so one Crash Gem wipes out a +big cluster. + +## Power Gems + +Build a solid rectangle of one color (2×2 or bigger) and it fuses into a giant +**Power Gem**. Power Gems count extra when destroyed, so building big before +you crash sends a much bigger attack. + +## Chains + +When gems are destroyed, everything above falls. If that drop brings another +Crash Gem into contact with its color, you get a **chain** — and every link in +the chain multiplies your attack. + +## Counter Gems + +Attacks land on the enemy board as **Counter Gems** — numbered blocks that +can't be crashed right away. Each number ticks down once per piece you drop; +at zero they become normal gems. You can also destroy a Counter Gem early by +clearing gems of its color right next to it. If you attack while garbage is +queued against you, your attack cancels it out first. + +## The Diamond + +Every 25th piece carries a **Diamond**. It destroys every gem of whatever +color it lands on — a great panic button, but it sends a weaker attack. + +## The Ladder + +Each level is a single battle against one fighter. Beat them to unlock the +next — opponents get smarter and faster as you climb. Lose, and you can retry +as many times as you like. diff --git a/public/src/main.js b/public/src/main.js index 6f0ea31..f1da53a 100644 --- a/public/src/main.js +++ b/public/src/main.js @@ -65,6 +65,7 @@ import RushHourGame from './games/rushhour/RushHourGame.js'; import HexsweeperGame from './games/hexsweeper/HexsweeperGame.js'; import PuddingMonstersGame from './games/puddingmonsters/PuddingMonstersGame.js'; import ShiftGame from './games/shift/ShiftGame.js'; +import BlockFighterGame from './games/blockfighter/BlockFighterGame.js'; const config = { type: Phaser.AUTO, @@ -143,6 +144,7 @@ const config = { HexsweeperGame, PuddingMonstersGame, ShiftGame, + BlockFighterGame, ], }; diff --git a/public/src/scenes/GameRoomScene.js b/public/src/scenes/GameRoomScene.js index a81541c..461b897 100644 --- a/public/src/scenes/GameRoomScene.js +++ b/public/src/scenes/GameRoomScene.js @@ -22,7 +22,7 @@ export default class GameRoomScene extends Phaser.Scene { } create() { - const slugDispatch = { backgammon: 'Backgammon', holdem: 'HoldemGame', blackjack: 'BlackjackGame', parchisi: 'ParchisiGame', yatzi: 'YatziGame', skipbo: 'SkipBoGame', phase10: 'Phase10Game', chinesecheckers: 'ChineseCheckersGame', gofish: 'GoFishGame', uno: 'UnoGame', craps: 'CrapsGame', roulette: 'RouletteGame', mexicantrain: 'MexicanTrainGame', hearts: 'HeartsGame', catan: 'CatanGame', tickettoride: 'TicketToRideGame', nerts: 'NertsGame', bingo: 'BingoGame', baccarat: 'BaccaratGame', dominion: 'DominionGame', checkers: 'CheckersGame', chess: 'ChessGame', wordle: 'WordleGame', scrabble: 'ScrabbleGame', ghost: 'GhostGame', wordladder: 'WordLadderGame', wordsearch: 'WordSearchGame', hangman: 'HangmanGame', sudoku: 'SudokuGame', othello: 'OthelloGame', go: 'GoGame', battleship: 'BattleshipGame', mastermind: 'MastermindGame', connect4: 'Connect4Game', boggle: 'BoggleGame', oldmaid: 'OldMaidGame', blokus: 'BlokusGame', spellingbee: 'SpellingBeeGame', minicrossword: 'MiniCrosswordGame', forbiddenisland: 'ForbiddenIslandGame', solitairetour: 'SolitaireTourGame', splendor: 'SplendorGame', tectonic: 'TectonicGame', labyrinth: 'LabyrinthGame', videopoker: 'VideoPokerGame', farkel: 'FarkelGame', stratego: 'StrategoGame', kiitos: 'KiitosGame', monopoly: 'MonopolyGame', triominoes: 'TriominoesGame', freecell: 'FreecellGame', rushhour: 'RushHourGame', hexsweeper: 'HexsweeperGame', puddingmonsters: 'PuddingMonstersGame', shift: 'ShiftGame' }; + const slugDispatch = { backgammon: 'Backgammon', holdem: 'HoldemGame', blackjack: 'BlackjackGame', parchisi: 'ParchisiGame', yatzi: 'YatziGame', skipbo: 'SkipBoGame', phase10: 'Phase10Game', chinesecheckers: 'ChineseCheckersGame', gofish: 'GoFishGame', uno: 'UnoGame', craps: 'CrapsGame', roulette: 'RouletteGame', mexicantrain: 'MexicanTrainGame', hearts: 'HeartsGame', catan: 'CatanGame', tickettoride: 'TicketToRideGame', nerts: 'NertsGame', bingo: 'BingoGame', baccarat: 'BaccaratGame', dominion: 'DominionGame', checkers: 'CheckersGame', chess: 'ChessGame', wordle: 'WordleGame', scrabble: 'ScrabbleGame', ghost: 'GhostGame', wordladder: 'WordLadderGame', wordsearch: 'WordSearchGame', hangman: 'HangmanGame', sudoku: 'SudokuGame', othello: 'OthelloGame', go: 'GoGame', battleship: 'BattleshipGame', mastermind: 'MastermindGame', connect4: 'Connect4Game', boggle: 'BoggleGame', oldmaid: 'OldMaidGame', blokus: 'BlokusGame', spellingbee: 'SpellingBeeGame', minicrossword: 'MiniCrosswordGame', forbiddenisland: 'ForbiddenIslandGame', solitairetour: 'SolitaireTourGame', splendor: 'SplendorGame', tectonic: 'TectonicGame', labyrinth: 'LabyrinthGame', videopoker: 'VideoPokerGame', farkel: 'FarkelGame', stratego: 'StrategoGame', kiitos: 'KiitosGame', monopoly: 'MonopolyGame', triominoes: 'TriominoesGame', freecell: 'FreecellGame', rushhour: 'RushHourGame', hexsweeper: 'HexsweeperGame', puddingmonsters: 'PuddingMonstersGame', shift: 'ShiftGame', blockfighter: 'BlockFighterGame' }; if (slugDispatch[this.game.slug]) { this.scene.start(slugDispatch[this.game.slug], { game: this.game, diff --git a/public/src/scenes/PreloadScene.js b/public/src/scenes/PreloadScene.js index 44d8210..d6ce2c1 100644 --- a/public/src/scenes/PreloadScene.js +++ b/public/src/scenes/PreloadScene.js @@ -62,6 +62,7 @@ export default class PreloadScene extends Phaser.Scene { this.load.json('rushhour', '/data/rushhour.json'); this.load.json('puddingmonsters', '/data/puddingmonsters.json'); this.load.json('shift-artwork', '/data/shift-artwork.json'); + this.load.json('blockfighter', '/data/blockfighter.json'); this.load.audio('sfx-water-splash', '/assets/fx/water-splash.mp3'); this.load.audio('sfx-water-sink', '/assets/fx/water-sink.mp3'); diff --git a/server/games/registry.js b/server/games/registry.js index 4083249..06dca4e 100644 --- a/server/games/registry.js +++ b/server/games/registry.js @@ -80,3 +80,4 @@ registerGame({ slug: 'rushhour', name: 'Rush Hour', category: registerGame({ slug: 'hexsweeper', name: 'Hexsweeper', category: 'logic', minPlayers: 1, maxPlayers: 1, minOpponents: 0, maxOpponents: 0, iconFrame: 52 }); registerGame({ slug: 'puddingmonsters', name: 'Jell-o Monsters', category: 'logic', minPlayers: 1, maxPlayers: 1, minOpponents: 0, maxOpponents: 0, hasTutorial: true, iconFrame: 53 }); registerGame({ slug: 'shift', name: 'Shift', category: 'logic', minPlayers: 1, maxPlayers: 1, minOpponents: 0, maxOpponents: 0, iconFrame: 55 }); +registerGame({ slug: 'blockfighter', name: 'Block Fighter', category: 'logic', minPlayers: 1, maxPlayers: 1, minOpponents: 0, maxOpponents: 0, hasTutorial: true, iconFrame: 56 }); diff --git a/server/scripts/verifyBlockFighter.js b/server/scripts/verifyBlockFighter.js new file mode 100644 index 0000000..1910a01 --- /dev/null +++ b/server/scripts/verifyBlockFighter.js @@ -0,0 +1,334 @@ +// Headless verification for Block Fighter. +// node server/scripts/verifyBlockFighter.js [--quick] +// Exits non-zero on any failure. +// +// 1. Fixture tests: exact engine behavior on hand-built boards. +// 2. AI-vs-AI self-play with invariant checks after every lock. +// 3. Skill differentiation matrix (higher skill should win more). +// 4. Level bank lint (public/data/blockfighter.json vs opponents.json). + +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; + +import { + WIDTH, HEIGHT, SPAWN_COL, KIND, COUNTER_START, SPEED_GRAVITY_MS, + createMatch, spawnPiece, stepDown, hardDrop, + moveLeft, moveRight, rotateCW, rotateCCW, +} from '../../public/src/games/blockfighter/BlockFighterLogic.js'; +import { createAI, planPlacement, nextAction } from '../../public/src/games/blockfighter/BlockFighterAI.js'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const QUICK = process.argv.includes('--quick'); + +let failures = 0; +function check(name, cond, detail = '') { + if (cond) { console.log(` ok ${name}`); } + else { failures += 1; console.error(`FAIL ${name}${detail ? ` — ${detail}` : ''}`); } +} + +// ── Fixture helpers ────────────────────────────────────────────────────────── +const gem = (color) => ({ color, kind: KIND.GEM }); +const crash = (color) => ({ color, kind: KIND.CRASH }); +const counter = (color, count = COUNTER_START) => ({ color, kind: KIND.COUNTER, count }); + +function freshMatch() { return createMatch({ seed: 42 }); } + +function put(player, r, c, cell) { player.board[r][c] = cell; } + +// Give the player a specific piece and hard-drop it. +function dropPiece(match, pIdx, a, b, col, orient = 0) { + const player = match.players[pIdx]; + player.piece = { a, b, row: 1, col, orient }; + return hardDrop(match, pIdx); +} + +function cellAt(player, r, c) { return player.board[r][c]; } +function countCells(player, pred) { + let n = 0; + for (let r = 0; r < HEIGHT; r++) for (let c = 0; c < WIDTH; c++) { + if (player.board[r][c] && pred(player.board[r][c])) n++; + } + return n; +} + +const BOTTOM = HEIGHT - 1; + +// ── 1. Fixtures ────────────────────────────────────────────────────────────── +console.log('Fixtures:'); +{ + // Crash gem clears its connected same-color group. + const m = freshMatch(); + const p = m.players[0]; + put(p, BOTTOM, 0, gem(0)); + put(p, BOTTOM - 1, 0, gem(0)); + dropPiece(m, 0, crash(0), gem(1), 0); + check('crash clears connected group', countCells(p, (c) => c.color === 0) === 0); + check('non-matching half survives', countCells(p, (c) => c.color === 1) === 1); + check('cleared cells settle', cellAt(p, BOTTOM, 0)?.color === 1); +} +{ + // Lone crash gem stays put. + const m = freshMatch(); + const p = m.players[0]; + dropPiece(m, 0, crash(0), gem(1), 0); + check('lone crash gem stays', countCells(p, (c) => c.kind === KIND.CRASH) === 1); +} +{ + // 2x2 fusion into a power gem; then column extension to 2x3. + const m = freshMatch(); + const p = m.players[0]; + dropPiece(m, 0, gem(2), gem(2), 0); + dropPiece(m, 0, gem(2), gem(2), 1); + check('2x2 fuses into power gem', p.powerGems.size === 1); + const g0 = [...p.powerGems.values()][0]; + check('power gem is 2x2', g0 && g0.w === 2 && g0.h === 2); + dropPiece(m, 0, gem(2), gem(2), 2); + const g1 = [...p.powerGems.values()][0]; + check('power gem extends to 3 wide', p.powerGems.size === 1 && g1.w === 3 && g1.h === 2, + JSON.stringify([...p.powerGems.values()])); +} +{ + // Rigid power-gem gravity: gem bridges a hole and falls as a unit. + const m = freshMatch(); + const p = m.players[0]; + // pillar in col 0 only; power gem sits on rows 10-11 across cols 0-1, hole below col 1 + put(p, BOTTOM, 0, gem(3)); + put(p, BOTTOM - 1, 0, gem(3)); + const pg = { id: 99, color: 1, x: 0, y: BOTTOM - 3, w: 2, h: 2 }; + p.powerGems.set(pg.id, pg); + for (let r = pg.y; r < pg.y + pg.h; r++) for (let c = pg.x; c < pg.x + pg.w; c++) { + put(p, r, c, { color: 1, kind: KIND.GEM, powerId: 99 }); + } + // drop something far away to trigger a resolution pass + dropPiece(m, 0, gem(0), gem(2), 5); + const g = p.powerGems.get(99); + check('power gem rests on support, bridging the hole', g && g.y === BOTTOM - 3 && !cellAt(p, BOTTOM, 1), + JSON.stringify(g)); + // remove the pillar support and resolve again: gem should drop as a unit + put(p, BOTTOM, 0, null); + put(p, BOTTOM - 1, 0, null); + dropPiece(m, 0, gem(0), gem(2), 5); + const g2 = p.powerGems.get(99); + check('power gem falls rigidly when support clears', g2 && g2.y === BOTTOM - 1, + JSON.stringify(g2)); +} +{ + // Counter gems: tick per lock, mature into normal gems at 0. + const m = freshMatch(); + const p = m.players[0]; + put(p, BOTTOM, 0, counter(2, 2)); + dropPiece(m, 0, gem(0), gem(1), 5); + check('counter ticks down on lock', cellAt(p, BOTTOM, 0)?.count === 1); + dropPiece(m, 0, gem(0), gem(1), 4); + check('counter matures into gem', cellAt(p, BOTTOM, 0)?.kind === KIND.GEM); +} +{ + // Counter destroyed when an adjacent same-color group clears. + const m = freshMatch(); + const p = m.players[0]; + put(p, BOTTOM, 0, gem(0)); + put(p, BOTTOM, 1, counter(0)); + dropPiece(m, 0, crash(0), gem(1), 0); + check('adjacent same-color counter cleared', countCells(p, (c) => c.kind === KIND.COUNTER) === 0); +} +{ + // Diamond wipes the color beneath it; vanishes on bare floor. + const m = freshMatch(); + const p = m.players[0]; + put(p, BOTTOM, 0, gem(3)); + put(p, BOTTOM, 3, gem(3)); + put(p, BOTTOM, 5, counter(3)); + put(p, BOTTOM, 1, gem(2)); + dropPiece(m, 0, { color: null, kind: KIND.DIAMOND }, gem(2), 0, 0); // diamond (bottom half) lands on yellow + check('diamond wipes all of that color (incl. counters)', countCells(p, (c) => c.color === 3) === 0); + check('other colors survive diamond', countCells(p, (c) => c.color === 2) === 2); + check('diamond itself is gone', countCells(p, (c) => c.kind === KIND.DIAMOND) === 0); + const m2 = freshMatch(); + dropPiece(m2, 0, { color: null, kind: KIND.DIAMOND }, gem(2), 0, 2); + check('diamond on bare floor vanishes', countCells(m2.players[0], (c) => c.kind === KIND.DIAMOND) === 0); +} +{ + // Attack is offset by own pending garbage before reaching the opponent. + const m = freshMatch(); + const p = m.players[0]; + p.pendingGarbage = 100; + put(p, BOTTOM, 0, gem(0)); + put(p, BOTTOM - 1, 0, gem(0)); + put(p, BOTTOM, 1, gem(0)); + put(p, BOTTOM - 1, 1, gem(0)); + dropPiece(m, 0, crash(0), gem(1), 2, 3); // crash next to the 2x2... orient 3: b left + check('clear happened for offset test', p.lastResolve.cleared >= 5, `cleared=${p.lastResolve.cleared}`); + check('attack offsets own pending garbage', p.pendingGarbage < 100 && m.players[1].pendingGarbage === 0, + `pending=${p.pendingGarbage}, opp=${m.players[1].pendingGarbage}`); +} +{ + // Unoffset attack lands on the opponent; garbage drops as counters. + const m = freshMatch(); + const p = m.players[0]; + for (let r = 0; r < 4; r++) for (let c = 0; c < 2; c++) put(p, BOTTOM - r, c, gem(1)); + dropPiece(m, 0, crash(1), gem(0), 2, 3); + const sent = m.players[1].pendingGarbage; + check('attack reaches opponent', sent > 0, `sent=${sent}`); + spawnPiece(m, 1); + const counters = countCells(m.players[1], (c) => c.kind === KIND.COUNTER); + check('garbage drops as counter gems on spawn', counters === Math.min(sent, 24), + `counters=${counters} sent=${sent}`); + check('counter colors follow the drop pattern', + m.players[1].board.flat().filter(Boolean).every((c) => c.kind !== KIND.COUNTER || c.color != null)); +} +{ + // Chain: red crash dropped in col 1 clears the reds; the green crash above + // them falls beside the green gem and triggers a second clear. + const m = freshMatch(); + const p = m.players[0]; + put(p, BOTTOM, 0, gem(0)); + put(p, BOTTOM - 1, 0, gem(0)); + put(p, BOTTOM - 2, 0, crash(1)); + put(p, BOTTOM, 1, gem(1)); + dropPiece(m, 0, crash(0), gem(2), 1); // lands beside the red pair + check('chain of 2 detected', p.lastResolve.chain === 2, `chain=${p.lastResolve.chain}`); + check('chain cleared everything green', countCells(p, (c) => c.color === 1) === 0); +} +{ + // Spawn-blocked loss in the spawn column. + const m = freshMatch(); + const p = m.players[0]; + for (let r = 0; r < HEIGHT; r++) put(p, r, SPAWN_COL, gem(r % 4)); + spawnPiece(m, 0); + check('blocked spawn loses the match', p.lost && m.over && m.winner === 1); +} + +// ── 2 & 3. Self-play with invariants + skill matrix ───────────────────────── +function checkInvariants(match, tag) { + for (const p of match.players) { + for (let r = 0; r < HEIGHT - 1; r++) { + for (let c = 0; c < WIDTH; c++) { + const cell = p.board[r][c]; + if (cell && !p.board[r + 1][c] && cell.powerId == null) { + throw new Error(`${tag}: floating cell at ${r},${c}`); + } + } + } + for (const g of p.powerGems.values()) { + if (g.w < 2 || g.h < 2) throw new Error(`${tag}: degenerate power gem ${JSON.stringify(g)}`); + for (let r = g.y; r < g.y + g.h; r++) for (let c = g.x; c < g.x + g.w; c++) { + const cell = p.board[r]?.[c]; + if (!cell || cell.powerId !== g.id || cell.color !== g.color) { + throw new Error(`${tag}: power gem cell mismatch at ${r},${c}: ${JSON.stringify(g)}`); + } + } + } + for (let r = 0; r < HEIGHT; r++) for (let c = 0; c < WIDTH; c++) { + const cell = p.board[r][c]; + if (cell?.powerId != null && !p.powerGems.has(cell.powerId)) { + throw new Error(`${tag}: orphaned powerId at ${r},${c}`); + } + } + if (p.pendingGarbage < 0) throw new Error(`${tag}: negative pendingGarbage`); + } +} + +const TICK_MS = 25; +function playMatch(skillA, skillB, seed, speed = 3, pieceCap = 1200) { + const match = createMatch({ seed }); + const ais = [ + createAI({ skill: skillA, speed, seed: seed * 2 + 1 }), + createAI({ skill: skillB, speed, seed: seed * 3 + 7 }), + ]; + const gravityMs = SPEED_GRAVITY_MS[speed]; + const gravTimer = [0, 0]; + let now = 0; + let pieces = 0; + for (const i of [0, 1]) { + spawnPiece(match, i); + if (match.players[i].piece) planPlacement(ais[i], match, i); + } + while (!match.over && pieces < pieceCap) { + now += TICK_MS; + for (const i of [0, 1]) { + if (match.over) break; + const p = match.players[i]; + if (!p.piece) continue; + let locked = false; + const act = nextAction(ais[i], match, i, now); + if (act === 'left') moveLeft(match, i); + else if (act === 'right') moveRight(match, i); + else if (act === 'rotateCW') rotateCW(match, i); + else if (act === 'rotateCCW') rotateCCW(match, i); + else if (act === 'softDrop') locked = stepDown(match, i).locked; + else if (act === 'hardDrop') { hardDrop(match, i); locked = true; } + gravTimer[i] += TICK_MS; + if (!locked && p.piece && gravTimer[i] >= gravityMs) { + gravTimer[i] = 0; + locked = stepDown(match, i).locked; + } + if (locked) { + pieces += 1; + checkInvariants(match, `match(seed=${seed},${skillA}v${skillB})`); + if (!match.over) { + spawnPiece(match, i); + if (match.players[i].piece) planPlacement(ais[i], match, i); + } + } + } + } + return { winner: match.over ? match.winner : null, pieces }; +} + +console.log('\nSelf-play invariants:'); +{ + const games = QUICK ? 10 : 40; + let decided = 0, totalPieces = 0; + for (let s = 1; s <= games; s++) { + const { winner, pieces } = playMatch(5, 5, s * 101); + if (winner !== null) decided += 1; + totalPieces += pieces; + } + check(`self-play runs clean (${games} games)`, true); + check('most games reach a decision', decided >= games * 0.8, `${decided}/${games}`); + console.log(` avg pieces/game: ${(totalPieces / games).toFixed(1)}`); +} + +console.log('\nSkill differentiation:'); +{ + const games = QUICK ? 8 : 24; + const pairs = [[2, 8], [3, 6], [5, 6], [1, 10]]; + for (const [lo, hi] of pairs) { + let hiWins = 0, decided = 0; + for (let s = 1; s <= games; s++) { + // alternate sides to cancel any side bias + const flip = s % 2 === 1; + const { winner } = playMatch(flip ? lo : hi, flip ? hi : lo, s * 977 + lo * 13 + hi); + if (winner === null) continue; + decided += 1; + if ((flip && winner === 1) || (!flip && winner === 0)) hiWins += 1; + } + const rate = decided ? hiWins / decided : 0; + const need = hi - lo >= 5 ? 0.7 : 0.5; + check(`skill ${hi} beats ${lo} (${(rate * 100).toFixed(0)}% of ${decided})`, rate >= need); + } +} + +// ── 4. Level bank lint ─────────────────────────────────────────────────────── +console.log('\nLevel bank:'); +{ + const bank = JSON.parse(readFileSync(join(__dirname, '../../public/data/blockfighter.json'), 'utf8')); + const roster = JSON.parse(readFileSync(join(__dirname, '../../public/data/opponents.json'), 'utf8')); + const ids = new Set((roster.opponents ?? roster).map((o) => o.id)); + const levels = bank.levels ?? []; + check('bank has levels', levels.length > 0); + let ok = true; + levels.forEach((lv, i) => { + if (lv.level !== i + 1) ok = false; + if (!(lv.skill >= 1 && lv.skill <= 10)) ok = false; + if (!(lv.speed >= 1 && lv.speed <= 5)) ok = false; + if (lv.dropPattern && !(Array.isArray(lv.dropPattern) && lv.dropPattern.length === 4 + && lv.dropPattern.every((row) => /^[RGBY]{6}$/.test(row)))) ok = false; + if (!ids.has(lv.opponentId)) console.warn(` warn: level ${lv.level} opponent '${lv.opponentId}' not in roster`); + }); + check('levels contiguous with valid skill/speed/dropPattern', ok); +} + +console.log(failures ? `\n${failures} FAILURE(S)` : '\nAll checks passed.'); +process.exit(failures ? 1 : 0);