From 86489aca2261c1d74ebb1d1cb094189bda94f9cc Mon Sep 17 00:00:00 2001 From: Thomas Rimelen Date: Wed, 26 Aug 2026 17:53:42 +0200 Subject: [PATCH] pacman v1 --- .gitignore | 8 + Makefile | 0 README.md | 1 + asset/enemy/ghostA_D.png | Bin 0 -> 262 bytes asset/enemy/ghostA_L.png | Bin 0 -> 246 bytes asset/enemy/ghostA_R.png | Bin 0 -> 249 bytes asset/enemy/ghostA_U.png | Bin 0 -> 262 bytes asset/enemy/ghostB_D.png | Bin 0 -> 262 bytes asset/enemy/ghostB_L.png | Bin 0 -> 246 bytes asset/enemy/ghostB_R.png | Bin 0 -> 249 bytes asset/enemy/ghostB_U.png | Bin 0 -> 262 bytes asset/enemy/ghostC_D.png | Bin 0 -> 262 bytes asset/enemy/ghostC_L.png | Bin 0 -> 246 bytes asset/enemy/ghostC_R.png | Bin 0 -> 249 bytes asset/enemy/ghostC_U.png | Bin 0 -> 262 bytes asset/enemy/ghostD_D.png | Bin 0 -> 262 bytes asset/enemy/ghostD_L.png | Bin 0 -> 246 bytes asset/enemy/ghostD_R.png | Bin 0 -> 249 bytes asset/enemy/ghostD_U.png | Bin 0 -> 262 bytes asset/enemy/ghost_blue.png | Bin 0 -> 250 bytes asset/player/player_DA.png | Bin 0 -> 227 bytes asset/player/player_DB.png | Bin 0 -> 228 bytes asset/player/player_DC.png | Bin 0 -> 222 bytes asset/player/player_LA.png | Bin 0 -> 221 bytes asset/player/player_LB.png | Bin 0 -> 224 bytes asset/player/player_LC.png | Bin 0 -> 220 bytes asset/player/player_RA.png | Bin 0 -> 221 bytes asset/player/player_RB.png | Bin 0 -> 225 bytes asset/player/player_RC.png | Bin 0 -> 221 bytes asset/player/player_UA.png | Bin 0 -> 226 bytes asset/player/player_UB.png | Bin 0 -> 230 bytes asset/player/player_UC.png | Bin 0 -> 225 bytes asset/walls/0.png | Bin 0 -> 119 bytes asset/walls/1.png | Bin 0 -> 115 bytes asset/walls/10.png | Bin 0 -> 95 bytes asset/walls/11.png | Bin 0 -> 110 bytes asset/walls/12.png | Bin 0 -> 117 bytes asset/walls/13.png | Bin 0 -> 111 bytes asset/walls/14.png | Bin 0 -> 111 bytes asset/walls/15.png | Bin 0 -> 119 bytes asset/walls/2.png | Bin 0 -> 115 bytes asset/walls/3.png | Bin 0 -> 114 bytes asset/walls/4.png | Bin 0 -> 115 bytes asset/walls/5.png | Bin 0 -> 97 bytes asset/walls/6.png | Bin 0 -> 116 bytes asset/walls/7.png | Bin 0 -> 111 bytes asset/walls/8.png | Bin 0 -> 115 bytes asset/walls/9.png | Bin 0 -> 113 bytes config.json | 13 ++ mazegenerator-00001-py3-none-any.whl | Bin 0 -> 4336 bytes score.json | 26 +++ src/__init__.py | 0 src/__main__.py | 22 +++ src/collectibles.py | 61 +++++++ src/config.py | 74 +++++++++ src/error.py | 28 ++++ src/game.py | 160 +++++++++++++++++++ src/input.py | 93 +++++++++++ src/player.py | 80 ++++++++++ src/render.py | 231 +++++++++++++++++++++++++++ src/score.py | 69 ++++++++ 61 files changed, 866 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.md create mode 100644 asset/enemy/ghostA_D.png create mode 100644 asset/enemy/ghostA_L.png create mode 100644 asset/enemy/ghostA_R.png create mode 100644 asset/enemy/ghostA_U.png create mode 100644 asset/enemy/ghostB_D.png create mode 100644 asset/enemy/ghostB_L.png create mode 100644 asset/enemy/ghostB_R.png create mode 100644 asset/enemy/ghostB_U.png create mode 100644 asset/enemy/ghostC_D.png create mode 100644 asset/enemy/ghostC_L.png create mode 100644 asset/enemy/ghostC_R.png create mode 100644 asset/enemy/ghostC_U.png create mode 100644 asset/enemy/ghostD_D.png create mode 100644 asset/enemy/ghostD_L.png create mode 100644 asset/enemy/ghostD_R.png create mode 100644 asset/enemy/ghostD_U.png create mode 100644 asset/enemy/ghost_blue.png create mode 100644 asset/player/player_DA.png create mode 100644 asset/player/player_DB.png create mode 100644 asset/player/player_DC.png create mode 100644 asset/player/player_LA.png create mode 100644 asset/player/player_LB.png create mode 100644 asset/player/player_LC.png create mode 100644 asset/player/player_RA.png create mode 100644 asset/player/player_RB.png create mode 100644 asset/player/player_RC.png create mode 100644 asset/player/player_UA.png create mode 100644 asset/player/player_UB.png create mode 100644 asset/player/player_UC.png create mode 100644 asset/walls/0.png create mode 100644 asset/walls/1.png create mode 100644 asset/walls/10.png create mode 100644 asset/walls/11.png create mode 100644 asset/walls/12.png create mode 100644 asset/walls/13.png create mode 100644 asset/walls/14.png create mode 100644 asset/walls/15.png create mode 100644 asset/walls/2.png create mode 100644 asset/walls/3.png create mode 100644 asset/walls/4.png create mode 100644 asset/walls/5.png create mode 100644 asset/walls/6.png create mode 100644 asset/walls/7.png create mode 100644 asset/walls/8.png create mode 100644 asset/walls/9.png create mode 100644 config.json create mode 100644 mazegenerator-00001-py3-none-any.whl create mode 100644 score.json create mode 100644 src/__init__.py create mode 100644 src/__main__.py create mode 100644 src/collectibles.py create mode 100644 src/config.py create mode 100644 src/error.py create mode 100644 src/game.py create mode 100644 src/input.py create mode 100644 src/player.py create mode 100644 src/render.py create mode 100644 src/score.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..734025d --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +**/add.mypy_cache +orga.txt +Project Management.xlsx +cell.txt +.venv +mazegeneratordemerde.txt +a.txt +**/__pycache__ \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md new file mode 100644 index 0000000..f541daa --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +*This project has been created as part of the 42 curriculum by agilliar and trimelen* diff --git a/asset/enemy/ghostA_D.png b/asset/enemy/ghostA_D.png new file mode 100644 index 0000000000000000000000000000000000000000..177aaf41942796542fca7e9fae25358064c0dacb GIT binary patch literal 262 zcmeAS@N?(olHy`uVBq!ia0y~yU~m9oCT0c(hU@GzeHj=ScmjMvTp1V`{xdNA{|_P- zGVXiGz`(##666;Q656cm(80jKu+-DVF{EP7+e?OghZIDP6-rnzeLuL++V{uxC4BEn zKbY7$wQs7B>VNQnQKn>*re?sTKDAjtbf5hFbWd4@<70AQ!sjTXqidwE>7LP4`y?r< zaP|emx`>I>POzR4)0oA0%Puc;i*kuGi|_M;{U4=zzt=vlWf4n|_S4^S$JoN=Va@4; z{|%2#n;#k0|6sVXaE;QN6?4`tPZs7mmDwHRz`Z1tZQ{O%;?H>V7$yW+tB52sFfcH9 My85}Sb4q9e09tHk5C8xG literal 0 HcmV?d00001 diff --git a/asset/enemy/ghostA_L.png b/asset/enemy/ghostA_L.png new file mode 100644 index 0000000000000000000000000000000000000000..919077ce78671150205f55e3dd0426f52c7f83e8 GIT binary patch literal 246 zcmeAS@N?(olHy`uVBq!ia0y~yU~m9oCT0c(hU@GzeHj=ScmjMvTp1V`{xdNA{|_P- zGVXiGz`(##666;Q656cm(80jKFxAt=F{EP7+e_A5M-&9u5_F$&6w7b-$Uk7uZRzxG zvhtD+_d7{91xnQfb(lHdm>9`_4!AI*ot5$5jOd()=UAT$y$LLpsn%%xR?rmCdqHtq zLpEb#L_^`Ul`Vy=%Ul}_T@}`CYWvvx`L{4@07LExW06l{tu_3uuMF#VF#MWQ#-v}; t`1@@}6Eo|#4Xif~R23Qg;?0+^w-1_7Z=Gs=>j{H?;3pzbkcqR`7b3iY3uReCBT1RnJ7JSz4sraKs=& zWRtjehzYbwO58V^YM^((gxg>l#}U_S29KVbA6d`9wSc#>&FS8v3GWvM*goSIcbI?4 y??J0x!G2laf&~ZUwlPRY`185#f5@B6`u2)K%;jnK*;U1>*yJHT2sFT|x?!dGqk?Y`oE(zxu za;!XxJ;jVylmC5Se3ATy#q5R5_lFrxjF#Ija7H*r8wLE7u1nm@=(KWLbXk3nBgiwJ Lu6{1-oD!MJ?bm4$1R-mI9jZh5jW&#BDr7zgepp==ZPJrsY&o5wIA$XZ1t RnSp_U!PC{xWt~$(698F8XhHx0 literal 0 HcmV?d00001 diff --git a/asset/enemy/ghostB_L.png b/asset/enemy/ghostB_L.png new file mode 100644 index 0000000000000000000000000000000000000000..b1534d41931e4a1f4b12d76062a1c376d1c9972e GIT binary patch literal 246 zcmeAS@N?(olHy`uVBq!ia0y~yU~m9oCT0c(hU@GzeHj=ScmjMvTp1V`zD?Em|NlP& z!~ba$j$UD4U|=Z;@(X78|NsAHU55?^28OAgE{-7;bKYLE<~pJvz?PuE8SSi$|7JwzJUqwxT$%$WL@UkVCbr_Zd2RG-p{{aiFTm;1_ScguQ*xgnH{#UNd;~-2BLT2CfCXm2FP<7EO4+Fu?X1 zzqrHvOMVYp^$PaO`W7rWAh(S{I>MjNZT~~wWY+hG`sY1PnJL1+z`)??>gTe~DWM4f DBn@56 literal 0 HcmV?d00001 diff --git a/asset/enemy/ghostB_U.png b/asset/enemy/ghostB_U.png new file mode 100644 index 0000000000000000000000000000000000000000..a260f74d99ed845c19447914053da9d276798a9c GIT binary patch literal 262 zcmeAS@N?(olHy`uVBq!ia0y~yU~m9oCT0c(hU@GzeHj=ScmjMvTp1V`zD?C&VEF(4 z|Np5y3ET_}3@jxd!lO9n8}z}%~qy<)9Xfj!(rcs!aYpqcgGz5P$#!X+<|FJBGz$jC)NmDamQlHwaAG%Nee!8cu!tpUVFyV8Q(a|;1*L2Tl zs(q3aRXF>CVO_+;X(w3Eh-u7Xyk(acx<$FfnZ@_{!Tyg@z29pe*RqHuNc-vUxMOT# z^RVW0!vBWHrp=EG>whp@S-3{&&5AkemM07IoXYHuao}DO$~JM|L-A+4c?=VRtW`vk R85kHCJYD@<);T3K0RV@5XEp!; literal 0 HcmV?d00001 diff --git a/asset/enemy/ghostC_L.png b/asset/enemy/ghostC_L.png new file mode 100644 index 0000000000000000000000000000000000000000..bdd3e177d3c645cb702eb38bfda308d755b35eee GIT binary patch literal 246 zcmeAS@N?(olHy`uVBq!ia0y~yU~m9oCT0c(hU@GzeHj=ScmjMvTp1V`{x|&p|NlP& z!~X=HLT&~I29}Z_zhH*{|Nn2+b?9JVV3_LZ;uumf=j|nHt|JNpYzey0IEv-Bd*mOm z=eBfuH(7Z}hx?tRn*yb3f;!BcZ%mBjKL=cx(ay^FZ$@;^!*i_9h28{~%2aDKek*7S z=)Iu0ts$E+F`}Vx+RBzf)@7~@hOP?hHnn~1{rp>)HGm=agt5pcvDO;?)>nr0I~aaV yDPz(vY5e^*qluaI+XmJf2dat;e(~l@*xLt9sJBivKFGkpz~JfX=d#Wzp$Pz;`dc9Y literal 0 HcmV?d00001 diff --git a/asset/enemy/ghostC_R.png b/asset/enemy/ghostC_R.png new file mode 100644 index 0000000000000000000000000000000000000000..f4c823de4a2041754afa5fa62a3b7da99bf7f204 GIT binary patch literal 249 zcmeAS@N?(olHy`uVBq!ia0y~yU~m9oCT0c(hU@GzeHj=ScmjMvTp1V`{x|&p|NlP& z!~X=HLT&~I29}Z_zhH*{|Nn2+b?9JVV3^_Q;uumf=k2B4yhjvx*b+2sn!Z2m`r`7) zf1==~^Su+4Ow14Y7XDV@N>KNhGEwN|9fkT&%ql&V5dshTcODgc7gL~jP%iZN#&)3$ zeszwk9I{E=JH!OqBqi<}O*POvV8U&%jN^#wHG@ab&5x{S;99_2+2(X_(S-L418kr1 zi#yD}1~p28Yn$HlIz6Q<|DCxBQb>$XzvX{*#u zNp?+0w`f`&_C(dHF_ST|nypOzrq_-5hQq!Mg?pIJ?~XbAp-yg(xC7IcM6QGTxg?xt z$g%P$_7pQ-P5$?R@kR0*7PA*J-ydc)F4g6ck4>8&8P@+`xUz7K(wh}?)-6vK<~fzw9pk{gB$RF9zK7z^c=H%01X-(yBr`BD OFnGH9xvXs literal 0 HcmV?d00001 diff --git a/asset/enemy/ghostD_L.png b/asset/enemy/ghostD_L.png new file mode 100644 index 0000000000000000000000000000000000000000..0e4fe14c0e48d41a69b9e8b749439f4bfadeefb5 GIT binary patch literal 246 zcmeAS@N?(olHy`uVBq!ia0y~yU~m9oCT0c(hU@GzeHj=ScmjMvTp1V`8vcU-1H*r- zw13773=AwKL4Lsu|NsBrtn1Lhz`!uo)5S5QV$R!3)?7yv1lSUEpK%n+Z}-SQV9#yo z^lq~9k`DJfNjC*b)dY2zIp3HV$$t*GFr%H7@!yQ-oQLOFp9{SSES0I&X#7^t6wrG? zaa%(+V`4-@;k1=4g{;e58w_0))@^G0*!%gnFlzuq?g?X&Phzb#{H?DH>vu5xno`E3 vU()#dZAKF_>$eT8Hx5)48T{hSm$0`Fnow_@YJ8A^fq}u()z4*}Q$iB}OGjJ^ literal 0 HcmV?d00001 diff --git a/asset/enemy/ghostD_R.png b/asset/enemy/ghostD_R.png new file mode 100644 index 0000000000000000000000000000000000000000..706f7612fef4336d942f759c60192740c872afe5 GIT binary patch literal 249 zcmeAS@N?(olHy`uVBq!ia0y~yU~m9oCT0c(hU@GzeHj=ScmjMvTp1V`8vcU-1H*r- zw13773=AwKL4Lsu|NsBrtn1Lhz`!uW)5S5QV$R!3yLpc&@USIl*ff2A*!9Kbk^e-& zP3L_AaJC@1R`h@r~_58T{%T zS2<*pxOa#Nv`I?bH=1gocff?(U>U~|*J}oko|_+8&%m{Sx3bOY-l7Tb7Y5iq;}>_B zf64Dbt6sr=S>J*M2jsRfNJsedx$S?*o6P$DQ2)HgDKkYF7#J8lUHx3vIVCg!01xL~ AsQ>@~ literal 0 HcmV?d00001 diff --git a/asset/enemy/ghostD_U.png b/asset/enemy/ghostD_U.png new file mode 100644 index 0000000000000000000000000000000000000000..1b4b62509843e5ab22e96765be9b60f5e6d5db47 GIT binary patch literal 262 zcmeAS@N?(olHy`uVBq!ia0y~yU~m9oCT0c(hU@GzeHj=ScmjMvTp1V`8vg%hVEF(4 z|9{J>=r;@u3@jxd!lO9n8}z}%~qy<)9Xfj!(rcs!aYpqcgGz5P$#!X+<|FJBGP zW?*1oE(!7rX88Z#CE%JO0|UccPZ!6KiaA#=S@RuM;9*OUsA0L^&Udr(NPoyTy*uje z2@1<|G#(17JTsAVs5x|On!uw($A)+3h1^%XZhbBKC+WnCVh!ih4|&owE_R(@%3=#~ z2;ab15Y5`w-N64U<>dGab;j&`2U}w<90y+ z0|Nt7NswPK!~f`sN?REi7`i-N978JRY`wUfw^@;gC1C=$ms^5}Tk}PmQ}#@%tdE3l zYZP62qIi@yzHr(Dj)(l0x6KuN(Zv$e#pL74IxV%acGI-F29})L+vc+#Ud*w~T~w!E z?Z*A&?cr;F$!)e-%r(vZ;ECtw*dq;Wgw`$bS7Xf9EidW6Fk{V&Qz@xpYOBo+BOh)& g{_nDUorU?WLp#D-jg^)#FfcH9y85}Sb4q9e0IG9S{{R30 literal 0 HcmV?d00001 diff --git a/asset/player/player_DB.png b/asset/player/player_DB.png new file mode 100644 index 0000000000000000000000000000000000000000..71ab862c38a3a8d08912bd2d8cd22ee8e280c116 GIT binary patch literal 228 zcmeAS@N?(olHy`uVBq!ia0y~yU~m9oMrH;Eh6&3b&t+g>U<>dGab;j&`2U}w<90y+ z0|Nt7NswPK!~f`sN?REi7`i=O978JR?7d*kcUXakEkT*h?OI;U5-ss=7WdPEMJ+u% z9s55z3#Pul5H;-woAgb!Uyf52u4i7Qz|gM1=sTft-PQveqSo$vD7m{VyFO)(vC!hP zCwlA_8yx#Ck?MM0J1VO1n171Q!DoglZ|U<>dGab;j&`2U}w<90y+ z0|Nt7NswPK!~f`sN?REi7+O7D978JRoV{Sn#iGc=nsD^cCFP1KGQRz0?lYAmqB`u%jzQP;?_Nb%2uExF|^yOfx>FJcOIJg_M)VSe1}e+ry>v$y?Jl8_5Eu=Utr zGb^FK-<5syKEqQ7=kl-n;?~dQ#IcP(+&6%AmvOj?hD>QiN}rOypG>5kV$qU!AH|U<>dGab;j&`2U}w<90y+ z0|Nt7NswPK!~f`sN?REi7+O4C978JRY@KM$*PtNaq9`^^u5odJiR~=+j~vrPQjQ-u z)F5Gfw{7}{`8Okz>pDWeY&|e7k*$x1k3-T%z*Ky~q}4ZDG!L%38ns`=qPl?7iQmrg zjbgp$TOC)o*;=79LoT}hvucu^{PxBKWs5Ja=Y2mH>#y?>p7-yU<>dGab;j&`2U}w<90y+ z0|Nt7NswPK!~f`sN?REi7}`Bu978JRY@N7~x50pi#ZhER&xAk;-JW9_Jq|OOeg?kz z!Rc7?Ku!H=ZoOUXcW2j)o7u%$1#gHjO%O?YV4$nunw@oMLgLoE*Yl0un7#=8v@V40 z){6%dT<<7uUmK~C7kH;+qkQxW`9~M{o^a_cU<>dGab;j&`2U}w<90y+ z0|Nt7NswPK!~f`sN?REi7@9p@978JRY@N7Ku)%#VG7?SP5?Q>uVqEn4 zSm#J{c}&(%iJsE6 zwWZqU<>dGab;j&`2U}w<90y+ z0|Nt7NswPK!~f`sN?REi7+O4C978JRY@K+Jw?To&)lkmHifOTdRL?Vwz6TSVA4+HZ zG*P^_MLh0Bey07ozXBUi>||JZkZlgH%>-$d2V%MbA=+7oq8?7jT6tePMy#eaM=@lF z?vZ(2dyB7hvw13hFPzk*m)NjUG3{r@3H7*VGt=jNe{{+u@mu>fUgJ!C-sj>Ug7@{Z ZPY&2^nXviLTLuOO22WQ%mvv4FO#oF{Q=I?+ literal 0 HcmV?d00001 diff --git a/asset/player/player_RB.png b/asset/player/player_RB.png new file mode 100644 index 0000000000000000000000000000000000000000..153771009a09c706823c0645722511c993c5cebf GIT binary patch literal 225 zcmeAS@N?(olHy`uVBq!ia0y~yU~m9oMrH;Eh6&3b&t+g>U<>dGab;j&`2U}w<90y+ z0|Nt7NswPK!~f`sN?REi7&<&%978JRY@N7~x50pi#ZhER&xAk;Ubo~aJq|OOK2Er? zQ{kf1tT|J*-~U%~mp|R|?YpI04`>`^a`a+Ndugz?WY@JT2PPzL&RRX+=#4+eSIsM{ zc5gI2GvUs`JL1zH%v22R5DjCGf5d2GYWURT$RU@Wg9$ySKD^%>F;n2)Pm}ONDsfED fxqnQ_KhL_bV%^@2Yykxf3=9mOu6{1-oD!MU<>dGab;j&`2U}w<90y+ z0|Nt7NswPK!~f`sN?REi7+O4C978JRY@N7~x50pi#ZhER&jF1dw#9~A$^k(OY*e=K zE2?ODOxC{l?RRXb{T{=c@!r9WVNqOPE`=pbD7)-+a_cg-Sq+U<>dGab;j&`2U}w<90y+ z0|Nt7NswPK!~f`sN?REi7&<*&978JRY`wUV_lN-pYk<3mo^g}=$Bb_oJ&y!~c5Qg@ zXW_$3E|xF%#q43)z@qur(A|EQqA{;?;;gn)TX=ds*MzxWlXGNPEbZ`r<@Gv2))|7K z=@&&WGWimSVU<>dGab;j&`2U}w<90y+ z0|Nt7NswPK!~f`sN?REi7>v=@v4wTP?N9-cV%)`dnWI-&e(_7b8Tb8 izE=MGQY`<8&Fa7^?(+v{U<>dGab;j&`2U}w<90y+ z0|Nt7NswPK!~f`sN?REi7&<&%978JRoV~b_mqn3><$xU<>dGab*C3{~f7eP7DkT zI-V|$Ar*6yCnPX1bFx_+W;!$>@dsn#3FDdugCCM^2R#@*yl7KrsF-xXLzf}+3+vRS SPh#pp26(#qxvXU<>dGab*C3{~f7eP7DkT z8lEnWAr*6y6AtkHb7pKZ N_H^}gS?83{1OV2x9rOSI literal 0 HcmV?d00001 diff --git a/asset/walls/10.png b/asset/walls/10.png new file mode 100644 index 0000000000000000000000000000000000000000..1efcfbda8cdbe60fa1edd3eaa9c70478b631c9e1 GIT binary patch literal 95 zcmeAS@N?(olHy`uVBq!ia0y~yU~m9oMrH;Eh6&3b&t+g>U<>dGab*C3{~f7eP7DkT sBAzaeAr*6yCnPX1bG8K@(`YVXV`ygPVcg2!D+yBQ>FVdQ&MBb@0EGY*G5`Po literal 0 HcmV?d00001 diff --git a/asset/walls/11.png b/asset/walls/11.png new file mode 100644 index 0000000000000000000000000000000000000000..c2f3e8ed1d6e8dad6817859771ae0a5bf19f7cfe GIT binary patch literal 110 zcmeAS@N?(olHy`uVBq!ia0y~yU~m9oMrH;Eh6&3b&t+g>U<>dGab*C3{~f7eP7DkT z%APKcAr*6y6AtkH_}_exovo7NF?%zQ%b_Mmrb81XQhN&67~(i2?bSPOEClKEboFyt I=akR{0D3tccK`qY literal 0 HcmV?d00001 diff --git a/asset/walls/12.png b/asset/walls/12.png new file mode 100644 index 0000000000000000000000000000000000000000..5cb94d8dc653a2055d623ad961b5773fd02030e2 GIT binary patch literal 117 zcmeAS@N?(olHy`uVBq!ia0y~yU~m9oMrH;Eh6&3b&t+g>U<>dGab*C3{~f7eP7DkT zTAnVBAr*6yCnPX1bF!7R@$?+zPu_4?f@g70;)w~3hEM)GGceotG)gltOnW85e>~sf PC`h}ftDnm{r-UW|GAJMG literal 0 HcmV?d00001 diff --git a/asset/walls/13.png b/asset/walls/13.png new file mode 100644 index 0000000000000000000000000000000000000000..2b184039ec38b7a65a6489c5e5a7179ea18446ab GIT binary patch literal 111 zcmeAS@N?(olHy`uVBq!ia0y~yU~m9oMrH;Eh6&3b&t+g>U<>dGab*C3{~f7eP7DkT zDxNNmAr*6y6AtkH`0vcnY}2F2Jb98tDvvdrP0_=Py#5S37#Vu{6(bduCz^pYdb;|# JtaD0e0swC#9AN+e literal 0 HcmV?d00001 diff --git a/asset/walls/14.png b/asset/walls/14.png new file mode 100644 index 0000000000000000000000000000000000000000..c4b299e924dbb839d88812133b7a707ee4fdac2b GIT binary patch literal 111 zcmeAS@N?(olHy`uVBq!ia0y~yU~m9oMrH;Eh6&3b&t+g>U<>dGab*C3{~f7eP7DkT zDxNNmAr*6yCnPX1bG8K@<48Z@AR)%{>Hon8?9BEp9~&9X85lCsB<&eC^@0py@O1Ta JS?83{1OQyN96|s9 literal 0 HcmV?d00001 diff --git a/asset/walls/15.png b/asset/walls/15.png new file mode 100644 index 0000000000000000000000000000000000000000..b01342bbe46eb6295d0dbc060f484359634374e4 GIT binary patch literal 119 zcmeAS@N?(olHy`uVBq!ia0y~yU~m9oMrH;Eh6&3b&t+g>U<>dGab*C3{~f7eP7DkT zI-V|$Ar*6y6AtkH_}_exovo7NF?%zQ%b_Mmrb81XQhDUrY>FOUU<>dGab*C3{~f7eP7DkT z8lEnWAr*6yCnPX1bFx_+W;*tRF}bF}P{K`8xq-1iuY##TowMf(6T>G_u7%szT{Qt| N_H^}gS?83{1OT!j9i0FG literal 0 HcmV?d00001 diff --git a/asset/walls/3.png b/asset/walls/3.png new file mode 100644 index 0000000000000000000000000000000000000000..3b26f8ae6bd74547e97f661b5fd7cae5d8aeb24e GIT binary patch literal 114 zcmeAS@N?(olHy`uVBq!ia0y~yU~m9oMrH;Eh6&3b&t+g>U<>dGab*C3{~f7eP7DkT z>Ygr+Ar*6y6AtkHb3VW>@tED`5Yw><5~)1S$3VQEM8;%~1;z}Uyu>f}n%}Dd>GgE= Kb6Mw<&;$VOiytxo literal 0 HcmV?d00001 diff --git a/asset/walls/4.png b/asset/walls/4.png new file mode 100644 index 0000000000000000000000000000000000000000..66f49a14f3b5383723efc2d562f039161f48555b GIT binary patch literal 115 zcmeAS@N?(olHy`uVBq!ia0y~yU~m9oMrH;Eh6&3b&t+g>U<>dGab*C3{~f7eP7DkT z8lEnWAr*6yCnPX1bFx_+W;!$>@dsn#3FDdugCCM^2R#`6FJw4)fS1AiyU=deNqz<( N&7Q7)F6*2Ung9SlADjRH literal 0 HcmV?d00001 diff --git a/asset/walls/5.png b/asset/walls/5.png new file mode 100644 index 0000000000000000000000000000000000000000..63b380416f53e9e6062f205dba988afeebf7a3af GIT binary patch literal 97 zcmeAS@N?(olHy`uVBq!ia0y~yU~m9oMrH;Eh6&3b&t+g>U<>dGab*C3{~f7eP7DkT uVxBIJAr*6y6AtkHb7pKZU<>dGab*C3{~f7eP7DkT znw~C>Ar*6y4H6ibIoT{0GaUQDkZjXnDB&h~cwXYc5AsZP2PEVr3YZx>nI)88U-Ehc O((UQ$=d#Wzp$P!W%N^1H literal 0 HcmV?d00001 diff --git a/asset/walls/7.png b/asset/walls/7.png new file mode 100644 index 0000000000000000000000000000000000000000..36c05c2905b13f79a326adc94342e78208f704ca GIT binary patch literal 111 zcmeAS@N?(olHy`uVBq!ia0y~yU~m9oMrH;Eh6&3b&t+g>U<>dGab*C3{~f7eP7DkT zDxNNmAr*6y6AtkHb3VW>@tED`5Yw><5~)1aZLCVW9&$36GcZioRAp7$wD%N9qo=E% J%Q~loCIE0#9Jv4h literal 0 HcmV?d00001 diff --git a/asset/walls/8.png b/asset/walls/8.png new file mode 100644 index 0000000000000000000000000000000000000000..7632e522cd5816cbbd161fd62467aba64024edcc GIT binary patch literal 115 zcmeAS@N?(olHy`uVBq!ia0y~yU~m9oMrH;Eh6&3b&t+g>U<>dGab*C3{~f7eP7DkT z8lEnWAr*6yCnPX1bF!7R@$@M2Cx19B!LztW@IZsRxeeoi$t+4+85o{jX8S6y_HzwL Nv!|<{%Q~loCIHCh9*_V4 literal 0 HcmV?d00001 diff --git a/asset/walls/9.png b/asset/walls/9.png new file mode 100644 index 0000000000000000000000000000000000000000..adc0bc84c5f18e7fa8f4207d5769352d69a4b100 GIT binary patch literal 113 zcmeAS@N?(olHy`uVBq!ia0y~yU~m9oMrH;Eh6&3b&t+g>U<>dGab*C3{~f7eP7DkT zYMw5RAr*6y6AtkH`0vcnY}2F2Jb98tDo-_=eUBo4@`(cyLIrFLt2ji|m+~@C0crJg L^>bP0l+XkKn+Y6B literal 0 HcmV?d00001 diff --git a/config.json b/config.json new file mode 100644 index 0000000..39bbc53 --- /dev/null +++ b/config.json @@ -0,0 +1,13 @@ +{ + "highscore_filename": "score.json", + "levels": 10, + "width": 20, + "height": 20, + "lives": 3, + "pacgum": 80, + "points_per_pacgum": 1, + "points_per_super_pacgum": 5, + "points_per_ghost": 20, + "time": 180, + "cheat_mod": false +} \ No newline at end of file diff --git a/mazegenerator-00001-py3-none-any.whl b/mazegenerator-00001-py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..03d6ac1f833cbcd8bebc37ef8fb54557d738144a GIT binary patch literal 4336 zcmWIWW@Zs#U|`^2IAXgz=D1;YpeX|bgBt?_gCqk3LvCVKYI*1PW!I$^7TLKt?Q+8Qa|X#8Ql~5T7e%}xpp09*}3DEmBSTZGPCj-JR6}#mzQm^B;zG7uy2oh&tki)PG#b&*N%DEBI`M1qP>b|dkV6;m+FU z0dFG2rYeLUy8OuE^r}Z5-(2Ti-kZR>o-_Ei=HHSYzCkNxi+o8J`Hsj=#2&bITD zcd6B@#=nrCvU>ULn>`1-GTk1{C{%lNV}0hl)+K52FQ#<_&%E4GC@A#f#zgi<>)$zT z+Qsb@bV;i@IOk+?;FArXt0!GInewT}b?1s%75<;^Xy!{!N_~)IeK=EXr`YNVI$?VI zW?d@jE^*k#bK;Th&wzqmb9sMPmPy-h4v}eKI@>eNI{UlB&C8A@x3#NxZLBt$*?A?NOsmD(~ew9DVOex&;(iN4&apC1O@scxDvutf+RM6-^$R zvv*C1HCTCl@x@&{m*gDg$Vy2EKi-m=+$-Gn(*KxMKc|?>xh%nx{AXNzj%G}e?X=j_ zwZYDz@lwFx*F>FKPZefWqHOgCU<6)^*UUy zYoensC48A;^Y};egM&SsGuiGhJ-kXrP9@sFKrwD=M8=eZsk#%y!?h1i;s2T=({QCD z_=l35X2O0~qxfSY!eL4$s!v4EoMF{DciQEM&FeMhC@ni4WMVWy`JRgJ}V6}y1-t%z+*;3{QY(8srtt3mKJJn)>MZNd zzTWXn?fNSwai*rz;!#I8Tlv|k>-fukUMD&?Ri4XJd4KBP*|kzlkNUTqdbo0|;3 z-l@%|XBijPy|BA>dfwerecLYni=NxYxT1F1vPB1vscR+mJ+IVRb<3nEOX^C_`*m&; zZ+>p)uhsF2yc)ZcL8g|~^!0>0hkZTnxjlGpYjbcuSBu$)oByITd~RjrE`9y7J$Ra9 z1KY>6ze~?6dh#pA{0x$sv`8oU55IQq1B1KA7g`q|=`{W#l#>79cfy4eoXpNiDxHr? zu4Em~n76Iz?P7@|7HciMYv+XCHOdcR=r=lMlpQ7hP`pX}$45N_Wf7)?c~q@$%fa2UQs|H#CZ>dA3f}QTg%6*@*Az+gXyOESEk^6}PdQ z$0F8v*yXs2`H3CP$@Qtki!K1=4;F8@EN&{XQ_a$#FX!4uaT z7w=;WFaD|K^lnyySnQtrl@D$!de7tM;d0G1YDwU0e`1+waIEP6Hk}6?Ge4Y_nLL+w z#pQiA4KIU&jgzyUY&hf2nV+XnJdIs%bKtz&ZZ~&)`Q+rVZs)G!t<#Jd*GzuSXd8PV z+G9_g!}Y`KJwN2_6pnQ(?u++R)Bd{t}{ zbLz&GG2Ed&Cq=&O*8FE7%PaQH_fy={AnTca^SrfVuap)4HLTQ*%?o{5I3Z5z?b2^A zkLPlB#o7H1fAG=5TzamG`<0;SD$h%{x7ztGJt2QI=n~VJtUfROlU%muuO04Qbo(j# zbn54{wfj0GeY9Fz)opatIUX&K*PSx&^XZr=Nz1sNPAy^1uv?_9S;KnIb5fA6<3FE+ zF>4bI-xO7ttUYxpLf&H9CB2k)%~3nH{}WA6;8fSSvS|||^SuqX6*T5gU4BX9ZDhum zX+XQ6;M3`zVB zS!#dXy#6}b&YOGrYcXo!tH};p&t$}_`2TfX}`(WL%&-1Pfv(CdFUGBziGcaB-lT> z8p&N;I(3f7BF&i>KUA9Ztb1V3@33I=x6S9=&i;LsWAf&**Q%$d9&bJ!@qWpD%Sm&# zhn~9`{y4&vJ)qf=Lu!_WU5m!MyW8|8E!pC+;M^ngWgDk2NdC2b!miDS|C{`|^Y_;s zyVrGp*Y2*ie|Y4;YO(J233~CrO&mAX)OQ}P?a0*rxFG9gxM{!G%LUsxFaNx2TKmAq zq0ZZ`@}lUAN!GzTEFO7v?R(5n+ns66;#WH{UABc`-TUJ$^$(ORU-SHa$g(Axy>#!q z*v>;I>rTAdoOpJ__1(8irxvbnmpUO~zqoI`h5DH$w-f&NWiQ*d+f51MvAu7q`2KQ? zo=a%O@vlLXlQ-mlsbi6B64H`sP1iXk&-pU0VWS}9s*SJPJ?`YxF-@8G{P&}tS3 z<7BRFxvGg>sT)%->#)8x-(?n7kXj%%hjWHUQq=eEQzy%ACzxAS{ABlVex;*zp>OTZ zX!S$9FJ@HE6?30ry2PsV!=@!KU(R4%@>ESlH0eJ}`{L^&w{Ju~mbI5ESYz)IvhUzfh@n)u1xJ^Qmt z<*^NxJpv~>B>mVYKHrzfu=t@ugD*?WC+!F|skEJ(G7UDJ6M}>PD(#t~~p!@0>SlKIOgd*!r*Y&gAn--&g2g@%LGf_Vqx6+J3{Y zr<`ST1+0RTx(#p0ZS*f;PKy>?e6J&Svh-K(yiM699HC-?owqYrifg6C>#y+qRM@(e zZSOOAx1;HT`p-gI?{=&`_UP7$;8VPcbLy(8TgITQVfoZv@}j73{NZlfWMFpBoO_oh;Ey)ih0ie|z=IbyqI^jn!>=^eAhVK-K)J zX>TqIy>n?z^8PgAql(Q(iJ~uUZt5GqwxmuFH!FJ_yVoK63Y-15f{JTWlOuKBtl7%7 zx2hsQD)>oaK;GtCQ-o(2u{<%=i`YN4Rb=7Lc#h?tuGtIi?)`co?%Wx^b*G*@ayz^G zYq_@0_rCu~-2_ktpziamD~5rAA)0}KL789$5bojX>ce`_kn6Ak59@{4lU=dt(Cx%ol|Ib23qJ@wn<>3$eqxyP{b!oInR_lD z*LiaF$z9p6KhpT#FIf7=X#REQDPyD>*eSH)8V&_`BZ|8PO z-zhsM>vSe%$@)zfH|z;(eBJss?wv;8zLW@tz`9@#nbZ6^3~KfP-i%Bl%(y$g3{bG7 z5yU|0j3Re~k&VIDrv+&S;U$eZ=#i+8VyIq0C9+wdgpc0x0ht5BOByG!q4mshwu8{k zM9)kh^FVk><76H}W`eU8x`F5!0Av^lFKIO3C1fBr4?odgYrWQPE6 VRyL3hHU>6^$BYaN-$9v_0RYw6k>LOU literal 0 HcmV?d00001 diff --git a/score.json b/score.json new file mode 100644 index 0000000..02d2466 --- /dev/null +++ b/score.json @@ -0,0 +1,26 @@ +[ + { + "name": "Garobert", + "score": 1 + }, + { + "name": "ssss", + "score": 0 + }, + { + "name": "player", + "score": 51 + }, + { + "name": "dd", + "score": 61 + }, + { + "name": "", + "score": 0 + }, + { + "name": "kkk", + "score": 0 + } +] \ No newline at end of file diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/__main__.py b/src/__main__.py new file mode 100644 index 0000000..d2af9a8 --- /dev/null +++ b/src/__main__.py @@ -0,0 +1,22 @@ +from .game import Game +from .config import load_config +from .error import error +import importlib +import sys + + +for lib in ["pydantic", "mazegenerator", "pygame"]: + try: + importlib.import_module(lib) + except ModuleNotFoundError: + error("import missing, use Make install") + +try: + file_path = sys.argv[1] +except IndexError: + file_path = "" +config = load_config(file_path) +try: + game = Game(config) +except KeyboardInterrupt: + error("keyboardInterrupt.") diff --git a/src/collectibles.py b/src/collectibles.py new file mode 100644 index 0000000..c5e6d8e --- /dev/null +++ b/src/collectibles.py @@ -0,0 +1,61 @@ +from mazegenerator import MazeGenerator # type: ignore[import-untyped] +from abc import ABC +from .config import Configuration +from .player import Player +import random + + +class Collectibles(ABC): + def __init__(self, point: int, size: int) -> None: + self.point = point + self.size = size + + def collect(self) -> int: + return self.point + + +class Pacgum(Collectibles): + def __init__(self, config: Configuration): + point = config.points_per_pacgum + super().__init__(point, 3) + + +class SuperPacgum(Collectibles): + def __init__(self, config: Configuration): + point = config.points_per_pacgum + super().__init__(point, 6) + + +class CollectableManager: + def __init__(self) -> None: + self.collectibles: dict[tuple[int, int], Collectibles] = {} + + def fill(self, config: Configuration, maze: MazeGenerator, + player: Player) -> None: + collectibles: dict[tuple[int, int], Collectibles] = {} + h = maze._height + w = maze._width + for i in range(w): + for j in range(h): + if (i, j) == player.coordonate or maze.maze[j][i] == 15: + continue + if (i, j) in [(0, 0), (h - 1, 0), (0, w - 1), (h - 1, w - 1)]: + collectibles[(i, j)] = SuperPacgum(config) + else: + if random.randint(0, 100) < config.pacgum: + collectibles[(i, j)] = Pacgum(config) + + self.collectibles = collectibles + + def collect(self, pos: tuple[int, int]) -> tuple[int, bool]: + point = 0 + hunt = False + if self.collectibles.get(pos): + pacgum = self.collectibles.pop(pos) + point = pacgum.collect() + if isinstance(pacgum, SuperPacgum): + hunt = True + return (point, hunt) + + def is_empty(self) -> bool: + return len(self.collectibles) == 0 diff --git a/src/config.py b/src/config.py new file mode 100644 index 0000000..b45b7d4 --- /dev/null +++ b/src/config.py @@ -0,0 +1,74 @@ +from pydantic import ValidationError, BaseModel, Field +from pathlib import Path +from .error import error +import json + + +CELL_SIZE = 64 + + +class Configuration(BaseModel): + highscore_filename: str = Field(default="score.json") + levels: int = Field(ge=1, le=256, default=10) + width: int = Field(ge=5, le=20, default=13) + height: int = Field(ge=5, le=20, default=13) + lives: int = Field(ge=1, le=99, default=3) + pacgum: int = Field(ge=10, le=100, default=80) + points_per_pacgum: int = Field(ge=1, le=99, default=1) + points_per_super_pacgum: int = Field(ge=1, le=99, default=5) + points_per_ghost: int = Field(ge=1, le=99, default=20) + cheat_mod: bool = Field(default=False) + time: int = Field(ge=60, le=300, default=180) + + +def load_config(path: str) -> Configuration: + if not path: + error("no file path given, default configuration applied.", False) + return Configuration() + + file_path = Path(path) + + if not file_path.exists(): + error(f"file not found: '{path}', default configuration applied.", + False) + return Configuration() + + text = "" + for item in file_path.read_text(encoding="utf-8").split("\n"): + if ( + not item.strip().startswith("#") and + not item.strip().startswith("//") + ): + if text: + text += "\n" + text += item + + try: + data = json.loads(text) + except json.JSONDecodeError: + error(f"invalid JSON in {path}, default configuration applied..", + False) + return Configuration() + + try: + config = Configuration.model_validate(data) + + except ValidationError as err: + errors = err.errors() + + for e in errors: + field = e["loc"][0] + + if e["type"] == "missing": + error(f"missing '{field}' configuration, default" + "value applied", False) + else: + error(f"wrong value for '{field}' configuration, " + "default value applied", False) + + for e in errors: + data.pop(e["loc"][0], None) + + config = Configuration.model_validate(data) + + return config diff --git a/src/error.py b/src/error.py new file mode 100644 index 0000000..b597bae --- /dev/null +++ b/src/error.py @@ -0,0 +1,28 @@ +import sys + + +def error(err: str, close: bool = True, line: int = 0) -> None: + """ + Display an error message to the standard error stream and terminate + the program with exit code 1. + + Args: + err (str): Description of the error. + line (int, optional): Line number where the error occurred. + If provided and greater than 0, it is included in the error + message. Defaults to 0. + + Returns: + NoReturn: This function never returns because it terminates + the program. + + Raises: + SystemExit: Always raised with exit code 1. + """ + if line: + sys.stderr.write("\nError in line " + str(line) + ": " + err + "\n") + else: + sys.stderr.write("\nError: " + err + "\n") + sys.stderr.flush() + if close: + exit(1) diff --git a/src/game.py b/src/game.py new file mode 100644 index 0000000..6fe1004 --- /dev/null +++ b/src/game.py @@ -0,0 +1,160 @@ +from mazegenerator import MazeGenerator # type: ignore[import-untyped] +from .config import Configuration +from .score import ScoreManager +from .render import MenuRenderer, GameRenderer, EndingRenderer +from .render import Renderer, RenderInfo +from .input import MenuInput, GameInput, EndingInput, Input +from .player import Player +from .collectibles import CollectableManager +import pygame +import time + + +class Game: + def __init__(self, config: Configuration): + self.config = config + self.screen = pygame.display.set_mode((2560, 1440)) + self.score_manager = ScoreManager(config.highscore_filename) + self.score = 0 + self.lives = config.lives + self.maze = MazeGenerator( + (self.config.height, self.config.width), + False, (0, 0), (1, 1), 42 + ) + self.level = 0 + self.render_info = self.get_render_info() + self.renderer: Renderer = MenuRenderer( + self.screen, + self.render_info, + self.score_manager) + self.input: Input = MenuInput(self.renderer, self.new_game, self.quit) + self.collectibles = CollectableManager() + self.player = Player(self.maze.maze) + self.running = True + self.start_time = 0.0 + self.now_time = 0.0 + self.run() + + def add_collectibles(self) -> None: + self.collectibles.fill(self.config, self.maze, self.player) + + def finish_game(self, victory: bool) -> None: + self.renderer = EndingRenderer(self.screen, self.render_info, victory) + self.input = EndingInput(self.renderer, self.add_score) + + def win(self) -> None: + self.finish_game(True) + + def loose(self) -> None: + self.finish_game(False) + + def time_out(self) -> None: + if isinstance(self.renderer, GameRenderer): + self.renderer.time_out() + pygame.display.flip() + time.sleep(3) + self.loose() + + def update_time(self) -> None: + self.now_time = time.time() + remain_time = self.config.time - (self.now_time - self.start_time) + self.render_info.remain_time = round(remain_time, 1) + if remain_time <= 0: + self.time_out() + + def restart(self) -> None: + self.renderer = MenuRenderer( + self.screen, + self.render_info, + self.score_manager) + self.input = MenuInput(self.renderer, self.new_game, self.quit) + + def get_render_info(self) -> RenderInfo: + return RenderInfo( + fps=0, + width=self.config.width, + height=self.config.height, + maze=self.maze.maze, + level=self.level, + score=self.score, + lives=self.lives, + remain_time=0.0 + ) + + def next_level(self) -> None: + if self.level == 10: + self.win() + return + self.level += 1 + self.maze.generate() + self.render_info.level = self.level + self.render_info.maze = self.maze.maze + self.player.next_level((1, 1), self.maze.maze) + if isinstance(self.renderer, GameRenderer): + self.renderer.next_level() + self.add_collectibles() + self.start_time = time.time() + + def new_game(self) -> None: + self.score = 0 + self.level = 0 + self.lives = self.config.lives + self.renderer = GameRenderer( + self.screen, + self.render_info, + self.player, + self.collectibles) + self.input = GameInput(self.player, self.next_level, self.kill_player) + self.next_level() + + def hunt(self) -> None: + print("Hunting ghost") + pass + + def kill_player(self) -> None: + self.lives -= 1 + self.render_info.lives = self.lives + if self.lives == 0: + self.loose() + else: + self.player.reset() + + def pick_up(self) -> None: + point, hunt = self.collectibles.collect(self.player.coordonate) + self.score += point + self.render_info.score = self.score + if hunt: + self.hunt() + if self.collectibles.is_empty(): + self.next_level() + + def quit(self) -> None: + self.running = False + + def run(self) -> None: + pygame.init() + pygame.font.init() + clock = pygame.time.Clock() + + while self.running: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + self.running = False + self.input.get_event(event) + keys = pygame.key.get_pressed() + self.input.get_key(keys) + self.render_info.fps = round(clock.get_fps(), 2) + if isinstance(self.input, GameInput): + self.player.move() + self.update_time() + if self.player.can_move(): + self.pick_up() + self.screen.fill("black") + self.renderer.render() + pygame.display.flip() + clock.tick(60) + pygame.quit() + + def add_score(self, name: str) -> None: + self.score_manager.add_score(name, self.score) + self.restart() diff --git a/src/input.py b/src/input.py new file mode 100644 index 0000000..91d325a --- /dev/null +++ b/src/input.py @@ -0,0 +1,93 @@ +from abc import ABC +from collections.abc import Callable +from .render import MenuRenderer, EndingRenderer +from .player import Player +import pygame +import time + + +class Input(ABC): + def get_event(self, event: pygame.event.Event) -> None: + return + + def get_key(self, keys: pygame.key.ScancodeWrapper) -> None: + return + + +class MenuInput(Input): + def __init__(self, renderer: MenuRenderer, new_game: Callable[[], None], + quit: Callable[[], None]) -> None: + self.renderer = renderer + self.index = 0 + self.functions = { + 0: new_game, + 1: quit, + } + self.cooldown = 0.0 + + def change_index(self, value: int) -> None: + self.index = max(0, min(1, self.index + value)) + self.renderer.index = self.index + self.cooldown = time.time() + + def validation(self) -> None: + self.functions[self.index]() + + def get_event(self, event: pygame.event.Event) -> None: + if event.type == pygame.KEYDOWN: + if event.key in [1073741906, 119]: + self.change_index(-1) + elif event.key in [1073741905, 115]: + self.change_index(1) + elif event.key in [13, 32]: + self.validation() + + +class GameInput(Input): + def __init__(self, player: Player, next_lvl: Callable[[], None], + kill: Callable[[], None]): + self.player = player + self.next_level = next_lvl + self.kill = kill + + def get_key(self, keys: pygame.key.ScancodeWrapper) -> None: + if keys[pygame.K_UP] or keys[pygame.K_w]: + self.player.go_to((0, -1)) + elif keys[pygame.K_DOWN] or keys[pygame.K_s]: + self.player.go_to((0, 1)) + elif keys[pygame.K_LEFT] or keys[pygame.K_a]: + self.player.go_to((-1, 0)) + elif keys[pygame.K_RIGHT] or keys[pygame.K_d]: + self.player.go_to((1, 0)) + + def get_event(self, event: pygame.event.Event) -> None: + if event.type == pygame.KEYDOWN: + if event.unicode == "n": + self.next_level() + if event.unicode == "k": + self.kill() + + +class EndingInput(Input): + def __init__(self, renderer: EndingRenderer, + add_score: Callable[[str], None]) -> None: + self.renderer = renderer + self.add_score = add_score + self.name = "" + + def get_event(self, event: pygame.event.Event) -> None: + if event.type == pygame.TEXTINPUT: + c: str = event.text + if c.isalnum(): + if len(self.name) < 10: + self.name += event.text + elif event.type == pygame.KEYDOWN: + if event.key == 8: + if self.name: + self.name = self.name[0:-1] + elif event.key == 32: + if len(self.name) < 10: + self.name += ' ' + elif event.key == 13: + self.add_score(self.name) + self.renderer.name = self.name diff --git a/src/player.py b/src/player.py new file mode 100644 index 0000000..7427558 --- /dev/null +++ b/src/player.py @@ -0,0 +1,80 @@ +from .config import CELL_SIZE as CELL_SIZE +import pygame + + +class Player: + def __init__(self, maze: list[list[int]]): + self.position = (0, 0) + self.coordonate = (0, 0) + self.direction = (0, 0) + self.start_coordonate = (0, 0) + self.speed = 4 + self.maze = maze + self.spritesheet = self.load_player_sprites() + self.sprite = self.spritesheet[(1, 0)] + + def update_position(self) -> None: + self.position = (self.coordonate[0] * CELL_SIZE, + self.coordonate[1] * CELL_SIZE) + + def next_level(self, pos: tuple[int, int], maze: list[list[int]]) -> None: + self.maze = maze + self.start_coordonate = pos + self.reset() + + def reset(self) -> None: + self.sprite = self.spritesheet[(1, 0)] + self.direction = (0, 0) + self.coordonate = self.start_coordonate + self.update_position() + + @staticmethod + def load_player_sprites() -> dict[tuple[int, int], pygame.Surface]: + player_textures = {} + for t, l in [((0, 1), "D"), + ((0, -1), "U"), + ((1, 0), "R"), + ((-1, 0), "L")]: + filename = "asset/player/player_" + str(l) + "A.png" + img = pygame.image.load(filename) + player_textures[t] = img + return player_textures + + def get_sprite(self) -> pygame.Surface: + if self.direction != (0, 0): + self.sprite = self.spritesheet[self.direction] + return self.sprite + + @staticmethod + def binary(cell: int) -> list[int]: + b = bin(cell)[2:].zfill(4) + r = [1 if i == "1" else 0 for i in b] + return r[::-1] + + def can_go(self, dir: tuple[int, int]) -> bool: + cell = self.maze[self.coordonate[1]][self.coordonate[0]] + index = [(0, -1), (1, 0), (0, 1), (-1, 0)].index(dir) + return self.binary(cell)[index] == 0 + + def can_move(self) -> bool: + x = self.position[0] + y = self.position[1] + return (x % CELL_SIZE == 0 and y % CELL_SIZE == 0) + + def go_to(self, dir: tuple[int, int]) -> None: + if not self.can_go(dir) or not self.can_move(): + return + self.direction = dir + + def move(self) -> None: + if self.direction == (0, 0): + return + x = self.position[0] + self.direction[0] * self.speed + y = self.position[1] + self.direction[1] * self.speed + self.position = (x, y) + if self.can_move(): + x = self.coordonate[0] + self.direction[0] + y = self.coordonate[1] + self.direction[1] + self.coordonate = (x, y) + if not self.can_go(self.direction): + self.direction = (0, 0) diff --git a/src/render.py b/src/render.py new file mode 100644 index 0000000..6a9c386 --- /dev/null +++ b/src/render.py @@ -0,0 +1,231 @@ +from pydantic import BaseModel +from abc import ABC, abstractmethod +from .config import CELL_SIZE as CELL_SIZE +from .score import ScoreManager +from .player import Player +from .collectibles import CollectableManager +import pygame +import time + + +class RenderInfo(BaseModel): + fps: float + width: int + height: int + maze: list[list[int]] + level: int + score: int + lives: int + remain_time: float + + +class Renderer(ABC): + def __init__(self, screen: pygame.Surface, info: RenderInfo) -> None: + self.screen = screen + self.info = info + super().__init__() + + @abstractmethod + def render(self) -> None: + ... + + def draw_text(self, text: str, font_color: tuple[int, int, int], + font_size: int, position: tuple[int, int]) -> int: + font = pygame.font.Font(None, font_size) + txt = font.render(text, True, font_color) + self.screen.blit(txt, position) + return txt.get_width() + + +class Animate_chararacter: + def __init__(self, sprite: str): + self.sprite = {letter: pygame.image.load("asset/enemy/ghost_blue.png") + if ("ghost" in sprite and letter == "R") + else pygame.image.load(sprite.replace("*", letter)) + for letter in ["L", "R"]} + + def get_sprite(self, dir: str) -> pygame.Surface: + return self.sprite[dir] + + +class MenuRenderer(Renderer): + def __init__(self, screen: pygame.Surface, info: RenderInfo, + score_manager: ScoreManager) -> None: + super().__init__(screen, info) + self.index = 0 + self.characters = self.add_animated_characters() + self.pos = (-1000, 100) + self.dir = "R" + self.score_manager = score_manager + + def add_animated_characters(self) -> list[Animate_chararacter]: + char = [Animate_chararacter("asset/player/player_*A.png")] + for letter in "ABCD": + s = "asset/enemy/ghost" + letter + "_*.png" + char.append(Animate_chararacter(s)) + return char + + def move(self) -> None: + self.pos = (self.pos[0] + 8 * (1 if self.dir == "R" else -1), + self.pos[1]) + + if self.pos[0] > 2700: + self.dir = "L" + if self.pos[0] < -600: + self.dir = "R" + + def draw_character(self) -> None: + self.move() + padding = 80 + i = 0 + for c in self.characters: + pos = (self.pos[0] + i * padding + (100 if i else 0), self.pos[1]) + self.screen.blit(c.get_sprite(self.dir), pos) + i += 1 + + def highscore(self) -> None: + x = 1800 + y = 400 + padding = 50 + + self.draw_text("High Score", (255, 255, 255), 100, (x + 50, y)) + + count = 0 + lst = sorted(self.score_manager.score_list, + key=lambda x: x.score, reverse=True) + for s in lst: + count += 1 + txt = str(count) + ". " + s.name + " : " + str(s.score) + self.draw_text(txt, (255, 255, 255), 40, + (x + 100, y + 100 + count * padding)) + if count == 10: + break + + def instructions(self) -> None: + x = 100 + y = 400 + padding = 70 + size = 40 + self.draw_text("Instructions", (255, 255, 255), 100, (x + 50, y)) + self.draw_text("Use arrows or WASD to move in the maze.", + (255, 255, 255), size, (x, y + 2 * padding)) + self.draw_text("Pick up every pacgum to win the level.", + (255, 255, 255), size, (x, y + 3 * padding)) + self.draw_text("If a ghost touch you, you lose a life.", + (255, 255, 255), size, (x, y + 4 * padding)) + self.draw_text("If your life or the timer go to 0 you lose.", + (255, 255, 255), size, (x, y + 5 * padding)) + self.draw_text("If you finish every level you win.", + (255, 255, 255), size, (x, y + 6 * padding)) + + def render(self,) -> None: + self.instructions() + self.highscore() + self.draw_character() + x = 1130 + y = 600 + padding = 150 + self.draw_text("PAC-MAN", (255, 255, 0), 200, (x - 180, 200)) + len = self.draw_text("New Game", (255, 255, 255), 80, (x, y)) + self.draw_text("Quit", (255, 255, 255), 80, (x + 80, y + padding)) + rect = pygame.Rect(x - 20, y - 20 + self.index * padding, len + 40, 92) + pygame.draw.rect(self.screen, (255, 255, 255), rect, 4) + + +class GameRenderer(Renderer): + def __init__(self, screen: pygame.Surface, info: RenderInfo, + player: Player, collectibles: CollectableManager): + super().__init__(screen, info) + self.maze = self.render_maze() + self.player = player + self.collectibles = collectibles + self.x = 500 + self.y = 80 + (1280 - self.info.height * CELL_SIZE) / 2 + + @staticmethod + def load_wall_textures() -> dict[int, pygame.Surface]: + return {n: pygame.image.load("asset/walls/" + str(n) + ".png") + for n in range(16)} + + def render_maze(self) -> pygame.Surface: + maze_surface = pygame.Surface( + (self.info.width * CELL_SIZE, self.info.height * CELL_SIZE)) + walls_textures = self.load_wall_textures() + y = 0 + for row in self.info.maze: + x = 0 + for cell in row: + img = walls_textures[cell] + maze_surface.blit(img, (x * CELL_SIZE, y * CELL_SIZE)) + x += 1 + y += 1 + return maze_surface + + def next_level(self) -> None: + self.maze = self.render_maze() + + def draw_HUD(self) -> None: + x = 200 + y = 100 + padding = 40 + self.draw_text("FPS: " + str(self.info.fps), (255, 255, 255), + 30, (x, y)) + self.draw_text("Level: " + str(self.info.level), (255, 255, 255), + 30, (x, y + padding)) + self.draw_text("Lives: " + str(self.info.lives), (255, 255, 255), + 30, (x, y + 2 * padding)) + self.draw_text("Score: " + str(self.info.score), (255, 255, 255), + 30, (x, y + 3 * padding)) + self.draw_text("Time: " + str(self.info.remain_time), (255, 255, 255), + 30, (x, y + 4 * padding)) + + def render(self) -> None: + self.draw_HUD() + self.screen.blit(self.maze, (self.x, self.y)) + player_sprite = self.player.get_sprite() + x = self.x + self.player.position[0] + y = self.y + self.player.position[1] + self.screen.blit(player_sprite, (x, y)) + for i in self.collectibles.collectibles: + pacgum = self.collectibles.collectibles[i] + size = pacgum.size + center = ( + self.x + i[0] * CELL_SIZE + CELL_SIZE // 2, + self.y + i[1] * CELL_SIZE + CELL_SIZE // 2 + ) + pygame.draw.circle(self.screen, (255, 255, 255), center, size) + + def time_out(self) -> None: + x = self.x + (self.info.width * CELL_SIZE) / 2 - 200 + y = self.y + (self.info.height * CELL_SIZE) / 2 - 50 + rect = pygame.Rect(x - 22, y - 21, 372, 110) + pygame.draw.rect(self.screen, (0, 0, 0), rect) + pygame.draw.rect(self.screen, (255, 255, 255), rect, 4) + + +class EndingRenderer(Renderer): + def __init__(self, screen: pygame.Surface, info: RenderInfo, + victory: bool) -> None: + super().__init__(screen, info) + self.name = "" + self.victory = victory + + def render(self) -> None: + x = 900 + y = 500 + if self.victory: + self.draw_text("YOU WIN !", (255, 255, 50), 200, (x + 50, y - 100)) + self.draw_text("Enter your name", (255, 255, 255), 50, + (x + 250, y + 300)) + else: + self.draw_text("GAME OVER", (200, 50, 50), 200, (x, y - 100)) + self.draw_text("Enter your name, loser", (255, 255, 255), 50, + (x + 200, y + 300)) + self.draw_text("Your score : " + str(self.info.score), (255, 255, 255), + 80, (x + 200, y + 200)) + txt = self.draw_text(self.name, (255, 255, 255), 50, + (x + 400 - 10 * len(self.name), y + 400)) + if round(time.time()) % 2: + rect = pygame.Rect(x + 405 + txt - 10 * len(self.name), + y + 395, 4, 40) + pygame.draw.rect(self.screen, (255, 255, 255), rect) diff --git a/src/score.py b/src/score.py new file mode 100644 index 0000000..39b8a12 --- /dev/null +++ b/src/score.py @@ -0,0 +1,69 @@ +from pydantic import ValidationError, BaseModel +from pathlib import Path +from .error import error +import json + + +class ScoreDefinition(BaseModel): + name: str + score: int + + +class ScoreManager: + def __init__(self, path: str) -> None: + file_path = Path(path) + self.score_list: list[ScoreDefinition] = [] + self.file_path = file_path + if not file_path.exists(): + error("No highscore file found : new file created", False) + self.save() + else: + self.load() + + def score_error(self) -> None: + error("Highscore file corrupted : new file created", False) + self.save() + + def load(self) -> None: + try: + data = json.loads(self.file_path.read_text(encoding="utf-8")) + except json.JSONDecodeError: + return self.score_error() + + if not isinstance(data, list): + return self.score_error() + + score_list: list[ScoreDefinition] = [] + + for item in data: + try: + score_list.append(ScoreDefinition.model_validate(item)) + except ValidationError: + return self.score_error() + self.score_list = score_list + + def save(self) -> None: + data = [item.model_dump() for item in self.score_list] + self.file_path.parent.mkdir(parents=True, exist_ok=True) + self.file_path.write_text( + json.dumps(data, indent=4, ensure_ascii=False), + encoding="utf-8" + ) + + def add_score(self, name: str, score: int) -> None: + try: + item = ScoreDefinition( + name=name, + score=score + ) + except ValidationError: + return + exist = False + for i in self.score_list: + if i.name == name: + exist = True + if i.score < score: + i.score = score + if not exist: + self.score_list.append(item) + self.save() -- 2.53.0