From 30e324d7e2d420a695c1837731e97bb2b6fb1f7b Mon Sep 17 00:00:00 2001 From: Andrea Manzi Date: Mon, 26 Oct 2026 15:59:10 +0100 Subject: [PATCH] Scopa agli 11 all'oratorio, e il tressette rivisto dopo la revisione critica di Codex MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Scopa: toscane o napoletane (mattoni o denari), presa obbligatoria, la carta uguale prima delle somme, scelta fra più prese toccando la tavola, coi pulsanti o con frecce e Invio; scope (non all'ultima carta), carte, mattoni, settebello, primiera; tre livelli tarati (92, 64, 52%); test e prova nel browser. Tressette: fuori ai 21 presa per presa, vittoria subito definitiva (Esc non la cancella più), accuse dette al proprio turno e col seme che manca del tris, striscio e busso meno rigidi, il socio risponde al busso, tiene il 3 e il 2, non esce d'Asso scoperto, legge il volo; tastiera col fuoco sulla carta scelta, tavola descritta a parole, riepilogo che aspetta Andrea. Stesso Esc corretto anche nella scopa. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01A1dxvtsZx8NE9fwjXUNDBr --- assets/voices/andrea-d8dfb86266.mp3 | Bin 0 -> 5108 bytes assets/voices/giocatore_carte-445e962a1b.mp3 | Bin 0 -> 29300 bytes assets/voices/giocatore_carte-9a43eb8b2c.mp3 | Bin 0 -> 39380 bytes assets/voices/giocatore_carte-af54c11857.mp3 | Bin 0 -> 30644 bytes assets/voices/index.json | 16 ++ docs/SCOPA.md | 35 ++++ docs/STATO.md | 4 +- docs/TRESSETTE.md | 40 ++-- index.html | 29 ++- package.json | 1 + src/engine/game.js | 2 + src/engine/render.js | 2 + src/engine/scopa-render.js | 84 ++++++++ src/engine/scopa.js | 210 +++++++++++++++++++ src/engine/tressette-render.js | 6 +- src/engine/tressette.js | 96 +++++---- src/main.js | 77 ++++++- src/story/story.js | 21 ++ style.css | 16 +- tests/scopa.browser.mjs | 149 +++++++++++++ tests/scopa.test.mjs | 108 ++++++++++ tests/tressette.browser.mjs | 9 +- tests/tressette.test.mjs | 85 +++++--- tools/calibrate_scopa.mjs | 24 +++ tools/calibrate_tressette.mjs | 4 +- 25 files changed, 924 insertions(+), 94 deletions(-) create mode 100644 assets/voices/andrea-d8dfb86266.mp3 create mode 100644 assets/voices/giocatore_carte-445e962a1b.mp3 create mode 100644 assets/voices/giocatore_carte-9a43eb8b2c.mp3 create mode 100644 assets/voices/giocatore_carte-af54c11857.mp3 create mode 100644 docs/SCOPA.md create mode 100644 src/engine/scopa-render.js create mode 100644 src/engine/scopa.js create mode 100644 tests/scopa.browser.mjs create mode 100644 tests/scopa.test.mjs create mode 100644 tools/calibrate_scopa.mjs diff --git a/assets/voices/andrea-d8dfb86266.mp3 b/assets/voices/andrea-d8dfb86266.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..a1453c889e303a3ea9c43d3d9abd85c43515fc46 GIT binary patch literal 5108 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 aM>LE0I2+*gUjhH~Liyk5LG{0RkN*QWWN(xJ literal 0 HcmV?d00001 diff --git a/assets/voices/giocatore_carte-9a43eb8b2c.mp3 b/assets/voices/giocatore_carte-9a43eb8b2c.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..65e638a8c17544c58f49fde32220b6818fc68885 GIT binary patch literal 39380 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 ZK!h8Q?hMTKZAr2Mbw|62&T zbsq*`jD0B9k;DW%qXbQV?k;?gn#aOKMK$OT0w84~4D=5UJcJ%i7GC#@VEhik)U_zZ zk2GgyAeQ}te(Uw${P3(})e znBcT495KQ=y~V9oIMCIrLN4TqnEX)FR+gu7DwCap>aeU6F)dxzx_OMq_oZ+7A@Qoq ze$wD5yTL2Kf;hBx0dn^J>Fi@*x6Rk`JAa)s(KAS)y|&Vs&+HeEheXKAo#9S{+PD3+ zKk@z-8@K(0hJD|f_*1-6JDQqD1K&6?sZJi}uV&={hrtADYsb=UnRj7X6GrF>R6;&wl4S?Yw!9o70oI^(Q^E zMV~V>ziR|}fvpfh-7E%8KHrMLaPSPt&1)5PV&8%wKCxM?vT4@kvo-^Y8^kw#|&l2 zC&FM@=#TCp{?8_lv7V&;0N>zz_##lBVm^FGhsJd3Ksj0&^T=QqkN#2EV$QT1S^A38 z8z&t@=)x$iAM#%>0Y{jP$=FXSMi$zzs!Sca@F-e(Lsd7{Xt0BmXEdhIzcN~zk9e#{ zjaeOTZsoT(s`wSXEszgTN_S6u7n!1qZjbQY^>mjdfcIRAQ<5!^+ZR8~8@JY9T`i)5 znCZ>kdddpp(#sw-$`LXsS))`FYC(7(&QSP4t{nQ2I`)%Jj);uaz9D+pbkm?T1hB3_ zY{Ob+5{knea>yGeHAQLjcXZR??Tj#7a~*2E>(iFd0c7+Voi)TDRU$5%KX)2x%r(dS zM%nb-pPmVq^-JC}YPNy-b*nv>ejLhw>~*`lyO+nCsnK7H;1ZSccr3S#SDN;~bjDe0 zy>TV<|4ag`uq>H`2irk+)xoi4e%))<#>h*y9Jga*!c4?DR zSM>I9#ciMTNUe|Row7+8+^8zk@}IKF+Eq{&KFF@^vG)_2j)9}YO=jwE><}O!QT{Fk zE=t5Hq6~)url7(O#@*~4??5JyMt#&=S%*)=6l2)mIJKx43pUBUClx8Fz2um<7CgPJqvP*;{1 z>3(mmwpF;Xt8&+v40TohVhxdTQ(cHo=?sjoSzH{RB6tXB7Dey6$|nhFxNQK4M-q`koDuOzGWNS+}X_t%N`%|bGOZLw>M6EDiq?n!6Eo8 zCgYENd1;W_DH<5BkwOyh@|Vl+!#u~@ty+i{6h#gl438vbB}TR0)#`^`qZiAC2@{8? zPzFY(LDc3-!^0@9{GI7S3sWth1jt)yDbrlX$l=jrWsD1YWl)(^l!V(fBplR=HVekx zK9G_zyd!Y;D%zVj%@!Xf5@<#kUzI?H(hLt9kI5BV>JwP!1oRa?D9sss76HmUX z0@Ur6V=laEcb^xQ{N?I$YS{f_WW7-r%Y+dVq}ZuvF*M^&R{Q<@dr1TPOQe2uejOJ^ ziR&E$R{#JQmV%l|KY6MeH}YijsZ<%CmAgVC#b=m+oBO3>QTrhMPp=cr+Q@O5@bY=7 zdiSy!CyuSU0||>g9!?2E5Cg8@8|N@3^GxIYKv)ltlThGYrhQtpNb`bmxu-~I5glh& zSJn^Go0{S)0S}Av8u{m38=77skVmuak>1C0+DR`-ktGGS)lY3NdyyF(g6M6#%m66F zZ|>E-CM}9&5?1`o?U)$93YKk@Mlo07o%MTl>K?SV)|wpfyR|-_WnPq5Eb8W>r{{YZ zI?mn#<4@V8xHM%E?eMTSZX{v{$q;~DW`b9miD_l0-c#C7yG*<25<{*CjR`i!lDcL+Jp zism@O_#HuF!rp{A*g1z^DVti)ULXebv$}#f`J@(H@7S|ZU10AN%7uI1 z6k;7vhY7+>AuOkkWR-HWLn~thvD-8)B?)uS8o>rvw>Vk7>yAg0KHzG(ltNOjKQ(^i zgaFToqu}&y0SnfYj8Z%t0jn|WD%m<1v=x};_B*W7Y-z)^E`;Usz5*b_&1fzHRk-RQ zJ}dVOlDaMv9{T1><3VswL)rO&^-`}m@qv2aZDskS#*%PTJb1Zo(etp&7g>Kkmxpz$ z30+-*)F+_V=(GCE_xE)czRxiq4OY$#CJsh~N>NK3oFw*t)P&KU)A-G}UP{T^PUHM!~)(F)UY;aO6eCv)GPrIGf{tE;3Q9Z_6 z4Jp`pjWG&1cV;1gW}n}`D&9EVL}59rptAY_thg6BdNK0`P5f~3ezdTU5OV#P6+lm9)!^yY^VOtIu;7m$o6?JB ze#_2rf&2Z>;~go+`*eVOr)a8aDs|Xk@O7Zt8z(l60r7&)xENHzJPY$DB7OZ*)oRxT z&hyljig#MM_7S*6Ysg8MQ4*%N7?Cgo--Jp4FS)VbPtwF_ts})tCKjd5Qo`IAj02DO z`=kr}@<;TMuOQKWx(ajtdT02{_P4taqJKx1hu;Zr?2@Tr9cx7y9wyI&ctk%O?7mi+ zZ?-m%#zT#m4-;~qQv}7aJgY<`Nz+DQ$40Yy$8x#nR+kWXe&CIhf|gppt;`R38+^Z$ zW^LVaD>C`MG*+g>uA0vFMo)>#8w*p0etu)o>9?TgXBR2BG716gxQxkV4_8`rSTR-A zv+do=?I@@eqo$*w-NJ)oJVxiFg8Q)jgt}B zJ<|n+rjQ)`6|amNtl9AC;@hdo zi*A0!S;eKxd(Q271HV-OVubRgJOu+;N=}qA9)UQuH2BzX&o5gvhf<2IT*_-GI4V5d zYi!|G2^q1$$1J*Hn2U@9elM(o)2S6Ae%b#5rFj~|Zo020KeG$@!z-igXf$|rhSYkc z=HB2Iy7k{K*vMCt*G9gpSY-BhIFoI~gdW(OfGfDP$O)vs8^;E{#mF2X0z zT;>+*-&?!ihaPr2uAU*sel2xzW|5fQM3##ZB!4S1ytafzYpm0IYNn<-5)ey{T`9#C zuJw>C8$9JX4u6L^mX7zGOWp<2C(BVnog zu0rS3kQiVPeYv?ruwqWT+Jfx^!0M0L*xe4GwI6pTDIPwsK|or6Qx{>4FAVg2cm_*% z{HH4ecYySS_qI{DNHjmX#;db4#h~*i88Y~QK|cuXxUk$B2dXFN9kDh{hoyao_+cC% zMj(*j_y_*p+=Ig+zh*iCfgcENJ6n&0isNsbq7+1)-{CnGuiN=O0(MfF z=-NCrMxF($wWYnj!&$~dMP@W*ZA4|pMMp@Xv>RL~(Z&_ca0hdUVX|w|p$zg8@C`7Q zq@~(5h0oCuDuIuy?u)J3wRPsUxeA6!~yNQEX`L%v#WfsceC_65CjT}QFT@z@2oZUX$KXD}t! ztDQV3@5~Mbx<7)E$k~mTZ~X?Yn?&#=W*~G93>s$YWzpF@xWilzg_{%?H11 zbcKAJN0W<}r7tr-YBCrP{doIc?k*EfTe+%Qq*3h4V3{fLjEa*r9=uhebK1G0hu4m+ zt$)|e|7+iKDW4X;+?&Hif%^;M@L4js&5q`JWF^WeJPgrZ`fM@8Z~;ITZYX6;br{q# zPTC&LB`)@T@ST&lwQH~LlIE`y>$+K?$;*eA&(c2I+u$;VAD^rkJ{1>KJxI34>E)9> zlbk{NEO24jk5U7fxTh|z761S_>u@n7+Oi0lka;ccxdUC@JqKOGYg>BKG)b8TJH`2{ za`rotg7>jg?|=c=$bJ)0@cnrke5VYl^4=xaoHBJ85|h+?%adWH3P1QhKoy0m*g zDpYfNm)s(K%F=%R8;uhKO!ZgH{3F6bC*19dlgV1gUuYdcTq|_50Q^Fgcrr{P3*K}qPj zEgA?r%#s$$^i$#^{Xv^(BzGff+UKwFmT#QXz(vm7+8!K~f2;y*Ptt>=;6w%^<+ts7&=N}82b#VNrGTjW0c ztB!{0H86g#;L2pd#KFuBEf~#<@=sIuTp}3J=5l6eK2F zt@X{i4qQV|X7Om?5f~&>)UPF`m7T^k0bNrEI~$&%@`F(*A_f9^fkVqB=-VF{#@$4Q z{NR;;Q?Z7QklC_VT~-J|{~fIwFDtmht-WfAprEh`haf+1u25fHC0cQ@v=Doci?t>s z)7AP`mx&+Tq%3gM%mT*Df;^v7nj`MT2VIv=^R=7!r9JTAt%DBpSnRq5%{hN4b4x!57cwBREjGZeZY>3e`SQ_8V_G|xPL*gZ?%Ij0YTg1~`-7#? z(l^duGzWBdmh98n>htO=Wl?#X+}F*t39Vo=+Z2JDvRau0?u`>m|joM#%p&I&mzy13tU1b=Cf6mv*jelLzWg-ZPbhkYr@iqR3-o#28j_M!;L^S z_B*iC;zT1%Q3%=oOqD7w8hcHQ8$xATjfG@ho{-LGSnL9b0}`Nw!bZAnf2EVv6LgbQvY=RP0n_Q24)euLZKW@41G* zB|{v6ZkBMBaFv9A1@<2=r7asS8!jmZVId*C@xB)5UhE3Z`L=TYOJ|K%_?GUC^I3w0 zzi5mj1wbFIgo9lo5fv5{)&}i&otaGpfsDOa${xIatRU@yH=&t_L8pN%eGo|CBOyC? zbEB$QXnA^Y2<+j#9^|F=-Cf2ci)#Ns=_D?uK;+F=p2E{O5U!3ut#8%FR)e2BDk2p;?L|XWFgx}NZ$6303CM> zdq=P=z!%pU`8;t^Va-J8JuG|24Mvh^)bIEchU6#e1(7iyT*%+N8%C}fiHXfu6Z3$z zbESr6WDEXsW-XtbNQMS^C^>v{=(=|wR@+C}oo(X&5W1PU;IpKEZVsMztZT?8!lj$uKjnfj zzXjdaQ&ckSQW<+1jmS zEN_0jZ@XuDc|z?imt3Ttc)zYa^}(o{R)Zu?b#H4-alWG$X|dV*U_Nw8NZ0ev?oMVg zmDJ4lIH1ep`$#9gUU0Mc_zevo24_>=8|M~(82>lu$u)n07mvkSz185REmA1a;rRp@8SxQR8~S_gR7E9 zX;wm9I%nHTf{JbDk#_x&(WJqdzIMCsqW*m9hERJZhs7IbGXFW_7ObzXc-_L`Y6G{c zpMnBCL+dB*j`1U(($pRBhN}e*+unSdL$`FT_PpCMv!9+vDc!36WnRJX!dbn6qfL&0 zk#9rT@W`gYLQ1+y*Zi$Q)qTC?;a0AN>?-X-kR}TiF-9EXFf>;s^6Nd?DRHc>WnPJr zX2hMYrx_fEe$(75}uZp=Asg}mX^-z zy6X$znuRMYv@0-{a4Tb z)Z~8>^ItvxC+huY-v8?PpPKwnV*ab=|3tn2%==$G|5KCyNz8xs{GX`zpLzeQ=YMMQ zKZ*ITp8pf|{xk3YpY%k`d{!jK!^cB|MLl-HoNXz#Z6}aB`fNrDm!EDXu!{5HPL)X8h=mrpy%15ku=SPrL9394Y zLHnNp>bf%pJA|5LOAM`+kfn?*> z{HHqaCO8P2dQK&s{`Tp9CALq6X#Ae!ydIMI zkiPf%#l*J{tZo#s-4P(`;ik(IVau!D>hRg1d+jRU6D8_9dZ@WeS9qkA>0DrCwYAs~ zh~CEIg8#hKftepb^g_m#+g@>DadIieGqV~9K_v63_`<_k>Hp3~ZMCy`_p^1DHTB;= zr_8Qhc~>IYYZ(UF$wLXWi?H>AN^#RK7KMT654qF7r^ntn{Y3My7b&s_U_K!0wlQRd zbWdjX3Aw&_`OiL0AHRy}SSQ#zfZMy9DNi-AvGoP6G|go8O1s4Z0(pvrKyCw`_(yNz zM9|Am^**9Y;jl&0zd-CEfjQeEzB4m{|N0^RZGQTtH9w_t{&t-FGELg8RUB0w6{hwz z{t&d~5lE){`9s96T#|c@u;^b32;?X22&v{JJ0ekHR+Ymn@yp8_Cjo=s{&@PVF2L+b zK{3AKCYWtmjXzgup?KUL%dV=DvB+r=ukO3LSZmMa&Iw8Hv{2&@>eM7yC?YV`S94UK z$5I5=_;h&^eJj-?4ynVL&<6dafPu*hj*L96Dl1Ov+WWxO+2?@3 zO=_p8UdY1}oArzN=l8#ly!SGjAYb%TH5DRkRx3;y(lt5qC(f6VL+ggFN^|D}@pxr=uA*KSs z(OC-K@L94yg4DqbGnD3s!L!^_6l@h`$v11VK&jJ}si0Cz*tFwWgQ#+=Q$lMZ-BOEy z&s`ot)?uNrZ=9sGF&}4ShTwZB-9=jxTql;hncdnG1%2wc%e2D2a52&|;SF>Ush8k| zIkagf3@$h16>PL^XF~>Ut@#jpst}`mnq<(#L|gw%{>c+LL9sw|%PlBRWAedQ3_}B0 zOc0NjR!vh@&kqJQnpSzT1Pq=Sf3=bhMk_=k*uzdP|)iP zwk)o_bJwh^BE_A=l%s17AN@CONXr2B9i$za>!?>V>Z7%NWcyJjus!5H!h8>0Qt$Gf zhSRS$A9j3c_#~)RSEMLHTryt3@+u6m?(iK@>OpWX7SRJCN(5AMFJxwD32+`y#LpBNFPpXQ?$$O}98+ zw~k|_ubG~3b}gGSS&sJ^<#TmY^bj5RyAcib^DxndsUqLVw5S-GTG`ems%I;i>0lin zw1qaCqjB@TB*c`GSK}Qgm(Zls6SDdV&W5nl;MI5oPgGyqeymlf zDFFIKH6<-w#}^t)hU|QNUrcssLJ^;RMxlK|3&5wCIUM4`5`6kr??5UA>WLMfSX`p= zF?oHzY5BrTu0~^XUIU$<*>U$WILQ%}@eeO|Pox_mDqPv=_)2N2emajHvhCdQQiAJA zMltA&l6qK4byk6XKkBMtw%=*rEjLV*(83HUC~Rdn#uwDgT=3-=)GuV2KZC=@y^UOP>c5>u@kql!Qpto5g>Gb_t#nJ@QKzttjDVf_@mlyYre zD%id#O4Wc`>aC=umenBUO#J|~yp37A`yFIU^%|{~xR2c*{I^yjDz)r1f4VYA4W}V! zIEIz=EH6^$Bxht7I<5|IWnMJgTs)2_Nt-s!%Q@#I zk6{~X;G#)0=4iO9+oq)^GcgEoPD^w1aBMOvW7|zxZxwE|)!Z0cbA2xCe$lCjzTF*F zu?c_(a^K6Pe^o&V)_nK$hd7?dPdbz8bzCB;He5exgmd4 z$&oyL!DC)AckA+3aj>iqS;6#;GoEz*sKNxq4jB1h!o~vILuc;$p%e${Cn9~c&N_f6MesDQnJ2I6D!=ED7*z&{?x@TsW89Ppwh5c(p2wn@f6@# z-~nfNQ>F>r5wi(L_esIZR8(}R$vyou^RwQ(pz)2fRdh5H4SPS@mP?jQ?kW~oh^QYG&DKbn?5^}lA(ul8DMC|mN8zapf~e3NQt?7i~y?=RHp zQ&_yByBwEQN$I3h_S9dtVI;NqZ=6eoniP6?azG4fTajV< zYk>c*i?GCoijeztD#}vn&Mh*JYFGn?=XD%zR6Q>Q{-YhXkU?7uGu~X_@HcEuz|Hsk*?=myA6I?tBkdF&9+;A#aX3FY zSbUx#+7xXoUjCR}9$2HAtturv8WL2@>V0Nv;Bl3TCof(B* z#DYA@6e5B0{tyCC302FFHu7;Q-3W{ejZPxDp%o6O-d|q7Xj_2;l{*Bs;7;L)fRT;q z)Dl8zZmRavuqetZT$vlq-Wgfilf`{Y=6N>s-W&u#BdJk0dUo^t9BgY0y0=F@=d5ID zp&Tmn&UM{5T_1v*k3eQN(%05riI9IJv2oPm{+M{P>3*?$1lii<)i!qew7L7#r)JTl zI<%F(^j7a5sRRc%v?r0XS7+NkdSv5;8=6#eKwIb_~XaD z7By|k{6TRmn~jNkAr}F`SOf4Oqq>z+m}U(&i$~SlT7Il&7G0}+ z->$91%}?w0U}7nP;0Yf8zHz3}bXJ^^PcEeGW09MYe&A@oTSQ3$H?C(zxXuq0|NPm1 zBS=P8A*@W7jU^*99J(0dW&i`K4v3&%zb`r_EA*W8ed^wuS?UFj&5IAQlN~fCFR}CR z4Nk9KO0XLFE{u(&jcXyRGQG(#2sWO$o@;#6T`vqwT09%4X=e;=obG$AazmRf)Gz9m z{?Pl5lzqmVulj|u)pJ{8CD5C0df>wT6h{N0X9mc6eTSR zF;JB~8H*rEJn?0!Xs|yjtr|fni!^HIX$x2-AURrE9?u=xG^>&QcN=S+d|9&hHUQu0 zgMG5Q;4)L9WHX0iUDF)PDa(XH_=)!Z%MDrw^tPKIgITgiHc_E z6c!Z|CzVrG`L};w@A~5zSU=O&#$FXFX-ql|!0D`u8cORTFv3H`b-9WPTMwdv7r_0F z`QsOJ@>fAHiD@Kn>g@7(c_hZYXq|K@81|@(qK<_pj0;H7y(}LcX6wqx4;eW98%aiebs(L?eS4F z$H=rWgD-5IwDBCQ|NI#q=+71wiRGrYLuwHaD_Srq^}jX9l@&uJ%t@ZuUJjt>my)B0 z3Ug!f!(_SVZR7#BxtGa!6nBMux|j9emGd*M%`>p%dqBe5_2+a#ANr!%tT1GRGks#S zs{%}+$|sR=d{N3IRM(ZoP!i!Vk=crBm~?~WH{3#>Y;Ra4XpotHdBvDZ+Zb@c9$H%q z6;D(g^@Lwvi5x{FdTvV#inFVl4I{O_>3PrC@;FP#sWd{o@WbWR&&2dAY=QWG(Hhmf z39J-w;!{uT{Z9HpW1{L%^5^5U#txqa-w8*))`cN#f4?t;6EqCF!*YEi_Lol=S0^6X zUhzbX0%=0l9~#l%;r)F$f%+H^|yJJfesC!LJo~x4J7rz zIOVT&sdd2_(qufV80DQ2Az6IB!a!ffnpH+KGz}!UsDJ)8r!==AyTl+_>ChgUoM%B3 z!U%GsauQf;-@*m{+h@kqH3&%|&OjX=D=6p(EI#@Yw(pR!Zu}#x(zuiLn1{?Sm5(7i z$(LBd6;W}Kw9RXF-P_ELER5rQup8)1s_YV?!1*ESe}PB!ueq5Y zRBgi7t@S2>jQeT08Q<&F7eOenD^(lq98hcJ*!6NPoO;Ly%0jgH9mh=n+RZy4$LKEU zCW6!z^P+rQJX~I8-WAn0*$?Hbrz_38DBto=FA0A1_K=T>M#9fH>de{=OVsOn9?v*s zIU=sx6PG$4^$9#_<~5BEm|!W1A>TVrxsqEpqQ^mDYE?yyr2hTWxRQ;+ghZ$!8X( zt^Vg%c}1s)zoI>%AO%F4&_<@$R8Om~&gC6t6eneGlx5UVKXeyH^i!v%Rum|(;=QqA znjXaE8jt4C66@!%c1|P1w`UFvamQu#G4*&WDbuDyVulI4XH-fKt%;; zF+tXdQx6WMmP!AA{Y7+FMpfouI>2sZ$+|Gm|=Zb-saE z3z*>cJd#B!OC&8m4#tbBxC?f}3 z`?f`npm~jMwsQRhkF-e+aoSAx6yuyX&O16>_$baH_#b$)mOg zYpSrmVvJ1Z2uEsX3~(t$tIgqZtfY6_qa~|!-O(c(TQ8(eoJ!9J8%+1RzdNg9+Z z6=-&~sd#mq==J>9XGVAEx~cCg3)|>B8KZ{_i?(+3^yF2t=od&1WEb-A2lD3@l6%>> zdxriYr^fucu%XsjGGUt%J2F6y??aN?v)Gk?oaoFyc;r|Nt_fM{UwkE zwdM`*u8GK#j2WRJ<$H(Pa>7<7tax~!p7(`rThzu>60@`o#3XJ|k)bdU&P=@jS^!hJ z&cYYeM9THH=yUoz{TPe%ntIjcbnnt&y>HS9W0Rj3w#OS3IG8^yg*P_Kgbg39O?`3B zwDk4Za352U%g>T3sFINKEzGU%edDbmPJ_h^zc7}GfQ*NvGN{0>w_d6 zDTz-E`*wY{^zN=`zSVgd)uRagygH;X!ONKYu90hTlBZVH(@WD>ltDj~2o;Zb?`r3! z>dpvVa&}Hz15|e`&yMf7p2{_Pfw>!H+gOhQkX~|aELIy2EXI$ep5=kx2t!{kYi)_7 zFEw0g2vfuV29@NtR2F2HX=5miVaPuMhQ5<^z`$irSJ7r*K@T{ zofj+qYQs4aE~RVNSpDWckE(GW-Ox(`4ONS~MbZP^y-V?#o{9a%Ais0t*}6cs;cle! zh<`+i_iO$TgEmBbS?V+cVLPYKNz37r}v@49eaE z(Jhqj<0eb$SSvEMa=O&=i3A<*6LUHKaZ;3!7eNof=XuS2p%N9Kn z)OETYF*IW5aatE@5JtXD z>_oK$0XjGtd5G*RVo0So&$Dcz?ThXw^bD3qZ)uPZVv;Fw25Ems6H;KLu9q_dQ&!T* z1js5!G*b~(6XJ4nRgVv2`r49*(8S#6(kqQjv-CK;aq>}$>pP?G5CT_yO6jI2EK2WK z);GD9D~=a-H@UbvCc9+{w4kW=s);s}bTzZOL9f%5m`>D-YRK&n>`HV~_jLkHoNXJu zUbb)g=QV0Qg``uy$-9E;yrR$uU9Ad)hlY#mCDBcBdzF%YRHv{KyW&`Y-?cZE`#N#F zFKTo~f}Y%n=yym6pJz}L8v%+-lYUyNi{R2Y}n5$yplh5x)L#&P+V?@GE68yL3~zDODxa?Vj*I z`_qGBpu*jF5#Id^NfH=(y!OQ66d)=JfjpmiM_jPU4A%u^4)MZM9+oC&!USa@Cts+_ zQZ)#GcgFXp=1s}|;F@j4i6ZY&3^O2DwurBBZLNp>oI3X3b$}zK*AMY{?QM!QJ{i-K z`tD0u5!w?>=pzH&Ag>8J z(QRRA<1i5Bb^5!3AJ;t|hR;tFgVai$I8KvWO0&x^(eJRY%C4ghBcs_!kr{77#XAQM zCFIJce^w%?`?F#MLYCt@xh@hY7prmMX;TlH@`o5f4rC{=6{zEX1M>BOyuRk=xky&r zZJnwcHZj|TSYdy&4u~aTxfMBlHn86~#aM5d>LwZQmXl1j|&;w$Fl-N`7QIb;>J@%7TPq6MiuLcQE-lckc z7BtQYqhDS)r>%P`$NV()wU#(Xv;^|3b#qzO?rhF!O_b^p=eh%}Iu)xD*RsP>(R&Fn z3A!}$=WQjLhPLLqyUfd;r`-sqnebp+;cikICA~kfd*h^`jBTFa^_9w}oy2CY%7DsP z#>5_?v77K?nH0!TP%$QF=d+_3OcCsB`Ebs*VhmAzj7NQYwS zcj{?yzI#8|-VjT@{LT?4pgbU-`FR51<-h+-vV_|6!WLh1qGk{UgPLoSf$vFvNuIMj zjWH>MF9qgMvbd*jLAFUIbWk#s8_F0h$_rY8)5odyn(QW4O@`O2v;F$UDM%jMJY(z& z+v96R~FoS$6ASv%Dm*9PVkjxi^(Icd48hkSn4uCs@{r1#G3 zJ_0+NUfr3yG||J+w2Gh}#qu#sQs(rz#S320bzo2$+F$3C|;%z71TG&VW9R-_d)oSx16 zo5yEBq0unN>noox&&vDhsVNw(|e{QFYYXoep^}q&^Rk%cQ@`vkk+azv0kCL&}*hl$Wt zx{-E5C#lh$mcluB)6<)R7~I=#7SSPIwxq`-1^`Y6Z`L|R-$2bh%G08U1X;>qOC{Ml zE`?@hS8GXkSKr5m@^d(#I`SUiAJhh(AE$=gmo>-PS%bz*Xu6Xm5n1QO)_|%KguW-O=_w_hJA2C?CFB>H3YoebEN2qJ*0tI37n zV5_|ax$G_7+;v>=a^*!Jy1VlAzvpKLIZS)=Jt(Y0xQ%cZ<^Z!O^q^<@&#C|4Pp5JK zP@IE3-OD&gOL1U0ig3ReN<0Lp!{;1V2%czD(p%c69;D@BdYYTEAbemF7W zIIl?7=S*PI$^sQ>355>2jv-ec^+oJmzqt(3T%g4R5l}ZdiE1+N8Tifl>yeS^BFA&I zPZbyjVCGX;^a0;EjM5b;8A?vHq})I+QTD^s zy}+)zx6!0Zi{Nawy>IHC*ay|JzKq{A^!2sY0^xGE%z)c>q`TZ^wA8PO5%AO0N;h6U1J9}?j$6@uhl^Tq^x)RuPPXf&Li;o`N$7p_kf!Uzp6P2c$ z4aY*|5Z)86f8!kxW<&G|ZDmZIL8ur_Dl8L7bqr8C9UTlNV~}Y$?aE3t!VCNadDhd~ zO31Ov3z<)ljoH<6$xQEK(4o zB&orw#4}2jCp4=u5_G06)JsL#O1UI5a#guEcaqyHnzX7<(%6@W*sahTE6P+25bo;&3CRtXf68Z?iu9QOAaL)bhPmJ9A8;e1-vR zQ8aWvgF3gFHdhU!I~R|(bs^6cy5!Lt=O20l>p1xNa2DLg>A!J-M0)c%I|{^^g$?D) zwY~(8tYq;_JbjRtvzG#hdTWibNwTW)SSvs2Ati_K%YB*dG9s)&vUCI{4|Og|P0r+m zX0zBKs(gBRNSCMSz57TicTb&zjJ2&U=P{rN7na<#e}PLUj8W*XhGZ@4Du|H^?Aj;v zFrG{^g;^!dJ*HZQ${9n2MRs>V3ZzFSoR5xWhzwy%dE>mKR=9MBog3k|d@1YZD4ZFB zJn9!d)3G&SH62K6;eM;R$W3Rt@XdU9mJi&qnvGW|9OW?z)QW_TW=ygd(0(tEXi87J zl6Gw2ZT;zxX=(&{zc&NR8CaPvAg zdTn~giY+~Y>74?m@YPrul06mwChl@Tb$XOG{p^qBtOWfA5Pc6`oRirbCxrfNLz8n* z=DKebl}UZ?XFKO`{l?*Gxh5$2&a+!7-wxv#Y_!bx!hVYRfl2Ui>gGlW5_;y zI3tb*4q8u_9EOwxAd#jL{rst;z?UxmW6z>OrPTH)LG8z$1dg-|4P1^ImH5*t;plr$ zc)~Z%7a)l;A8Ji~#|^Br>NP8lHX~5Yz1pDMGJ}vP3Rg?w-#r z;xV1t8h0S;tEf;q?ATpZ-Iz6bNYIAw#CFwtTAdm(F2D@a#}zc4ni-7b+3w5w7<0e+ z)H~GloCSooD=04e{d#>v)`BRdH}yah$dhIrNf?#G@N*oeoL(+Vn7Pr6RjnWW0|)nt%cWg@0gu(+ z=Ceus2Sf&V`jhBIIpb)|fSdu_6tQ(~!F*^{9J`43zu(LK8`r*~Zh1uQ*Buq-f@S|6 zrHsj5ih)%{_S3u2pB^K>cs2g%`HOj~v$|hFmpl{Mq@@+%*w6z;{E9Qkh;a|Jc*2Pj z^*+{Lv7VR+&2C9Rq|A#!jRf0k z(sP+7BUw9U@+hBWoli#Dp67JyI-gu}|l`S-r1u!0^uJ0 zWyNSf-W%r~u!rg|z*kXtO>PRXpCx=NJ7hUpG&of}Z3Le!p_NZ^E@4~O_ zXa7O|UA2`R?p}J=?S&I{Tpasa?rCSQQ3o31<^<7Z+kodUQ!TLvw2i(l71nArGz zWzsJtKH;UfFT2csR>J5OR3&a4lM9#5k@6z%nM;Y}wmOe$xergFJN2~fL(a=ls61qH z??I`p8@x^#E8(*zLM2s{$saWwY)dNIVg>g4FYTS~;|jmkjf-a7R|v`f_uV_rAR3-uq=HnM@|X{MStO-fOS* zALMbbaoA?6BG-%rr~7u|4^__X6Pq^+2JylhEqZCr^5b&5*)=PaTeR#13=e}%c0GPj z*G&T){H15dyOT8@wd)yFCM!k{9nI(;LZh~*N(at@1r4sYGEMhAuN0CC8D&H) z90$2$4At1~ulxC#M&{OrmIuZu(^3wKsq@Y2kyW+_AD2qi$;He^lrI~~F7b#T(IeF| zWs{efUj~5qgP#*7mXx_(Iq&Hu>0jSZ|LqUaKFh=41L#+p#m8RYIS*5bN&NKSvB+9y z>s+*0=KiKr3C)T z!$OIS{^sG{(LwnwA|KC*3pJvc3ym!X-0wt}w~kNuUkTaaOvHmqNd>nUF4VGzf<825 z$T=_WXJ5P&V$w$rjjt35>bKC}$o0OHdgVL^2~uIDDYC)V5;nhawACTb^`h`myuE$-R9{8BBKk(^qM)+^XcK}IYS*qB(K`%>g zK&q&tz=bcmcHj8=i+{-&-=X3A7wG-#ZF#SUSI$F8eU5;@%wAZt#GRUFyE}5PCnLdY z=#+NcTB^kKZj?p$#zM4kARRsB*osz~n#O$eG|YlakcnnhU*@UigC@t|`(1kp!@{xf z5k6{mc+iE!<&HjKiz`!qKs;S^3@Nf65V8NVK8Zo<+K2re%?PBD>6@hp8<|c8I{RTSs+isvWr^*wkuJR7+hKa~j zY97Rdv+x+>-aN^86S1;*YExMbLu(v0gCsXO5XzH%pm)14V{e@OF&2C)gzEKgUxti& ztFqvycbMV^Q1!;ePr049ok>?fzLdX`vEvw%L)AIT*}b8DOt9*tRIpqGkgjkZ%bAg= z*uel0Ft_J#9A|1im^TA9pLp7>btM$A1~;O%QDes8Q$|Pj!D0m)`%|(*kQc03-Z8<2 z#loVacZr9>!-{OIuGOzWqP=0>II^Y~v4|$%1X0&E@P1EFCCJ3XS>Q#2H9q5V;az4z zJ1h33vwPJOMBC_5axjK?;NmJFilM?m2n>iT*C9?jbpprU3aS~~Q#r%>(uLeqJ(zx- zq=ds)gP9Hr%9$Z^9Xb8Gd&o+aLeH5wx3N1K8G=t)wMgl}njU(RISN&LN(~-Kw$AUT zo^$aX;A|KUp{SGicC~vM!BjU&%taeU!^9xmLTgsA$-Ab*$v83RZVD7D6^{~R;3hh=$wdW-^T;`x@el5_K`Nrj>@-zxD z!nAo${18RCQciaxn%z$0BVz}mJ?`q?6{&7km`-%-zZF=89f^wA{wv*r+)A7-j|&i6mqXMq3wmn5h@Hca97 zPj(XFJeYO01bzmgb8ggHv8xaYT@gctkU~or)_jkOij9i8z`hC8xppN=yasxD&o3=} zUky#$(|lRKmIU-X-GNVtu-*wgvJ4_vd~(A?i9cLlq4JySL&{oE<{lT^BqH|XaTR&DuL-p8{HX9btoYD721AO3aqxtdUcguHp8d1sV zn~Se{YEV4ZE|L4lpQJ8$?5bZ=l{H=FFrExTG)?)(zc_#uxXD(IhImgOJ)wWxdw;2; zdVg=~VGe~=|B(0=cve#%JKRlRi-^c9J+h%)Q+>QE9|HcC`}?|8!Ahs}hrN6oU_9I5 zgpYNHRF?fC@-~Ob!Ikydczb{h`H~A&zK>9x+tuyd*BsL3rp`Dm4NK+rIC&p%+=T6O z?yPUKK1dp^TDhJi{FT#*f||Db)@zUpOqIm0zZM;X%gDv*80G?l90Wjt1o9azWa+{_ zD0uV5wXrW)Dn>o7$@On?+GXz9FEsM=IfQv%GOX#3NnNomuGD_D4S@q;D}bvKfD2+= zGs;LZRBWGdz2#N=3l1(T+6fQ72%R@}sSbehgd^ieX4Y#bvVQ!KH3?OXFtbPD&tJAU z6Q+8TuQ;+9jELYseLjp}BRbc$Ui5EW97hS)P&+w>()P!=+}KN~FhS!J76MXQIJd{X zcT4d6k>Phe>z@mzeJfdGpR&hyW-UEACZFkwoskrzXWsurrq%<7bC+T<)b-&uSmGcf zeJp~3`H3Mx-nH+}p}1;zA~Cpyz&h1;wXsvpdGa;{qMtu^vW=bI-x1SVs=w-4Oy$eaLOX_%U34LPXUj@( z`N&Eb_eU{a5@cmdVP!`PzT}t~yV18!OKuod=lFmT&u^qS!qJdTdSIsaauOyfrQX17 zsZJQ)&~Ts}H+d>7$9)#|0<9$fUQd9}Sjb6+i1|0Ce-PV!oRo^$7bKR+|C=_BT?g9A zXh9&VaBMQ;;a~sh+h=Z*4}9-VS36-{1+OR4dAIg^T*e&9pvLq;*ytAv(pG{_z1b{#G9A9^%L{-DNpqa>H`y}g?Y$@ z$Sq4ei!TW?Xf|Ra#HN(qR6C!KFV(gLA#JklK7|(wsf>U{7|BG9DnI}IQ%sox=Z_I; zT~ucD#b>!bogdJkiuDeNxr57*g{RPM~ zNRd6K*WI!ipwSU>GdP;u9RbJHoLg_)HEDEL(-kKuuZC1?AVb(lS62&Zh{@n8?#a+Z zRTN8G8(LdX(|qVE0Q@a#y)K_QIO}}nyrU;6ZGr2PN$j1!C=-{AkU{eE=&W^@G*rQ! zSz%{(rB1V$uu_5{nMnFoZ9bmC~oCQ!VKa{>DHW}sPnWdZFS2`Oi^ z+DlE2+C;Mpx=qd(TldGDUp>ag7XH{5j40G^#w9kK{9idw1<_dQLwqU7z~pla!RGJR z7KRawV^hSoRMUshT^I}u#BAn}GeiumW zN3mTTNK}keN%FG#kv>cjGm1$x<0OJ))U?hu-3j)Q8PbF1%z4!Is-mPqNx%b@>OU!X z2F&8u=Eu!&E`VRnR+QZHpS1UV!Hz>Z`+1Qy?5lEk;E6YwF=Omfe-=w#IWw5nF(xm? zl}5~l94Wh9KQ$VvmRUKUsvs>WHKuziJh6o2I#X44uC7&71Qx)MJ4L+>pJkL)VeCiy zLpz#0X7E{U&h}V-3-he_XHD5?feh(-763R*%29GCr~d1C)DRE#A;(l$?{_rP3*C}7 zd3?tLTRBw;lHaLZEpnAWK^LRe?a47iV>4vD_<{BZ&apxX$gsat%;e39Z&u-2VA*N+ z)8|*t0P?EJeNx#$vhqwj0PNIsl_i)#F(kd>SX`^I1oV*DCZantkbe838Cb9t+Dnd$$fQ`{B9?j43n5k8CAl&RWxF*@;RLS?!5)T5R&(Rw(|NdNq~ z4s+SfcTt+4F}Lq%?#!qWNhuPaT#w+LG>2ViG}0!YCMvz{m*1$6;Y`5Ap%)?JBr?go z{fugdLbh4~kIW7Dm#57o-#XeRmJ6>&fCv5Yg#SD>^~(U|%--=`y7%K_wk!6D{%~YT zc@Y|n;(+lY`$jn#K3@MM39Urfh-x1jOm#mN0>QL6{Kw^ByW1~x*(@Pt!~vOU**McE zrfUatHsUQl+~i+3zpZZA`pSEUdp)dB0U%T5v;%Xf`Fhg9TERcTLHQasirN%BCA9iN z(irZTblr)vgWQjK#I6-zbm!hs;0ETs>e&TYnkJb_djvNnE|FW0Um6O;h>Xt*=k(Q^V%Y=7F|3o}fD|iRmH< zGis!Mhfg>puDJna(WEs<)}AAcovpNi$EAe?1adN-Du@_7pVXvlwQlxXel6EesyLR-noUsY7qt>cw*9#D9B`6L&9xm19axTN9E z9t#?Bjy)H3l(4zp2hpev_C- zl!O@4ZlGtImn;qrhIw23ZSpYVTe$1HPc!(=tC_hWpfL%CGz$nr(tn1eG z8H!<^o;X}a*QINEwO4Iw`#kCPQRrboas`n z4?=RNZH0@XH@`slH~!jB9bG$};tL<0KkRuO{ZZO0vh-AYuJcGhc`}6!{7Tg>oP}A;(I|!rZ16l@@cU zI&Y&9dW@XMp@ewF_!{>;5NBog&e(LeRyKw1w6@#t0aFXVRsih+Y;LA{H7MM4T>fua zdW$JRNlscKFer?9y2LNdJm9%ui9vvW`9N$r-s*?u=N*SGy}%hD8P$(TSM$wdHpJOr z7#Mj>YHjOU2p5Vf3R4+sv&CU4Dw;r`L8i$GB*xp;CVf6u+U_=FvT+Z$!TX0{nn;`r zf3cD|E_^?0+k~Z2s}l-NxOi1v9hUGb=TG3{2s@^h&ppDje9Nlp>dU9FZqQ%>P<7>`e~27G|u0iFj^qP+eXSXiE4Qrg8MA(B9@d% z=|x>A?nipT1AApWd@#Ih2T8)0MgLOqesGw>!h#g@zkJ<3L=U*|L|1r>^*$DunuC$% zfFNOqVD%wn(14&u9;P=4-omGO50D5{`xxA^2;@j26o4~I^M$=cW;`SAsyE7JS)LwX zJ~^#Et-Q9|k7b=2?0=lzGjb@{ImA!#J7O!jbuLgCa3aOJs;@T$Wx1F z6`W;nYywSO^A~``5!)Ys{!vDKg9-W2ww)U6CXs}jgsaa3VwkllO_9gRN^aJ7ot#Qp zpGU{@yU>6a|2$sI@_w1C;C*GPYB`Hj4yC{;BxW{?e^&8XF&~*07trdiN4Tq|Hkp%{ z{j9to?ZwY<^fldjbGpP`hkux!x;4xBMy*L{ILWo7$9ie5x=$ua&AOZSR1+*@^GxEHK$N?#cg2U(lWo*~1d3>{stpthLB8?({$NlQ-nTBvEsr+cIe8H5^KH!nKaZ)0Og zK^Uf|S(re43|!H3!;TE7`l_WG;wVnZRu)+neKx7aqNOaVnC?L)KXJXyYn!B-H278? zw$2c=Aq9nr+fDlFvPJ$j$6T>fR!Pkm4yv_z!up>P*dAnrGBNB_JDT0NtI`CI{Jb`0 z$TSx)^h|Pooy|{kZ5~LWl*7`TDlylu9!idn^wsrn-=t0Foo}UO z8JIAm6Pgfl5y;8havyc9#DMj-zAbd6uElaWn1$_o`O_w;-ruyNL>+g)z#UE6^2OoZ(FLi94dc|quuM~9nH#|IM-~) z%4wK*snug{#TvIkhJ3oErxNgSjGex1?jfmrU$> z_!<}{NEpvrH|p|Ra^rENQxiFu*d{fm-*<;|usqQm8P)D6OQ)7k#w)aQ@3IJrgSI%z z3_itC&5e)!YI`!c3*JUUDC3&Xc64`+NK5PIE|`?ac;&pJz2aA8)MJBjXxKY?Y`pXD zjkai~0nT@CS{?E+1<>b7OHM86iDnJWo6c8x?eiJ&`&EO%%T<<0i4wDVvn04~1RxrY z@;nhVhl#0K3pqaAM%Jv@34Rt_URP79Ih$H1#`a0W8Wp*?b*M;D@e{8j>;24fD=53~ z$tOG|>E6A=2u&tS|MtVzv{j|$lTD_{2{R`_D72>vd)#5{&N1-aUqkP|oR`uk91TXF zdSORcW`41x=ml)bDSFJk&){k!W+n-w{Qjb&^~p_Rd2YtSjMS~llVV7ta@)Ydx{M}S z|6YpDK`cHK0w2^_@csA>;swR%^dj311e;`22b)^cAnsA0D&~*l{gRf%RZ+}~RLN{@{}`xb?ru^K_e@P4k{jk-Ciyf$CwEt7uc4tv*-Lp(4*?m> z|HgM0fW+;j(G($!?~x-q=K83g2TdX73f`_=C^9;>W8hvLz_>KQ}Hms?}+i;}%Kp-zmlRbbhO`G5?Eh)tZ@0-mW*_A7YKUmrJ2 zHn`a0nA`hZHXtz3De5!(fpeQ_hLn{mi)!$9rgD9!7Oghq_hPb^65MZ+Mn9SK$Aw?p zTGv0CyM7r~gOiSvr4w>ewzU^4t~_-!8nu%vB(lZcYK^R(Y6is36RArcF6~R2CVj=M zJ~73N>LT8lXuTfwWUQ)4-UGD=O-Ojq zOOzo09ombH9vIHdYg35i%^I3+8|g_G*^|;n7Y@&%_a*KIXy&+HqHJz>iR(2Qb)Q@I zt^)6Ud3ky~M){@sHSq~_`3Vh)!XwjJmiD7a|YyhxP z@z@xmdflfIfZ588bv9J`NahrUG=M=QWD>GGo!J^q1E$jn3p>|f|ChI^1&AXSr1wc@ z>lQaWMpq?utOl28Vglq$Np7GG)#Z^Ld@mheNYtlQD1NVI1Xag0?$eB^hm1$0VZ9+e zIV;Z`kNVqrUzr&YQEmzUHba;N{!F!}cAGN%J?HocBBz6ixFXs^Ynirxk9p7FO62_Djeq&ky=RIFPb#N~zQH$5+!R6>T~pK&JI2WXy?cQc1;#KoqPPXO ztA#C}sl*sS&#xsF7#Re34jDL#;5c*vFnk|yaS?w&6ELwC-$l22rq-M@Hrxik>5Ev> zS1enE!(5;_omNW8p3o3I9Mz#>bmeABgVk(R7r_AREFK70k-e06JVq`seD9O~?9i7rjve&kGTPnO{JBvvl z9hnt6>1GL#y9M=Rmk8`}Hl4O!?tC}ut93uMbaNWnKHEx}9e(C*`%Ty;gS!=qXDS*X zh&mi05h9!%^JrZr@^l-3Le0ed&qv~xwrI5WwjZ%8G%SkjvujV&lrfq?N>HA^>#F+^?&b@$PsSUdfk*P80z5aWwQ0&Pc$l62)!Z}tC|Lln? zr6gIk))kRU-jS;Da`>s(ZE2+AV zLqFv>Y?qQ%C=L1QmRfqd)zE)~KRHFDEDoQ*t;WJpD@QJMYfN46mGgvhRCnn1TVgdu z=kuI95RqA&DE?0onmJI`*jO9^l{96EE!y(Sca@j;}UiE(+ugiZ)N zt6{}C;FOow^_2cXXe_sdgDnUz#GcQQ9CE#MI8Wd7i)hDKUDm01%Y7= zlwrG}rnrCTnMHyY@L@^3XM|K*(&+hVL|cVA(|1Z>Xh@(81p(mauQ912ZP=ywvIFZ$h+3ruXIFnwmoqs9OnA*aep$hxnq8Lq@*8R-U-8UlhUJ_pbBw+p>mL7VzpwbPoo{k8@ec+0sXf1XgVSdL= zc@pt810|k%J9r1x=~aIuS+(qUH>R&z;W^2mn?ri|`E1_VnysIzd2 zLrX)%M3W!YeKl#f1M6wa9rM}<0mfd?uM(@PUI;H~9F|#J-;uVA^Dh$tOhrj^=k)E8 zqo5;?3?G|}U*p}Qea=7c(yH8cW{O3BvCnb#ymH>rjq=xo^dm){RNzEfYa^4Q6KsC@ zk{-SO;m6<^pUP6>G@i^P-DAXMS3t#8pv#x508}*IJeijjw(}cklmdp?q{Us^}cGY)5Z&+MDn&+sMoG$zjyjO*DIFNTfOot8E+2e z@T_3Sa&HfosFQ}QSK7rALzaK)$ZWuy`lJ4|VeftFekbwRbmigv%6UmkSz5Q#Kc2JS zuv|DbC~IZAyT-~OiKL8j#x~{z5x(1}y_dp=M-}E7_gCF_UwQ69r_9IAKsudY>w!9c zy>-kaPU5H9AiG^S^vt?5r9jfveMYUbBM!V|Y=@zOrDHvW9t*kct$4V;#r{Jmps0(z zFOJbClmhsh;N&FIW#D7BSp1{W_Ir|!G_B;6qWec*^~2Tv;wU}lC%nyq6dVqe)|pq% zYla8fe7G?rrx{iLZ?L8@LKOrna5gY z(H(#!tq)E!?<>rEVzBh)>jY8Wfz#gUrYVXcLhY|;2_y3;0`ev7iteY`ho`aFP=Zt;MrffDX#2O&Hj=# z#hZwMt(ffe%DGK7=TQ&&7K4V^HLf)Y-M8I|CIAS9PKG;JGBJgXAX(qOj7OsVLH;-u zFM2g$?0*=DjpFEs1zIZB$77l~HvKQZU*z+P!5CTjxe2aNj%HX_cw{Vhi>p<3Gi^%hO8z=-T<(*32zySRMY-Yv1>{!p4Hv_u$s7J2 zFV2uCEeSiL?Ys3?&fiprd)e@RZI)O3$~m!`l$JiUo24Z|^;Fhi7GJiOLVHIxZ)e^c zQGPJ33Rp9GIbjck4kD7pXoal6u+@B&w`xmf)4;Q6)+Rl~KbJKQFK+%z*1x|+a5o8Z zEq71}`5X(Ui?-Ip|N9&LXd;Uy?O_O-(d(<1ym&e3_bEH54@ zal+`WJ#oc7j@$*B=blH~r4rAlRr%xj+k)IC2lEfE%t;Xvq8T->fSQ`DdifVi$*kQq zD`RRhCK$9Ky|f$zE5vC!0{g1jrJ@i#S%W6kz(xDq;+oZ>QpNt5LJuYLQ=Sy+U%sPF zsIlV{zFfGP!L+9@pP6Is3Q+F7=iP_Q7Q{m^ z0{dj3n3C2P@jDFvx5g(pQuKV}k6_|NY*8XMbar&rEV^&g7<6yN)sm8uqF0w17Hw>z zq7I|yl7MnFMA$)cnp%~Wi~Lc<(w2em;aBbV#zzJ!o2x7Y*907dw}0{=g1^8LyQ1@(dTVSLd}|6d!v|F0|mn|%KX@XGn$_4oeI JXZ>%U{{uDS@Z$gg literal 0 HcmV?d00001 diff --git a/assets/voices/index.json b/assets/voices/index.json index 94e70f4..2320693 100644 --- a/assets/voices/index.json +++ b/assets/voices/index.json @@ -1531,6 +1531,10 @@ "file": "andrea-98e4366a56.mp3", "sec": 0.83 }, + "andrea|Una scopa?": { + "file": "andrea-d8dfb86266.mp3", + "sec": 0.65 + }, "andrea|Un tressette ai 21?": { "file": "andrea-4fbe84fada.mp3", "sec": 1.66 @@ -1547,6 +1551,10 @@ "file": "giocatore_carte-8dfadb4acc.mp3", "sec": 3.4 }, + "giocatore_carte|Ai undici. E occhio al settebello: quello è sempre mio.": { + "file": "giocatore_carte-af54c11857.mp3", + "sec": 4.27 + }, "giocatore_carte|Ci vuole quattro: chiamo quei due che giocano qui dietro. Uno ti fa da socio.": { "file": "giocatore_carte-3e2671e35e.mp3", "sec": 4.88 @@ -2099,6 +2107,14 @@ "file": "giocatore_carte-5dbd39a25f.mp3", "sec": 4.44 }, + "giocatore_carte|Settebello, primiera e pure le scope. Oggi l'hai vista bene.": { + "file": "giocatore_carte-9a43eb8b2c.mp3", + "sec": 5.54 + }, + "giocatore_carte|Undici! Il settebello lo sento arrivare da lontano.": { + "file": "giocatore_carte-445e962a1b.mp3", + "sec": 4.1 + }, "mariano|Aspetta, ci vuole il metro...": { "file": "mariano-e8b4ebf3d9.mp3", "sec": 1.62 diff --git a/docs/SCOPA.md b/docs/SCOPA.md new file mode 100644 index 0000000..1965e02 --- /dev/null +++ b/docs/SCOPA.md @@ -0,0 +1,35 @@ +# Scopa agli 11 + +All'oratorio, dal giocatore di carte: «Una scopa?». In due, Andrea contro il giocatore di carte, con le toscane o +le napoletane (stessa scelta della briscola). Motore `src/engine/scopa.js` (Claude), tavolo `src/engine/scopa-render.js`. + +## Regole + +- Valori: Asso 1, poi i numeri, gobbo (fante) 8, donna (cavallo) 9, regio (re) 10. +- Quattro carte in tavola (con tre o quattro re si rimescola), tre a testa; finite le tre, se ne danno altre tre. + Gioca per primo chi non fa le carte; il mazziere cambia a ogni smazzata. +- Una carta prende quella dello stesso valore (se ce n'è più d'una si sceglie quale); se non c'è, un gruppo di carte + che somma il suo valore (più gruppi: si sceglie). Se la presa c'è, è obbligatoria. Se no, la carta resta in tavola. +- **Scopa**: chi pulisce la tavola fa un punto; non vale con l'ultima carta della smazzata. +- A fine smazzata le carte in tavola vanno all'ultimo che ha preso. Un punto per: **carte** (più di 20), **mattoni** + (con le napoletane i denari, più di 5), **settebello** (il 7 di mattoni o di denari), **primiera** (la carta + migliore di ogni seme: 7 = 21, 6 = 18, Asso = 16, 5 = 15, 4 = 14, 3 = 13, 2 = 12, figure 10; senza un seme non + si fa), e uno per scopa. A pari, il punto non va a nessuno. +- Si va **agli 11**, contati a fine smazzata: vince chi ne ha di più; pari (o nessuno agli 11), un'altra smazzata. + +## Avversari + +- «Alla bona»: una mossa a caso fra quelle possibili. +- «Seria»: sceglie la mossa che vale di più (settebello, sette, mattoni, primiera, scope) e teme a occhio di + lasciare una scopa. +- «A fassi male»: come «Seria», ma conta le carte non viste per stimare se l'altro può fare scopa, e non lascia 7 + in tavola. + +Taratura (`node tools/calibrate_scopa.mjs 300`, Andrea che gioca come «A fassi male»): Andrea vince 92% contro +«Alla bona», 64% contro «Seria», 52% contro «A fassi male»; circa quattro smazzate a partita. + +## Prove + +`npm test` (`tests/scopa.test.mjs`) e `npm run browser:scopa`: partita intera col mouse, una smazzata col tocco +(telefono e orizzontale) e con la tastiera; in ogni caso una presa da scegliere fra due (toccando la tavola, con +un pulsante o con frecce e Invio). Schermate in `/tmp/regolo-shots/scopa-*.png`. diff --git a/docs/STATO.md b/docs/STATO.md index 14b1457..188bd8f 100644 --- a/docs/STATO.md +++ b/docs/STATO.md @@ -25,7 +25,9 @@ Online su 50037.manzolo.it tutto fino agli effetti ambientali di Codex. Aperto, disegno `briscola-render.js` (Codex), dettagli e taratura in `docs/BRISCOLA.md`. Prova: `npm run browser:briscola`. **Tressette ai 21** (stesso giocatore, «Un tressette ai 21?»): in quattro a coppie, busso/striscio/volo e accuse, motore e tavolo di Claude, regole e taratura in `docs/TRESSETTE.md`, prova `npm run browser:tressette`. - Prossimo: la **scopa** (7 bello, primiera, carte, mattoni/denari), poi basta coi giochi di carte. + **Scopa agli 11** («Una scopa?»): carte, mattoni (o denari), settebello, primiera e scope, presa a scelta fra + più possibili; `docs/SCOPA.md`, prova `npm run browser:scopa`. Con la scopa i giochi di carte sono finiti + (decisione di Andrea, 26 ottobre). Tressette rivisto dopo la revisione critica di Codex (in `docs/TRESSETTE.md`). - **Multimedia (25 ottobre, modifiche locali)**: il libro sfogliabile già presente conserva immagini, battute e riapertura dall'inventario; il fruscio ora segue il volume degli effetti, non quello delle voci. diff --git a/docs/TRESSETTE.md b/docs/TRESSETTE.md index cb397da..807c644 100644 --- a/docs/TRESSETTE.md +++ b/docs/TRESSETTE.md @@ -10,27 +10,43 @@ Motore `src/engine/tressette.js` (Claude), disegno `src/engine/tressette-render. - Si gira verso destra (antiorario); esce chi sta a destra del mazziere, e il mazzo passa a destra a ogni smazzata. - Obbligo di rispondere al seme. Prende la più alta del seme di uscita: 3, 2, Asso, regio, donna, gobbo, 7, 6, 5, 4. - Punti in terzi: Asso 3 terzi, 3, 2 e figure 1 terzo; l'ultima presa 1 punto. 35 terzi per smazzata, i terzi - avanzati si perdono (11 punti in tutto, a volte 10). -- **Accuse** a inizio smazzata, contate subito nel punteggio: napoletana (Asso, 2 e 3 dello stesso seme) 3 punti; - tre Assi, tre 2 o tre 3 fanno 3 punti, quattro fanno 4. Sono pubbliche: chi conta sa dove sono quelle carte. -- **Segni**, solo di chi esce e onesti (il gioco rifiuta quelli falsi): busso (prendi e ritornami il seme; me ne - resta almeno un'altra), striscio (ne ho ancora almeno due), volo (era l'ultima). Si sentono: due nocche sul - tavolo, la carta strusciata, la carta lasciata cadere. -- Si va **ai 21**: vince chi ci arriva a fine smazzata; pari sopra i 21 si gioca un'altra smazzata. + avanzati si perdono: le due coppie fanno sempre 11 punti in tutto. +- **Accuse**, dette da ognuno al proprio turno giocando la prima carta della smazzata: napoletana (Asso, 2 e 3 + dello stesso seme) 3 punti; tre Assi, tre 2 o tre 3 fanno 3 punti (dicendo il seme che manca), quattro fanno 4. + Restano scritte in alto per tutta la smazzata: Andrea sa quel che sanno gli avversari. +- **Segni**, solo di chi esce: busso (prendi con la migliore e ritornamelo; si può sempre), striscio (ne ho + altre), volo (era l'ultima). Volo e striscio sono onesti: il gioco rifiuta quelli falsi. Si sentono: due nocche + sul tavolo, la carta strusciata, la carta lasciata cadere. +- Si va **ai 21**, presa per presa: il punteggio in alto conta accuse e punti interi già presi, e la coppia che + arriva a 21 per prima è fuori e ha vinto, anche a metà smazzata (le accuse contano alla fine della presa in cui + si dicono). Il risultato è subito definitivo: uscire dal riepilogo non lo cancella. Il riepilogo di fine + smazzata resta finché Andrea non va avanti. -Scelte da confermare con Andrea: accuse anche oltre i 21 a inizio smazzata (ora si controlla solo a fine -smazzata); nessun «cappotto»; nessuna napoletana lunga. +Scelte da confermare con Andrea: «fuori» automatico ai 21 (al tavolo vero bisogna chiamarsi fuori, e se si +sbaglia si perde); nessun «cappotto»; nessuna napoletana lunga; segni ammessi anche alla prima presa. ## Avversari - «Alla bona»: una carta a caso fra quelle buone, nessun segno. - «Seria»: gioca a regola (carico al socio quando prende di sicuro, presa sicura con la carta più bassa, scarta quel che costa meno, segni onesti) ma non conta: un 3 uscito per lui può essere ancora in giro. -- «A fassi male»: conta le carte uscite, capisce chi ha finito un seme, ricorda le accuse, ritorna il seme al socio - che ha bussato. Il socio di Andrea gioca sempre così. +- «A fassi male»: conta le carte uscite, capisce chi ha finito un seme (anche da chi vola), ricorda le accuse, + risponde al busso del socio prendendo con la migliore e poi gli ritorna il seme. Il socio di Andrea gioca sempre così. +- Tutti: il carico al socio che prende di sicuro, ma senza buttarci il 3 o il 2; a parità di lunghezza si esce dal + seme con meno punti da esporre (niente Asso scoperto). Taratura (`node tools/calibrate_tressette.mjs 300`, Andrea che gioca come il socio): Andrea e il socio vincono -86% contro «Alla bona», 59% contro «Seria», 52% contro «A fassi male»; circa tre smazzate a partita. +87% contro «Alla bona», 61% contro «Seria», 48% contro «A fassi male»; circa tre smazzate a partita. + +## Revisione di Codex (26 ottobre) + +Revisione critica sul primo giro. Corretti: vittoria persa uscendo dal riepilogo; accuse tutte pubbliche prima +della prima carta; il seme mancante del tris noto all'IA ma non ad Andrea; striscio troppo stretto e busso con +un requisito inventato; il socio che non rispondeva al busso, sprecava il 3 su una presa già sua o usciva +d'Asso scoperto; il volo non letto da «A fassi male»; tastiera che poteva giocare la carta sbagliata; il +riepilogo che ripartiva da solo dopo sette secondi; la tavola descritta a parole per chi non vede il disegno. +Restano proposte (non fatte): una tabella di tavolo accessibile completa e la taratura contro una versione +precedente dell'IA. ## Prove diff --git a/index.html b/index.html index 0df0802..1432bc5 100644 --- a/index.html +++ b/index.html @@ -103,6 +103,24 @@
+ diff --git a/package.json b/package.json index acb4ea1..8991bc0 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "browser": "node tests/browser.mjs", "browser:biliardo": "node tests/biliardo.browser.mjs", "browser:tressette": "node tests/tressette.browser.mjs", + "browser:scopa": "node tests/scopa.browser.mjs", "browser:briscola": "node tests/briscola.browser.mjs", "browser:effects": "node tests/ambient.browser.mjs", "serve": "python3 -m http.server 8000" diff --git a/src/engine/game.js b/src/engine/game.js index 0486ed1..f147ce1 100644 --- a/src/engine/game.js +++ b/src/engine/game.js @@ -5,11 +5,13 @@ import * as bocce from './bocce.js'; import * as biliardo from './biliardo.js'; import * as briscola from './briscola.js'; import * as tressette from './tressette.js'; +import * as scopa from './scopa.js'; // I minigiochi in primo piano: comandi opzionali per il tiro o per la scelta di una carta. export const MINIGAMES = { briscola: { create: (seed, level, deck) => briscola.newBriscola(seed, { level, deck }), update: briscola.update, play: briscola.play }, tressette: { create: (seed, level, deck) => tressette.newTressette(seed, { level, deck }), update: tressette.update, play: tressette.play }, + scopa: { create: (seed, level, deck) => scopa.newScopa(seed, { level, deck }), update: scopa.update, play: scopa.play }, bocce: { create: (seed, level) => bocce.newBocce(seed, { level }), update: bocce.update, press: bocce.press, release: bocce.release, cancel: bocce.cancel, aim: bocce.aimScreen }, biliardo: { create: (seed, level) => biliardo.newBiliardo(seed, { level }), update: biliardo.update, press: biliardo.press, release: biliardo.release, cancel: biliardo.cancel, aim: biliardo.aimScreen }, }; diff --git a/src/engine/render.js b/src/engine/render.js index 76b1941..38082c4 100644 --- a/src/engine/render.js +++ b/src/engine/render.js @@ -7,6 +7,7 @@ import { drawBocce } from './bocce-render.js'; import { drawBiliardo } from './biliardo-render.js'; import { drawBriscola } from './briscola-render.js'; import { drawTressette } from './tressette-render.js'; +import { drawScopa } from './scopa-render.js'; import { versioned } from '../version.js'; export const WIDTH = 640, HEIGHT = 360; @@ -670,6 +671,7 @@ function drawCloseup(ctx, game, time) { if (!def) return; if (def.type === 'briscola') { drawBriscola(ctx, c, def); return; } if (def.type === 'tressette') { drawTressette(ctx, c, def); return; } + if (def.type === 'scopa') { drawScopa(ctx, c, def); return; } if (def.type === 'torch') { if (!backgrounds.has(def.picture)) loadBackground(def.picture); drawTorch(ctx, c, def, backgrounds.get(def.picture)); diff --git a/src/engine/scopa-render.js b/src/engine/scopa-render.js new file mode 100644 index 0000000..d6fd9b7 --- /dev/null +++ b/src/engine/scopa-render.js @@ -0,0 +1,84 @@ +import { canPlay, TARGET } from './scopa.js'; +import { drawCard, drawTable } from './briscola-render.js'; +import { text, panel } from './bocce-render.js'; + +// Coordinate condivise da disegno e tocco: la mano di Andrea (grande) e le carte in tavola. +export const HAND = { y: 262, w: 64, h: 92, gap: 72 }; +export const handX = (n, i) => Math.round(320 - (n * HAND.gap - 8) / 2 + i * HAND.gap); +const TABLE = { w: 42, h: 60, gap: 47, row: 8 }; +export function tableXY(n, i) { + const row = Math.floor(i / TABLE.row), inRow = Math.min(TABLE.row, n - row * TABLE.row); + return [Math.round(320 - (inRow * TABLE.gap - 5) / 2 + (i % TABLE.row) * TABLE.gap), n > TABLE.row ? 104 + row * 66 : 132]; +} +export function cardAt(c, [x, y]) { + if (!canPlay(c)) return -1; + return c.hands.player.findIndex((_, i) => x >= handX(c.hands.player.length, i) && x < handX(c.hands.player.length, i) + HAND.w && y >= HAND.y - 8 && y < HAND.y + HAND.h); +} +export function tableAt(c, [x, y]) { + return c.table.findIndex((_, i) => { const [tx, ty] = tableXY(c.table.length, i); return x >= tx && x < tx + TABLE.w && y >= ty && y < ty + TABLE.h; }); +} +const gold = '#ffe09b', paper = '#fff0cd', mine = '#9fe3ff', theirs = '#ffb0a3'; +const frame = (ctx, x, y, w, h, color = '#ffdb76') => { ctx.fillStyle = color; ctx.fillRect(x - 2, y - 2, w + 4, h + 4); }; + +function board(ctx, c, names) { + // Un giocatore per riga: i nomi lunghi non si accavallano. + panel(ctx, 26, 34, 150, 50); + for (const [who, y, color] of [['player', 45, mine], ['cpu', 59, theirs]]) { + text(ctx, names[who], 34, y, 7, color); text(ctx, String(c.score[who]), 168, y, 7, color, 'right'); + } + text(ctx, `Smazzata ${c.round} · ai ${TARGET}`, 101, 74, 5, paper, 'center'); + // Il mazzo a sinistra, le prese a destra con le scope. + if (c.stock.length) for (let i = Math.min(4, Math.ceil(c.stock.length / 8)); i >= 0; i--) drawCard(ctx, null, 40 + i, 150 - i, 42, 60, true); + text(ctx, `Mazzo: ${c.stock.length}`, 61, 222, 6, paper, 'center'); + for (const [who, y] of [['cpu', 92], ['player', 196]]) { + if (c.taken[who].length) for (let i = 0; i < Math.min(4, c.taken[who].length / 4); i++) drawCard(ctx, null, 560 + i, y - i, 42, 60, true); + if (c.scope[who]) text(ctx, `Scope: ${c.scope[who]}`, 581, y + 70, 6, gold, 'center'); + } +} +function table(ctx, c) { + const take = c.step === 'take' && c.last.captured.length, shown = take ? [...c.table, ...c.last.captured] : c.table; + const marked = new Set((take ? c.last.captured : c.step === 'choose' ? c.choice.options[c.choice.selected] : []).map(a => a.id)); + shown.forEach((card, i) => { + const [x, y] = tableXY(shown.length, i); + if (marked.has(card.id)) frame(ctx, x, y, TABLE.w, TABLE.h); + else if (c.step === 'take' && !take && card.id === c.last.card.id) frame(ctx, x, y, TABLE.w, TABLE.h, '#9fe3ff'); + drawCard(ctx, card, x, y, TABLE.w, TABLE.h); + }); + if (take) { + drawCard(ctx, c.last.card, 299, 200, TABLE.w, TABLE.h); + if (c.last.scopa) { panel(ctx, 260, 98, 120, 22); text(ctx, 'SCOPA!', 320, 109, 9, gold, 'center'); } + } +} +function hands(ctx, c) { + c.hands.cpu.forEach((_, i) => drawCard(ctx, null, 320 - (c.hands.cpu.length * 48 - 6) / 2 + i * 48, 38, 42, 60, true)); + const n = c.hands.player.length, playing = canPlay(c) || c.step === 'choose'; + c.hands.player.forEach((card, i) => { + const x = handX(n, i), chosen = c.step === 'choose' ? c.choice.index === i : canPlay(c) && c.selected === i; + const up = chosen ? 8 : 0; + if (chosen && playing) frame(ctx, x, HAND.y - up, HAND.w, HAND.h); + drawCard(ctx, card, x, HAND.y - up, HAND.w, HAND.h); + }); +} +function summary(ctx, c, names, title) { + const s = c.summary; + panel(ctx, 140, 80, 360, 200); + text(ctx, title, 320, 96, 8, gold, 'center'); + text(ctx, names.player, 380, 116, 6, mine, 'center'); text(ctx, names.cpu, 455, 116, 6, theirs, 'center'); + s.rows.forEach((r, i) => { + const y = 136 + i * 20, show = v => r.key === 'settebello' ? (v ? 'sì' : '—') : String(v); + text(ctx, r.label, 160, y, 7, paper); + text(ctx, show(r.player) + (r.winner === 'player' ? ' +1' : ''), 380, y, 7, r.winner === 'player' ? gold : paper, 'center'); + text(ctx, show(r.cpu) + (r.winner === 'cpu' ? ' +1' : ''), 455, y, 7, r.winner === 'cpu' ? gold : paper, 'center'); + }); + text(ctx, `Smazzata: ${s.points.player} a ${s.points.cpu}. Siamo ${c.score.player} a ${c.score.cpu}`, 320, 246, 7, paper, 'center'); + text(ctx, c.step === 'result' ? 'Esc o Invio per tornare all’oratorio' : 'Invio o un tocco per la prossima smazzata', 320, 266, 5, paper, 'center'); +} +export function drawScopa(ctx, c, def) { + const names = def?.names ?? { player: 'Andrea', cpu: 'Il giocatore' }; + ctx.save(); ctx.imageSmoothingEnabled = false; + drawTable(ctx, 'SCOPA ALL’ORATORIO'); + board(ctx, c, names); table(ctx, c); hands(ctx, c); + if (c.step === 'smazzata') summary(ctx, c, names, `Fine della smazzata ${c.round}`); + if (c.step === 'result') summary(ctx, c, names, c.result.winner === 'player' ? 'Hai vinto!' : `Vince ${names.cpu}`); + ctx.restore(); +} diff --git a/src/engine/scopa.js b/src/engine/scopa.js new file mode 100644 index 0000000..61883ba --- /dev/null +++ b/src/engine/scopa.js @@ -0,0 +1,210 @@ +// Scopa in due, ai 11: regole e avversari senza DOM, come la briscola. Carte, mattoni (o denari), settebello, +// primiera e scope. Con le toscane il seme «buono» è mattoni, con le napoletane denari. +import { DECKS, cardName } from './briscola.js'; + +export { cardName }; +export const LEVELS = ['bona', 'seria', 'male']; +export const TARGET = 11; +export const COIN = { toscane: 'mattoni', napoletane: 'denari' }; +// Valore per le prese: Asso 1, poi i numeri, figure 8, 9 e 10. Primiera: 7 vale 21, 6 18, Asso 16, 5 15, 4 14, +// 3 13, 2 12, le figure 10. +const PRIME = [16, 12, 13, 14, 15, 18, 21, 10, 10, 10]; +const build = ({ suits, ranks }) => Object.freeze(suits.flatMap((suit, s) => ranks.map((rank, r) => + Object.freeze({ id: s * 10 + r, suit, rank, value: r + 1, prime: PRIME[r] })))); +export const DECK_CARDS = Object.fromEntries(Object.entries(DECKS).map(([name, deck]) => [name, build(deck)])); +export const other = who => who === 'player' ? 'cpu' : 'player'; +const lcg = n => (Math.imul(n, 1664525) + 1013904223) >>> 0; +function rng(seed) { let n = seed >>> 0; return () => (n = lcg(n)) / 4294967296; } +function shuffle(cards, random) { + for (let i = cards.length - 1; i > 0; i--) { const j = Math.floor(random() * (i + 1)); [cards[i], cards[j]] = [cards[j], cards[i]]; } + return cards; +} + +// Le prese possibili con `card`: se in tavola c'è una carta dello stesso valore si prende quella (una sola, a +// scelta se sono più d'una); altrimenti ogni gruppo di carte che somma il suo valore. Se una presa c'è, è obbligatoria. +export function captures(table, card) { + const same = table.filter(a => a.value === card.value); + if (same.length) return same.map(a => [a]); + const out = []; + for (let mask = 1; mask < 1 << table.length; mask++) { + const pick = table.filter((_, i) => mask & 1 << i); + if (pick.length > 1 && pick.reduce((n, a) => n + a.value, 0) === card.value) out.push(pick); + } + return out; +} +const isSettebello = (card, deck) => card.suit === COIN[deck] && card.value === 7; +// Primiera: la carta migliore di ogni seme; chi non ha tutti e quattro i semi non la fa. +export function primiera(cards, deck) { + const suits = DECKS[deck].suits; + if (!suits.every(s => cards.some(a => a.suit === s))) return 0; + return suits.reduce((n, s) => n + Math.max(...cards.filter(a => a.suit === s).map(a => a.prime)), 0); +} +// I punti di una smazzata: carte (più di 20), mattoni o denari (più di 5), settebello, primiera, una per scopa. +export function tally(taken, scope, deck) { + const coin = COIN[deck], rows = [], who = (a, b) => a > b ? 'player' : b > a ? 'cpu' : null; + const count = w => taken[w].length, coins = w => taken[w].filter(a => a.suit === coin).length, prime = w => primiera(taken[w], deck); + rows.push({ key: 'carte', label: 'Carte', player: count('player'), cpu: count('cpu'), winner: who(count('player'), count('cpu')) }); + rows.push({ key: 'coin', label: coin === 'mattoni' ? 'Mattoni' : 'Denari', player: coins('player'), cpu: coins('cpu'), winner: who(coins('player'), coins('cpu')) }); + const seven = taken.player.some(a => isSettebello(a, deck)) ? 'player' : 'cpu'; + rows.push({ key: 'settebello', label: 'Settebello', player: +(seven === 'player'), cpu: +(seven === 'cpu'), winner: seven }); + rows.push({ key: 'primiera', label: 'Primiera', player: prime('player'), cpu: prime('cpu'), winner: who(prime('player'), prime('cpu')) }); + const points = { player: 0, cpu: 0 }; + for (const r of rows) if (r.winner) points[r.winner]++; + rows.push({ key: 'scope', label: 'Scope', player: scope.player, cpu: scope.cpu, winner: null }); + points.player += scope.player; points.cpu += scope.cpu; + return { rows, points }; +} + +function deal(c) { + // Con tre o quattro re in tavola si rimescola. + let stock; + do { c.seed = lcg(c.seed); stock = shuffle([...DECK_CARDS[c.deck]], rng(c.seed)); } + while (stock.slice(0, 4).filter(a => a.value === 10).length >= 3); + c.table = stock.splice(0, 4); c.stock = stock; + c.hands = { player: [], cpu: [] }; c.taken = { player: [], cpu: [] }; c.scope = { player: 0, cpu: 0 }; + c.current = other(c.dealer); c.lastTaker = null; c.round++; + refill(c); c.events.push({ kind: 'card-shuffle' }); +} +// Tre carte a testa, prima a chi gioca per primo. +function refill(c) { + for (let i = 0; i < 3; i++) for (const who of [other(c.dealer), c.dealer]) c.hands[who].push(c.stock.shift()); + c.step = 'play'; c.t = 0; c.selected = 0; c.choice = null; +} +export function newScopa(seed = Date.now() >>> 0, { level = 'seria', deck = 'toscane' } = {}) { + const c = { level: LEVELS.includes(level) ? level : 'seria', deck: DECKS[deck] ? deck : 'toscane', score: { player: 0, cpu: 0 }, + round: 0, seed: seed >>> 0, aiSeed: (seed ^ 0x27d4eb2f) >>> 0, age: 0, events: [], last: null, summary: null }; + c.dealer = rng(seed)() < 0.5 ? 'player' : 'cpu'; + deal(c); return c; +} +export const canPlay = c => c.step === 'play' && c.current === 'player'; +export function select(c, index) { + if (c.step === 'choose' && Number.isInteger(index) && c.choice.options[index]) { c.choice.selected = index; return true; } + if (!canPlay(c) || !Number.isInteger(index) || !c.hands.player[index]) return false; + c.selected = index; return true; +} +export function nudge(c, direction) { + if (c.step === 'choose') { const n = c.choice.options.length; return select(c, (c.choice.selected + direction + n) % n); } + const n = c.hands.player.length; + return select(c, (c.selected + direction + n) % n); +} +// Andrea gioca una carta: se le prese possibili sono più d'una, sceglie lui quale (passo «choose»). +export function play(c, index = c.selected, who = 'player', option = null) { + if (c.step !== 'play' || c.current !== who || !Number.isInteger(index) || !c.hands[who][index]) return false; + const options = captures(c.table, c.hands[who][index]); + if (options.length > 1 && option === null) { + if (who !== 'player') return false; + c.step = 'choose'; c.choice = { index, options, selected: 0 }; return true; + } + return execute(c, who, index, options.length ? options[option ?? 0] : null); +} +export function take(c, k = c.choice?.selected) { + if (c.step !== 'choose' || !c.choice.options[k]) return false; + const { index, options } = c.choice; c.step = 'play'; c.choice = null; + return execute(c, 'player', index, options[k]); +} +export function cancel(c) { if (c.step !== 'choose') return false; c.step = 'play'; c.choice = null; return true; } +function execute(c, who, index, captured) { + if (!captured && captures(c.table, c.hands[who][index]).length) return false; + const [card] = c.hands[who].splice(index, 1); + if (captured) { + const ids = new Set(captured.map(a => a.id)); + c.table = c.table.filter(a => !ids.has(a.id)); c.taken[who].push(card, ...captured); c.lastTaker = who; + } else c.table.push(card); + // L'ultima carta della smazzata non fa scopa. + const lastPlay = !c.stock.length && !c.hands.player.length && !c.hands.cpu.length; + const scopa = !!captured && !c.table.length && !lastPlay; + if (scopa) c.scope[who]++; + c.last = { who, card, captured: captured ?? [], scopa }; + c.events.push({ kind: captured ? 'card-tap' : 'card-slide', who }, ...(scopa ? [{ kind: 'card-busso', who }] : [])); + c.step = 'take'; c.t = 0; + if (who === 'player') c.selected = Math.min(c.selected, Math.max(0, c.hands.player.length - 1)); + return true; +} +function after(c) { + c.current = other(c.last.who); c.t = 0; c.step = 'play'; + if (c.hands.player.length || c.hands.cpu.length) return; + if (c.stock.length) { refill(c); return; } + // Fine smazzata: quel che resta in tavola va all'ultimo che ha preso. + if (c.lastTaker) c.taken[c.lastTaker].push(...c.table); + c.table = []; + const t = tally(c.taken, c.scope, c.deck); + c.score.player += t.points.player; c.score.cpu += t.points.cpu; + c.summary = { ...t, score: { ...c.score } }; c.t = 0; + // Il risultato è subito definitivo: uscire dal riepilogo non può cancellare una vittoria. + if (finished(c)) { c.step = 'result'; c.result = { winner: c.score.player > c.score.cpu ? 'player' : 'cpu', ...c.score }; } + else c.step = 'smazzata'; +} +// Si va agli 11, contati a fine smazzata: vince chi ne ha di più; pari (o nessuno agli 11), se ne fa un'altra. +export const finished = c => Math.max(c.score.player, c.score.cpu) >= TARGET && c.score.player !== c.score.cpu; +// Dopo il riepilogo (resta finché Andrea non va avanti) si fa la smazzata dopo. +export function advance(c) { + if (c.step !== 'smazzata') return false; + c.dealer = other(c.dealer); deal(c); + return true; +} + +// La sola porta d'ingresso dell'avversario: la sua mano, la tavola e le carte prese (si son viste tutte). +export function observation(c, who = 'cpu') { + return { who, deck: c.deck, hand: [...c.hands[who]], table: [...c.table], taken: { player: [...c.taken.player], cpu: [...c.taken.cpu] }, + opponentCount: c.hands[other(who)].length, stock: c.stock.length, scope: { ...c.scope } }; +} +// Le carte che non si sono viste: in mano all'altro o nel mazzo. +export function unseen(v) { + const seen = new Set([...v.hand, ...v.table, ...v.taken.player, ...v.taken.cpu].map(a => a.id)); + return DECK_CARDS[v.deck].filter(a => !seen.has(a.id)); +} +// Quanto vale una mossa: le carte prese (settebello, sette, mattoni, primiera, scopa) meno il rischio di lasciare +// una scopa all'altro. «Seria» il rischio lo stima a occhio; «A fassi male» conta quante carte di quel valore +// possono essere ancora in mano all'altro. +export function score(v, card, captured, counting) { + const coin = COIN[v.deck], last = !v.stock && v.hand.length === 1 && !v.opponentCount; + const left = captured ? v.table.filter(a => !captured.includes(a)) : [...v.table, card]; + const worth = a => 1 + (a.suit === coin ? 1.5 : 0) + (isSettebello(a, v.deck) ? 12 : 0) + (a.value === 7 ? 3 : a.value === 6 ? 1.2 : a.value === 1 ? 0.8 : 0); + let value = captured ? [card, ...captured].reduce((n, a) => n + worth(a), 0) + (!left.length && !last ? 20 : 0) : -worth(card) * 0.6; + const sum = left.reduce((n, a) => n + a.value, 0); + if (left.length && sum <= 10 && v.opponentCount) { + let p = 0.5; + if (counting) { + const pool = unseen(v), same = pool.filter(a => a.value === sum).length; + p = pool.length ? 1 - (1 - same / pool.length) ** v.opponentCount : 0; + } + value -= p * 20; + } + // Lasciare in tavola un 7 o il settebello vuol dire regalarli. + value -= left.filter(a => a.value === 7).length * (counting ? 1.5 : 0) + (left.some(a => isSettebello(a, v.deck)) ? 6 : 0); + return value; +} +export function moves(v) { + return v.hand.flatMap((card, index) => { + const opts = captures(v.table, card); + return opts.length ? opts.map((captured, option) => ({ index, option, captured })) : [{ index, option: null, captured: null }]; + }); +} +// «Alla bona» gioca a caso; «Seria» e «A fassi male» scelgono la mossa che vale di più. +// Taratura (tools/calibrate_scopa.mjs, 300 partite): Andrea vince 92% contro «Alla bona», 64% contro «Seria», 52% contro «A fassi male». +export function choose(v, level = 'seria', seed = 1) { + const all = moves(v); + if (level === 'bona') return all[Math.floor(rng(seed)() * all.length)]; + const counting = level === 'male'; + return all.reduce((a, b) => score(v, v.hand[b.index], b.captured, counting) > score(v, v.hand[a.index], a.captured, counting) ? b : a); +} +export function update(c, dt) { + if (!Number.isFinite(dt) || dt < 0 || c.step === 'result') return; + c.t += dt; + if (c.step === 'take' && c.t >= (c.last.captured.length ? 1.3 : 0.6)) after(c); + else if (c.step === 'play' && c.current === 'cpu' && c.t >= 0.75) { + c.aiSeed = lcg(c.aiSeed); + const m = choose(observation(c, 'cpu'), c.level, c.aiSeed); + play(c, m.index, 'cpu', m.option); + } +} +export function status(c, names = { player: 'Andrea', cpu: 'Il giocatore' }) { + if (c.step === 'result') return `${c.result.winner === 'player' ? 'Hai vinto' : `Vince ${names.cpu}`}, ${c.score.player} a ${c.score.cpu}!`; + if (c.step === 'smazzata') return `Smazzata finita: siamo ${c.score.player} a ${c.score.cpu}. Invio per continuare.`; + if (c.step === 'choose') return 'Più prese possibili: scegli quale (frecce o tocca una carta in tavola), Invio prende.'; + if (c.step === 'take') { + const l = c.last; + return l.scopa ? `Scopa di ${names[l.who]}!` : l.captured.length ? `${names[l.who]} prende con ${cardName(l.card)}.` : `${names[l.who]} posa ${cardName(l.card)}.`; + } + return c.current === 'player' ? 'Tocca una carta per giocarla. Frecce o 1–2–3 scelgono, Invio gioca.' : `${names.cpu} pensa…`; +} diff --git a/src/engine/tressette-render.js b/src/engine/tressette-render.js index 2ecf513..eee8d2a 100644 --- a/src/engine/tressette-render.js +++ b/src/engine/tressette-render.js @@ -44,8 +44,8 @@ function scoreboard(ctx, c) { text(ctx, `Smazzata ${c.round} · ai 21`, 85, 62, 5, paper, 'center'); } function accuses(ctx, c, names) { - // Le accuse si vedono finché non è finita la prima presa. - if (c.tricks.length || !c.accuse.length) return; + // Le accuse dette restano in vista per tutta la smazzata. + if (!c.accuse.length) return; panel(ctx, 456, 34, 158, 12 + c.accuse.length * 11); c.accuse.forEach((a, i) => text(ctx, `${names[a.who]}: ${accuseName(a)} +${a.points}`, 463, 41 + i * 11, 5, a.who % 2 ? theirs : ours)); } @@ -87,6 +87,6 @@ export function drawTressette(ctx, c, def) { drawTable(ctx, 'TRESSETTE AI 21'); scoreboard(ctx, c); others(ctx, c, names); accuses(ctx, c, names); trick(ctx, c, names); hand(ctx, c); if (c.step === 'smazzata') summary(ctx, c, `Fine della smazzata ${c.round}`); - if (c.step === 'result') summary(ctx, c, c.result.winner === 'player' ? 'Avete vinto!' : 'Hanno vinto loro'); + if (c.step === 'result') summary(ctx, c, c.result.winner === 'player' ? 'Fuori! Avete vinto' : 'Sono fuori loro'); ctx.restore(); } diff --git a/src/engine/tressette.js b/src/engine/tressette.js index f4d33f0..c315d55 100644 --- a/src/engine/tressette.js +++ b/src/engine/tressette.js @@ -27,7 +27,8 @@ function shuffle(cards, random) { const byMin = (list, key) => list.reduce((a, b) => key(b) < key(a) ? b : a); // Le accuse: napoletana (Asso, 2 e 3 dello stesso seme) 3 punti; buongioco, tre o quattro Assi, 2 o 3, 3 o 4 punti. -export function accuse(hand) { +// Del tris si dice il seme che manca (`suits`, i quattro semi del mazzo): chi ascolta sa dove sono le altre tre. +export function accuse(hand, suits = [...new Set(hand.map(a => a.suit))]) { const out = [], tops = ['Asso', '2', '3']; for (const suit of new Set(hand.map(a => a.suit))) { const cards = hand.filter(a => a.suit === suit && tops.includes(a.rank)); @@ -35,11 +36,12 @@ export function accuse(hand) { } for (const rank of tops) { const cards = hand.filter(a => a.rank === rank); - if (cards.length >= 3) out.push({ kind: 'buongioco', rank, points: cards.length, cards }); + if (cards.length >= 3) out.push({ kind: 'buongioco', rank, points: cards.length, cards, missing: cards.length === 3 ? suits.find(s => !cards.some(a => a.suit === s)) : null }); } return out; } -export const accuseName = a => a.kind === 'napoletana' ? `napoletana di ${a.suit}` : `${a.cards.length === 4 ? 'quattro' : 'tre'} ${a.rank === 'Asso' ? 'Assi' : a.rank}`; +export const accuseName = a => a.kind === 'napoletana' ? `napoletana di ${a.suit}` + : `${a.cards.length === 4 ? 'quattro' : 'tre'} ${a.rank === 'Asso' ? 'Assi' : a.rank}${a.missing ? `, manca ${a.missing}` : ''}`; // Si risponde al seme quando si può; chi esce gioca quel che vuole. export function legal(hand, table) { @@ -47,12 +49,13 @@ export function legal(hand, table) { return same.length ? same : hand.map((_, i) => i); } export const best = table => table.reduce((a, p) => p.card.suit === table[0].card.suit && p.card.power > a.card.power ? p : a); -// I segni sono onesti: volo con l'ultima carta del seme, striscio se ne restano almeno due, busso se ne resta una. +// I segni li fa solo chi esce. Volo: era l'ultima del seme; striscio: ne ho altre; busso (prendi con la migliore e +// ritornamelo) si può fare sempre. Volo e striscio sono onesti: il gioco rifiuta quelli falsi. export function signalOk(hand, index, table, signal) { if (!signal) return true; if (table.length || !SIGNALS.includes(signal) || !hand[index]) return false; const rest = hand.filter((a, i) => i !== index && a.suit === hand[index].suit).length; - return signal === 'volo' ? rest === 0 : signal === 'striscio' ? rest >= 2 : rest >= 1; + return signal === 'volo' ? rest === 0 : signal === 'striscio' ? rest >= 1 : true; } // Perché una carta non si può giocare: la frase da mostrare ad Andrea, o '' se va bene. export function reason(c, index, signal = c.signal) { @@ -60,8 +63,9 @@ export function reason(c, index, signal = c.signal) { if (!card) return 'Scegli una carta.'; if (!legal(hand, c.table).includes(index)) return `Devi rispondere a ${c.table[0].card.suit}.`; if (!signal || signalOk(hand, index, c.table, signal)) return ''; + if (!SIGNALS.includes(signal)) return 'Segno non riconosciuto.'; if (c.table.length) return 'I segni li fa solo chi esce.'; - return { volo: `Volo solo con l'ultima carta di ${card.suit}.`, striscio: `Striscio solo se di ${card.suit} te ne restano almeno due.`, busso: `Busso solo se di ${card.suit} te ne resta almeno un'altra.` }[signal]; + return { volo: `Volo solo con l'ultima carta di ${card.suit}.`, striscio: `Striscio solo se di ${card.suit} te ne resta almeno un'altra.` }[signal]; } const order = (a, b) => a.suit === b.suit ? b.power - a.power : a.id - b.id; @@ -69,11 +73,12 @@ function deal(c) { c.seed = lcg(c.seed); const stock = shuffle([...DECK_CARDS[c.deck]], rng(c.seed)); c.hands = SEATS.map(seat => stock.slice(seat * 10, seat * 10 + 10).sort(order)); - // Fa le carte chi sta a sinistra di chi ha fatto prima; esce chi sta alla destra del mazziere. + // Il mazzo passa a destra a ogni smazzata; esce chi sta alla destra del mazziere. c.dealer = c.round ? next(c.dealer) : c.dealer; c.round++; - c.current = c.mano = next(c.dealer); c.table = []; c.tricks = []; c.thirds = [0, 0]; - c.accuse = SEATS.map(i => (c.mano + i) % 4).flatMap(who => accuse(c.hands[who]).map(a => ({ who, ...a }))); - for (const a of c.accuse) c.score[team(a.who)] += a.points; + c.current = c.mano = next(c.dealer); c.table = []; c.tricks = []; c.thirds = [0, 0]; c.base = [...c.score]; + // Le accuse si dicono al proprio turno, giocando la prima carta: fino ad allora restano in mano. + c.pending = SEATS.map(who => accuse(c.hands[who], DECKS[c.deck].suits).map(a => ({ who, ...a }))); + c.accuse = []; c.step = 'play'; c.t = 0; c.selected = 0; c.signal = null; c.refused = ''; c.events.push({ kind: 'card-shuffle' }); } @@ -103,6 +108,10 @@ export function play(c, index = c.selected, who = 0, signal = who === 0 ? c.sign if (who === 0) c.refused = reason(c, index, signal); return false; } + if (!c.tricks.length && c.pending[who].length) { + c.accuse.push(...c.pending[who]); c.pending[who] = []; recount(c); + c.events.push({ kind: 'accusa', who }); + } const [card] = c.hands[who].splice(index, 1); c.table.push({ who, card, signal: signal || null }); c.t = 0; c.landing = true; // Il segno si sente: busso batte sul tavolo, striscio la fa strusciare, volo la lascia cadere dall'alto. @@ -112,28 +121,35 @@ export function play(c, index = c.selected, who = 0, signal = who === 0 ? c.sign else c.current = next(who); return true; } +// Il punteggio si tiene presa per presa: quel che c'era a inizio smazzata, le accuse dette e i punti interi presi. +const accused = (c, t) => c.accuse.filter(a => team(a.who) === t).reduce((n, a) => n + a.points, 0); +function recount(c) { c.score = [0, 1].map(t => c.base[t] + accused(c, t) + Math.floor(c.thirds[t] / 3)); } export function collect(c) { if (c.step !== 'trick') return false; - const winner = c.last.winner; - c.thirds[team(winner)] += c.table.reduce((n, p) => n + p.card.thirds, 0); + const winner = c.last.winner, end = !c.hands[0].length; + // L'ultima presa vale un punto; i terzi avanzati non contano. + c.thirds[team(winner)] += c.table.reduce((n, p) => n + p.card.thirds, 0) + (end ? 3 : 0); c.tricks.push({ leader: c.table[0].who, winner, cards: c.table }); c.table = []; c.current = winner; c.t = 0; c.events.push({ kind: 'card-slide' }); - if (c.hands[0].length) { c.step = 'play'; return true; } - // L'ultima presa vale un punto; i terzi avanzati non contano. - c.thirds[team(winner)] += 3; - const points = c.thirds.map(t => Math.floor(t / 3)), accused = [0, 1].map(t => c.accuse.filter(a => team(a.who) === t).reduce((n, a) => n + a.points, 0)); - c.score = c.score.map((s, t) => s + points[t]); - c.summary = { thirds: [...c.thirds], points, accused, lastTeam: team(winner), score: [...c.score] }; - c.step = 'smazzata'; + recount(c); + // Si va ai 21: chi ci arriva per primo è fuori e ha vinto, anche a metà smazzata (una presa porta punti a una + // coppia sola, quindi non ci arrivano mai insieme). + const out = [0, 1].find(t => c.score[t] >= TARGET); + if (end || out !== undefined) { + c.summary = { thirds: [...c.thirds], points: c.thirds.map(t => Math.floor(t / 3)), accused: [0, 1].map(t => accused(c, t)), + lastTeam: end ? team(winner) : null, score: [...c.score], out: out ?? null }; + if (out !== undefined) { c.step = 'result'; c.result = { winner: out === 0 ? 'player' : 'cpu', score: [...c.score] }; } + else c.step = 'smazzata'; + return true; + } + c.step = 'play'; return true; } -// Si va ai 21: chi arriva per primo a fine smazzata; pari sopra i 21, si gioca un'altra smazzata. -export const finished = c => Math.max(...c.score) >= TARGET && c.score[0] !== c.score[1]; +export const finished = c => c.step === 'result'; +// Dopo il riepilogo (resta finché Andrea non va avanti) si fa la smazzata dopo. export function advance(c) { if (c.step !== 'smazzata') return false; - if (finished(c)) { c.step = 'result'; c.result = { winner: c.score[0] > c.score[1] ? 'player' : 'cpu', score: [...c.score] }; } - else deal(c); - return true; + deal(c); return true; } // La sola porta d'ingresso degli avversari e del socio: la propria mano e quello che si è visto al tavolo. @@ -151,7 +167,11 @@ export function knowledge(v, full) { const out = DECK_CARDS[v.deck].filter(a => !gone.has(a.id)); const voids = SEATS.map(() => new Set()), holder = new Map(); if (full) { - for (const t of [...v.tricks, { cards: v.table }]) t.cards.forEach((p, i) => { if (i && p.card.suit !== t.cards[0].card.suit) voids[p.who].add(t.cards[0].card.suit); }); + // Chi non risponde al seme l'ha finito; chi vola anche. + for (const t of [...v.tricks, { cards: v.table }]) t.cards.forEach((p, i) => { + if (i && p.card.suit !== t.cards[0].card.suit) voids[p.who].add(t.cards[0].card.suit); + if (!i && p.signal === 'volo') voids[p.who].add(p.card.suit); + }); for (const a of v.accuse) if (a.who !== v.seat) for (const card of a.cards) if (!gone.has(card.id)) holder.set(card.id, a.who); } const could = (seat, card) => seat !== v.seat && !voids[seat].has(card.suit) && (!holder.has(card.id) || holder.get(card.id) === seat); @@ -159,7 +179,8 @@ export function knowledge(v, full) { const beatable = (card, seats) => out.some(a => a.suit === card.suit && a.power > card.power && seats.some(s => could(s, a))); return { out, voids, could, beatable, full }; } -const rich = cards => byMin(cards, a => -a.thirds * 20 + a.power); // il carico più grosso, senza buttare i 3 +// Il carico più grosso, ma il 3 e il 2 si tengono: sono le carte che fanno le prese dopo. +const rich = cards => byMin(cards, a => -a.thirds * 20 + a.power + (a.power >= 8 ? 25 : 0)); const cheap = cards => byMin(cards, a => a.thirds * 20 + a.power); // la carta che costa meno function lead(v, k) { const hand = v.hand, foes = [next(v.seat), (v.seat + 3) % 4]; @@ -174,7 +195,8 @@ function lead(v, k) { if (sure.length) { const card = rich(sure); return { index: hand.indexOf(card), signal: signalFor(hand, card, k) }; } // Altrimenti dal seme più lungo, la più bassa; il segno dice al socio com'è fatto il seme. const count = suit => hand.filter(a => a.suit === suit).length; - const longest = byMin(hand, a => -count(a.suit) * 10 - a.thirds).suit; + // A parità di lunghezza, il seme con meno punti da esporre (un Asso scoperto lo prendono gli altri). + const longest = byMin(hand, a => -count(a.suit) * 10 + hand.filter(b => b.suit === a.suit).reduce((n, b) => n + b.thirds, 0)).suit; const card = cheap(hand.filter(a => a.suit === longest)); return { index: hand.indexOf(card), signal: signalFor(hand, card, k) }; } @@ -190,6 +212,12 @@ function follow(v, k, moves) { const cards = moves.map(i => v.hand[i]), top = best(v.table), mate = team(top.who) === team(v.seat); const after = [1, 2, 3].map(n => (v.seat + n) % 4).slice(0, 3 - v.table.length).filter(s => team(s) !== team(v.seat)); const pick = card => v.hand.indexOf(card); + // Il socio ha bussato: vuole che prenda con la migliore (poi gli ritorno il seme). + const knock = v.table[0].who === partner(v.seat) && v.table[0].signal === 'busso' && k.full; + if (knock && cards[0].suit === top.card.suit) { + const over = cards.filter(a => a.power > top.card.power); + if (over.length) return pick(over.reduce((a, b) => b.power > a.power ? b : a)); + } // Il socio prende di sicuro: gli si dà il carico (anche l'Asso di un altro seme, se il nostro è finito). if (mate && !k.beatable(top.card, after)) return pick(rich(cards)); if (cards[0].suit === top.card.suit) { @@ -201,7 +229,7 @@ function follow(v, k, moves) { // «Alla bona» butta una carta a caso fra quelle buone; «Seria» gioca a regola ma non conta; «A fassi male» conta // le carte uscite, i semi finiti, le accuse e risponde al busso del socio. Il socio di Andrea gioca sempre «A fassi male». // Taratura (tools/calibrate_tressette.mjs, 300 partite ai 21 per livello, Andrea e il socio che giocano attenti): -// Andrea e il socio vincono 86% contro «Alla bona», 59% contro «Seria», 52% contro «A fassi male» (docs/TRESSETTE.md). +// Andrea e il socio vincono 87% contro «Alla bona», 61% contro «Seria», 48% contro «A fassi male» (docs/TRESSETTE.md). export function choose(v, level = 'seria', seed = 1) { const moves = legal(v.hand, v.table); if (level === 'bona') return { index: moves[Math.floor(rng(seed)() * moves.length)], signal: null }; @@ -214,7 +242,6 @@ export function update(c, dt) { c.t += dt; if (c.landing && c.t >= 0.18) { c.events.push({ kind: 'card-tap' }); c.landing = false; } if (c.step === 'trick' && c.t >= 1.2) collect(c); - else if (c.step === 'smazzata' && c.t >= 7) advance(c); else if (c.step === 'play' && c.current !== 0 && c.t >= 0.75) { c.aiSeed = lcg(c.aiSeed); const seat = c.current, { index, signal } = choose(observation(c, seat), seat === 2 ? 'male' : c.level, c.aiSeed); @@ -224,13 +251,12 @@ export function update(c, dt) { export const NAMES = ['Andrea', 'Il giocatore', 'Il socio', 'L\'altro']; export function status(c, names = NAMES) { if (c.step === 'result') return `${c.result.winner === 'player' ? 'Avete vinto voi' : 'Hanno vinto loro'}, ${c.score[0]} a ${c.score[1]}!`; - if (c.step === 'smazzata') return `Smazzata finita: noi ${c.summary.points[0]}, loro ${c.summary.points[1]}. Siamo ${c.score[0]} a ${c.score[1]}. Invio per continuare.`; + if (c.step === 'smazzata') return `Smazzata finita: noi ${c.summary.points[0]}, loro ${c.summary.points[1]}. Siamo ${c.score[0]} a ${c.score[1]}. Invio per la prossima.`; if (c.step === 'trick') return `Prende ${names[c.last.winner]}.`; - if (c.current !== 0) { - const p = c.table.at(-1); - return p?.signal ? `${names[p.who]}: «${p.signal[0].toUpperCase() + p.signal.slice(1)}!»` : `${names[c.current]} pensa…`; - } + // Quel che c'è in tavola, a parole: serve anche a chi non vede il disegno. + const table = c.table.map(p => `${names[p.who]}: ${cardName(p.card)}${p.signal ? ` («${p.signal}!»)` : ''}`).join('; '); + if (c.current !== 0) return `${table ? table + '. ' : ''}Tocca a ${names[c.current]}…`; if (c.refused) return c.refused; - if (c.table.length) return `Rispondi a ${c.table[0].card.suit}, se ce l'hai. Frecce scelgono, Invio gioca.`; + if (c.table.length) return `${table}. Rispondi a ${c.table[0].card.suit}, se ce l'hai. Frecce scelgono, Invio gioca.`; return `Esci tu${c.signal ? `, col segno «${c.signal}»` : ''}. B, S, V per bussare, strisciare o volare; Invio gioca.`; } diff --git a/src/main.js b/src/main.js index b7e17e5..547c803 100644 --- a/src/main.js +++ b/src/main.js @@ -14,6 +14,8 @@ import * as briscola from './engine/briscola.js'; import { cardAt } from './engine/briscola-render.js'; import * as tressette from './engine/tressette.js'; import { cardAt as tressetteAt } from './engine/tressette-render.js'; +import * as scopa from './engine/scopa.js'; +import { cardAt as scopaAt, tableAt as scopaTableAt } from './engine/scopa-render.js'; import { VERSION, versioned } from './version.js'; const $ = id => document.getElementById(id); @@ -403,11 +405,18 @@ canvas.addEventListener('pointerdown', e => { if (!running || e.button > 0) return; // Nel primo piano un tocco sfoglia (o salta la battuta): gli oggetti della scena dietro non si toccano. if (game.closeup) { - const deck = CARD_AT[story.closeups[game.closeup.id].type]; + const type = story.closeups[game.closeup.id].type, deck = CARD_AT[type]; if (deck) { if (game.skip()) return; - // Tressette: fra una smazzata e l'altra un tocco va avanti. - if (tressette.advance(game.closeup)) return; + // Tressette e scopa: fra una smazzata e l'altra un tocco va avanti. + if (MINI[type].advance?.(game.closeup)) return; + // Scopa, più prese possibili: si tocca una carta in tavola; se sta in una presa sola, si prende quella. + if (type === 'scopa' && game.closeup.step === 'choose') { + const c = game.closeup, card = c.table[scopaTableAt(c, scenePoint(e))]; + const options = c.choice.options.flatMap((o, k) => o.includes(card) ? [k] : []); + if (options.length === 1) scopa.take(c, options[0]); else if (options.length) scopa.select(c, options[0]); + return; + } press = { card: deck(game.closeup, scenePoint(e)), state: game.closeup, at: deck }; canvas.setPointerCapture(e.pointerId); return; @@ -463,6 +472,7 @@ canvas.addEventListener('pointerleave', () => { mouse = null; }); // Comandi dei minigiochi separati dai verbi: carte oppure mira, forza e lancio. const MINI = { + scopa: { ...scopa, back: 'Torna all’oratorio', label: 'Scopa: tocca una carta per giocarla. Se le prese sono più d’una, tocca una carta in tavola o usa le frecce, Invio prende. Esc esce.' }, tressette: { ...tressette, back: 'Torna all’oratorio', label: 'Tressette: tocca una carta per giocarla. Frecce scelgono, Invio gioca; B, S, V fanno busso, striscio e volo. Esc esce.' }, briscola: { ...briscola, back: 'Torna all’oratorio', label: 'Briscola: tocca una carta per giocarla. Frecce o 1, 2, 3 scelgono; Invio gioca. Esc esce.' }, bocce: { ...bocce, nudge: (c, dir) => bocce.aim(c, c.aim.u, c.aim.w + dir * 0.3), back: 'Torna al pallaio', label: 'Bocce: tocca il campo o il dettaglio per mirare. Regola la forza e premi Lancia.' }, @@ -473,7 +483,14 @@ const mini = () => MINI[story.closeups[game.closeup.id].type]; $('bocce-power').addEventListener('input', e => { const c = miniState(); if (c) mini().setPower?.(c, Number(e.target.value) / 100); }); $('bocce-throw').addEventListener('click', () => { const c = miniState(); if (c && !game.speech) mini().shoot?.(c); }); // Le carte da gioco: dove si tocca la carta sul tavolo, gioco per gioco. -const CARD_AT = { briscola: cardAt, tressette: tressetteAt }; +const CARD_AT = { briscola: cardAt, tressette: tressetteAt, scopa: scopaAt }; +for (let i = 0; i < 3; i++) { + const button = $('scopa-card-' + i); + button.addEventListener('click', () => game?.closeupPlay(i)); + button.addEventListener('focus', () => { if (game?.closeup?.id === 'scopa' && game.closeup.step === 'play') scopa.select(game.closeup, i); }); +} +$('scopa-options').addEventListener('click', e => { const k = e.target.dataset?.option; if (k !== undefined && game?.closeup?.id === 'scopa') scopa.take(game.closeup, Number(k)); }); +$('scopa-next').addEventListener('click', () => { if (game?.closeup?.id === 'scopa') scopa.advance(game.closeup); }); for (let i = 0; i < 10; i++) { const button = $('tressette-card-' + i); button.addEventListener('click', () => game?.closeupPlay(i)); @@ -575,6 +592,27 @@ window.addEventListener('keydown', e => { if (!$('map').hidden) { if (e.key === 'Escape' || e.key === 'p' || e.key === 'P') $('map').hidden = true; return; } // Nel primo piano: Invio, spazio o freccia sfogliano (o saltano la battuta), Esc chiude il libro. if (running && game.closeup) { + if (story.closeups[game.closeup.id].type === 'scopa') { + const inControls = e.target.closest?.('#scopa-controls'), c = game.closeup, key = e.key.toLowerCase(); + if (e.repeat && ['enter', ' '].includes(key)) { e.preventDefault(); return; } + if (inControls && ['BUTTON', 'SUMMARY'].includes(e.target.tagName) && ['enter', ' '].includes(key)) return; + if (['arrowleft', 'arrowright', 'arrowup', 'arrowdown', 'enter', ' '].includes(key) || /^[1-9]$/.test(key)) { + e.preventDefault(); + if (e.repeat || game.skip()) return; + if (/^[1-9]$/.test(key)) scopa.select(c, Number(key) - 1); + else if (key.startsWith('arrow')) scopa.nudge(c, ['arrowleft', 'arrowup'].includes(key) ? -1 : 1); + else if (c.step === 'result') game.closeupClose(); + else if (c.step === 'choose') scopa.take(c); + else if (!scopa.advance(c) && key === 'enter') game.closeupPlay(); + // Il fuoco segue la carta scelta: Invio su un pulsante gioca sempre quella che si vede scelta. + if (inControls && scopa.canPlay(c)) $('scopa-card-' + c.selected).focus(); + return; + } + // Esc: nella scelta della presa si torna alla mano, altrimenti si lascia il tavolo. + if (e.key === 'Escape') { if (!scopa.cancel(c)) game.closeupClose(); } + else if (key === 'm') toggleMute(); + return; + } if (story.closeups[game.closeup.id].type === 'tressette') { const inControls = e.target.closest?.('#tressette-controls'), c = game.closeup; if (e.repeat && ['Enter', ' '].includes(e.key)) { e.preventDefault(); return; } @@ -588,6 +626,8 @@ window.addEventListener('keydown', e => { else if (key.startsWith('arrow')) tressette.nudge(c, ['arrowleft', 'arrowup'].includes(key) ? -1 : 1); else if (c.step === 'result') game.closeupClose(); else if (!tressette.advance(c) && key === 'enter') game.closeupPlay(); + // Il fuoco segue la carta scelta: Invio su un pulsante gioca sempre quella che si vede scelta. + if (inControls && tressette.canPlay(c) && !signal) $('tressette-card-' + c.selected).focus(); return; } if (e.key === 'Escape') game.closeupClose(); @@ -700,11 +740,38 @@ function updateUi() { updateChoices(); updateSpeech(); const mg = miniState() && mini(), def = mg && story.closeups[c.id]; - const cards = def?.type === 'briscola' || def?.type === 'tressette'; + const cards = ['briscola', 'tressette', 'scopa'].includes(def?.type); $('bocce-controls').hidden = !mg || cards; $('bocce-controls').inert = !running; $('briscola-controls').hidden = def?.type !== 'briscola'; $('briscola-controls').inert = !running; + $('scopa-controls').hidden = def?.type !== 'scopa'; + $('scopa-controls').inert = !running; + if (def?.type === 'scopa') { + $('panel').hidden = $('choices').hidden = true; + const line = scopa.status(c, def.names); + if ($('scopa-status').textContent !== line) $('scopa-status').textContent = line; + for (let i = 0; i < 3; i++) { + const button = $('scopa-card-' + i), card = c.hands.player[i]; + const label = card ? `${i + 1}. ${scopa.cardName(card)}` : `${i + 1}. —`; + if (button.textContent !== label) button.textContent = label; + button.disabled = !scopa.canPlay(c) || !card || !!game.speech; + button.setAttribute('aria-pressed', String(c.selected === i && !!card && c.step === 'play')); + } + // Le prese fra cui scegliere, una per pulsante. + const options = c.step === 'choose' ? c.choice.options.map(o => o.map(scopa.cardName).join(' + ')) : []; + const box = $('scopa-options'); + if (box.dataset.list !== options.join('|')) { + box.dataset.list = options.join('|'); + box.replaceChildren(...options.map((o, k) => { + const b = Object.assign(document.createElement('button'), { type: 'button', textContent: `Prendi ${o}` }); + b.dataset.option = k; return b; + })); + } + box.hidden = !options.length; + [...box.children].forEach((b, k) => b.setAttribute('aria-pressed', String(c.choice?.selected === k))); + $('scopa-next').hidden = c.step !== 'smazzata'; + } $('tressette-controls').hidden = def?.type !== 'tressette'; $('tressette-controls').inert = !running; if (def?.type === 'tressette') { diff --git a/src/story/story.js b/src/story/story.js index 0570fac..a1352fe 100644 --- a/src/story/story.js +++ b/src/story/story.js @@ -124,6 +124,14 @@ export const story = { draw: [], quit: [{ say: 'Vai, ti tengo il posto. Le carte mica scappano.', who: 'giocatore_carte' }], }, + // La scopa ai 11 (idea di Andrea): carte, mattoni (o denari), settebello, primiera e scope. + scopa: { + type: 'scopa', names: { player: 'Andrea', cpu: 'Il giocatore' }, decks: { napoletane: 'carte_napoletane' }, + win: [{ set: 'scopa_vinta' }, { say: 'Settebello, primiera e pure le scope. Oggi l\'hai vista bene.', who: 'giocatore_carte', pose: 'talk' }], + lose: [{ say: 'Undici! Il settebello lo sento arrivare da lontano.', who: 'giocatore_carte', pose: 'laugh' }], + draw: [], + quit: [{ say: 'Vai, ti tengo il posto. Le carte mica scappano.', who: 'giocatore_carte' }], + }, // Le bocce al pallaio contro Mariano, il campione (idea di Andrea, 25 ottobre): solo per gloria. bocce: { type: 'bocce', @@ -2154,6 +2162,7 @@ export const story = { start: { options: [ { text: 'Una briscola?', when: '!briscola_giocata', next: 'mazzo', then: [{ say: 'Gnamo. In due, senza soci e senza scuse.', who: 'giocatore_carte', pose: 'shuffle' }] }, { text: 'La rivincita?', when: 'briscola_giocata', next: 'mazzo', then: [{ say: 'Il tavolo è qui. Il coraggio ce lo metti te.', who: 'giocatore_carte', pose: 'laugh' }] }, + { text: 'Una scopa?', next: 'mazzo_scopa', then: [{ say: 'Ai undici. E occhio al settebello: quello è sempre mio.', who: 'giocatore_carte', pose: 'shuffle' }] }, { text: 'Un tressette ai 21?', next: 'mazzo_tressette', then: [{ say: 'Ci vuole quattro: chiamo quei due che giocano qui dietro. Uno ti fa da socio.', who: 'giocatore_carte', pose: 'talk' }] }, { text: 'Ora cerco il palio. A dopo.', next: 'end' }, ] }, @@ -2169,6 +2178,18 @@ export const story = { { text: 'A fassi male.', next: 'end', then: [{ say: 'Io le conto tutte. Anche quelle che butti via per sbaglio.', who: 'giocatore_carte', pose: 'shuffle' }, { closeup: 'briscola', level: 'male' }] }, { text: 'Ci ripenso.', next: 'end' }, ] }, + // La scopa: stesse scelte (e stesse battute) della briscola. + mazzo_scopa: { options: [ + { text: 'Con le toscane.', next: 'livello_scopa', then: [{ unset: 'carte_napoletane' }, { say: 'Brao. Gobbo, donna e regio, come Dio comanda. Come la vuoi?', who: 'giocatore_carte' }] }, + { text: 'Con le napoletane.', next: 'livello_scopa', then: [{ set: 'carte_napoletane' }, { say: 'Le napoletane? Le tengo per i villeggianti. Come la vuoi?', who: 'giocatore_carte', pose: 'shuffle' }] }, + { text: 'Ci ripenso.', next: 'end' }, + ] }, + livello_scopa: { options: [ + { text: 'Alla bona.', next: 'end', then: [{ say: 'Alla bona. I carichi li regalo, oggi.', who: 'giocatore_carte' }, { closeup: 'scopa', level: 'bona' }] }, + { text: 'Seria.', next: 'end', then: [{ say: 'Allora posa il gomito e guarda le carte.', who: 'giocatore_carte' }, { closeup: 'scopa', level: 'seria' }] }, + { text: 'A fassi male.', next: 'end', then: [{ say: 'Io le conto tutte. Anche quelle che butti via per sbaglio.', who: 'giocatore_carte', pose: 'shuffle' }, { closeup: 'scopa', level: 'male' }] }, + { text: 'Ci ripenso.', next: 'end' }, + ] }, // Il tressette: stesse scelte (e stesse battute) della briscola, poi il tavolo in quattro. mazzo_tressette: { options: [ { text: 'Con le toscane.', next: 'livello_tressette', then: [{ unset: 'carte_napoletane' }, { say: 'Brao. Gobbo, donna e regio, come Dio comanda. Come la vuoi?', who: 'giocatore_carte' }] }, diff --git a/style.css b/style.css index 99e2449..083300a 100644 --- a/style.css +++ b/style.css @@ -26,15 +26,17 @@ body { display: flex; justify-content: center; overflow-x: hidden; -webkit-tap-h #scene { width: 100%; height: 100%; display: block; image-rendering: pixelated; cursor: crosshair; } #scene.torch { touch-action: none; } #scene.bocce { touch-action: none; } -#tressette-controls { background: #242b28; padding: 10px 14px; border-top: 2px solid #897b57; font-size: 11px; line-height: 1.7; } -#tressette-status { color: #f3dcac; min-height: 2em; margin: 0 0 8px; } +#scopa-controls, #tressette-controls { background: #242b28; padding: 10px 14px; border-top: 2px solid #897b57; font-size: 11px; line-height: 1.7; } +#scopa-status, #tressette-status { color: #f3dcac; min-height: 2em; margin: 0 0 8px; } .tressette-signals, .tressette-hand { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; } -.tressette-signals button, .tressette-hand button { flex: 1 1 18%; min-width: 0; min-height: 40px; padding: 6px; background: #38453c; color: #fff0cd; border: 2px solid #897b57; font-size: 9px; line-height: 1.5; cursor: pointer; } +.tressette-signals button, .tressette-hand button { flex: 1 1 18%; overflow-wrap: anywhere; min-width: 0; min-height: 40px; padding: 6px; background: #38453c; color: #fff0cd; border: 2px solid #897b57; font-size: 9px; line-height: 1.5; cursor: pointer; } .tressette-signals button { flex-basis: 22%; } .tressette-signals button[aria-pressed="true"], .tressette-hand button[aria-pressed="true"] { border-color: #ffdb76; color: #ffdb76; } .tressette-signals button:disabled, .tressette-hand button:disabled { opacity: .5; cursor: default; } -#tressette-controls summary { cursor: pointer; padding: 8px 0; } -#tressette-controls p { max-width: 76em; } +#scopa-controls summary, #tressette-controls summary { cursor: pointer; padding: 8px 0; } +#scopa-controls p, #tressette-controls p { max-width: 76em; } +#scopa-next { width: 100%; min-height: 40px; margin-bottom: 8px; background: #38453c; color: #fff0cd; border: 2px solid #897b57; font-size: 9px; cursor: pointer; } +.briscola-hand + .tressette-signals { margin-top: 8px; } #briscola-controls { background: #242b28; padding: 10px 14px; border-top: 2px solid #897b57; font-size: 11px; line-height: 1.7; } #briscola-status { color: #f3dcac; min-height: 2em; margin: 0 0 10px; } .briscola-hand { display: flex; gap: 8px; } @@ -291,7 +293,9 @@ body.full #sound { position: fixed; z-index: 13; top: max(8px, env(safe-area-ins #stage { flex: none; width: min(calc(100dvh * 16 / 9), 68vw); align-self: flex-start; } #stage.biliardo { width: min(calc((100dvh - 90px) * 16 / 9), 68vw); } #stage.briscola { width: min(calc((100dvh - 90px) * 16 / 9), 68vw); } - #briscola-controls, #tressette-controls { overflow-y: auto; min-height: 0; padding: 8px; font-size: 9px; } + #briscola-controls, #tressette-controls, #scopa-controls { overflow-y: auto; min-height: 0; padding: 8px; font-size: 9px; } + /* Nella colonna stretta accanto al tavolo: carte e segni su due colonne, parole che vanno a capo. */ + .tressette-hand button, .tressette-signals button { flex-basis: 40%; overflow-wrap: anywhere; } .briscola-hand { flex-direction: column; gap: 4px; } #side { flex: 1; min-width: 0; display: flex; flex-direction: column; background: var(--panel); } #sentence { font-size: 10.08px; } diff --git a/tests/scopa.browser.mjs b/tests/scopa.browser.mjs new file mode 100644 index 0000000..7c9e39a --- /dev/null +++ b/tests/scopa.browser.mjs @@ -0,0 +1,149 @@ +// Scopa ai 11 nel browser: carte con mouse, tocco e tastiera, scelta fra più prese; si accelerano solo le attese. +// npm run browser:scopa — /tmp/regolo-shots/scopa-*.png +import { chromium } from 'playwright'; +import { mkdirSync } from 'node:fs'; +import assert from 'node:assert/strict'; +import { staticServer } from './helpers.mjs'; + +const out = '/tmp/regolo-shots'; mkdirSync(out, { recursive: true }); +const server = await staticServer(), errors = []; +let browser; +async function tap(page, mode, selector) { + if (mode === 'touch') { const r = await page.locator(selector).boundingBox(); await page.touchscreen.tap(r.x + r.width / 2, r.y + r.height / 2); } + else await page.click(selector); +} +async function at(page, mode, [px, py]) { + // Sul telefono i pulsanti stanno sotto il tavolo: dopo averli toccati la pagina può essere scesa. + await page.locator('#scene').scrollIntoViewIfNeeded(); + const r = await page.locator('#scene').boundingBox(), x = r.x + px / 640 * r.width, y = r.y + py / 360 * r.height; + if (mode === 'touch') await page.touchscreen.tap(x, y); else await page.mouse.click(x, y); +} +async function playCard(page, mode, index) { + if (mode === 'keyboard') { + await page.keyboard.press(String(index + 1)); + assert.equal(await page.evaluate(() => game.closeup.selected), index); + await page.keyboard.press('Enter'); + return; + } + const p = await page.evaluate(async i => { + const { handX, HAND } = await import('/src/engine/scopa-render.js'); + return [handX(game.closeup.hands.player.length, i) + 32, HAND.y + 46]; + }, index); + await at(page, mode, p); +} +async function wait(page) { + return page.evaluate(async () => { + const { canPlay } = await import('/src/engine/scopa.js'); + // Un tocco mentre qualcuno parla salta la battuta, non gioca: la battuta si salta qui. + for (let i = 0; i < 60; i++) { + if (game.speech) game.skip(); + else if (canPlay(game.closeup) || ['smazzata', 'result', 'choose'].includes(game.closeup.step)) break; + else game.update(1.4); + } + return game.closeup.step; + }); +} +try { + browser = await chromium.launch({ args: ['--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] }); + const scenarios = [ + { name: 'desktop-mouse', mode: 'mouse', viewport: { width: 1280, height: 860 }, full: true }, + { name: 'telefono-touch', mode: 'touch', viewport: { width: 390, height: 844 } }, + { name: 'desktop-keyboard', mode: 'keyboard', viewport: { width: 1280, height: 860 } }, + { name: 'orizzontale-touch', mode: 'touch', viewport: { width: 844, height: 390 } }, + ]; + for (const { name, mode, viewport, full } of scenarios) { + const page = await browser.newPage({ viewport, hasTouch: true, isMobile: viewport.width < 900 }); + page.on('pageerror', e => errors.push(`${name}: ${e.message}`)); + page.on('console', e => { if (e.type() === 'error') errors.push(`${name}: ${e.text()}`); }); + await page.goto(`http://127.0.0.1:${server.port}/`); + await page.click('#splash'); await page.click('#new-game'); + await page.click('#rione [data-rione="campomigliaio"]'); await page.click('#skip-intro'); + await page.evaluate(async () => { const { settle } = await import('/tests/helpers.mjs'); settle(game); game.go('oratorio'); settle(game); game.act('talk', 'giocatore_carte'); settle(game); }); + await page.locator('#choices li', { hasText: 'Una scopa?' }).click(); + await page.evaluate(async () => (await import('/tests/helpers.mjs')).settle(game)); + const deck = name.startsWith('telefono') ? 'napoletane' : 'toscane'; + await page.locator('#choices li', { hasText: deck === 'napoletane' ? 'Con le napoletane.' : 'Con le toscane.' }).click(); + await page.evaluate(async () => (await import('/tests/helpers.mjs')).settle(game)); + await page.locator('#choices li', { hasText: 'Seria.' }).click(); + await page.evaluate(async () => (await import('/tests/helpers.mjs')).settle(game)); + assert.deepEqual(await page.evaluate(() => [game.closeup.deck, game.closeup.level]), [deck, 'seria']); + await page.waitForSelector('#scopa-controls:not([hidden])'); + assert.ok(await page.isHidden('#tressette-controls')); assert.ok(await page.isHidden('#briscola-controls')); + await wait(page); + await page.screenshot({ path: `${out}/scopa-${name}-inizio.png` }); + // Una volta per scenario: tavola preparata con due prese possibili per il 7, scelta toccando la tavola, + // con un pulsante o con frecce e Invio. + await page.evaluate(async () => { + // Una partita nuova in cui esce Andrea, così in tavola ci sono proprio quattro carte. + const { newScopa } = await import('/src/engine/scopa.js'), old = game.closeup; + let seed = 1, c; + do c = newScopa(seed++, { level: old.level, deck: old.deck }); while (c.current !== 'player'); + const pool = [...c.stock, ...c.table, c.hands.player[0]]; + const free = v => pool.splice(pool.findIndex(a => a.value === v), 1)[0]; + c.table = [free(3), free(4), free(2), free(5)]; c.hands.player[0] = free(7); c.stock = pool; + game.closeup = { id: old.id, ...c }; + }); + // La tavola preparata non perde né duplica carte. + const ids = await page.evaluate(() => { const c = game.closeup; return [...c.stock, ...c.table, ...c.hands.player, ...c.hands.cpu, ...c.taken.player, ...c.taken.cpu].map(a => a.id); }); + assert.equal(new Set(ids).size, 40); assert.equal(ids.length, 40); + await playCard(page, mode, 0); + assert.equal(await page.evaluate(() => game.closeup.step), 'choose'); + await page.waitForSelector('#scopa-options:not([hidden])'); + await page.screenshot({ path: `${out}/scopa-${name}-scelta.png` }); + if (mode === 'keyboard') { await page.keyboard.press('ArrowRight'); await page.keyboard.press('Enter'); } + else if (mode === 'touch') { + // Il 5 in tavola sta solo nella presa 2+5: toccarlo la sceglie. + const p = await page.evaluate(async () => { + const { tableXY } = await import('/src/engine/scopa-render.js'), c = game.closeup; + const i = c.table.findIndex(a => a.value === 5), [x, y] = tableXY(c.table.length, i); return [x + 21, y + 30]; + }); + await at(page, mode, p); + } else await tap(page, mode, '#scopa-options button:nth-child(2)'); + assert.equal(await page.evaluate(() => game.closeup.step), 'take'); + assert.deepEqual(await page.evaluate(() => game.closeup.last.captured.map(a => a.value).sort()), [2, 5]); + await page.screenshot({ path: `${out}/scopa-${name}-presa.png` }); + let played = 1; + for (let steps = 0; steps < 400; steps++) { + const step = await wait(page); + if (step === 'result') break; + if (step === 'smazzata') { + await page.screenshot({ path: `${out}/scopa-${name}-smazzata.png` }); + if (!full) break; + if (mode === 'keyboard') await page.keyboard.press('Enter'); else await tap(page, mode, '#scopa-next'); + continue; + } + if (step !== 'play' && step !== 'choose') throw new Error('partita ferma in ' + step); + if (step === 'choose') { + if (mode === 'keyboard') await page.keyboard.press('Enter'); else await tap(page, mode, '#scopa-options button:first-child'); + continue; + } + const move = await page.evaluate(async () => { + const { observation, choose } = await import('/src/engine/scopa.js'); + return choose(observation(game.closeup, 'player'), 'male', 3); + }); + const before = await page.evaluate(() => game.closeup.hands.player.length); + await playCard(page, mode, move.index); played++; + const after = await page.evaluate(() => game.closeup.step === 'choose' ? -1 : game.closeup.hands.player.length); + // Il gioco intanto va avanti da solo: dopo l'ultima carta la mano può essere già ridata. + assert.ok(after === -1 || after === before - 1 || (before === 1 && after === 3), `un gesto gioca una carta (${name}: ${before} → ${after})`); + } + const count = await page.evaluate(() => game.closeup.summary ? game.closeup.summary.rows : null); + assert.ok(count, 'conteggio della smazzata'); + if (full) { + assert.equal(await page.evaluate(() => game.closeup.step), 'result'); + await page.screenshot({ path: `${out}/scopa-${name}-risultato.png` }); + const winner = await page.evaluate(() => game.closeup.result.winner); + await page.keyboard.press('Enter'); + await page.evaluate(async () => (await import('/tests/helpers.mjs')).settle(game)); + assert.equal(await page.evaluate(() => game.closeup), null); + assert.equal(await page.evaluate(() => !!game.state.flags.scopa_vinta), winner === 'player'); + } else { + await page.keyboard.press('Escape'); + assert.equal(await page.evaluate(() => game.closeup), null); + assert.equal(await page.evaluate(() => !!game.state.flags.scopa_vinta), false); + } + console.log(`${name}: ${played} carte giocate, presa scelta fra due${full ? ', partita ai 11 finita' : ', una smazzata'} OK`); + await page.close(); + } + assert.deepEqual(errors, []); console.log(`Schermate: ${out}/scopa-*.png`); +} finally { await browser?.close(); server.kill(); } diff --git a/tests/scopa.test.mjs b/tests/scopa.test.mjs new file mode 100644 index 0000000..2661685 --- /dev/null +++ b/tests/scopa.test.mjs @@ -0,0 +1,108 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { DECK_CARDS, COIN, newScopa, captures, primiera, tally, play, take, cancel, select, nudge, update, advance, finished, observation, unseen, choose, canPlay, status, TARGET } from '../src/engine/scopa.js'; +import { simulate, calibrate } from '../tools/calibrate_scopa.mjs'; + +const card = (suit, rank, deck = 'toscane') => DECK_CARDS[deck].find(a => a.suit === suit && a.rank === rank); + +test('valori: Asso 1, numeri, gobbo 8, donna 9, regio 10; primiera 7 = 21, 6 = 18, Asso 16 … figure 10', () => { + assert.deepEqual(['Asso', '5', 'Gobbo', 'Donna', 'Regio'].map(r => card('cuori', r).value), [1, 5, 8, 9, 10]); + assert.deepEqual(['7', '6', 'Asso', '5', '4', '3', '2', 'Regio'].map(r => card('fiori', r).prime), [21, 18, 16, 15, 14, 13, 12, 10]); + assert.equal(COIN.toscane, 'mattoni'); assert.equal(COIN.napoletane, 'denari'); +}); + +test('prese: la carta uguale ha la precedenza; se no ogni somma; più carte uguali, si sceglie', () => { + const t = [card('cuori', '3'), card('fiori', '4'), card('picche', '7'), card('mattoni', '2'), card('cuori', '5')]; + assert.deepEqual(captures(t, card('mattoni', '7')).map(o => o.map(a => a.id)), [[card('picche', '7').id]], 'col 7 si prende il 7, non 3+4 o 2+5'); + const sums = captures([card('cuori', '3'), card('fiori', '4'), card('mattoni', '2'), card('cuori', '5')], card('picche', '7')); + assert.equal(sums.length, 2, '3+4 oppure 2+5'); + assert.deepEqual(captures([card('cuori', '3'), card('fiori', '3')], card('picche', '3')).length, 2); + assert.deepEqual(captures([card('cuori', '3')], card('picche', 'Donna')), []); +}); + +test('primiera: la migliore per seme, e senza un seme non si fa', () => { + const all = [card('cuori', '7'), card('mattoni', '6'), card('fiori', 'Asso'), card('picche', 'Regio'), card('picche', '2')]; + assert.equal(primiera(all, 'toscane'), 21 + 18 + 16 + 12); + assert.equal(primiera(all.slice(0, 3), 'toscane'), 0); +}); + +test('conteggio: carte, mattoni, settebello, primiera e scope; pari non si dà il punto', () => { + const deck = DECK_CARDS.toscane, mine = deck.filter(a => a.suit === 'mattoni' || (a.suit === 'cuori' && a.value <= 2)); + const theirs = deck.filter(a => !mine.includes(a)); + const t = tally({ player: mine, cpu: theirs }, { player: 2, cpu: 0 }, 'toscane'); + const row = k => t.rows.find(r => r.key === k); + assert.equal(row('carte').winner, 'cpu'); assert.equal(row('coin').winner, 'player'); assert.equal(row('coin').label, 'Mattoni'); + assert.equal(row('settebello').winner, 'player'); assert.equal(row('primiera').winner, null, 'a nessuno ci sono tutti e quattro i semi'); + assert.deepEqual(t.points, { player: 4, cpu: 1 }); + const half = tally({ player: deck.slice(0, 20), cpu: deck.slice(20) }, { player: 0, cpu: 0 }, 'napoletane'); + assert.equal(half.rows[0].winner, null, '20 a 20: le carte non le prende nessuno'); + assert.equal(tally({ player: [card('denari', '7', 'napoletane')], cpu: [] }, { player: 0, cpu: 0 }, 'napoletane').rows[2].winner, 'player'); +}); + +test('smazzata: 4 in tavola (mai tre re), tre a testa, la presa è obbligatoria, la scelta fra più prese', () => { + for (let s = 1; s < 60; s++) assert.ok(newScopa(s).table.filter(a => a.value === 10).length < 3); + const c = newScopa(3); c.current = 'player'; + assert.equal(c.table.length, 4); assert.equal(c.hands.player.length, 3); assert.equal(c.stock.length, 30); + c.table = [card('cuori', '3'), card('fiori', '4'), card('mattoni', '2'), card('cuori', '5')]; + c.hands.player = [card('picche', '7'), card('picche', 'Regio')]; + assert.ok(play(c, 0)); assert.equal(c.step, 'choose'); assert.equal(c.choice.options.length, 2); + assert.ok(nudge(c, 1)); assert.ok(cancel(c)); assert.equal(c.step, 'play'); assert.equal(c.hands.player.length, 2); + assert.ok(play(c, 0)); assert.ok(select(c, 1)); assert.ok(take(c)); + assert.equal(c.step, 'take'); assert.equal(c.table.length, 2); assert.equal(c.taken.player.length, 3); +}); + +test('scopa: chi pulisce la tavola fa un punto, ma non con l\'ultima carta della smazzata', () => { + const c = newScopa(4); c.current = 'player'; c.table = [card('cuori', '3'), card('fiori', '4')]; + c.hands.player = [card('picche', '7'), card('picche', '2')]; + assert.ok(play(c, 0)); assert.equal(c.scope.player, 1); assert.equal(c.last.scopa, true); assert.match(status(c), /Scopa di Andrea/); + const d = newScopa(4); d.current = 'player'; d.stock = []; d.hands.cpu = []; d.table = [card('cuori', '3')]; + d.hands.player = [card('picche', '3')]; + assert.ok(play(d, 0)); assert.equal(d.scope.player, 0, 'all\'ultima non è scopa'); +}); + +test('a fine smazzata le carte in tavola vanno all\'ultimo che ha preso; tutte le 40 carte si contano', () => { + const c = newScopa(5, { level: 'seria' }); + while (c.step !== 'smazzata') { + if (canPlay(c)) { const m = choose(observation(c, 'player'), 'seria'); assert.ok(play(c, m.index, 'player', m.option)); } + update(c, 20); + } + assert.equal(c.taken.player.length + c.taken.cpu.length, 40); + assert.equal(c.table.length, 0); + const before = c.dealer; + if (!finished(c)) { advance(c); assert.notEqual(c.dealer, before); assert.equal(c.current, before, 'gioca per primo chi non fa le carte'); } +}); + +test('si va agli 11: a fine smazzata vince chi ne ha di più; pari, un\'altra; il risultato è subito definitivo', () => { + const c = newScopa(6); c.stock = []; c.hands = { player: [], cpu: [card('cuori', '4')] }; + c.table = [card('fiori', '4')]; c.current = 'cpu'; c.score = { player: 9, cpu: 10 }; + c.taken = { player: DECK_CARDS.toscane.filter(a => a.suit !== 'fiori' || a.value !== 4).slice(0, 30), cpu: [] }; + assert.ok(play(c, 0, 'cpu')); update(c, 2); + assert.equal(c.step, 'result', 'senza passare dal riepilogo: uscire non cancella la vittoria'); + assert.equal(c.result.winner, 'player'); + const d = newScopa(6); d.step = 'smazzata'; assert.ok(advance(d)); assert.equal(d.step, 'play'); + assert.ok(finished({ score: { player: TARGET, cpu: 9 } })); assert.ok(!finished({ score: { player: 12, cpu: 12 } })); +}); + +test('partite deterministiche col seme; l\'avversario vede solo la sua mano, la tavola e le prese', () => { + assert.deepEqual(simulate(31, 'male').score, simulate(31, 'male').score); + const c = newScopa(12), v = observation(c, 'cpu'); + assert.equal(v.hands, undefined); assert.equal(v.stock, 30); + assert.equal(unseen(v).length, 33, 'mano dell\'altro e mazzo'); + const other = structuredClone(c); other.hands.player.reverse(); other.stock.reverse(); + assert.deepEqual(choose(observation(c, 'cpu'), 'male', 3), choose(observation(other, 'cpu'), 'male', 3)); +}); + +test('l\'avversario prende il settebello e la scopa quando può, e non lascia scope facili', () => { + const v = { who: 'cpu', deck: 'toscane', hand: [card('cuori', '7'), card('fiori', 'Regio')], table: [card('mattoni', '7'), card('picche', '4')], taken: { player: [], cpu: [] }, opponentCount: 3, stock: 20, scope: { player: 0, cpu: 0 } }; + const m = choose(v, 'seria'); assert.equal(v.hand[m.index].rank, '7', 'prende il settebello'); + const w = { ...v, hand: [card('cuori', '7'), card('fiori', '4')], table: [card('picche', '3'), card('mattoni', '4')] }; + const s = choose(w, 'male'); assert.equal(w.hand[s.index].rank, '7', 'scopa con 3+4'); + const x = { ...v, hand: [card('cuori', '5'), card('fiori', 'Regio')], table: [card('picche', '2')] }; + const d = choose(x, 'male'); assert.equal(x.hand[d.index].rank, 'Regio', 'col 5 lascerebbe 7 in tavola: meglio il regio (12)'); +}); + +test('taratura: Alla bona < Seria < A fassi male', () => { + const runs = calibrate(60); + assert.ok(runs[0].playerPercent > runs[1].playerPercent, JSON.stringify(runs)); + assert.ok(runs[1].playerPercent > runs[2].playerPercent, JSON.stringify(runs)); +}); diff --git a/tests/tressette.browser.mjs b/tests/tressette.browser.mjs index aea3692..a8b90b7 100644 --- a/tests/tressette.browser.mjs +++ b/tests/tressette.browser.mjs @@ -28,13 +28,20 @@ async function playCard(page, mode, index, signal) { const { handX, CARD } = await import('/src/engine/tressette-render.js'); return [handX(game.closeup.hands[0].length, i) + 21, CARD.y + 30]; }, index); + // Sul telefono i pulsanti stanno sotto il tavolo: dopo averli toccati la pagina può essere scesa. + await page.locator('#scene').scrollIntoViewIfNeeded(); const r = await page.locator('#scene').boundingBox(), x = r.x + at[0] / 640 * r.width, y = r.y + at[1] / 360 * r.height; if (mode === 'touch') await page.touchscreen.tap(x, y); else await page.mouse.click(x, y); } async function wait(page) { return page.evaluate(async () => { const { canPlay } = await import('/src/engine/tressette.js'); - for (let i = 0; i < 12 && !canPlay(game.closeup) && !['smazzata', 'result'].includes(game.closeup.step); i++) game.update(1.3); + // Un tocco mentre qualcuno parla salta la battuta, non gioca: la battuta si salta qui. + for (let i = 0; i < 60; i++) { + if (game.speech) game.skip(); + else if (canPlay(game.closeup) || ['smazzata', 'result'].includes(game.closeup.step)) break; + else game.update(1.3); + } return game.closeup.step; }); } diff --git a/tests/tressette.test.mjs b/tests/tressette.test.mjs index 29906ea..c284417 100644 --- a/tests/tressette.test.mjs +++ b/tests/tressette.test.mjs @@ -1,6 +1,6 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; -import { DECK_CARDS, newTressette, accuse, legal, best, signalOk, reason, setSignal, play, collect, advance, update, observation, choose, knowledge, canPlay, finished, team, status, TARGET } from '../src/engine/tressette.js'; +import { DECK_CARDS, newTressette, accuse, accuseName, legal, best, signalOk, reason, setSignal, play, collect, advance, update, observation, choose, knowledge, canPlay, finished, team, status, TARGET } from '../src/engine/tressette.js'; import { simulate, calibrate } from '../tools/calibrate_tressette.mjs'; const card = (suit, rank, deck = 'toscane') => DECK_CARDS[deck].find(a => a.suit === suit && a.rank === rank); @@ -30,18 +30,22 @@ test('accuse: napoletana 3 punti, tre o quattro Assi, 2 o 3 per 3 o 4 punti, anc assert.deepEqual(a.map(x => [x.kind, x.points]), [['napoletana', 3], ['buongioco', 3]]); assert.equal(accuse(hand([['cuori', '2'], ['picche', '2'], ['fiori', '2'], ['mattoni', '2']]))[0].points, 4); assert.deepEqual(accuse(hand([['cuori', 'Asso'], ['cuori', '2'], ['picche', '3']])), []); + // Del tris si dice il seme che manca, così Andrea sa quello che sa l'avversario. + const tris = accuse(hand([['picche', '3'], ['fiori', '3'], ['mattoni', '3']]), ['cuori', 'mattoni', 'fiori', 'picche'])[0]; + assert.equal(tris.missing, 'cuori'); assert.equal(accuseName(tris), 'tre 3, manca cuori'); }); -test('segni onesti: volo con l\'ultima, striscio con almeno due rimaste, busso con almeno una; solo chi esce', () => { - const h = hand([['cuori', '4'], ['cuori', '5'], ['cuori', '6'], ['fiori', '7']]); - assert.ok(signalOk(h, 3, [], 'volo')); assert.ok(!signalOk(h, 0, [], 'volo')); - assert.ok(signalOk(h, 0, [], 'striscio')); assert.ok(!signalOk(h, 3, [], 'striscio')); - assert.ok(signalOk(h, 0, [], 'busso')); assert.ok(!signalOk(h, 3, [], 'busso')); +test('segni: volo con l\'ultima, striscio se ne resta un\'altra, busso sempre; solo chi esce', () => { + const h = hand([['cuori', '4'], ['cuori', '5'], ['fiori', '7']]); + assert.ok(signalOk(h, 2, [], 'volo')); assert.ok(!signalOk(h, 0, [], 'volo')); + assert.ok(signalOk(h, 0, [], 'striscio')); assert.ok(!signalOk(h, 2, [], 'striscio')); + assert.ok(signalOk(h, 0, [], 'busso')); assert.ok(signalOk(h, 2, [], 'busso')); assert.ok(!signalOk(h, 0, [{ who: 1, card: card('cuori', '7') }], 'busso')); - const c = newTressette(5); c.current = 0; c.table = []; c.hands[0] = [...h]; + const c = newTressette(5); c.current = 0; c.table = []; c.hands[0] = [...h]; c.pending[0] = []; assert.ok(setSignal(c, 'volo')); assert.equal(play(c, 0), false); assert.match(c.refused, /Volo solo/); - assert.equal(c.hands[0].length, 4, 'la carta resta in mano'); - assert.ok(play(c, 3)); assert.equal(c.table[0].signal, 'volo'); assert.equal(c.signal, null); + assert.equal(c.hands[0].length, 3, 'la carta resta in mano'); + assert.equal(play(c, 0, 0, 'xyz'), false); assert.equal(c.refused, 'Segno non riconosciuto.'); + assert.ok(play(c, 2)); assert.equal(c.table[0].signal, 'volo'); assert.equal(c.signal, null); assert.ok(c.events.some(e => e.kind === 'card-volo')); }); @@ -52,30 +56,43 @@ test('la carta che non risponde al seme si rifiuta con la ragione', () => { assert.ok(canPlay(c)); assert.ok(play(c, 0)); }); -test('una smazzata: 10 prese, 11 punti fra terzi e ultima presa, accuse nel punteggio, poi la seconda', () => { +test('una smazzata: accuse dette al proprio turno, 10 prese, sempre 11 punti fra terzi e ultima presa', () => { const c = newTressette(1234, { level: 'seria' }); - const accused = [0, 1].map(t => c.accuse.filter(a => team(a.who) === t).reduce((n, a) => n + a.points, 0)); - assert.deepEqual(c.score, accused); + assert.deepEqual(c.score, [0, 0]); assert.deepEqual(c.accuse, [], 'prima di giocare nessuno ha accusato'); + const owed = c.pending.flat(); assert.equal(c.current, (c.dealer + 1) % 4, 'esce chi sta a destra del mazziere'); assert.ok(c.hands.every(h => h.length === 10)); const firstDealer = c.dealer; - while (c.step !== 'smazzata') { + while (!['smazzata', 'result'].includes(c.step)) { if (canPlay(c)) { const m = choose(observation(c, 0), 'seria'); assert.ok(play(c, m.index, 0, m.signal)); } update(c, 20); + if (c.tricks.length === 1) assert.equal(c.accuse.length, owed.length, 'dopo la prima presa hanno accusato tutti'); + } + if (c.step === 'smazzata') { + assert.equal(c.tricks.length, 10); + assert.equal(c.summary.thirds[0] + c.summary.thirds[1], 35); + assert.equal(c.summary.points[0] + c.summary.points[1], 11, 'i terzi avanzati sommano sempre a due'); + assert.deepEqual(c.score, [0, 1].map(t => c.summary.accused[t] + c.summary.points[t])); + assert.ok(advance(c)); assert.equal(c.dealer, (firstDealer + 1) % 4); assert.equal(c.step, 'play'); } - assert.equal(c.tricks.length, 10); - assert.equal(c.summary.thirds[0] + c.summary.thirds[1], 35); - assert.ok([10, 11].includes(c.summary.points[0] + c.summary.points[1]), 'si perdono al più i terzi avanzati'); - assert.deepEqual(c.score, [0, 1].map(t => accused[t] + c.summary.points[t])); - if (!finished(c)) { advance(c); assert.equal(c.dealer, (firstDealer + 1) % 4); assert.equal(c.step, 'play'); } }); -test('si va ai 21: vince chi ci arriva per primo; pari sopra i 21 si continua', () => { - const c = newTressette(3); - c.step = 'smazzata'; c.score = [21, 21]; assert.ok(!finished(c)); advance(c); assert.equal(c.step, 'play'); - c.step = 'smazzata'; c.score = [TARGET, 17]; advance(c); assert.equal(c.step, 'result'); assert.equal(c.result.winner, 'player'); - c.step = 'smazzata'; c.score = [22, 24]; advance(c); assert.equal(c.result.winner, 'cpu'); - assert.match(status(c), /Hanno vinto loro, 22 a 24/); +test('si va ai 21: chi ci arriva per primo è fuori, anche a metà smazzata, e la vittoria non si perde', () => { + const c = newTressette(3); c.pending = [[], [], [], []]; + c.base = [20, 19]; c.score = [20, 19]; c.thirds = [2, 0]; + // Una presa da un terzo porta noi a 21 (3 terzi = 1 punto): fuori subito. + c.table = [{ who: 0, card: card('cuori', '3') }, { who: 1, card: card('cuori', '4') }, { who: 2, card: card('cuori', '5') }, { who: 3, card: card('cuori', '6') }]; + c.step = 'trick'; c.last = { winner: 0, cards: c.table.map(p => p.card) }; + assert.ok(collect(c)); assert.equal(c.step, 'result'); assert.equal(c.result.winner, 'player'); assert.deepEqual(c.score, [21, 19]); + assert.ok(finished(c)); assert.equal(advance(c), false); + assert.match(status(c), /Avete vinto voi, 21 a 19/); + // Anche le accuse bastano: dette giocando, contano alla fine di quella presa. + const d = newTressette(3); d.base = [0, 19]; d.score = [0, 19]; d.current = 1; d.table = []; d.tricks = []; + d.pending = [[], [{ who: 1, kind: 'napoletana', suit: 'fiori', points: 3, cards: [] }], [], []]; + d.hands[1] = [card('fiori', '7'), ...d.hands[1].slice(1)]; + assert.ok(play(d, 0, 1)); assert.equal(d.score[1], 22); + for (const seat of [2, 3, 0]) { d.current = seat; play(d, choose(observation(d, seat), 'seria').index, seat); } + assert.equal(d.step, 'trick'); collect(d); assert.equal(d.step, 'result'); assert.equal(d.result.winner, 'cpu'); }); test('partite intere deterministiche col seme; i due mazzi danno le stesse carte', () => { @@ -117,6 +134,26 @@ test('il socio ritorna il seme a chi ha bussato', () => { assert.equal(c.hands[2][m.index].suit, 'cuori'); assert.equal(c.hands[2][m.index].rank, '4'); }); +test('chi riceve il busso prende con la migliore; il socio non spreca il 3 su una presa già sua', () => { + const c = newTressette(8); c.pending = [[], [], [], []]; c.current = 2; + c.table = [{ who: 0, card: card('cuori', '4'), signal: 'busso' }, { who: 1, card: card('cuori', '5') }]; + c.hands[2] = hand([['cuori', '2'], ['cuori', '6'], ['fiori', '7']]); + assert.equal(c.hands[2][choose(observation(c, 2), 'male').index].rank, '2', 'al busso risponde col 2'); + const d = newTressette(8); d.pending = [[], [], [], []]; d.current = 3; + d.table = [{ who: 0, card: card('fiori', '6') }, { who: 1, card: card('fiori', '2') }, { who: 2, card: card('fiori', '4') }]; + d.hands[3] = hand([['fiori', '3'], ['fiori', '5'], ['cuori', '7']]); + // L'avversario del posto 3: il suo socio (1) prende col 2 e lui è l'ultimo; tiene il 3 e dà il 5. + assert.equal(d.hands[3][choose(observation(d, 3), 'male').index].rank, '5'); +}); + +test('chi vola ha finito il seme: per «A fassi male» non può più avere il 3', () => { + const c = newTressette(8); c.pending = [[], [], [], []]; c.current = 1; + c.tricks = [{ leader: 2, winner: 0, cards: [{ who: 2, card: card('cuori', '4'), signal: 'volo' }, { who: 3, card: card('cuori', '5') }, { who: 0, card: card('cuori', '6') }, { who: 1, card: card('cuori', '7') }] }]; + c.hands[1] = hand([['cuori', '2'], ['fiori', '4']]); c.table = []; + const k = knowledge(observation(c, 1), true); + assert.equal(k.could(2, card('cuori', '3')), false); +}); + test('taratura: Alla bona < Seria < A fassi male, contro Andrea e il socio che giocano attenti', () => { const runs = calibrate(60); assert.ok(runs[0].playerPercent > runs[1].playerPercent, JSON.stringify(runs)); diff --git a/tools/calibrate_scopa.mjs b/tools/calibrate_scopa.mjs new file mode 100644 index 0000000..f3af1cc --- /dev/null +++ b/tools/calibrate_scopa.mjs @@ -0,0 +1,24 @@ +// node tools/calibrate_scopa.mjs [partite per livello, predefinite 300] +// Stesse carte per i tre livelli; Andrea gioca come «A fassi male», l'avversario al livello scelto. +import { newScopa, observation, choose, play, update, advance, canPlay, LEVELS } from '../src/engine/scopa.js'; + +export function simulate(seed, level, me = 'male') { + const c = newScopa(seed, { level }); + for (let steps = 0; steps < 5000 && c.step !== 'result'; steps++) { + if (canPlay(c)) { const m = choose(observation(c, 'player'), me, steps + 1); if (!play(c, m.index, 'player', m.option)) throw new Error('mossa rifiutata'); } + if (!advance(c)) update(c, 20); c.events.length = 0; + } + if (c.step !== 'result') throw new Error('Partita non conclusa'); + return c; +} +export function calibrate(n = 300, me = 'male') { + return LEVELS.map(level => { + let wins = 0, rounds = 0; + for (let i = 1; i <= n; i++) { + const c = simulate(Math.imul(i, 2654435761) >>> 0, level, me); + wins += c.result.winner === 'player'; rounds += c.round; + } + return { level, games: n, playerPercent: 100 * wins / n, rounds: rounds / n }; + }); +} +if (process.argv[1] === new URL(import.meta.url).pathname) console.log(JSON.stringify(calibrate(Number(process.argv[2]) || 300), null, 2)); diff --git a/tools/calibrate_tressette.mjs b/tools/calibrate_tressette.mjs index b87415d..2fde9d1 100644 --- a/tools/calibrate_tressette.mjs +++ b/tools/calibrate_tressette.mjs @@ -1,12 +1,12 @@ // node tools/calibrate_tressette.mjs [partite per livello, predefinite 300] // Stesse carte per i tre livelli; Andrea gioca come il suo socio («A fassi male»), gli avversari al livello scelto. -import { newTressette, observation, choose, play, update, canPlay, LEVELS } from '../src/engine/tressette.js'; +import { newTressette, observation, choose, play, update, advance, canPlay, LEVELS } from '../src/engine/tressette.js'; export function simulate(seed, level, me = 'male') { const c = newTressette(seed, { level }); for (let steps = 0; steps < 5000 && c.step !== 'result'; steps++) { if (canPlay(c)) { const m = choose(observation(c, 0), me, steps + 1); if (!play(c, m.index, 0, m.signal)) throw new Error('mossa rifiutata: ' + c.refused); } - update(c, 20); c.events.length = 0; + if (!advance(c)) update(c, 20); c.events.length = 0; } if (c.step !== 'result') throw new Error('Partita non conclusa'); return c;