From a5a660b32f178b94bcb61f2ee9e729da003fddd3 Mon Sep 17 00:00:00 2001 From: claude Date: Fri, 23 Oct 2026 19:36:45 +0200 Subject: [PATCH] =?UTF-8?q?M2:=20t=C3=A5g=20med=20tidtabell,=20sp=C3=A4rra?= =?UTF-8?q?r=20med=20kort,=20skyltar,=20vinst/f=C3=B6rlust?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - level: Schedule (ankomst/intervall/uppehåll) med Status(t), spårnummer, avgift, skylttexter per i-ruta, Path (BFS) och UngatedPlatforms – regeln att perronger bara nås via spärrar testas för alla banor - sim.Run: klocka, tågkort (saldo dras vid spärr, tomt kort stoppas), ombordstigning med Enter när tåget är inne, vinst/förlust; headless bot-test klarar bana 001 och förlorar på fel tåg - game: perronginfo (avgår om / nästa tåg om), skyltruta, nekad spärr, tågvagn som ritas när tåget är inne, inforutan flyttas upp när spelaren står nära nederkanten; resultat med spärrpassager och saldo - bana 001 ombyggd så spärrarna är flaskhalsar; bana 002 omgenererad med spärrar runt perrongerna och skylttexter - tools/shots räknar ut vägar med BFS; skriptkommandot waittrain - docs/README/wiki uppdaterade med nya bilder Co-Authored-By: Claude Fable 5.1 --- README.md | 18 +- doc/img/level-001.png | Bin 16935 -> 17313 bytes doc/img/level-002.png | Bin 16769 -> 17199 bytes doc/img/menu.png | Bin 14569 -> 14550 bytes doc/img/platform.png | Bin 0 -> 20271 bytes doc/img/result.png | Bin 22149 -> 23863 bytes doc/img/sign.png | Bin 0 -> 21358 bytes doc/mal-och-tester.md | 8 +- doc/plan.md | 2 +- doc/roadmap.md | 31 ++-- internal/game/assets.go | 11 +- internal/game/scene_level.go | 134 +++++++++++---- internal/game/scene_result.go | 56 ++++--- internal/game/script.go | 10 +- internal/game/text.go | 27 +++ internal/level/level.go | 173 ++++++++++++++++++-- internal/level/level_test.go | 114 ++++++++++++- internal/sim/run.go | 138 ++++++++++++++++ internal/sim/run_test.go | 182 +++++++++++++++++++++ resources/data/levels/001-flygplatsen.json | 78 +++++++-- resources/data/levels/002-shinjuku.json | 58 ++++++- resources/sprites/tiles.png | Bin 599 -> 692 bytes resources/text/en.json | 12 +- resources/text/sv.json | 12 +- tools/genart/main.go | 15 +- tools/shots/main.go | 87 ++++++++-- 26 files changed, 1031 insertions(+), 135 deletions(-) create mode 100644 doc/img/platform.png create mode 100644 doc/img/sign.png create mode 100644 internal/game/text.go create mode 100644 internal/sim/run.go create mode 100644 internal/sim/run_test.go diff --git a/README.md b/README.md index 6760975..f53c8fe 100644 --- a/README.md +++ b/README.md @@ -10,19 +10,21 @@ Storyn följer några polare på semester i Japan, från landningen på flygplatsen till avresan. Efter storyn låses ett oändligt läge och New Game+ upp. Hela speldesignen finns i [doc/spelbeskrivning.md](doc/spelbeskrivning.md). -**Status:** M1 klar (2026-10-23) – man kan gå runt i labyrinterna med kamera -som följer, välja bana i menyn och nå perrongen. Nästa är M2: tåg med -tidtabell, spärrar och första riktigt spelbara banan. Se +**Status:** M2 klar (2026-10-23) – de två första banorna går att spela på +riktigt: labyrint, skyltar, spärrar som drar från tågkortet, tåg som kommer +och går enligt tidtabell, och vinst eller förlust beroende på vilket tåg du +kliver på. Nästa är M3: japanska skyltar och språksystemet. Se [doc/roadmap.md](doc/roadmap.md). ![Bana 001 – Flygplatsen](doc/img/level-001.png) -Så fungerar det: du startar på den gröna dörrmattan, letar dig genom -stationen förbi skyltar, automater och spärrar, och ställer dig på en -perrong (gul ledlinje). Rätt perrong tar dig till resmålet, fel perrong ger -fel tåg. +Så fungerar det: du startar på den gröna dörrmattan och letar dig genom +stationen. Skyltar visar vilket spår som går vart. Spärrarna släpper bara +igenom dig om kortet har saldo. På perrongen (gul ledlinje) ser du vilket +tåg som går, och när. Står tåget inne trycker du Enter för att kliva på. +Rätt tåg tar dig till resmålet, fel tåg avslutar banan med förlust. -![Resultat – rätt tåg](doc/img/result.png) +![Perrongen – tåget är inne](doc/img/platform.png) Fler bilder och en genomgång finns i [wikin](https://gitea.brasse-pc.eu/brasse/metro-puzzle-jp/wiki). diff --git a/doc/img/level-001.png b/doc/img/level-001.png index 7071982c12f66cabf578bc87c6273908329aa16c..c9df6fdbdb6f014d8410633628623156b5cfa5f2 100644 GIT binary patch literal 17313 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 diff --git a/doc/img/level-002.png b/doc/img/level-002.png index 55063cd7d96f35bfc1c1f29800e46c64f29f2996..216f6ef20ed99cf0076d2b453e0ad9124602640f 100644 GIT binary patch delta 9675 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 Txi-MW_&agj+PKQ_Lj3;#$NPIS delta 9044 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 YZrdNn?KJgNV`F{Hj#-(M7+;M1AE51J)c^nh diff --git a/doc/img/menu.png b/doc/img/menu.png index c8483a7f9dd7d94796166fa13d6f4aa6d5c5494c..f6a73fa32fc0d99afe02fc61d1d48999c9c686ff 100644 GIT binary patch delta 1153 zcmaD^c&%`P3gg_3sz;2%-j%LtYqaPxFX{MR@1^D9&-iESt&4qcVorPJ)jejr`$Jo; zwe*=mfQ5#P2p<=FYxAMP*jDF`j}=8p8b8ne%Af!AwB57QHJ|f8pUpm>v#;|0AM4uB zB7Bo=jkN{y*A$${uRD{kredXPbzk9P=VU_@F*D^09A6f5ZsEoTzZ_xxb^E_PhvGGN zt(+wr+%C)M<@^yfTjs|z*^pP-PpWaBRbyDIvnXGvMaaVdG=|^WLlb%4ctp=zYCG+} ztyxyqvx(Q%#66GPcXi_S8#`aDT|Do`#{Z59S5K~=m7ad5@ciV1JaR$lOl!H?wPaQm ztT?m~jWH*xb@8(`*?)fh{Tw`B{5AiTocA98zTa4}vf`u3tGY|-i$7&JueSElvEOye zOlRZgBYC#x&y^jwoy;gEI@!U5-A(zze-0C1fH^)~P=FMEjt?zDA9~!L@N~Vf+U?o# zS@V+@pR?_KWAORP+3Tuh$IktYc``LS`rl2PolklEH%^@#C?&<}wm|TO>107u$>5An zw|u$m=4NLL?NGM-`(@?+o*u9H$<{eZDbl_&(xHnh*RKpbtGJ~2R}atn+DVt?B}3(X zzgk{f_Rnu~@5*aES#itdVvV)DO@z&AKdso7b|`dt7bsRh4rYpPnykknR-dgQW7=pv z>FwFYOR}@S?c}>Ju(~YbdFHPxGlfelWUsokrC&Kw-FwD(QhL7F*{I5p=|4XHJw5aF zBIVzbFE`uFa?bFp%L@6m#MJ$IkCf&7FDpMkxp*Rq!-T2V0T}fVs$A_t{f*ttB0Ccm zxDG$zi+sYg^S|h3uUt7Bm74n#F6QmaK=hw@HlOM8(O!hJsucsYZ*Okw|O}wqSTjT2#_jr?Yesy)`VPW^Ic1_x98uo3g z5j!aHut+ujOPajSL`>!0!UCT)F%Rx-ec-L6J%{^ypvb$ZpE>8e7ftzfM=$J2`*-(^ zRiIFxJi(NWQ5P8MV%C3F94ef?dc&qqe957)iA-W1_Nxlc)V@BYnqL>R zt~T8-O5o=uqsn7p*N$?Xw`a4F(J4u#$%Ra^_4(=-q%wH7uthPiZM^1?y}%U0ty<=;6Hnfn6|>3fm(M)+#p!eA*37%H#->;x0~nCNP#sMV#(1 ZX9(A8pQQ3o;x_{jc)I$ztaD0e0s#5FB?15d delta 1165 zcmcas__A<<3gd>2sz;1M`bzJ%B{FpB-R=7KKfFw9S9yjvi=soorbp?~?onM4Q$(&d z7gWkSKU`1{V4)!+!pC*IRFu#6_4c{K&J*vSssF7L|8183=X3VY=3f8)=3eFdKa=lO zPPRAJ78dDa*OvJ-d;QO|8A5Y~=JI=#C`~S8k~H3H+E|7a%I$Ed|NWoe_wu#vD>ewW zns)8mx#45@4GAm5$s4$3-G04di4x@twFr3_(CUoFxUr1OJ*C=u?+K3er==(|!ZdyQV@_sED`+fXN zjw-j$D?YM%(=q3tzji+P2Q)NHXVTH(5Gl3Tr-DMk}P*DZV;0kN8&U=+xKbf^f35M3pttoL0(Fyd4=;?IdDK5OuDimU}5Xj{E4xL<; zxMDfWV^3TP)K^uAJaoMhAb%_IVsXgw(nU$_bLH+odhq?!=aNrvjxO35W^>A0WdH2R z;%|YLr&Vh6EwuQ#aD~L>pC=b8ttz%NR68%+ZQ=Mjkm1&6m+Jri zOK+`GU3H~n^R+q2=WcmfTK8`bU1irj>tMS2U90Oy=P4c3nYCQww`RZGmGgOZ|2)Ie z?(NzowDoHExvfTQlMQ*LS)>~43?@%x64T5NwFt7Gp_N<5UcYq5{!4cI|3!1Xdn(R( zZjoFFuYUC2_aaZj2BelyX zD@w~{=Br--W*0aKl5i*+_qhxj-6e+{>{$$cZ%hfZ<)HH=*iR-HL8n0ZJzu#NBZU| zSx~l;t)K6=K=p!92Im%*C??&;X%5~H?iV+vzi*F!pI`rf-|AGxa<7d`EoKKw&U>q) zb$C15^nagR_1$vM##HG=Ejsm}X3N#0i!%$ZGzoHUIzQv5=j86+n%+}#r#wzMFFS3; z+JmaATFyVHG^sjv^X&89vuBLi8r>EMW&i^mNKLlk6)*t?JrBZaWS2;Dewok6@Gqj~ U4|~&gV+J7bboFyt=akR{0GH<}y#N3J diff --git a/doc/img/platform.png b/doc/img/platform.png new file mode 100644 index 0000000000000000000000000000000000000000..3cc3bdd9e60bdaa727b102230268ec2a0ed40f7a GIT binary patch literal 20271 zcmd^m2Rxih8!sVR)Cfrsgd{oj*LiDy9EqaSymyl>d^v;K{`s&?^ zx)NozwODJf)4ng~J919Wcklh~{oVWi=V#s--kE2fGV{zcBly0$5(OCp86F-Ug^IF* zCLSKaKk)F*Fp-=&si_3s=f%TQ-&0YLd+3(BmIkw?mAe4dQDJ=cPT5w=#_HL~F6&uE zed!godXnP`--}zziac-c6B?9gszt?naFX;!hFscfkY|>}uuHDl9Uk;ujhu0CN$q^v zN6*$zD;t~pZfMzi>z6OVE-j6a$zC{ zf^eVq|M}ty_vMwl);tF1HTt%JE@)V?++Mlo{R9$ z9M!41?*AvP{L+UI&8b64YmptZKrCO27~9yeCS9ngIZ$Zn0Nbth^z|qwD$Z=^?Ys8m zykFF%M`5s!b3L#jOrgV5t^(J{QJc*>XE0ioTX{}k7kE=_O!Oq&<3=X>I80o|Z>Q;{ zjg$7*CsGm;QJ<#(*5biOg|5&~69O-W|;s~PY@T2bP z!;%#!;1zb;O{(=Mx`D;k?^vJtSzV0uzsZ4PT=O-gZFYgo{g=)2Xjo#}tj88Ba@m^fl z4P&fgp#u8F%m%*X9Y!74a|}X;hH_DMW2N$*CbRqS;A}@z#9g$GRa$*lL!AZ=OxV1& zg&u!#*H{r6HS7NtM9Xxzc@XFiuMmdK2o21{Xb!wtT%JEMfbd$d+-W+9{0l~3cdwX! zpk&fs)z-+0%>B?d5N>l_n+mlK;sZ{ZTnj^HQa*fRef>+OOnzlTzukDKga;t6jq>)J z0c8;rm*nz@^wJ%3S1o|3^P=`z#V%OC!?m@fsYP8b|Dbf}DWY3F#?1SAP42qsn-8p& zKo$Py#Y=#=d|-_J#;jiS$Cf$;)>F);9DD{F*|2jEUO+UzUz$R#UjAIHyuY-cRiE_g zHGgVi{v#a8RR`Ix_#)Q=Zm6oZWd=mOzx|Ix9+bCx%+^#DuoROd?zCVDjD+ruZhMUq z&Eb6)E57#o$X&$6z22j8Lw7<^LSQ*n(Sv^T%NL8^>lD2RuVi>Zc_vm{-#qhnZHN!N zVJGA1ix8$Ixe*8z=tv+}f`M}C>vojgW78XNRvQ-HJu(@7sw3VdKe9m>?3rZ%K3G9X zl|pkY?e|}6);|;*@ci_0|0d;o_d6p`#jkcOd`)|ExdHB%d<*~j`Fp~bkwNDv|awG|#NR+Do{k z{ang;nKkb`@wHesRP%B=l^4JaT;sv~;Bs<0=+^5{)i3kejk*z!qk3|EvWES-hHQ8H ztQ;+lcC~fHqO<&?($blE2hu5|0thyLf|+?U6*c7#BO>lLAbYI1VGKDlf4S4YP3O@k z#izG>yn`WM?VWbDTFI$cJcIP+b7HGMSV!qFYl(5SjYPjXK|c{DBk29N7nnD3@>$_?R`mV6 z8G5p1uPpF;&-_|JcW$L{r>UyYk;8V`)*n2U|xWy^7i@!pi# z)DWZKF4U}2g7>pmNH2tkQMKLRQhXOX|8Ba4was#)vonh*0=4paYWrhFEX)KlZ`3a- zI&MzyE$7&QbhFJ8wBY|R1UdZK0n+ky)9ID~h6-si_(JBKWfN`?K=#^nHcIg|SLD=(`;7*iFWelY zCB!FQWV?}!@Ql!r`UGfJ+;w(0antc&4mQyp;v2m1P@nGEOV8npK6%@jN3{DNi;rKP zi&Nr~aTHk)CVuCU9V6k@xEc*x@{AE+OnPUm%BMYLxh8PJKzX#r$clFQXGU4)8jCxz zU%LQZ6zAGam#5mb9@b4|Q5h#NcRom}6mV@so5Knj>F8xJ>(34VaVZO~T~N@Po(S$S z)!xJLon>`{#>Wl%@3fJZ8}}~$Y;`Awm-+Eqz4(`+)IPz-wnJIGjbwCe%vWCrq=?

ARJ>^_p2lWcDm;UJ-X3csiZCGYq0Q`HPm&urdjx;~ot1n^?Ng z{;(xeQb!bo+V{M;do+ov>W0d(%YJ!@3x(x4EOICfy&9{l$`PDLHVV}63!rw#I2u@) z&XJf>pJ6AgRKNNWArLWz_v+ogB?=k|W+8j&nEZqQ>-cUR|GU%p_a^vpH-EELT#Nu_ z-Rq9imDaF)yrt$ZRM%~}p_oV<)^&42%;bbs>`QJLWON= z&R3f|rc(E-`WWQjd1z8rDnQyn;VwUmS5(_)W<=CEyO!4bf=Q9OO(|IIJoFYPpfZkd zA(7--5Fs%e|1|Z*V@o?j>M_GtpIHea9GD1wSfM2pPaJ^F^)|L>ueAh_cAzcF3Fb65 zyc=uWkjAe|WzJ%0=XC6KP{}3!#*tX$w(r%1(M)`$+$6%^#qQeG{`-Iod)c{H zE%*ThWW*OJIhlT`SP5^*JSuk4AaT)+D&j59d6Gt^8GGM&wl*omnMR{^im<^Dr47ce zmN{zEYo=VNer|Sgw&QW%C#_98dOdktsVYn}2J*i4GCeNUS9E@$ooR#dytjP2HsfqP z03^8WlP21k$=SisE4sYGZ2}|$^3D^T4B(4D{;Dkd!xSdW;&ii~Ayq}4T7+?p7ayHm z#`l3B@p=94}eWv^>>eKf3DJ(&|80sM0MsP4B~vLJ_w@S#q{KB zjJ_NnyXFBV8GI-RR9~4q95e};(1LF;ip!7G`x2`q58Y6zm57`L%uK(!^}wm=sb$(k znnIwLln={cMRM>CTYC+)?S~gRwYB?h4{4+5&xnm)E#5-e8!t8YCFJQYc|QESv(ezg zSa3s^D)b>NAyR3Ko5A!k#bvobUsS(9v`|(oEVJX62YFcK@x1JU1VXT?3DapxrcwgkUw4c^n z@x6m*$SKpE!QMELrxGtx9h+j`QP||lb6+?xy^AqyYK#lfwngoCkd-x0Xs)|#U8-Q4|bkH3C>WQ0_j!~Vf@qJB!|~v?`(cO z&Y*Mt-Ce)g{1tzAcT>89TealN7rp_Zs4J%Q7cXj+XfMw%?yfEIdGt3ZA>7|pR7_PA z%r;;;KUdX)aXZ8H`ByaOK7Te{ml`UTaqDbuRxb?io|rH}TnH#cYtGz%U~#h_8rh`L z=ArgcCv=h%2JZofe4r6_A$D`weL9{QY}GgR(r(5pC%$V8w;FeO7tyc+QW+bq44mj+ zUlWaGIn1<3k7BRQHM!lb7_GFFIx*#@rY^ve*uH?i&9-A~q_65+c&i)RAmFRF@AQ-c zM;1s~)8;D{w3elG2dRZS4xU``uC1+|2=+c|#3cG9r_xP%)-0LFXXr$uiENkUbJh>I z$plWH4RX%;+<0{w_(?J@8XJf26{j2ZMzb#D$*1QgUnM0f=Bi4~;KXl+;`19<-*g-$ zVSoj4x2@rdk%ni?`rf8FreCQ@>bSL%AJ?nXq!-IpHE>VT1d&!lQ~rPqRGKXB_oZuF zk)P}oG5#Xjv`|^vQ*n{7{~T0lW{nZ#@iHC5FOi9UeozeStFV%2cKI4wYxlZR6%0+eBcq!VHbutNfqggY=u1! zcr5QP>z%7OO0tpg{sXz$UB+cA#PQIY`mWs-6=6mPWKdPtUlx5U4n6 zug!=y4mL1uNSF4WT=y8)eE-yYL&MKNoCb5sYVw=GN4V$MAN9hqg?A_v9GEO|!y7^m zH}C9@O3t?AY?_|+_VXpV?OD)JR$dqM(J@2P?Ow8A${GlSxbd{CEU8oFU{6e(w!mID zpp~2H(cDL}KyAQ>7E`*2GTi`n3t6@&mHZYeY)|O9;OX{#4`6K9p-xEzX+cj^(Nkyv z^H~#2oYw`Tq8-HYa;@i`wtUL1knn;6F=8sx=|`}aQphh6*^)*@FB@{ks?>Ss-#WQ3 z-iUZcCs+^MuPkqMJY_2 zOwjw?q7yCgzdrw6TYtNNAnXs-5txlzM_5(OY^ZouaASy`l}*IVFNr1yNWMqOXBdN@H z?iZli_7RS;ThT!GV_wE`IfawQN%YLOL;smvG~SA{+ay7jw&dIaRoLbc$6)cJ_42{I zbQsBjqr=u)n`4YY;o(Sr+N0zFl!R)Ne#MaLHbQC#y1%#(V^yN-f+)k9JT{b zV;b824=g9@m&Wr*d&pjyue&y)Uhuqk89AuFeoz~M>s5Kx8phk^Vi(7z>w6>@N zE!F~Q16ttp0Gl`nz+QIV{{kugagfgpN{LmW#X9>V zHoK=gzRuFpC4N!I`LUFd^w5m<&_m;fq_aLmr@)T#yDk>yLIfnTQ6o@WTx$Q;715q) zYEv^|@$@TUkvwW?9k+C{;Q#P7T^wh9x4o8w9GqK!8wb5;yMKl&PJRBqm@~?>qk1Y0 zte8y;_L24bE-tcu&rh`qZ<%}dHnba&=Wqj&0OJuommlKPtOHQ^IzBnA5nk9`oCD+q zi%b@5J=;W!xeV3NU3o6OpuRZg7ddFEZD(iaQT%Qend7K!V$+9=VC4%=e}7%d1Qfax zI@IMzv-WoKz_pS&$I&_W5d?_6k!Ec)X`sDFu@_E>rl7Vu6)WF&N3AbJq-sWYX@+Z^ zN&R^B0oG0;?Y(1n%wv%dn@3M`XP!6gM{mJ5Vw~(MK3(+k@Rr8ttUZqEyV^k(%-p`G zOMFZM+<#oWZMgEj^qDFIbm%-E-cwXx<{iZ_bso~!wb9Pn|9$kb_Fp>{XGwQ!zjpZ4 zhRd*0JLxPLnG}%h#$W{o=cZXSNtLfO8;k&DFrp%R+-l=RE8HpXk)na&C&X4UE~+yl4#lo++RfBp^i2hgw0LjGq|94 z)QWsc#LkLJ(B@+9+L0MvmO_H=yuC4&n&=8XOZ!kjZ0|iAV_sxVI^o#B&6h-vUPy%; ze3rmZ>1O%LYBLQW2~G_fY^3Qix{D~tHx}<^9N%xGky4P;+VtYfWf=DrRhtQ2hFRXVT3e{Fic;4Mft$jmFYUm_aoVnVwe{?M^ zrPJ-DX317CO^w%k2CNEzZS7$1ZFJZL>2?E88Jmo~I zBLez1z$3twJ#LUdNUlddSKV`ZpXa`fiH*E+F;mq7!;5|i;U*ji<7P@$@L3)lbl)nS z=Xz5lNF~tO1urf?^bz}}djDPh^4C6U>PL$~kvBhX)r`?PkKH~iT9mcr&=%*D@NUDk zO@rD@oQ95kB!S=FE@IYvA>UyCzPGO;Oy{v*qvzzXWp7b2P9^Yk1wrzhQ9BhzQL@EU z$Y~Ym+3krronqWniec(|N1>!t$U~d#!<)yW%OD9II$TBrnWayDf|Dg?ixG_WV6kI) zXe=@M870YUTvJ_D65Th$f84kS0Y@HtnqUUtrltD1Z*E;0K3Gqgv)9dAac_3=2GniO zc)IcwKO;VzUs?%o@@(KM*y@ZUBSn$1$ql@reR~T&L!TwDo*j%u zJ@>L^ts(YPz8{o7$)g=ZY228c-R|qz*$(SKGQ*B}i&10Cm+xY*Ac+rU1a9k+9Lsbt z_+2&T9p-X5|85gS#^{Gz3qlmv#;MxeH^(<`QgYW=TWhb~*iYl#5`MZM6Sq&95kz(; zcYn)gNg)<`=VRFtp{%-1M?0EAab{^`qTW0Y?R9oFW(Qa7VVX({M77i2tBKjVmm~I~ zYDHPc*4r1FNo!>Nnc&}IS}x6~qGez|_esuJU}#sg zxSART3b)@^3Vsaa%pVO6&&ZRu<$n8BMcrrWZYx9N#p0CnCKNLd)fD|qMEEB0Q(wih z=Oef5M=2oS5B+Rw0b5b>YI6Z=bD~H5Px;zdfr=|IgUS_b6taFWBv_e8W}iq znvd^8&1HpM8+HPXx-!QdhMzZY#5ksHA1E<@<)S!iSzl zNT(-VnX*PArvvRQ+sk4+krBlk(&_w7oBUeA8{9Hrc&+)Ie6%2cAUe{*b-D$E^gbV(>U86kg3*;c;di`deq^!w z`4>-4f?BNk93mNvAbh)=&d)wd29q#aZ*evy$h&F?h9uN?uf=V1)KO}83G7fbggm*G zL@3}uovKoL#hS9t#0){&FK7Rh10B1KjEpk zjKBLiOUXoZ?D{1m>2vTnfdSaK_RI4Dt>8KSH6&6j;p#)Bz30WXlvEMEVhpiYJRqnd zFO$TZ>0t!e509mz=~RKob{;^-*197#PQ*nHA2sd#2wm~*0$uOv>*g&9;B>Ks%ov4X zw!96XLF zE=*=C@?G<`Wt9%uw6R_kW%rtPCG*U%;`?0$&s&z7@JM&i)~b|e&6JfX{2j^)c!9XJ z3B_g-bn+ol0ngz4Yz~cWF3-|Im+x-b8QjDQ$_mFti-yDpeKM0CgN1`!czWh;gJxfmp=e@sJ31?NK7Z&z-!2zdAKsB*Fd)UT_o2miwfFerxBTfTcT< zRawDp-feymQq4D2lJ@bcLT=c%;{1brcORK&ke&*mGb_zo1BhN{Qjq7K+2(m%{L2?O zW5)?H4bZ^`4(!SdK16|WYiouP>Nyt$)cyEx(hg>ZK16>l)=@LyxZe$~Y`sU*dR6Fp zH^g8AbXfff@g)-29WLepwE!AoK;sZ>PT)_Y{F6=NX=c@(X}_jO85_YQQP?_%qBVoFl2!-Iu5)~81icv*Vxr%lV-bMlKngP4 zch{^WQSNMq1JCH9<+;DET0YzBnk=EBqF>Ef@*qQLEdur4h-e^oTRrAiFH;tbds|~I z0Ux$qwTCnue^C}Z-LM%l$Dc)t*Uz}PG;%UMx6U;%<;9~^{R2PuP}nmLCi18P0}}|c zFBCUF|Gby4UE1^VO+=8^n@WNwc82x)4HSGi*2)tlOR!VKS|v?@74Wnin*z`T%ux^U zWT=r{(9bq%)_8@((N@nhlisha`;Z(-zY*l8uvIYn!u#b>T;#Gi;z`3ou(y;@x<1lf z$|97na{S4vwvCVdt5ZP!+P*_5*mFb=-ZV?*Omeg-)`co{kNQNuT`3pR&qMseml*LJ z5zaaVzVN}o3r%*A*b$BZr0ERtyn^$_n+<`hbT0VC(fcWDGY4(;{ zqix_Ma3{D)lKPi%UgkuDtm8MX|O*WY+v zfgPS@!Qbl17z|Ps;eS+R02}FW4soFYxgQuANvUSKg!Q*t#fGdWei>zrwxrIWtx~cc zGm)|J^}G+IVWjqIP_(|WpNMlqdoH!|gNp9Z9HzZJpEe_}?cW;Bn50r1okugwo1_g8 z?j%e|&5IH2eXx;qta(Z1BUXpA|IeV5A?tSvpI_ONN7{21G!kfB=j@gk3SCu&i`v8J zxH}DOLR5ouBK_QMrK==*_{(A3RG=iz>-c>AGzI;zILw}re!#*UG7@XiYLavoyR;y# zT#fm-h3V#tL2Jy!h3dT%4!W=0`4ZL0PY%Oa-nJEEp;WSC$~utUPEzw8uxd{?yh%`h zlORJj>$_mdJwtKoS)ia)i=a5p#aeU-=sy>%4t~~!Z^OSjrBZo^Ocj$l3}oj7 zKj`pR-J55=Q+Jv1PZWaT?4doK6?2CBt7ZIT;(vdsZ6?^wY4b3$8%rLWun~=P?gKV) zci(Y#o=zy3Qq5Cstnn}~LOqf3d?bSCBH(^eV0aatRgmE4b7AdpVK0F!XRFQ9L`Sur z(xqcrbL4Af^HdkClQ>N(RL|++)l4~p3r4j$mRXQ z;n(TL2)`>4=7;6QAml2buKZq)w~@{=;B?)fxM1~Y?lB$Z=|3)k=ptO++bn;blQ#mD zafVppz;93~Cm|Yh+?3Z=(VN5Fsy-}0KTrNHeuoKXiQ8mNyc`MJ?tN+ICe1jvD=+Rv zasWJUv^2i<*M!TZVVaJo$tcezOcymtO!6;~9`dYI_gFo+0*rtRzZlPWxyb|cq|U60 zv~#o+cPg5jCazRnlv(zEvU_)L_53Xw!vt3!jax~A^iDoUFFnfMwbQd#9~h;5S$w|%kO(kzK3X#_B^Xkfkq*NIQpLHB2qDWnZzvY9JUy}#Ea=YH z=qpB#GmLAUOXjz_hvl|{-*H|H=O%8_T>AXV2g6H~T99VRM*h_*0W+fWqN{guYT8H) zWZ$&V2=msEd7~Yxv@Yoy~p~pbl0tiFA8FuZZ;io3CabOj7 z0pXXB>mEpJ9nul$Ee839f1t#U~hzT_ij?Zp9#@s8$bU#<}&Q>1HND6%(l;#BPiVyHsnv})`oT8w#}@( zZ;%^ln)&`uzx$@utf&@2fVjqj?JdvL@<#_##e)sr%byXB&eE!_w%a#VcD|rwu+Y~@ zYB>d91J^3_7h}G!T2-)auO7>UgB4e7(QG%W?zjt`aJaZC{}<%`vs?-byBEwyRm*Na(rLOafEjTcQDBJJWfyR(aheqHf%w6woROSz}EUaaq zH$JCUCS>wUzwV)1#d5|)%!dc;YDUDIIq(xNCO zpSVdS(;+YyWv>jl#&n*NG|*qZll*$Ox%An5 z)dLg$N4AVj?&%yb#~S#&7#fOo#~fF~Hp(!mu8)EJQu?5{t;%zIs<>F9;XrWoAP|R#)L=F|M!yg zmHsgty6UhM4!#JgbHz@gb4|P@{35aA8Zgk>MEbh89Sp8hgkA!@oQeFWlKvSLKWVfe zDe?a+D!jY2yN_FL0A5yWGsi@Szlt6khC6=C*h-zt1lbRn9lF<2x03%6d+bcvZ zcpnrasL$w2_dkGRVXPh8kG+C!%ck7!6@K)_VMk4@jPnUwA6w-%VMzYSkD-|mo_=4k_$F-?6q?IH09*g(+aZg7~`bvOGlMoxaiIQ>VcooFBCFW>f#jLvwDkc< zk7YGRkL8=S>(82228^lz@E9`i|5mCM&!~!=!dlOdHJyXFU=pnl3Sdo<@KFx&*I*Vz z&gemjNyT0_X4r$}zeLdAqW-`3$F+nRz_m%iF29?XUd>T5%lWIHjU2>4U+Dm)RDy#VgBdO5A6;LG~U)_F_e(y79>{qNC_(=mWUFQR0 z3G`$p63K(}o?yXfmHqf4lp1RvHYQ$3rBNdU-Wh!6-kIu|p6_6;_Y~ z2*uE@Vz=s_NDDYV>D}^PY7)ShfuHXw{eXu4>@P()o~L|8WcB}PI+n-?^fnqi+~ht; zc~fB1C(Tz|t-4XOhO157i57=Lbdiw=x>ffiTI+~k<+pQu4DfL|vrafkT25=l(XXC? z!w?>J!mng0kSpL{xkxxP6vznBDc)++D`&BDOg%7+RKF!k^SXdHS zxHRfe!}*3sJ?iw zV79Q?UO%oAeudDu$!Ty-~^} zy|@RKfX+ODDG=E@x$)!EQ`a>nd8RNyBUty48IurfX+(|5Hsh}#IrRrWXO~Ep3i+bD zFTfjTwD0z2cPzQ)n0qua|9u-`6^#;)P;vC~zNln&53vKRZ)W6z=dK_BuoIhguLb{a zI(YxxRh9z(FwzL4^Ncxcfipd!D&amEzFf!W`D*}Dezu5)OAjn9=>*Re>20>T5O<;aL-- zNK?^Jv$0tdm`0?7jhhmb!$G)g&Ms7jYK8@kfMI^z&zB^HTBTYz9M`ldrC-1-S&F*H z9ZD{AUW?GKOoYWesnaX81a&*Y7Tg)!(uJ)Ty$7PvIu&Dwz#;6!?y(y)Y$ykzQg%lM zDzWXpa!h`7{D=MC_twk-%@!JBp|ZG{8AE8vC;p=RBlus?Vb!-p#H+DkNIta3 z0Wji}roNTk6(apVnbeim{TC$1b-|>fgp9rv&95KJe-uo8?B`>VaoLI}`I~G-pXfin z$2DW{>i7vV;(>nfTRif}sO$c(;-XsaFdF#=EeVUxQ%(o3s(g9mFZc5vo&FQM?ma-5 ztFqm)WU*V|oFI8(jaM5ar|n6JE-!tz9DiA_!9`Ex(_=M@m*|?1w#2U-J4vvHi)xve z-A0r3b)ydrWqQR;I6L+D?rMuL!Mx7Y2Q`i#xSUwvxt)wjkY^hR_=TDu-$yN6V^Yeo z94kS#O_$q#XnzRQDA4`@7eFSU3pO!JQclbgR5^~!8&ilMUpk}DM_>Y>pG?rLz05UBMuN%^`SV2V*of6I3y01g6$z3WX7Cj?*4nUzX4Mj##aW-WhsQxN|jYzqEsZuJj$ zbeC{9tpVfuj3;dVX1XlS|9r>8j{bL7CO2kko!9OwkCCt6U+GtrQ($tkr#3XYVo1Qx zPf6L=ce|g9+80p6t)L-$_3=jJK0F16<_(neCunAL-*AM5%Ch&92%ic64>Kd7Gc#Ve zIfGp`%^fGt2CK5CQVb$D+{eCrs^G0*AZLhSLSr1%6rScS92*v5gnl4M{Y%t$iIeHY zTbk%Y%fm1MOEt(A*SlEm>}g1WGx*0t)l30d6WFpV-scaFmdwg)(hcivE9Ui!IJguW zdY?1zBtoT@1tZ;i%+hUk3}>g?MF<&u?qtwdeVY~B$*`UK7|=>jX#lV}UU_t~fF4YYONWh)JvbA@@da{!yI&b-;o4{TnkCyK~ zMn^b=|x~@W|B+Q2Dk#ppmgdWvSh3(PFh*!8PJjK79Zl`EqdN+G7ZW4P^ zL;X9}{K_by^H#)p0OdA0Zc0sus1U!0Oxg3Sbl+5U4YFjvjn({eVc@Ooz?pA2yz+@; zS6b3F?7Sd#fP)uR2`?|nZSLm5EM2_`T{mGyMym41*Lj&++AV3rl6pdu24)}nk$2|c zqhZ}(;16*yvB3TVx_91O$Br+Oo|2DAACPQ2#|kNSK>pm{tQb%{2gOWr?P>F@N)+d0OCcO>mJTA_?TQ5EErBfjJ&trK zGe-vH@i{NmmM1#dBc(*#OF`#g*2_EF?gMQIMeF(>7A;hj40;UsbUej{{*?6g*+J^+ z8dc!OruuKn-a$p3o^GXk>UkFHQ| zbp=zVHjXH^@VKjQRD&L8(d(0mvc>1wl#jMdihV5YA<*3?=kgWMOc@OAjGm@hJ}1S2_yVjraPw0&BDTBG|_erQZ;+W;e)%$Y<=Xy2s#gq*+bX; z@Sy_c#8voDjum$c37jxU$kCu*DbC2P%H+#bvSMSvbC-2Z%#NYM#)8aG$?+O`q6IjF z>jro<`!>oy1q~Rd_`}Mff>oj&a@VY2l;%Y}AZb38#H; zE{<`AB_$c|1S0Q6haw$RXcAlJC=2abgO+UZv1er>A^wXj`zlkoMI9polH?sxCUq8P8J#mM-TR0qLi<*V zdYM$3T_IBHa-=sT{77bfLlYF(eJEWAmELfO^qem1kQSNE8A+cNFhQcv&LaP^ub;_@ zer?bByD|NKgnwS*5$F#G1f4t`lS^yQV!;+LM9x-~ehpJY`}g^HDqT}|0d|sThYzvp zOwTP`kKM&;k|0vdkYn}T=#owpedr?Qg#(Iqk77*9;^tjbGnNRL> zT$Wi#ty%V5K8W}cY5y#NtvLJLohb8E^iO=;yo~4(`EfcfQOxa&HC5A(KFHQD2r!Q? zH~4~!>5EgH(~p-DOX>6^YkP}bv;roeJgRT7vAszl9|lLb@R%l9SmM^Eq-F|i{Y~DB zOE-~p!*_0E(U$gv`_zTJgq{XGcUeAljlwxT3aI~GW9o9eZ$ZO+aEeF*o0ITt+p}_9j`yXTp|4mzBrfa*e8xyo<2P2aO|JiaL4WjFNg^Ial!p*rQ0yb z&;~sGuOXR-sTG{C!=-ULHR-i~Jj=NF%6NS*us-dlF-@BoPQ*(AiyNP(3*q`tU$;}} j|L0HTR<7e8O5-tiUfWb-fi3@ZPh3S&U7_gqW8eP)M@5?* literal 0 HcmV?d00001 diff --git a/doc/img/result.png b/doc/img/result.png index f933cf9c5e177bd4a92812ebc0ad30cd249e6b63..5e0e3e13a3a05284afd9866d890b4775a3196a73 100644 GIT binary patch literal 23863 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 literal 22149 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 MsxqbbAHDd00P}X!J^%m! diff --git a/doc/img/sign.png b/doc/img/sign.png new file mode 100644 index 0000000000000000000000000000000000000000..a1255159f8a77984b2d05f672ff3434f1b8d5b92 GIT binary patch literal 21358 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 PLdHFLwYw#E%!2<1Lkihf literal 0 HcmV?d00001 diff --git a/doc/mal-och-tester.md b/doc/mal-och-tester.md index b79eca6..29b6f84 100644 --- a/doc/mal-och-tester.md +++ b/doc/mal-och-tester.md @@ -83,6 +83,12 @@ att bygga på Linux som på Windows. | `internal/sim` | `TestContinuousMovementKeepsPace`, `TestStopsAtTileBoundaryWithoutInput`, `TestDirectionChangesOnlyAtTileBoundary` | Jämn fart med hållen tangent, stopp vid rutgräns, riktningsbyte bara vid rutgräns | | `internal/sim` | `TestWorldPosInterpolates`, `TestFrameExactArrival`, `TestHugeDtIsBounded` | Pixelposition mellan rutor; exakt 10 bildrutor per ruta vid 6 rutor/s; inga oändliga loopar | | `internal/sim` | `TestCamera*` | Kameran centrerar, klampas mot kanter och centrerar små världar | +| `internal/level` | `TestScheduleStatus`, `TestScheduleAndFareDefaults` | Tidtabellen ger rätt "inne/borta" och sekunder kvar; standardvärden fylls i | +| `internal/level` | `TestSignAt`, `TestPath`, `TestUngatedPlatforms` | Skylttext per ruta; BFS-väg är kortast och sammanhängande; perronger utan spärr framför upptäcks | +| `internal/level` | `TestAllShippedLevels` (utökat) | …plus: tidtabeller giltiga, alla skyltrutor har sv/en-text, alla tåg har spår, **inga perronger nås utan spärr** | +| `internal/sim` | `TestBotWinsLevel001` | **Simuleringstest M2:** bot går kortaste vägen, passerar spärren (saldo dras en gång), väntar in tåget, kliver på → vinst; Step är no-op efteråt | +| `internal/sim` | `TestBotLosesOnWrongTrain`, `TestBoardOnlyWhenTrainPresent` | Fel tåg → förlust; Enter utan tåg inne gör inget | +| `internal/sim` | `TestGateNeedsCardBalance`, `TestSignHereOnlyWhenStill` | Tomt kort stoppas och registreras för HUD; mellan två spärrutor gratis; skylt visas bara stilla | | `internal/archtest` | `TestOnlyPresentationPackagesImportEbitengine` | Beroenderegeln ovan | ### Planerade tester per milstolpe @@ -90,7 +96,7 @@ att bygga på Linux som på Windows. | Milstolpe (se roadmap) | Tester som ska finnas när den är klar | |------------------------|----------------------------------------| | ~~M1 Rörelse & kamera~~ ✓ | Kollision mot vägg, rörelse i 4 riktningar, kamera klampas till kartan – finns, se tabellen ovan | -| M2 Stationen | Tidtabell: nästa avgång beräknas rätt; spärr släpper igenom med giltigt kort/biljett och stoppar annars; vinst när rätt tåg tas, förlust vid fel tåg; simuleringstest: bot går kortaste vägen och klarar bana 001 | +| ~~M2 Stationen~~ ✓ | Tidtabell, spärr med/utan saldo, vinst/förlust, bot klarar bana 001 headless – finns, se tabellen ovan | | M3 Japanska & skyltar | Delvis översättning ger exakt de ord spelaren kan; andel engelska följer svårighetstabellen; ordboken: alla nycklar har kana/romaji/en/sv; alla skyltord i banorna finns i ordboken; hjälptid = f(japanska-nivå) | | M4 Lämmeltåget | Mätare sjunker med rätt takt; event-frekvens ökar med gruppstorlek; event väljs deterministiskt per seed | | M5 Minikarta | Upptäckta rutor markeras, återställs per bana; avgångar syns från start | diff --git a/doc/plan.md b/doc/plan.md index b259372..acc33f7 100644 --- a/doc/plan.md +++ b/doc/plan.md @@ -32,7 +32,7 @@ brasse-linux01-runnern, inte Pi5:n. ## Milstolpar -M0 skelett ✓ → M1 rörelse/kamera ✓ → M2 första spelbara banan → M3 japanska/skyltar +M0 skelett ✓ → M1 rörelse/kamera ✓ → M2 tåg/spärrar/vinst ✓ → M3 japanska/skyltar → M4 lämmeltåget → M5 minikarta → M6 kort/biljetter/minispel → M7 svårighet/poäng/debuffar → M8 progression/sparfiler → M9 navigeringslägen → M10 story-innehåll → M11 oändligt läge/NG+ → M12 ljud/polish/release. diff --git a/doc/roadmap.md b/doc/roadmap.md index 9eb479a..ae6661f 100644 --- a/doc/roadmap.md +++ b/doc/roadmap.md @@ -58,7 +58,7 @@ metro-puzzle-jp/ │ ├── res/ hittar och läser resources/ (ren Go) │ ├── rng/ seedad slump (ren Go) │ ├── level/ banformat, validering, BFS (ren Go) -│ ├── sim/ rutbaserad rörelse, kollision, kamera (ren Go) +│ ├── sim/ rutbaserad rörelse, kollision, kamera, banomgång (Run: klocka, kort, spärrar, tåg) – ren Go │ ├── game/ Ebitengine-loop, scener, input, rendering, skriptläge ← får använda Ebitengine │ ├── archtest/ beroenderegler │ └── … kommande: lang/, nav/, score/, save/, minigame/, ui/, audio/ @@ -140,18 +140,27 @@ på **brasse-linux01-runnern** (label `linux-amd64`), aldrig på Pi5:n. det naturliga slutet på en bana; tidtabell, spärrar och väntan på tåg kommer i M2 som planerat. -### M2 – Stationen: tåg, spärrar och vinst/förlust → första spelbara banan +### M2 – Stationen: tåg, spärrar och vinst/förlust ✓ (klar 2026-10-23) - **Mål:** bana 001 går att klara och misslyckas. -- Tåg med tidtabell som står vid perrongen en stund och avgår; nästa avgång - visas vid perrongen. -- Spärrar som kräver kort (kortet finns och har alltid saldo i M2). -- Vinst när rätt tåg tas; förlust när fel tåg tas. Resultatskärm. -- Interaktion med skyltar (visar text, än så länge på svenska/engelska). -- **Teknik:** `internal/sim/schedule`, `internal/level` utökas med - tidtabell + skyltinnehåll. -- **Klar när:** en simuleringstest låter en bot klara bana 001 headless; - manuellt spel från start till mål fungerar. +- Tåg med tidtabell per perrong (`schedule`: första ankomst, intervall, + uppehåll). Tågvagnen ritas bortom perrongkanten när tåget är inne; HUD:en + visar spår, linje, resmål och "avgår om / nästa tåg om". +- Ombordstigning är ett aktivt val: stå på perrongen och tryck Enter medan + tåget är inne. Rätt tåg = vinst, fel tåg = förlust, resultatskärm med tid, + steg, spärrpassager och kortsaldo. +- Spärrar kräver kort med saldo (startsaldo 2000 ¥, avgift per bana, dras + när man går in i en spärr från en vanlig ruta). Tomt kort stoppas med + meddelande. Riktig ekonomi och påfyllning kommer i M6. +- Skyltar har text (`signs` i bandatan, sv/en/ja) som visas när man står + stilla på skyltrutan. Japanskan ritas först i M3. +- **Regel för banor:** alla perronger ska ligga innanför spärrar. + `level.UngatedPlatforms` kontrolleras i det datadrivna testet; bana 001 + byggdes om när regeln avslöjade två genvägar. +- **Teknik:** `internal/sim.Run` (klocka, kort, spärrlogik, ombordstigning – + ren Go), `level.Schedule`, `level.Path` (BFS), skriptkommandot `waittrain`. +- **Klar när:** `TestBotWinsLevel001` låter en bot gå kortaste vägen, + passera spärren, vänta in tåget och kliva på – headless. ✓ ### M3 – Japanska och skyltsystemet diff --git a/internal/game/assets.go b/internal/game/assets.go index 6cf7b64..b012d34 100644 --- a/internal/game/assets.go +++ b/internal/game/assets.go @@ -21,6 +21,7 @@ type Assets struct { ResDir string LevelsDir string Tiles map[level.Tile]*ebiten.Image + Train *ebiten.Image // tågvagn, ritas vid perrongen när tåget är inne Player [4][3]*ebiten.Image // [riktning: ner, upp, vänster, höger][bildruta: stilla, gå1, gå2] Text map[string]string } @@ -38,13 +39,17 @@ func LoadAssets() (*Assets, error) { if err != nil { return nil, err } - if w := tiles.Bounds().Dx(); w < TileSize*len(tileOrder) { - return nil, fmt.Errorf("tiles.png är %d px bred, behöver minst %d", w, TileSize*len(tileOrder)) + sub := func(i int) *ebiten.Image { + return tiles.SubImage(image.Rect(i*TileSize, 0, (i+1)*TileSize, TileSize)).(*ebiten.Image) + } + if w, need := tiles.Bounds().Dx(), TileSize*(len(tileOrder)+1); w < need { + return nil, fmt.Errorf("tiles.png är %d px bred, behöver minst %d", w, need) } for i, t := range tileOrder { - a.Tiles[t] = tiles.SubImage(image.Rect(i*TileSize, 0, (i+1)*TileSize, TileSize)).(*ebiten.Image) + a.Tiles[t] = sub(i) } a.Tiles[level.WrongPlatform] = a.Tiles[level.Platform] // samma utseende; tåget avgör + a.Train = sub(len(tileOrder)) player, err := loadImage("sprites", "player.png") if err != nil { diff --git a/internal/game/scene_level.go b/internal/game/scene_level.go index 11725ab..4f49b22 100644 --- a/internal/game/scene_level.go +++ b/internal/game/scene_level.go @@ -17,15 +17,16 @@ import ( // Spelarens grundfart i rutor per sekund (blir en stat i M7). const playerSpeed = 6.0 -// LevelScene är själva spelandet av en bana. +// Så länge "spärren nekade" visas efter försöket. +const deniedShowSec = 2.0 + +// LevelScene är själva spelandet av en bana. All spellogik ligger i sim.Run; +// scenen översätter input och ritar. type LevelScene struct { - id string - lvl *level.Level - player *sim.Mover - cam *sim.Camera - animT float64 - elapsed float64 - steps int + id string + run *sim.Run + cam *sim.Camera + animT float64 } // NewLevelScene laddar banan och ställer spelaren på startrutan. @@ -35,20 +36,25 @@ func NewLevelScene(a *Assets, id string) (*LevelScene, error) { return nil, err } s := &LevelScene{ - id: id, - lvl: lvl, - player: sim.NewMover(lvl.SpawnPoint(), playerSpeed), - cam: sim.NewCamera(ScreenW, ScreenH, float64(lvl.Width()*TileSize), float64(lvl.Height()*TileSize)), + id: id, + run: sim.NewRun(lvl, playerSpeed), + cam: sim.NewCamera(ScreenW, ScreenH, float64(lvl.Width()*TileSize), float64(lvl.Height()*TileSize)), } s.followPlayer() return s, nil } // PlayerTile returnerar rutan spelaren står på/lämnar (används av skriptläget). -func (s *LevelScene) PlayerTile() level.Point { return s.player.Tile } +func (s *LevelScene) PlayerTile() level.Point { return s.run.Player.Tile } + +// TrainHere rapporterar om spelaren står på en perrong vars tåg är inne (skriptläget). +func (s *LevelScene) TrainHere() bool { + tr, at, _ := s.run.PlatformHere() + return tr != nil && at +} func (s *LevelScene) followPlayer() { - x, y := s.player.WorldPos(TileSize) + x, y := s.run.Player.WorldPos(TileSize) s.cam.Follow(x+TileSize/2, y+TileSize/2) } @@ -61,50 +67,116 @@ func (s *LevelScene) Update(g *Game) error { g.SetScene(m) return nil } - s.elapsed += dt - s.steps += s.player.Step(dt, dirInput(), s.lvl.CanEnter) - if s.player.Moving() { + s.run.Step(dt, dirInput(), confirmJustPressed()) + if s.run.Player.Moving() { s.animT += dt } else { s.animT = 0 - // Står vi på en perrong är banan slut (M2 ger tåg, tidtabell och spärrar). - if t := s.lvl.At(s.player.Tile.X, s.player.Tile.Y); t == level.Platform || t == level.WrongPlatform { - g.SetScene(NewResultScene(s, s.lvl.TrainAt(t))) - } } s.followPlayer() + if s.run.Outcome != sim.Ongoing { + g.SetScene(NewResultScene(s)) + } return nil } +// trainTile är rutan "bortom perrongkanten" där tågvagnen ritas: första +// väggruta nedanför, till höger, till vänster eller ovanför perrongen. +func (s *LevelScene) trainTile(p level.Point) level.Point { + for _, d := range [...]level.Point{{X: 0, Y: 1}, {X: 1, Y: 0}, {X: -1, Y: 0}, {X: 0, Y: -1}} { + n := level.Point{X: p.X + d.X, Y: p.Y + d.Y} + if s.run.Level.At(n.X, n.Y) == level.Wall { + return n + } + } + return p +} + func (s *LevelScene) Draw(g *Game, screen *ebiten.Image) { screen.Fill(color.RGBA{24, 20, 37, 255}) + lvl := s.run.Level ox, oy := s.cam.Offset() ox, oy = math.Round(ox), math.Round(oy) // Rita bara rutor som syns. x0 := max(0, int(s.cam.X)/TileSize) y0 := max(0, int(s.cam.Y)/TileSize) - x1 := min(s.lvl.Width(), int(s.cam.X+ScreenW)/TileSize+2) - y1 := min(s.lvl.Height(), int(s.cam.Y+ScreenH)/TileSize+2) + x1 := min(lvl.Width(), int(s.cam.X+ScreenW)/TileSize+2) + y1 := min(lvl.Height(), int(s.cam.Y+ScreenH)/TileSize+2) var op ebiten.DrawImageOptions for y := y0; y < y1; y++ { for x := x0; x < x1; x++ { op.GeoM.Reset() op.GeoM.Translate(ox+float64(x*TileSize), oy+float64(y*TileSize)) - screen.DrawImage(g.Assets.Tiles[s.lvl.At(x, y)], &op) + screen.DrawImage(g.Assets.Tiles[lvl.At(x, y)], &op) + } + } + // Tåg som står inne. + for _, t := range []level.Tile{level.Platform, level.WrongPlatform} { + if !s.run.TrainVisible(t) { + continue + } + for _, p := range lvl.Find(t) { + tt := s.trainTile(p) + op.GeoM.Reset() + op.GeoM.Translate(ox+float64(tt.X*TileSize), oy+float64(tt.Y*TileSize)) + screen.DrawImage(g.Assets.Train, &op) } } - px, py := s.player.WorldPos(TileSize) + px, py := s.run.Player.WorldPos(TileSize) op.GeoM.Reset() op.GeoM.Translate(ox+math.Round(px), oy+math.Round(py)) - screen.DrawImage(g.Assets.PlayerFrame(s.player.Facing, s.player.Moving(), s.animT), &op) + screen.DrawImage(g.Assets.PlayerFrame(s.run.Player.Facing, s.run.Player.Moving(), s.animT), &op) - // HUD + s.drawHUD(g, screen) +} + +func (s *LevelScene) drawHUD(g *Game, screen *ebiten.Image) { + r := s.run + a := g.Assets vector.DrawFilledRect(screen, 0, 0, ScreenW, 24, color.RGBA{24, 20, 37, 200}, false) // TODO(M3): visa japanska när en font med kana/kanji finns – debugfonten saknar dem. - ebitenutil.DebugPrintAt(screen, fmt.Sprintf("%s %s: %s", s.lvl.Name.Get("sv"), g.Assets.T("hud.destination"), - s.lvl.Destination.Get("romaji")), 4, 1) - ebitenutil.DebugPrintAt(screen, fmt.Sprintf("%s %3.0fs %s %d %s", g.Assets.T("hud.time"), s.elapsed, - g.Assets.T("hud.steps"), s.steps, g.Assets.T("hud.controls")), 4, 12) + ebitenutil.DebugPrintAt(screen, fmt.Sprintf("%s %s: %s", r.Level.Name.Get("sv"), a.T("hud.destination"), + r.Level.Destination.Get("romaji")), 4, 1) + ebitenutil.DebugPrintAt(screen, fmt.Sprintf("%s %3.0fs %s %d %s %d¥ %s", a.T("hud.time"), r.Time, + a.T("hud.steps"), r.Steps, a.T("hud.card"), r.Card.Balance, a.T("hud.controls")), 4, 12) + + // Inforutan: perronginfo, skylt eller nekad spärr. Visas inte när banan är avgjord. + if r.Outcome != sim.Ongoing { + return + } + var lines []string + accent := color.RGBA{58, 68, 102, 255} + if tr, at, secs := r.PlatformHere(); tr != nil { + lines = append(lines, fmt.Sprintf("%s %s %s -> %s", a.T("hud.track"), tr.Track, tr.Line.Get("romaji"), tr.Destination.Get("romaji"))) + if at { + accent = color.RGBA{99, 199, 77, 255} + lines = append(lines, fmt.Sprintf("%s %.0f s %s", a.T("hud.departs_in"), math.Ceil(secs), a.T("hud.board"))) + } else { + accent = color.RGBA{254, 174, 52, 255} + lines = append(lines, fmt.Sprintf("%s %.0f s", a.T("hud.next_train_in"), math.Ceil(secs))) + } + } else if sg := r.SignHere(); sg != nil { + lines = wrap(a.T("hud.sign")+": "+sg.Text.Get("sv"), textCols) + } else if r.DeniedAt >= 0 && r.Time-r.DeniedAt < deniedShowSec { + accent = color.RGBA{228, 59, 68, 255} + lines = []string{a.T("hud.gate_denied")} + } + if len(lines) == 0 { + return + } + h := float32(8 + 12*len(lines)) + // Rutan ligger i nederkant, men flyttas upp under HUD:en om spelaren + // (eller tåget under hen) annars skulle hamna bakom den. + _, py := r.Player.WorldPos(TileSize) + y := float32(ScreenH) - h + if float32(py-s.cam.Y)+2*TileSize > y { + y = 24 + } + vector.DrawFilledRect(screen, 0, y, ScreenW, h, color.RGBA{24, 20, 37, 230}, false) + vector.DrawFilledRect(screen, 0, y, ScreenW, 2, accent, false) + for i, l := range lines { + ebitenutil.DebugPrintAt(screen, l, 4, int(y)+4+12*i) + } } diff --git a/internal/game/scene_result.go b/internal/game/scene_result.go index 69724c3..9ee6650 100644 --- a/internal/game/scene_result.go +++ b/internal/game/scene_result.go @@ -8,21 +8,16 @@ import ( "github.com/hajimehoshi/ebiten/v2/ebitenutil" "github.com/hajimehoshi/ebiten/v2/vector" - "gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/level" + "gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/sim" ) -// ResultScene visas när spelaren nått en perrong. +// ResultScene visas när banan är avgjord (rätt eller fel tåg). type ResultScene struct { - from *LevelScene - train *level.Train + from *LevelScene } // NewResultScene bygger resultatet för banan som just spelades. -func NewResultScene(from *LevelScene, train *level.Train) *ResultScene { - return &ResultScene{from: from, train: train} -} - -func (r *ResultScene) correct() bool { return r.train != nil && r.train.Correct } +func NewResultScene(from *LevelScene) *ResultScene { return &ResultScene{from: from} } func (r *ResultScene) Update(g *Game) error { switch { @@ -44,22 +39,33 @@ func (r *ResultScene) Update(g *Game) error { func (r *ResultScene) Draw(g *Game, screen *ebiten.Image) { r.from.Draw(g, screen) // banan i bakgrunden - bg := color.RGBA{24, 20, 37, 230} - vector.DrawFilledRect(screen, 20, 40, ScreenW-40, 100, bg, false) - accent := color.RGBA{228, 59, 68, 255} - headline := g.Assets.T("level.wrong_train") - if r.correct() { - accent = color.RGBA{99, 199, 77, 255} - headline = g.Assets.T("level.cleared") - } - vector.DrawFilledRect(screen, 20, 40, ScreenW-40, 3, accent, false) + run := r.from.run + a := g.Assets + vector.DrawFilledRect(screen, 20, 36, ScreenW-40, 112, color.RGBA{24, 20, 37, 235}, false) - ebitenutil.DebugPrintAt(screen, headline, 28, 50) - if r.train != nil { - ebitenutil.DebugPrintAt(screen, fmt.Sprintf("%s -> %s", r.train.Line.Get("romaji"), - r.train.Destination.Get("romaji")), 28, 66) // TODO(M3): japanska när fonten finns + accent, headline := color.RGBA{228, 59, 68, 255}, a.T("level.wrong_train") + if run.Outcome == sim.Won { + accent, headline = color.RGBA{99, 199, 77, 255}, a.T("level.cleared") } - ebitenutil.DebugPrintAt(screen, fmt.Sprintf("%s %.1fs %s %d", g.Assets.T("hud.time"), r.from.elapsed, - g.Assets.T("hud.steps"), r.from.steps), 28, 86) - ebitenutil.DebugPrintAt(screen, g.Assets.T("result.hint"), 28, 118) + vector.DrawFilledRect(screen, 20, 36, ScreenW-40, 3, accent, false) + ebitenutil.DebugPrintAt(screen, headline, 28, 46) + + y := 62 + if tr := run.Boarded; tr != nil { + // TODO(M3): japanska när fonten finns + ebitenutil.DebugPrintAt(screen, fmt.Sprintf("%s %s %s -> %s", a.T("hud.track"), tr.Track, + tr.Line.Get("romaji"), tr.Destination.Get("romaji")), 28, y) + y += 12 + if run.Outcome == sim.Lost { + for _, l := range wrap(fmt.Sprintf(a.T("result.wrong_detail"), tr.Destination.Get("romaji"), + run.Level.Destination.Get("romaji")), 44) { + ebitenutil.DebugPrintAt(screen, l, 28, y) + y += 12 + } + } + } + y += 6 + ebitenutil.DebugPrintAt(screen, fmt.Sprintf("%s %.1fs %s %d %s %d %s %d¥", a.T("hud.time"), run.Time, + a.T("hud.steps"), run.Steps, a.T("result.gates"), run.GatePasses, a.T("result.card"), run.Card.Balance), 28, y) + ebitenutil.DebugPrintAt(screen, a.T("result.hint"), 28, 128) } diff --git a/internal/game/script.go b/internal/game/script.go index 99af1c7..d73f987 100644 --- a/internal/game/script.go +++ b/internal/game/script.go @@ -19,6 +19,7 @@ import ( // Stegen skiljs med ';': // // wait N vänta N bildrutor +// waittrain vänta tills tåget vid perrongen spelaren står på är inne // press KEY tryck enter | esc | r | up | down en bildruta // move DIR N gå N rutor åt up | down | left | right (släpper när rutorna är avklarade) // shot PATH spara aktuell bild som PNG (intern upplösning × shotScale) @@ -83,7 +84,7 @@ func parseScript(src string) (*script, error) { return nil, fmt.Errorf("script: 'shot PATH'") } st.arg = f[1] - case "quit": + case "quit", "waittrain": default: return nil, fmt.Errorf("script: okänt kommando %q", f[0]) } @@ -109,6 +110,9 @@ func parseDir(s string) sim.Dir { // playerTiler implementeras av scener som har en spelare i rutnätet. type playerTiler interface{ PlayerTile() level.Point } +// trainWaiter implementeras av scener där man kan vänta på ett tåg. +type trainWaiter interface{ TrainHere() bool } + // tick körs i början av varje Update. frame är senast ritade bild. // Returnerar ebiten.Termination när skriptet vill avsluta. func (s *script) tick(g *Game, frame *ebiten.Image) error { @@ -151,6 +155,10 @@ func (s *script) tick(g *Game, frame *ebiten.Image) error { return err } s.advance() + case "waittrain": + if tw, ok := g.scene.(trainWaiter); !ok || tw.TrainHere() { + s.advance() + } case "quit": return ebiten.Termination } diff --git a/internal/game/text.go b/internal/game/text.go new file mode 100644 index 0000000..39b8430 --- /dev/null +++ b/internal/game/text.go @@ -0,0 +1,27 @@ +package game + +import "strings" + +// Debugfonten är 6 px bred per tecken; 320 px ger drygt 50 tecken. +const textCols = 50 + +// wrap radbryter en text på ordgränser till högst width tecken per rad. +func wrap(s string, width int) []string { + var lines []string + cur := "" + for _, w := range strings.Fields(s) { + switch { + case cur == "": + cur = w + case len([]rune(cur))+1+len([]rune(w)) > width: + lines = append(lines, cur) + cur = w + default: + cur += " " + w + } + } + if cur != "" { + lines = append(lines, cur) + } + return lines +} diff --git a/internal/level/level.go b/internal/level/level.go index f4fbb07..8c521e8 100644 --- a/internal/level/level.go +++ b/internal/level/level.go @@ -9,6 +9,7 @@ package level import ( "encoding/json" "fmt" + "math" "os" "path/filepath" "sort" @@ -55,23 +56,62 @@ func (t Text) Get(lang string) string { return "" } +// Schedule är ett tågs tidtabell i sekunder från banstart: första ankomst +// vid OffsetSec, därefter var IntervalSec; tåget står DwellSec vid perrongen. +type Schedule struct { + OffsetSec float64 `json:"offsetSec"` + IntervalSec float64 `json:"intervalSec"` + DwellSec float64 `json:"dwellSec"` +} + +// Standardvärden som fylls i av Validate när fältet är 0. +const ( + DefaultIntervalSec = 30.0 + DefaultDwellSec = 10.0 + DefaultFareYen = 200 +) + +// Status returnerar om tåget står vid perrongen vid tiden t, och hur många +// sekunder det är kvar – till avgång om det står där, annars till ankomst. +func (s Schedule) Status(t float64) (atPlatform bool, seconds float64) { + if t < s.OffsetSec { + return false, s.OffsetSec - t + } + phase := math.Mod(t-s.OffsetSec, s.IntervalSec) + if phase < s.DwellSec { + return true, s.DwellSec - phase + } + return false, s.IntervalSec - phase +} + // Train är ett tåg (eller en buss) som avgår från en perrong i banan. type Train struct { - Platform string `json:"platform"` // vilket perrong-tecken (P/p) tåget står vid - Line Text `json:"line"` // linjens namn - Destination Text `json:"destination"` // slutstation - Correct bool `json:"correct"` // tar spelaren till rätt resmål + Platform string `json:"platform"` // vilket perrong-tecken (P/p) tåget står vid + Track string `json:"track"` // spårnummer som visas på skyltar/HUD, t.ex. "2" + Line Text `json:"line"` // linjens namn + Destination Text `json:"destination"` // slutstation + Correct bool `json:"correct"` // tar spelaren till rätt resmål + Schedule Schedule `json:"schedule"` +} + +// SignText är texten på en skyltruta ('i'). +type SignText struct { + X int `json:"x"` + Y int `json:"y"` + Text Text `json:"text"` } // Level är en bana. type Level struct { - ID string `json:"id"` - Name Text `json:"name"` - Difficulty int `json:"difficulty"` // 1 (lätt) … 5 (svår) - Destination Text `json:"destination"` // dit spelaren ska - TimeLimitSec int `json:"timeLimitSec"` // 0 = ingen tidsgräns - Grid []string `json:"grid"` - Trains []Train `json:"trains"` + ID string `json:"id"` + Name Text `json:"name"` + Difficulty int `json:"difficulty"` // 1 (lätt) … 5 (svår) + Destination Text `json:"destination"` // dit spelaren ska + TimeLimitSec int `json:"timeLimitSec"` // 0 = ingen tidsgräns + FareYen int `json:"fareYen"` // vad spärren drar från kortet (0 = standard) + Grid []string `json:"grid"` + Trains []Train `json:"trains"` + Signs []SignText `json:"signs"` } // Point är en rutkoordinat. @@ -161,7 +201,8 @@ func (l *Level) Validate() error { return fmt.Errorf("level %s: saknar tåg", l.ID) } correct := 0 - for i, tr := range l.Trains { + for i := range l.Trains { + tr := &l.Trains[i] if len(tr.Platform) != 1 || (Tile(tr.Platform[0]) != Platform && Tile(tr.Platform[0]) != WrongPlatform) { return fmt.Errorf("level %s: tåg %d har ogiltig perrong %q", l.ID, i, tr.Platform) } @@ -171,10 +212,87 @@ func (l *Level) Validate() error { if tr.Correct { correct++ } + sc := &tr.Schedule + if sc.IntervalSec == 0 { + sc.IntervalSec = DefaultIntervalSec + } + if sc.DwellSec == 0 { + sc.DwellSec = DefaultDwellSec + } + if sc.OffsetSec < 0 || sc.IntervalSec <= 0 || sc.DwellSec <= 0 || sc.DwellSec >= sc.IntervalSec { + return fmt.Errorf("level %s: tåg %d har orimlig tidtabell %+v (kräver offset ≥ 0, 0 < dwell < interval)", l.ID, i, *sc) + } } if correct == 0 { return fmt.Errorf("level %s: inget tåg är markerat som rätt", l.ID) } + if l.FareYen == 0 { + l.FareYen = DefaultFareYen + } + if l.FareYen < 0 { + return fmt.Errorf("level %s: negativ avgift", l.ID) + } + // Skyltar: varje post måste stå på en 'i'-ruta och varje 'i'-ruta ha text. + seen := map[Point]bool{} + for i, sg := range l.Signs { + p := Point{sg.X, sg.Y} + if l.At(p.X, p.Y) != Sign { + return fmt.Errorf("level %s: skylt %d på (%d,%d) står inte på en skyltruta", l.ID, i, p.X, p.Y) + } + if seen[p] { + return fmt.Errorf("level %s: två skyltar på (%d,%d)", l.ID, p.X, p.Y) + } + if len(sg.Text) == 0 { + return fmt.Errorf("level %s: skylt på (%d,%d) saknar text", l.ID, p.X, p.Y) + } + seen[p] = true + } + for _, p := range l.Find(Sign) { + if !seen[p] { + return fmt.Errorf("level %s: skyltrutan (%d,%d) saknar text i \"signs\"", l.ID, p.X, p.Y) + } + } + return nil +} + +// SignAt returnerar skylten på en ruta, eller nil. +func (l *Level) SignAt(p Point) *SignText { + for i := range l.Signs { + if l.Signs[i].X == p.X && l.Signs[i].Y == p.Y { + return &l.Signs[i] + } + } + return nil +} + +// Path returnerar kortaste vägen från from till to (båda inkluderade) via +// rutor som canEnter godkänner, eller nil om ingen väg finns. +func (l *Level) Path(from, to Point, canEnter func(Point) bool) []Point { + prev := map[Point]Point{from: from} + queue := []Point{from} + for len(queue) > 0 { + p := queue[0] + queue = queue[1:] + if p == to { + var path []Point + for ; p != from; p = prev[p] { + path = append(path, p) + } + path = append(path, from) + for i, j := 0, len(path)-1; i < j; i, j = i+1, j-1 { + path[i], path[j] = path[j], path[i] + } + return path + } + for _, d := range [...]Point{{1, 0}, {-1, 0}, {0, 1}, {0, -1}} { + n := Point{p.X + d.X, p.Y + d.Y} + if _, ok := prev[n]; ok || !canEnter(n) { + continue + } + prev[n] = p + queue = append(queue, n) + } + } return nil } @@ -215,6 +333,37 @@ func (l *Level) Solvable() bool { return false } +// UngatedPlatforms listar perronger som går att nå från start utan att +// passera någon spärr. Speldesignen kräver att alla perronger ligger +// innanför spärrar, så listan ska vara tom för skeppade banor. +func (l *Level) UngatedPlatforms() []Point { + start := l.SpawnPoint() + seen := map[Point]bool{start: true} + queue := []Point{start} + for len(queue) > 0 { + p := queue[0] + queue = queue[1:] + for _, d := range [...]Point{{1, 0}, {-1, 0}, {0, 1}, {0, -1}} { + n := Point{p.X + d.X, p.Y + d.Y} + t := l.At(n.X, n.Y) + if seen[n] || !t.Passable() || t == Gate { + continue + } + seen[n] = true + queue = append(queue, n) + } + } + var out []Point + for _, t := range []Tile{Platform, WrongPlatform} { + for _, p := range l.Find(t) { + if seen[p] { + out = append(out, p) + } + } + } + return out +} + // Unreachable listar interaktiva rutor (perronger, automater, skyltar) som // inte går att nå – praktiskt som varning när man bygger banor. func (l *Level) Unreachable() []Point { diff --git a/internal/level/level_test.go b/internal/level/level_test.go index 2013450..8289ff6 100644 --- a/internal/level/level_test.go +++ b/internal/level/level_test.go @@ -1,6 +1,7 @@ package level import ( + "math" "os" "path/filepath" "sort" @@ -69,15 +70,20 @@ func TestOutsideGridIsWall(t *testing.T) { func TestValidationErrors(t *testing.T) { cases := map[string]string{ - "ojämn rad": strings.Replace(sample, `"#.#.#.#"`, `"#.#.#.##"`, 1), - "okänt tecken": strings.Replace(sample, `"#.#.#.#"`, `"#.#.#X#"`, 1), - "två starter": strings.Replace(sample, `"#S..#P#"`, `"#S.S#P#"`, 1), - "ingen start": strings.Replace(sample, `"#S..#P#"`, `"#...#P#"`, 1), - "ingen perrong": strings.Replace(sample, `"#S..#P#"`, `"#S..#.#"`, 1), - "inget rätt tåg": strings.Replace(sample, `"correct": true`, `"correct": false`, 1), - "tåg vid fel perrong": strings.Replace(sample, `"platform": "P"`, `"platform": "p"`, 1), - "saknar id": strings.Replace(sample, `"id": "test"`, `"id": ""`, 1), - "trasig json": "{", + "ojämn rad": strings.Replace(sample, `"#.#.#.#"`, `"#.#.#.##"`, 1), + "okänt tecken": strings.Replace(sample, `"#.#.#.#"`, `"#.#.#X#"`, 1), + "två starter": strings.Replace(sample, `"#S..#P#"`, `"#S.S#P#"`, 1), + "ingen start": strings.Replace(sample, `"#S..#P#"`, `"#...#P#"`, 1), + "ingen perrong": strings.Replace(sample, `"#S..#P#"`, `"#S..#.#"`, 1), + "inget rätt tåg": strings.Replace(sample, `"correct": true`, `"correct": false`, 1), + "tåg vid fel perrong": strings.Replace(sample, `"platform": "P"`, `"platform": "p"`, 1), + "dwell >= interval": strings.Replace(sample, `"correct": true`, `"correct": true, "schedule": {"intervalSec": 10, "dwellSec": 10}`, 1), + "negativ offset": strings.Replace(sample, `"correct": true`, `"correct": true, "schedule": {"offsetSec": -1}`, 1), + "skylt utanför i-ruta": strings.Replace(sample, `"trains"`, `"signs": [{"x": 1, "y": 1, "text": {"sv": "x"}}], "trains"`, 1), + "i-ruta utan skylt": strings.Replace(sample, `"#S..#P#"`, `"#Si.#P#"`, 1), + "skylt utan text": strings.Replace(strings.Replace(sample, `"#S..#P#"`, `"#Si.#P#"`, 1), `"trains"`, `"signs": [{"x": 2, "y": 1, "text": {}}], "trains"`, 1), + "saknar id": strings.Replace(sample, `"id": "test"`, `"id": ""`, 1), + "trasig json": "{", } for name, src := range cases { if _, err := Parse([]byte(src)); err == nil { @@ -133,6 +139,19 @@ func TestAllShippedLevels(t *testing.T) { if u := l.Unreachable(); len(u) > 0 { t.Errorf("onåbara interaktiva rutor: %v", u) } + if u := l.UngatedPlatforms(); len(u) > 0 { + t.Errorf("perronger som nås utan att passera en spärr: %v – spärrarna måste vara flaskhalsar", u) + } + for i, tr := range l.Trains { + if tr.Track == "" { + t.Errorf("tåg %d saknar spårnummer", i) + } + } + for _, sg := range l.Signs { + if sg.Text["sv"] == "" || sg.Text["en"] == "" { + t.Errorf("skylt (%d,%d) saknar sv/en-text", sg.X, sg.Y) + } + } }) } } @@ -169,3 +188,80 @@ func TestListAndLoadFile(t *testing.T) { t.Fatal("saknad fil ska ge fel") } } + +func TestScheduleStatus(t *testing.T) { + s := Schedule{OffsetSec: 5, IntervalSec: 30, DwellSec: 10} + cases := []struct { + t float64 + at bool + secs float64 + }{ + {0, false, 5}, {5, true, 10}, {14.9, true, 0.1}, {15, false, 20}, {34.9, false, 0.1}, {35, true, 10}, {70, true, 5}, {80, false, 15}, + } + for _, c := range cases { + at, secs := s.Status(c.t) + if at != c.at || math.Abs(secs-c.secs) > 1e-9 { + t.Errorf("t=%v: at=%v secs=%.2f, vill ha at=%v secs=%.2f", c.t, at, secs, c.at, c.secs) + } + } +} + +func TestScheduleAndFareDefaults(t *testing.T) { + l := mustParse(t, sample) + sc := l.Trains[0].Schedule + if sc.IntervalSec != DefaultIntervalSec || sc.DwellSec != DefaultDwellSec || sc.OffsetSec != 0 { + t.Fatalf("standardtidtabell saknas: %+v", sc) + } + if l.FareYen != DefaultFareYen { + t.Fatalf("standardavgift saknas: %d", l.FareYen) + } +} + +func TestSignAt(t *testing.T) { + src := strings.Replace(strings.Replace(sample, `"#S..#P#"`, `"#Si.#P#"`, 1), + `"trains"`, `"signs": [{"x": 2, "y": 1, "text": {"sv": "Hej"}}], "trains"`, 1) + l := mustParse(t, src) + if sg := l.SignAt(Point{2, 1}); sg == nil || sg.Text.Get("sv") != "Hej" { + t.Fatalf("SignAt = %v", sg) + } + if l.SignAt(Point{1, 1}) != nil { + t.Fatal("ingen skylt på startrutan") + } +} + +func TestPath(t *testing.T) { + l := mustParse(t, sample) + path := l.Path(l.SpawnPoint(), Point{5, 1}, l.CanEnter) + // S(1,1) -> (1,2) -> (1,3) -> (2,3) -> (3,3) -> (4,3) -> (5,3) -> (5,2) -> (5,1) + if len(path) != 9 || path[0] != (Point{1, 1}) || path[len(path)-1] != (Point{5, 1}) { + t.Fatalf("path = %v", path) + } + for i := 1; i < len(path); i++ { + dx, dy := path[i].X-path[i-1].X, path[i].Y-path[i-1].Y + if dx*dx+dy*dy != 1 { + t.Fatalf("steg %d är inte en grannruta: %v -> %v", i, path[i-1], path[i]) + } + } + if l.Path(l.SpawnPoint(), Point{0, 0}, l.CanEnter) != nil { + t.Fatal("väg in i en vägg ska vara nil") + } + if p := l.Path(l.SpawnPoint(), l.SpawnPoint(), l.CanEnter); len(p) != 1 { + t.Fatalf("väg till sig själv = %v", p) + } +} + +func TestUngatedPlatforms(t *testing.T) { + // Perrong utan spärr framför -> rapporteras. + l := mustParse(t, sample) + if u := l.UngatedPlatforms(); len(u) != 1 || u[0] != (Point{5, 1}) { + t.Fatalf("UngatedPlatforms = %v, vill ha [(5,1)]", u) + } + // Spärr i enda passagen -> inget rapporteras, men banan är fortfarande lösbar. + l = mustParse(t, strings.Replace(sample, `"#.#...#"`, `"#.#.G.#"`, 1)) + if u := l.UngatedPlatforms(); len(u) != 0 { + t.Fatalf("UngatedPlatforms = %v, vill ha inga", u) + } + if !l.Solvable() { + t.Fatal("banan ska vara lösbar med spärren passerbar") + } +} diff --git a/internal/sim/run.go b/internal/sim/run.go new file mode 100644 index 0000000..7364bdf --- /dev/null +++ b/internal/sim/run.go @@ -0,0 +1,138 @@ +package sim + +import "gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/level" + +// Outcome är hur en banomgång står sig. +type Outcome int + +// Utfall. +const ( + Ongoing Outcome = iota + Won // klev på rätt tåg + Lost // klev på fel tåg +) + +// Card är tågkortet (IC-kortet). Saldo i yen. +type Card struct{ Balance int } + +// CanPay rapporterar om saldot räcker. +func (c *Card) CanPay(yen int) bool { return c.Balance >= yen } + +// Pay drar beloppet om det går. +func (c *Card) Pay(yen int) bool { + if !c.CanPay(yen) { + return false + } + c.Balance -= yen + return true +} + +// StartBalanceYen är kortets saldo vid banstart tills M6 ger riktig ekonomi. +const StartBalanceYen = 2000 + +// Run är tillståndet för en pågående bana: spelaren, klockan, kortet och +// utfallet. Ren logik – LevelScene ritar den, testerna kör den headless. +type Run struct { + Level *level.Level + Player *Mover + Card Card + Time float64 // sekunder sedan banstart (styr tidtabellerna) + Steps int + GatePasses int + DeniedAt float64 // när spärren senast nekade (för HUD); < 0 = aldrig + Outcome Outcome + Boarded *level.Train +} + +// NewRun startar banan med spelaren på startrutan. +func NewRun(l *level.Level, speed float64) *Run { + return &Run{ + Level: l, + Player: NewMover(l.SpawnPoint(), speed), + Card: Card{Balance: StartBalanceYen}, + DeniedAt: -1, + } +} + +// CanEnter avgör om spelaren får gå in på en ruta: inte vägg, och en spärr +// kräver att kortet kan betala avgiften (att gå mellan två spärrutor är gratis). +func (r *Run) CanEnter(p level.Point) bool { + t := r.Level.At(p.X, p.Y) + if !t.Passable() { + return false + } + if t == level.Gate && !r.inGate() && !r.Card.CanPay(r.Level.FareYen) { + r.DeniedAt = r.Time + return false + } + return true +} + +func (r *Run) inGate() bool { + return r.Level.At(r.Player.Tile.X, r.Player.Tile.Y) == level.Gate +} + +// Step för banan framåt dt sekunder. want är hållen riktning, board=true +// betyder att spelaren försöker kliva på tåget där hen står. +func (r *Run) Step(dt float64, want Dir, board bool) { + if r.Outcome != Ongoing { + return + } + r.Time += dt + before := r.Player.Tile + wasGate := r.inGate() + r.Steps += r.Player.Step(dt, want, r.CanEnter) + if r.Player.Tile != before && r.inGate() && !wasGate { + r.Card.Pay(r.Level.FareYen) + r.GatePasses++ + } + if board && !r.Player.Moving() { + if tr, at, _ := r.PlatformHere(); tr != nil && at { + r.Board(tr) + } + } +} + +// PlatformHere returnerar tåget vid perrongen spelaren står på (nil om +// spelaren inte står på en perrong), om det är inne, och sekunder kvar +// till avgång respektive ankomst. +func (r *Run) PlatformHere() (tr *level.Train, atPlatform bool, seconds float64) { + t := r.Level.At(r.Player.Tile.X, r.Player.Tile.Y) + if t != level.Platform && t != level.WrongPlatform { + return nil, false, 0 + } + tr = r.Level.TrainAt(t) + if tr == nil { + return nil, false, 0 + } + atPlatform, seconds = tr.Schedule.Status(r.Time) + return tr, atPlatform, seconds +} + +// SignHere returnerar skylten spelaren står stilla på, annars nil. +func (r *Run) SignHere() *level.SignText { + if r.Player.Moving() { + return nil + } + return r.Level.SignAt(r.Player.Tile) +} + +// Board kliver på ett tåg och avgör banan. +func (r *Run) Board(tr *level.Train) { + r.Boarded = tr + if tr.Correct { + r.Outcome = Won + } else { + r.Outcome = Lost + } +} + +// TrainVisible rapporterar om tåget för en perrongruta står inne just nu. +func (r *Run) TrainVisible(t level.Tile) bool { + tr := r.Level.TrainAt(t) + if tr == nil { + return false + } + at, _ := tr.Schedule.Status(r.Time) + return at +} diff --git a/internal/sim/run_test.go b/internal/sim/run_test.go new file mode 100644 index 0000000..b96d66b --- /dev/null +++ b/internal/sim/run_test.go @@ -0,0 +1,182 @@ +package sim + +import ( + "path/filepath" + "testing" + + "gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/level" + "gitea.brasse-pc.eu/brasse/metro-puzzle-jp/internal/res" +) + +const frame = 1.0 / 60 + +func loadLevel(t *testing.T, id string) *level.Level { + t.Helper() + dir, err := res.Path("data", "levels") + if err != nil { + t.Fatal(err) + } + l, err := level.LoadFile(filepath.Join(dir, id+".json")) + if err != nil { + t.Fatal(err) + } + return l +} + +func dirTo(from, to level.Point) Dir { + switch { + case to.X > from.X: + return Right + case to.X < from.X: + return Left + case to.Y > from.Y: + return Down + default: + return Up + } +} + +// walk låter en bot följa en väg ruta för ruta, en bildruta i taget. +func walk(t *testing.T, r *Run, path []level.Point) { + t.Helper() + for _, next := range path[1:] { + d := dirTo(r.Player.Tile, next) + for i := 0; i < 600; i++ { + if r.Player.Tile == next && !r.Player.Moving() { + break + } + r.Step(frame, d, false) + } + if r.Player.Tile != next { + t.Fatalf("boten fastnade på %v på väg mot %v", r.Player.Tile, next) + } + } +} + +// waitForTrain står still tills tåget vid perrongen är inne. +func waitForTrain(t *testing.T, r *Run) *level.Train { + t.Helper() + for i := 0; i < 60*120; i++ { + tr, at, _ := r.PlatformHere() + if tr == nil { + t.Fatalf("står inte på en perrong: %v", r.Player.Tile) + } + if at { + return tr + } + r.Step(frame, None, false) + } + t.Fatal("tåget kom aldrig") + return nil +} + +// TestBotWinsLevel001 är simuleringstestet för M2: en bot går kortaste vägen +// från start till rätt perrong i bana 001, passerar spärren, väntar in tåget +// och kliver på – helt utan fönster. +func TestBotWinsLevel001(t *testing.T) { + l := loadLevel(t, "001-flygplatsen") + r := NewRun(l, 6) + goal := l.Find(level.Platform)[0] + path := l.Path(r.Player.Tile, goal, r.CanEnter) + if path == nil { + t.Fatal("ingen väg till perrongen") + } + walk(t, r, path) + + if r.Outcome != Ongoing { + t.Fatal("banan ska inte vara avgjord innan man kliver på") + } + r.Step(frame, None, true) // Enter medan tåget kanske inte är inne får inte avgöra fel + tr := waitForTrain(t, r) + if !tr.Correct { + t.Fatal("rätt perrong ska ha rätt tåg") + } + r.Step(frame, None, true) + if r.Outcome != Won || r.Boarded != tr { + t.Fatalf("outcome=%v boarded=%v", r.Outcome, r.Boarded) + } + if r.GatePasses != 1 || r.Card.Balance != StartBalanceYen-l.FareYen { + t.Fatalf("spärrpassager=%d saldo=%d (vill ha 1 och %d)", r.GatePasses, r.Card.Balance, StartBalanceYen-l.FareYen) + } + if r.Steps != len(path)-1 { + t.Fatalf("steg=%d, vägen är %d rutor", r.Steps, len(path)-1) + } + // Efter utfall händer inget mer. + before := r.Time + r.Step(frame, Right, true) + if r.Time != before { + t.Fatal("Step ska vara en no-op när banan är avgjord") + } +} + +func TestBotLosesOnWrongTrain(t *testing.T) { + l := loadLevel(t, "001-flygplatsen") + r := NewRun(l, 6) + goal := l.Find(level.WrongPlatform)[0] + walk(t, r, l.Path(r.Player.Tile, goal, r.CanEnter)) + tr := waitForTrain(t, r) + r.Step(frame, None, true) + if r.Outcome != Lost || r.Boarded != tr || tr.Correct { + t.Fatalf("outcome=%v boarded=%+v", r.Outcome, r.Boarded) + } +} + +func TestGateNeedsCardBalance(t *testing.T) { + l := loadLevel(t, "001-flygplatsen") + r := NewRun(l, 6) + gate := l.Find(level.Gate)[0] + r.Card.Balance = 0 + if r.CanEnter(gate) { + t.Fatal("spärren ska stoppa ett tomt kort") + } + if r.DeniedAt < 0 { + t.Fatal("nekandet ska registreras för HUD:en") + } + if l.Path(r.Player.Tile, l.Find(level.Platform)[0], r.CanEnter) != nil { + t.Fatal("utan saldo ska perrongen vara onåbar") + } + r.Card.Balance = l.FareYen + if !r.CanEnter(gate) { + t.Fatal("med exakt avgiften ska spärren släppa igenom") + } + // Mellan två spärrutor är det gratis: ställ spelaren i spärren. + r.Player.Tile, r.Player.Target = gate, gate + r.Card.Balance = 0 + other := level.Point{X: gate.X + 1, Y: gate.Y} + if l.At(other.X, other.Y) == level.Gate && !r.CanEnter(other) { + t.Fatal("att gå mellan två spärrutor ska inte kosta") + } +} + +func TestBoardOnlyWhenTrainPresent(t *testing.T) { + l := loadLevel(t, "001-flygplatsen") + r := NewRun(l, 6) + goal := l.Find(level.Platform)[0] + walk(t, r, l.Path(r.Player.Tile, goal, r.CanEnter)) + // Spola fram till en tidpunkt då tåget är borta. + for { + _, at, _ := r.PlatformHere() + if !at { + break + } + r.Step(frame, None, false) + } + r.Step(frame, None, true) + if r.Outcome != Ongoing { + t.Fatal("man ska inte kunna kliva på ett tåg som inte är inne") + } +} + +func TestSignHereOnlyWhenStill(t *testing.T) { + l := loadLevel(t, "001-flygplatsen") + r := NewRun(l, 6) + sign := l.Find(level.Sign)[0] + walk(t, r, l.Path(r.Player.Tile, sign, r.CanEnter)) + if sg := r.SignHere(); sg == nil || sg.Text.Get("sv") == "" { + t.Fatal("stilla på skyltrutan ska ge skylten") + } + r.Step(frame, Left, false) + if r.Player.Moving() && r.SignHere() != nil { + t.Fatal("i rörelse visas ingen skylt") + } +} diff --git a/resources/data/levels/001-flygplatsen.json b/resources/data/levels/001-flygplatsen.json index ba78b00..29bfb2b 100644 --- a/resources/data/levels/001-flygplatsen.json +++ b/resources/data/levels/001-flygplatsen.json @@ -1,9 +1,19 @@ { "id": "001-flygplatsen", - "name": { "sv": "Flygplatsen", "en": "The Airport", "ja": "空港" }, + "name": { + "sv": "Flygplatsen", + "en": "The Airport", + "ja": "空港" + }, "difficulty": 1, - "destination": { "ja": "新宿", "romaji": "Shinjuku", "sv": "Shinjuku", "en": "Shinjuku" }, + "destination": { + "ja": "新宿", + "romaji": "Shinjuku", + "sv": "Shinjuku", + "en": "Shinjuku" + }, "timeLimitSec": 0, + "fareYen": 200, "grid": [ "####################", "#S.....#.....i.....#", @@ -13,23 +23,71 @@ "#...#....#.....#.#.#", "#.###.##.#####.#.#.#", "#.....#T.......#...#", - "#.#####.#########.##", + "#######.#########.##", "#....GG......i..GG.#", - "#..p.....##......P.#", + "#..p.############P.#", "####################" ], "trains": [ { "platform": "P", - "line": { "ja": "成田エクスプレス", "romaji": "Narita Ekusupuresu", "en": "Narita Express" }, - "destination": { "ja": "新宿", "romaji": "Shinjuku", "en": "Shinjuku" }, - "correct": true + "line": { + "ja": "成田エクスプレス", + "romaji": "Narita Ekusupuresu", + "en": "Narita Express" + }, + "destination": { + "ja": "新宿", + "romaji": "Shinjuku", + "en": "Shinjuku" + }, + "correct": true, + "track": "2", + "schedule": { + "offsetSec": 0, + "intervalSec": 30, + "dwellSec": 12 + } }, { "platform": "p", - "line": { "ja": "京成本線", "romaji": "Keisei Honsen", "en": "Keisei Main Line" }, - "destination": { "ja": "京成上野", "romaji": "Keisei-Ueno", "en": "Keisei-Ueno" }, - "correct": false + "line": { + "ja": "京成本線", + "romaji": "Keisei Honsen", + "en": "Keisei Main Line" + }, + "destination": { + "ja": "京成上野", + "romaji": "Keisei-Ueno", + "en": "Keisei-Ueno" + }, + "correct": false, + "track": "1", + "schedule": { + "offsetSec": 8, + "intervalSec": 30, + "dwellSec": 12 + } + } + ], + "signs": [ + { + "x": 13, + "y": 1, + "text": { + "sv": "Till tågen: följ gången neråt. Spår 1 Keisei-linjen till vänster, spår 2 Narita Express till höger.", + "en": "To the trains: follow the corridor down. Track 1 Keisei Line to the left, track 2 Narita Express to the right.", + "ja": "のりば ↓ 1番線 京成線 ← 2番線 成田エクスプレス →" + } + }, + { + "x": 13, + "y": 9, + "text": { + "sv": "Spärrar. <- Spår 1: Keisei-linjen mot Keisei-Ueno. -> Spår 2: Narita Express mot Shinjuku.", + "en": "Ticket gates. <- Track 1: Keisei Line for Keisei-Ueno. -> Track 2: Narita Express for Shinjuku.", + "ja": "改札 ← 1番線 京成線 京成上野方面 → 2番線 成田エクスプレス 新宿方面" + } } ] } diff --git a/resources/data/levels/002-shinjuku.json b/resources/data/levels/002-shinjuku.json index a24a240..eb4235c 100644 --- a/resources/data/levels/002-shinjuku.json +++ b/resources/data/levels/002-shinjuku.json @@ -6,6 +6,7 @@ "sv": "Shibuya" }, "difficulty": 2, + "fareYen": 200, "grid": [ "#################################", "#S#..i..#.#...........#.........#", @@ -24,9 +25,9 @@ "#.#.###.###.#.###.#.#.#######.#.#", "#...#T#.#..i#.#.#...#.....#...#.#", "#####.#.#.#.#.#.#####.#####.###.#", - "#.....#.#.#.#..i....#.......#G..#", - "#.#.#.#.#.#########.#########.#.#", - "#p..#...#.....................#P#", + "#.....#.#.#.#..i....#.......#...#", + "#G#.#.#.#.#########.#########.#G#", + "#pG.#...#.....................#P#", "#################################" ], "id": "002-shinjuku", @@ -35,6 +36,35 @@ "ja": "新宿駅", "sv": "Shinjuku station" }, + "signs": [ + { + "text": { + "en": "Shinjuku Station. Track 14 Yamanote Line for Shibuya: far bottom right. Track 8 Chuo Line for Tokyo: far bottom left.", + "ja": "新宿駅 14番線 山手線 渋谷方面 → 南東 8番線 中央線 東京方面 → 南西", + "sv": "Shinjuku station. Spår 14 Yamanote-linjen mot Shibuya: längst ner till höger. Spår 8 Chuo-linjen mot Tokyo: längst ner till vänster." + }, + "x": 5, + "y": 1 + }, + { + "text": { + "en": "\u003c- Track 8 Chuo Line (Tokyo) Track 14 Yamanote Line (Shibuya) -\u003e", + "ja": "← 8番線 中央線 東京方面 14番線 山手線 渋谷方面 →", + "sv": "\u003c- Spår 8 Chuo-linjen (Tokyo) Spår 14 Yamanote-linjen (Shibuya) -\u003e" + }, + "x": 11, + "y": 15 + }, + { + "text": { + "en": "Track 14 Yamanote Line for Shibuya: the gate in the south-east corner.", + "ja": "14番線 山手線 渋谷方面 → 南東口改札", + "sv": "Spår 14 Yamanote-linjen mot Shibuya: spärren i sydöstra hörnet." + }, + "x": 15, + "y": 17 + } + ], "timeLimitSec": 0, "trains": [ { @@ -49,21 +79,33 @@ "ja": "山手線", "romaji": "Yamanote-sen" }, - "platform": "P" + "platform": "P", + "schedule": { + "dwellSec": 8, + "intervalSec": 20, + "offsetSec": 3 + }, + "track": "14" }, { "correct": false, "destination": { "en": "Tokyo", "ja": "東京", - "romaji": "Tōkyō" + "romaji": "Tokyo" }, "line": { - "en": "Chūō Line", + "en": "Chuo Line", "ja": "中央線", - "romaji": "Chūō-sen" + "romaji": "Chuo-sen" }, - "platform": "p" + "platform": "p", + "schedule": { + "dwellSec": 8, + "intervalSec": 25, + "offsetSec": 10 + }, + "track": "8" } ] } \ No newline at end of file diff --git a/resources/sprites/tiles.png b/resources/sprites/tiles.png index c2df2e3d997988aa9b7d55991418a07e3f6f5857..ab95afebb34a5fbcb45a1c694c877170850dbc31 100644 GIT binary patch literal 692 zcmV;l0!#ggP)FMNkV@%;4%s9x`n`8lYAjU!`FMtuJz zsFX~47ZIHp9Y!KFD}adRI<64W*w33tX!I&Z<>f7?fW{1+%ZwuzrU?iDl|^91(CUWz z3sEVuZ1;P!1#5=v$^T>myg%JOKl`#3pgtw*Dim72aO>LPUczy#sevi0IwL zPV}@$%{K=>osaSW9mJ)QYO5IqiePX6{(LnQu6k=3uAQUnj(9?|%AC@q<$ z@b5N{`_YX92kU!m*Oo3e;mTI3{-op?N~NGm@BgRn-{TUk a_%#63Whg<*s+QLP0000c{CI7IMI2bT_Y5JX}@!GCp7YAAwh7l)#P zE&-Pk6bUMbph6ZA9v&!xBa7f91SPrqCU=+j;yce={`vV0{k->n={tV>(%(_z%(*)V z!tDMTY70|{i9~{{7FEkirPExEM7}{EU~)^xsK{?TqdHu_aFUAiSMF2s@WBRELMD4k zb%lzX_Z|ZpHiAAbIa{e+^$#QmIy`WqU zHk+}l02Y7U;zoc|&8U=aQzbMjJ|qQ=Aat5^cnaFN`$G1nszaqE{@w(;IPoe3cq0 zA %v i %s", from, to, l.ID) + } + var parts []string + dir, n := "", 0 + flush := func() { + if n > 0 { + parts = append(parts, fmt.Sprintf("move %s %d", dir, n)) + } + } + for i := 1; i < len(path); i++ { + d := dirName(path[i-1], path[i]) + if d != dir { + flush() + dir, n = d, 0 + } + n++ + } + flush() + return strings.Join(parts, "; "), nil +} + +func dirName(a, b level.Point) string { + switch { + case b.X > a.X: + return "right" + case b.X < a.X: + return "left" + case b.Y > a.Y: + return "down" + default: + return "up" + } } func main() { root := repoRoot() out := filepath.Join(root, "doc", "img") must(os.MkdirAll(out, 0o755)) + must(buildShots(filepath.Join(root, "resources", "data", "levels"))) build := exec.Command("go", "run", "./tools/build", "-mode", "debug") build.Dir, build.Stdout, build.Stderr = root, os.Stdout, os.Stderr @@ -43,14 +106,14 @@ func main() { exe += ".exe" } for _, s := range shots { - args := []string{"-script", fmt.Sprintf(s.script, filepath.Join(out, s.file))} + args := []string{"-script", strings.ReplaceAll(s.script, "$OUT", out)} if s.level != "" { args = append(args, "-level", s.level) } run := exec.Command(exe, args...) run.Dir, run.Stdout, run.Stderr = filepath.Dir(exe), os.Stdout, os.Stderr if err := run.Run(); err != nil { - fail(fmt.Errorf("%s: %w", s.file, err)) + fail(fmt.Errorf("%s: %w", s.script, err)) } } fmt.Println("klart:", out)