From eacad972402cb2f9a6a4a24b7e5ab1a9e07988b6 Mon Sep 17 00:00:00 2001 From: codex-bot Date: Wed, 9 Sep 2026 22:06:09 +0100 Subject: [PATCH] v0.6: real PNG textures (head with eyes, scaled body, corners, tapering tails, apple, board); renderPieces classifier + tests --- src/common/snake.js | 26 +++++++++++++- src/common/tex/apple.png | Bin 0 -> 902 bytes src/common/tex/board.png | Bin 0 -> 4416 bytes src/common/tex/body.png | Bin 0 -> 672 bytes src/common/tex/body_h.png | Bin 0 -> 694 bytes src/common/tex/body_v.png | Bin 0 -> 727 bytes src/common/tex/corner_en.png | Bin 0 -> 306 bytes src/common/tex/corner_es.png | Bin 0 -> 321 bytes src/common/tex/corner_nw.png | Bin 0 -> 237 bytes src/common/tex/corner_sw.png | Bin 0 -> 346 bytes src/common/tex/head_e.png | Bin 0 -> 841 bytes src/common/tex/head_n.png | Bin 0 -> 918 bytes src/common/tex/head_s.png | Bin 0 -> 913 bytes src/common/tex/head_w.png | Bin 0 -> 731 bytes src/common/tex/tail_e.png | Bin 0 -> 644 bytes src/common/tex/tail_n.png | Bin 0 -> 848 bytes src/common/tex/tail_s.png | Bin 0 -> 794 bytes src/common/tex/tail_w.png | Bin 0 -> 673 bytes src/manifest.json | 4 +-- src/pages/index/index.ux | 34 +++++++----------- tests/make_tex.py | 63 ++++++++++++++++++++++++++++++++ tests/page.cjs | 14 +++++--- tests/pieces.cjs | 68 +++++++++++++++++++++++++++++++++++ tests/tex-montage.png | Bin 0 -> 14091 bytes 24 files changed, 179 insertions(+), 30 deletions(-) create mode 100644 src/common/tex/apple.png create mode 100644 src/common/tex/board.png create mode 100644 src/common/tex/body.png create mode 100644 src/common/tex/body_h.png create mode 100644 src/common/tex/body_v.png create mode 100644 src/common/tex/corner_en.png create mode 100644 src/common/tex/corner_es.png create mode 100644 src/common/tex/corner_nw.png create mode 100644 src/common/tex/corner_sw.png create mode 100644 src/common/tex/head_e.png create mode 100644 src/common/tex/head_n.png create mode 100644 src/common/tex/head_s.png create mode 100644 src/common/tex/head_w.png create mode 100644 src/common/tex/tail_e.png create mode 100644 src/common/tex/tail_n.png create mode 100644 src/common/tex/tail_s.png create mode 100644 src/common/tex/tail_w.png create mode 100644 tests/make_tex.py create mode 100644 tests/pieces.cjs create mode 100644 tests/tex-montage.png diff --git a/src/common/snake.js b/src/common/snake.js index 88e34e4..734e401 100644 --- a/src/common/snake.js +++ b/src/common/snake.js @@ -34,6 +34,30 @@ function turn(current, desired) { return desired } +// Classify every segment as a texture piece (head/body/corner/tail + compass side), +// so the page can render each cell with the right PNG. Returns one name per segment. +// Sides are screen compass (y grows down): e/w/n/s = right/left/up/down. +function renderPieces(snakeCells, dir) { + const D = { '1,0': 'e', '-1,0': 'w', '0,-1': 'n', '0,1': 's' } + const opp = { e: 'w', w: 'e', n: 's', s: 'n' } + return snakeCells.map((s, i) => { + if (i === 0) return 'head_' + D[dir.x + ',' + dir.y] + const prev = snakeCells[i - 1] + const next = snakeCells[i + 1] + const from = D[(prev.x - s.x) + ',' + (prev.y - s.y)] // side toward head + if (!next) return 'tail_' + opp[from] // tip points away from body + const to = D[(next.x - s.x) + ',' + (next.y - s.y)] // side toward tail tip + if ((from === 'e' || from === 'w') && (to === 'e' || to === 'w')) return 'body_h' + if ((from === 'n' || from === 's') && (to === 'n' || to === 's')) return 'body_v' + // corner: name = the two sides it connects; pairs are alphabetically sorted + const pair = [from, to].sort() + if (pair[0] === 'e' && pair[1] === 'n') return 'corner_en' + if (pair[0] === 'n' && pair[1] === 'w') return 'corner_nw' + if (pair[0] === 'e' && pair[1] === 's') return 'corner_es' + return 'corner_sw' + }) +} + // Rotate a direction 90°: clockwise order right->down->left->up. Always ±90°, never a reversal. function rotateDir(dir, clockwise) { const order = [{ x: 1, y: 0 }, { x: 0, y: 1 }, { x: -1, y: 0 }, { x: 0, y: -1 }] @@ -56,4 +80,4 @@ function spawnFood(snake, N) { return { x, y } } -module.exports = { initialSnake, step, turn, rotateDir, spawnFood } +module.exports = { initialSnake, step, turn, renderPieces, rotateDir, spawnFood } diff --git a/src/common/tex/apple.png b/src/common/tex/apple.png new file mode 100644 index 0000000000000000000000000000000000000000..556ee0e69969ef7573e1554d805fa3b69e3c689e GIT binary patch literal 902 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 aeiWPa+1tSax%p=V06P(Qy#82J_J05ZZ?PNz literal 0 HcmV?d00001 diff --git a/src/common/tex/body.png b/src/common/tex/body.png new file mode 100644 index 0000000000000000000000000000000000000000..186c62715d45528b6050801ebc7db117713fc1a5 GIT binary patch literal 672 zcmV;R0$=@!P)wYkx9mO3`{9%X2mzQGUon#tvybh|`tskWmw){A zr^;VXe>!|T`u&?HL+`vSrYJK*2=D#ZfB=@F>H0+#-~WB}TX-<)KOdeAy}V+aMPKX} z?C=gNloUEO-{?YLx9*!Qa{v92RIDd0aOmQ<* zV6Az1*-o)@;z(vOMIG)RQjSX6*fMYD%-cB!g9FN8NlNK{mE9*HYGX}i@LZ6N( zh%qwg50JD?fk4PDo;1^DEiHzb;hdwHHY|&@jX5*Jdr$SDV)Ai9p5=Jw$wSU;K4W|} zMs7P8Kv-9G{^p!=R5I8b(99afS7Vx4gOkJ9;>hbjNkofWTwbhlCMUf7U6^$Og!nQO zVf{sj5W=c3X7<0zlnLh?BuH)A3}kKGHdR$#)NgB=sv+xV^p1O7NR9&ww2LL+N zP1Rt@77DlM2O%)X`q|O}>N16uKlAS0hT5Epegc-<*qaTjA`R9hZs>ym8#digHk;gbqetI)~b?1WRPOE+n z#ewtgQ?DkoI8Un!R(!Dn7RkMTc$Z;qu>x+}m-V0v@6o#L3W}4)hwy|v*TdCTGE)xJagcLOQz%IrrW&zZz5YyfUT_v7(75v%-#c30A_Dp zTyCv2?}!+Hc=~2n+IOyAU$`*)c;U*@nh3-gF$@r# zIz$A+km@}Es8?jXkYd83(R*qT4hn<^e)?8hS6q9!5muWVd6lK9){*{m- zs)`wvhzN(D4;l4F_@<}1+Qc_B&CGVz!mxPgYpwc*I>th84!GgOqt<|41( zzb2Nka;mWRVvl?~M^vz6WbKU3jSjBitTtcYP$Cv$koIDm)#X*nP!eKb6h^e>TAW)s zM@$i|DF+`9$lvDp%%e5MXP)7gA-z{U#EDoi^GJnGR?jfAN!UP@XQSRgL=X|mK{?&S zsIv9d`Z#vciNO9~zj~_bRMy z!#Rf&NB2vY?rs<79H~s4>YQTf+ESHYFf1Cai!DxHI*kPb0Y72OImhnaF8hOhvU)}l z3J!;dsE)Z8NFgzIevYi2QT9t*=BUlruxLnyG=2&l;M7r+1xLjZBJxA*r?O!9%rn!R z!OW)4#<`joKStD$3g8N8>?e|s|CE>@R5k>eoHOwvzQ{+WiL&j0`b07*qoM6N<$f>GF4H2?qr literal 0 HcmV?d00001 diff --git a/src/common/tex/body_v.png b/src/common/tex/body_v.png new file mode 100644 index 0000000000000000000000000000000000000000..f43235bfceda41f0e45a89b40e96c123ae04624c GIT binary patch literal 727 zcmV;|0x127P)NklDF!Bm{n!G`Z^pZncpg|*` zO_R6^VoR>1Bug$Q2U&__$2Kw-a=Dz3L;i+CVQxn#4@teB4t{9uu05)?Yll=O2m)Yc zvw4PzR|}Kyboz4q;P~n8S{mp~C(1wPptTgZD?A_X<)9MgI#FAmjNCEXt z7d8*_Vxw5Q57;>a&ytB~R|Ko7B)4~NQ*0HO89Vt7{a1Ykrvtot5FH^Qz2axO-r(E#B*_g2OD zDxe6U!r9+vgmIvmH)(CRNHKjq(3lW>f=_zUtoIi0=EjnGSJqh6UFzh~4R z;WCG7IK&Ajf1D7e0dd#w`;u%T@NxJN6vPBehB(2ZQT|^dDxWLQOmX{i?3 ztnZtC(ZA^P#Rx_4DVnu++GKe~d>^rBR39qV_t&X+>ZF*KZ;P3&2DS2G#?Y!HzM;1L z{Whn6o)X4^Y%8PtT^CEXNF(RP%n-9QTnTEqofp_!hrL2;l2ldA3=vsX_nHDbkI*QG5UZ literal 0 HcmV?d00001 diff --git a/src/common/tex/corner_en.png b/src/common/tex/corner_en.png new file mode 100644 index 0000000000000000000000000000000000000000..3bb5503319d18c22e5e7b66a7ed533159b259cbc GIT binary patch literal 306 zcmV-20nPr2P)ZuTM51Bo!nq08_(Q76D=;k^+V;wB1HQAaG3}1Xe*Zn4tm$KY~mcC}lcT z4DVeG2;?+RAbJ<@<;%I}aN~GW5%44=Yyn&V{|_+eP5_~$3ksaxmxhD~0|E2oBFUliLqVfrCKjeHH-_T@Zcv zm92KKx!UZu)?1$PlWa{>0iftZ5yvJwOMB&Gxtm|VimECNYn%QSTQMBEs1w=jA|1>g zik_JtVH8KT$4)jtEQOw6f=yhwAYXBu5P=;v4=4Q4v9^yOv@DEAi7jFZ!b@5DI$tr zWW4DewtW$uc=NgvHV&@czhQ&ssb8-h+UKmOf&u8)Vq^J9OzS2O`%zEV{r&7A%|%52 zJpKjf5QRgAGPGeo>gm8g$R4v?gvuCz7DtZYw$7VpI`BJ_U;w&EtlhcZ?Rs;%laA7f zFaW){xpWND$VN%|mz{l#T38JlMQTebUh`Y9w55aTU;qk1riJn8Q)b`{{3qZGea2nu T`7RdD00000NkvXXu0mjffVz%Q literal 0 HcmV?d00001 diff --git a/src/common/tex/corner_nw.png b/src/common/tex/corner_nw.png new file mode 100644 index 0000000000000000000000000000000000000000..9382c872d3ab7280e8bfa24228e4a0cfa5731e81 GIT binary patch literal 237 zcmV8WN5x$ zbZ~gWCH4pZ;BSJRV~}RlvKt8?z}_#lbOI8K#-xVJMEjE`6N|>2DmIz+ietQ-R7flu zbFTOn>s=nk8z?157rwDXZ}Tu(K`DtKu%ilzsr{5i>mCEp4F?6R$tqc<+ACf&dkljf ny90>H*4Kw96YUp=ZwvsQccNPNU`U9t00000NkvXXu0mjf6?8K7yytU zq27nNoRb^=vUeYMQnz{0)L4xeQKTz_pQu#5Ov!nkog10IB+!k&eAT6|g`_ ssvrpBrUsLMAc#vr zaHn>WViqL<|1LzSSPW8%4K+ziGm>buCT1p+%;e2`_qdoS4VguyJ#aXSd-?A7eVlVH z0QC1;t~E9=gh33$`0bDM_GY$a9~VQ>dw*3J#u(I7t}oTTT>NHgib0G3bU$~`K)$Da zu&}e}#2Qe&I#wcR>Z#0@lWJ*Ycy9FU+tT%P-wU1lx<2W+qliy^0`fn#D#jZ3_e_~T zCl+3G*_*OMg`T`m;}kp&wz4x~5D`=ruUqX%)6^Gw@~q5ehg_HscZmqPHc;LCtYX9nVLsgDR8?uAQ$&c<27z%r+xsLv+qyY3|1+oNCdjx5X_Cm7So8G#-XOL@92(B2utl<>1UBH0wMEYUHc7s{ox{%` zrc^5N;KL7b@80|Pdgc_-dcx;-XF2rpjl6NNgOU4l%w}2u{u|C%KKSWlDsh$Jw}zSc z;YY?sPV&q01x`$kQ4De{W}qGj#hRy3_k`L6+|tQjJz1Aw@~>%L8GoIz>91(NYdarJ zk8yV+Yb20F0`p&*z$Q3J z)Nw^MiMisA1d`a$no6l^YL{z6Zeje~aTnR0ak+lY>uX{B-0|zaV4Ek`@eB9|{ T)6L1300000NkvXXu0mjfM2nk= literal 0 HcmV?d00001 diff --git a/src/common/tex/head_n.png b/src/common/tex/head_n.png new file mode 100644 index 0000000000000000000000000000000000000000..89772e89e4a81ef9c67da175abf569dd7b2c436e GIT binary patch literal 918 zcmV;H18Mw;P)KZtFNorccrxIFQVYf? zSoB5l#*CmKN-se(eba!b*c3!6=u4y;lXQ}~oU`}xA(@MlM*p|7_g>%m*IIiop-WLI z^2&S5cI3#|9xofd>{2(3QxF6IMa2^p-^^>`Ui*62tR)5U1b<5(NwxOY%&w`&Td^IW#s(?6kz->!yiDJ$rdFMGDk6q+ zNl{Q64)y!Zh3fds=+v97>UzUE@x2R99ijNV zbi)CbfBTDl`yb+~>1m$3_Cc=7ZKrMyV96ogbQ174O3Gv{$^`ZyoF_df5IOCZ7p zxgippZr-HclocfqpM|8#cMCtWf8WDcO}TNV!qV82GXBHOF{)z z5Iixw@Z&rDwE8OtMt5`Z{5=BVsziL>NqQ)AV%3V{BH5O|VfswP#e8&6Y?n}-2 zzV8>p&<`_#=jTFW`aGAe)IY8@tGmCwXKJ}fQIVc`*i~df(2*~nxWsG?ug?Ej_g;5>h^U&OS?t+MJ;B+f0MgKc(h-1@ws@>ZBhQ_gpCgy}P sJpamR1MvBtew~{b(_v0(YeM(xA6Z3s{C|aZ9RL6T07*qoM6N<$f_ZqyjsO4v literal 0 HcmV?d00001 diff --git a/src/common/tex/head_s.png b/src/common/tex/head_s.png new file mode 100644 index 0000000000000000000000000000000000000000..6b97cccf93f12d09e4279e12890c7e18532d1671 GIT binary patch literal 913 zcmV;C18)3@P)57m6Q?H;A7I(I6$ZAm~PTdD4wo#o7c4 zMWNb-@mS=X!y;u^Td>&Z4{<-t6{@*0eQxMSl@O1$Q2m~n~o4!>I zr$$Q`LcMSQ(vDKwI4WJ~b|z*SO09+V z@#BB+g(OL8QItesY?Efh+_uW(GtXvIg@ql*ri>>e^S`p84eS*oiluNtLowakUY( zQd%ZXRrQ^JwN#ifMu7|nnurt;D5_nKoZ;;Dtb=!b^4Ses)u#{w(!i8hW&vpxM|96` zHN;5_5e9VsoQTMY>md`V=gLQSmx4-QAwmWs6cE|+(cL8(;_Vz-6^oS{D2KcD?Z?Q) zG*%{Gf&5zqm;s{hIEiB`CX?D4#^D117TuttdJ3%*(Uok06~^BCCvjiy4jekMfO8`w z$dt!`Vi8D^tI*D#kVsFy>azhtED5%l0v_Blio-`f!)vcjVK$$~@Z85p-#rR$wE)#u z2GUA41PTO(H0rj6^WiFXj!)qEmtMfjZUqp*Kg}u}Z6G2T^wf0$QivqMJ3k*naodeJ za`0Kan?Cuxx(9 z{lLz)vk3Agi&}gMf7hxoN~7T92Es^haC(V}$cWQx0Sm(+gOuYyAe$=NaLOu_7(nyd zL+^qH4uJq+v6i`h6-Xqm%1)cH(=b;^LF`1jhytv`tf1(h$Y z0)PNZ9SQOgw-b<2T4WQC0wI7+A)^3%xLon0N>EXNbS#t4o_Oly&bd=@E1D!nL#K_} za6befP2?1_CR%MATpT|z=ZU9+N?`kX=#$UxysN)|Z1xj-x&0&rOHhP9s6a>)lTj;4 nqy5(6qX%C1JWu)L_j3LN+CiE=gN-CN00000NkvXXu0mjf8EvTH literal 0 HcmV?d00001 diff --git a/src/common/tex/head_w.png b/src/common/tex/head_w.png new file mode 100644 index 0000000000000000000000000000000000000000..2878d2970e9730da49d0353f0f3b30a724affb10 GIT binary patch literal 731 zcmV<10wn#3P)7j`}bRG?X>~meG#b%_u>^eRrxv z@X}acY!@5XI%k(I{Blml-+c1a*fZnj_Z@3ur4=Cm)2pLFWUGsF2puBe9?N z9YIb!_BxN$$GNvF5{g_k} zNQJVK6RHt+hv8h&>9@|%8aqyI1*MggR`T}bYrJvz6*lBH-5=LjI`=a-=dL4BcLJg> zRYtLVK-5Uk00QR5t4CkN|1oZTv4j^+=i&;z#SWT})yx@&igMOY9RVl2bnpB7yusd(seADJIWh$)x zuuAN$_x>0SqldDP;pQqLxErP2ZPZrHwrj3p?#^Ii@TYcfbZ6R&9hF3sNa2BksD&n4 z$gYkQAq~2IZ9}^^x-)TkZhkgq;ozX%yXW<_JU2gk-xus==01J_{{psdMa@NE@ksyx N002ovPDHLkV1fZQYtjG! literal 0 HcmV?d00001 diff --git a/src/common/tex/tail_e.png b/src/common/tex/tail_e.png new file mode 100644 index 0000000000000000000000000000000000000000..1cbeb5241f16cad763ba2486f0534b73b688c1c9 GIT binary patch literal 644 zcmV-~0(Ge2mJI=vLVB0Bm$YA{!&^!UQKqg18`& zblT3`dwv&F2ipQs!`T3ARC(Y_hqJE!m1O8d*D9q+u@iC017 z(VJ%zcM=2*pv5R@nmU&_1VpG)z<^l996$!LPvStG+lTkPWp@GjZ3qA$2qzA!ORH$N z+7uBEh68^6{*|NgP@VTGPN9hcB4D%%BFGS}N5Myb@!W!F5o`C?u(G&tcCN^f4*)UASW|l0I?h#28qIw~Y zmRX2(uq8>Sg6&DhO6)4JQy>yyR7MMSjA(sMT2KC_^{yQbKSQ>A$ppG~INbR7x>o!j e0~R;^3-|?*-K*c4LMadu#eM;rbnp?@`3iJ2e1Q~c zC?XU^mS|`qQQ1WbFe!l0_|LA_yKm+m3VYYH-n?#R?)km@=gcLpot}F3lu!M$pT7A# zevo~cPDc)-0RUW{Ef-dvq+fph_W7$xY+WC(1NX5{01*9qYs9ZDRiIt)xF^PQFD0( zW^I)U5F^PzGC+(f6=2prYA&yU5PGwWnTPIHmjDo$9>pRq%2k22DhSF|#Ud^Srbl(n zdx7t6z9Rsz(nAIU;7mD}R51{&^sugZFR;_;_@(B7R}Mh)J%;F&1Jr{V&9sPV90)=? zKYq2ikjDedt?kKuuFrs5X69r6Yd9`9UqAWzY@(gXu(#VI02IO27`f`nMbMqTZMrSzu`w+wPA%X>nxOAp>06eb`W}0EA({c1*gl(5)iH|iMHeK4FI$QR8bzwsu^vzuEVZz{`jN`fE+1|0l(fRn)qf(0>>UiyCv zqrt|@CohjD3FvL7(Ld`Wiy2ax zaQM$5QkkY%f)tIQ%I!I0OJZlY6RP#*Z&qYGKC0a17A#>UTBdN1X}l5 z_rg&u?phv}ymGH^m}n8?ghoaEIEt20MLEbHJff2Vkcz5t}Ew2*FJ_U7>S+9 a&+$KYlyacYYM5aF00006~_6vuz(+$+0kWkN!2h`Tf?AyYm<3w9wxw@%eqC>^@CpTOu7v}=db58$a=2a!n7 zjwXpi1;*f#5JIsBCrFOu_wKv@4o|Wr*+b6sc=w(EIrslN=h@cdt=RvvpFaQn>mOHI zt3MaR^UB-;?-9zXR@PQSF*^V0@9+NjW$W=)^o^4Z0Z8msWtB1PeZ&ATtSYN4u?Mhm zvLQnlhQ9dit99ZBhh}y|m4so2k(DvKriPFOGf8hrlY4)C|K|z997B>Uyd0mBbz}_( zBuS*4h%q9Gth}rzYdB;bg_q+~NroW@Xz#YY-{~h{-(w&{4j#Ds$z2|N_JI4J-lw_R zB4Bw3uLnP-+@IX-%oK4PZ zQC$z@QV?h}1Ec3523{S%%AJugWTA!v1`2eRMgghM|YQKD;qr{pir6ud9^I%;rZ0A#0fBluw%HXZN~4bl=W$ z7dphyj&{8Jx;r#+;4vVS7sP74>@i^Cp!>Qz+|iCRHxWMnaj(~NI%ME!y#=&<}ZS|HbYIACB?Fnbo3!I9mor1L#@@9QYzR}Hb7sD|eBT`Le^{XDPgBj9nf`;jm`*jhyQBSg_OT{LNr(ur{`9yr)Z?9f;*8lP8n3SJ@Va-3Q_2 z0dNoLJ`r1~UnNbaQ=2`S*>AH)&f~{^5&g?wf7tN0zJtnVr1a3;Zz~w5Yk6SVh!S{P z@Xm>dvXbHFPbN3=N&BIQPs8kE&>cM9x5}<78(s>D1SP2J+L1g-LI@;4`r?=GzOtUa zj@Cbk;vZQ*->_^D5XJG<@#1b~erxlV)<^3ynGR15_2Q2g%$qqTJ4-4@__x*DZ~fT5 znuEaW&P%OD=@F7-9&Y1z$98k$CQa7>uyK8Zey`8Y^Bp42+iamYY!8~Q)Lj!@1jqzg zkR_#vP_OE-K3b<~o5hF%nzmukA21q>C|(v-17^r1GLX(rmq{nlbsfj^W2#&c5d=X_ znYL>=o*!c};*<>eG8ziuIJI*hAqeWGW_NFwx~)l;&U@&DThIYcU>CCt0+Vqv zKjGQ)XAB2JOvcgth_-82f&pZOhSjK-aVLrs#XVRW3NnPu7Lz
-
-
-
+ + +
GAME OVER {{ scoreText }} @@ -88,19 +88,20 @@ export default { if (this.over) this.restart() }, rebuild() { + const pieces = snake.renderPieces(this.model, this.dir) const out = [] for (let i = 0; i < this.model.length; i++) { const s = this.model[i] out.push({ - left: (OFFSET + s.x * CELL + 1) + 'px', - top: (OFFSET + s.y * CELL + 1) + 'px', - cls: i === 0 ? 'seg head' : 'seg' + left: (OFFSET + s.x * CELL) + 'px', + top: (OFFSET + s.y * CELL) + 'px', + src: '/common/tex/' + pieces[i] + '.png' }) } this.snake = out this.food = { - left: (OFFSET + this.foodModel.x * CELL + 3) + 'px', - top: (OFFSET + this.foodModel.y * CELL + 3) + 'px' + left: (OFFSET + this.foodModel.x * CELL) + 'px', + top: (OFFSET + this.foodModel.y * CELL) + 'px' } this.scoreText = 'SCORE ' + (this.model.length - 3) } @@ -117,27 +118,16 @@ export default { width: 466px; height: 466px; } .board { - position: absolute; left: 71px; top: 71px; + position: absolute; left: 73px; top: 73px; width: 320px; height: 320px; - border-radius: 26px; - border-width: 2px; - border-color: #2a3040; - background-color: #0a0f1a; } .seg { position: absolute; - width: 18px; height: 18px; - border-radius: 4px; - background-color: #35c26a; -} -.seg.head { - background-color: #7cf0a0; + width: 20px; height: 20px; } .food { position: absolute; - width: 14px; height: 14px; - border-radius: 7px; - background-color: #ff5a5a; + width: 20px; height: 20px; } .score { position: absolute; left: 0px; top: 24px; diff --git a/tests/make_tex.py b/tests/make_tex.py new file mode 100644 index 0000000..43669fe --- /dev/null +++ b/tests/make_tex.py @@ -0,0 +1,63 @@ +# Bake snake textures. Working resolution 80px, saved at 20px (cell size). +# Base tiles: body, head (eyes), apple, plus rotated variants written into src/common/tex/. +import os +from PIL import Image, ImageDraw + +TILE = 80 +OUT = os.path.expanduser('~/vela-snake/src/common/tex') +os.makedirs(OUT, exist_ok=True) + +def rr(d, box, rad, fill): + d.rounded_rectangle(box, radius=rad, fill=fill) + +def body(): + W = TILE + img = Image.new('RGBA', (W, W), (0, 0, 0, 0)) + d = ImageDraw.Draw(img) + rr(d, (0, 10, W, W - 10), 14, (60, 183, 90, 255)) + for cx, cy in ((18, 26), (W - 30, W - 38)): + d.ellipse((cx - 14, cy - 14, cx + 14, cy + 14), fill=(46, 160, 74, 255)) + d.ellipse((cx - 6, cy - 6, cx + 6, cy + 6), fill=(60, 183, 90, 255)) + return img + +def head(): + W = TILE + img = Image.new('RGBA', (W, W), (0, 0, 0, 0)) + d = ImageDraw.Draw(img) + rr(d, (10, 12, W, W - 12), 22, (64, 188, 110, 255)) + d.ellipse((W - 52, 24, W - 32, 44), fill=(255, 255, 255, 255)) + d.ellipse((W - 46, 30, W - 38, 38), fill=(30, 30, 30, 255)) + return img + +def apple(): + W = TILE + img = Image.new('RGBA', (W, W), (0, 0, 0, 0)) + d = ImageDraw.Draw(img) + d.ellipse((8, 14, W - 8, W - 6), fill=(224, 62, 62, 255)) + d.ellipse((16, 24, 34, 42), fill=(255, 255, 255, 70)) + d.polygon(((W // 2 - 4, 12), (W // 2 + 18, 4), (W // 2 + 10, 16)), fill=(56, 128, 56, 255)) + d.rectangle((W // 2 - 3, 10, W // 2 + 1, 18), fill=(90, 60, 30, 255)) + return img + +def head_open(): + W = TILE + img = head() + d = ImageDraw.Draw(img) + d.rectangle((0, 0, 10, W), fill=(0, 0, 0, 0)) + d.rectangle((10, 36, 34, 44), fill=(226, 62, 62, 255)) + d.polygon(((10, 36), (26, 28), (30, 40)), fill=(226, 62, 62, 255)) + return img + +def save20(img, name): + img.resize((20, 20), Image.LANCZOS).save(os.path.join(OUT, name)) + +# base tiles: body + head drawn mouth-E (E = right); rotated variants cover all directions +b = body() +save20(b, 'body.png') # 4-way base body tile +h = head_open() # head with tongue + eyes, W edge open +save20(h, 'head_e.png') # facing east +save20(h.transpose(Image.FLIP_LEFT_RIGHT), 'head_w.png') +save20(h.transpose(Image.ROTATE_90), 'head_n.png') # E->N under ccw +save20(h.transpose(Image.ROTATE_90).transpose(Image.FLIP_TOP_BOTTOM), 'head_s.png') +save20(apple(), 'apple.png') +print('base tiles done') diff --git a/tests/page.cjs b/tests/page.cjs index ffb5c3e..e1cae74 100644 --- a/tests/page.cjs +++ b/tests/page.cjs @@ -25,10 +25,11 @@ function mk() { let c = mk(); assert.equal(c.model.length, 3, 'initialized to length 3'); assert.equal(c.snake.length, 3, 'rendered 3 segments'); -assert.equal(c.snake[0].cls, 'seg head', 'head flagged'); -assert.equal(c.snake[1].cls, 'seg', 'body flagged'); +assert.match(c.snake[0].src, /head_e\.png$/, 'head texture'); +assert.match(c.snake[1].src, /body_h\.png$/, 'body texture'); +assert.match(c.snake[2].src, /tail_w\.png$/, 'tail texture'); assert.equal(c.scoreText, 'SCORE 0'); -assert.equal(c.snake[0].left, (73 + 6 * 20 + 1) + 'px', 'head x mapped to px'); +assert.equal(c.snake[0].left, (73 + 6 * 20) + 'px', 'head x mapped to px'); for (let i = 0; i < 5; i++) c.tick(); assert.equal(c.model[0].x, 11, 'head moved right 5 steps from x=6'); @@ -39,6 +40,7 @@ c.tick(); assert.equal(c.dir.x, 0, 'dir x'); assert.equal(c.dir.y, 1, 'direction turned down'); assert.equal(c.model[0].y, 8, 'head moved down'); +assert.match(c.snake[0].src, /head_s\.png$/, 'head faces south after turn'); c.onSwipe({ direction: 'up' }); c.tick(); @@ -57,11 +59,13 @@ assert.equal(c.over, false); assert.equal(c.model.length, 3); assert.equal(c.scoreText, 'SCORE 0'); -// template uses documented directives/events, no transforms, no crown remnants +// template uses image components for textures + documented directives/events +assert.match(ux, //); +assert.match(ux, / array of texture names, one per segment (head first). +const assert = require('assert/strict') +const fs = require('fs') +const vm = require('vm') + +const src = fs.readFileSync(__dirname + '/../src/common/snake.js', 'utf8') +const sandbox = { module: { exports: {} } } +vm.runInNewContext(src, sandbox) +const snake = sandbox.module.exports + +// compass sides: e/w/n/s (screen coords, y grows downward) +function pieces(list, dir) { return snake.renderPieces(list, dir) } + +// -- initial snake: head east, horizontal body, tail pointing west +assert.deepEqual( + pieces([{ x: 6, y: 7 }, { x: 5, y: 7 }, { x: 4, y: 7 }], { x: 1, y: 0 }), + ['head_e', 'body_h', 'tail_w']) + +// -- vertical snake moving south +assert.deepEqual( + pieces([{ x: 3, y: 5 }, { x: 3, y: 4 }, { x: 3, y: 4 + 1 }], { x: 0, y: 1 }) + .map((p, i) => i), // shape check below with explicit list instead + [0, 1, 2]) +assert.deepEqual( + pieces([{ x: 3, y: 5 }, { x: 3, y: 6 }, { x: 3, y: 7 }], { x: 0, y: 1 }), + ['head_s', 'body_v', 'tail_s']) + +// -- corner: head turned from east to south +// snake [(6,8),(6,7),(5,7)]: neck connects south (to head) and west (to rest); +// tail cell (5,7)'s neighbor is east, so its tip points west +assert.deepEqual( + pieces([{ x: 6, y: 8 }, { x: 6, y: 7 }, { x: 5, y: 7 }], { x: 0, y: 1 }), + ['head_s', 'corner_sw', 'tail_w']) + +// -- corner: head turned from east to north +assert.deepEqual( + pieces([{ x: 6, y: 6 }, { x: 6, y: 7 }, { x: 5, y: 7 }], { x: 0, y: -1 }), + ['head_n', 'corner_nw', 'tail_w']) + +// -- corner heading west turning south (es corner mirrored) +// tail cell (5,7)'s neighbor is west, so its tip points east +assert.deepEqual( + pieces([{ x: 4, y: 8 }, { x: 4, y: 7 }, { x: 5, y: 7 }], { x: -1, y: 0 }), + ['head_w', 'corner_es', 'tail_e']) + +// -- double corner (S shape): head moving south, west, then north — +// hand-verified: every middle cell of this tight zigzag is a corner +// [(6,8) head][ (6,7) sw ][ (5,7) en ][ (5,6) sw ][ (4,6) en ][ (4,5) tail ] +assert.deepEqual( + pieces([{ x: 6, y: 8 }, { x: 6, y: 7 }, { x: 5, y: 7 }, { x: 5, y: 6 }, { x: 4, y: 6 }, { x: 4, y: 5 }], { x: 0, y: 1 }), + ['head_s', 'corner_sw', 'corner_en', 'corner_sw', 'corner_en', 'tail_n']) + +// -- every returned piece maps to a texture that must exist +const TEX = ['head_e', 'head_w', 'head_n', 'head_s', + 'body_h', 'body_v', + 'corner_en', 'corner_es', 'corner_nw', 'corner_sw', + 'tail_e', 'tail_w', 'tail_n', 'tail_s'] +const dirTex = __dirname + '/../src/common/tex' +for (const p of TEX) { + assert.ok(fs.existsSync(dirTex + '/' + p + '.png'), 'missing texture ' + p) +} + +// -- food + board textures +assert.ok(fs.existsSync(dirTex + '/apple.png'), 'missing apple texture') +assert.ok(fs.existsSync(dirTex + '/board.png'), 'missing board texture') + +console.log('PIECE CLASSIFICATION PASS') diff --git a/tests/tex-montage.png b/tests/tex-montage.png new file mode 100644 index 0000000000000000000000000000000000000000..1135c15a966c4fb0a47bb21ef4a92a31cf4a3e3d GIT binary patch literal 14091 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 Px%736@0Z+t{Nn!rFEAX= literal 0 HcmV?d00001