From a767a88be91188c919caee9b57b1ffcaec661c26 Mon Sep 17 00:00:00 2001 From: Sourabh Verma Date: Fri, 16 Jun 2023 19:44:08 +0530 Subject: [PATCH] Use pip instead of pipenv and add pygbag --- .flake8 | 5 + .gitignore | 3 + .pre-commit-config.yaml | 22 ++ .vscode/settings.json | 2 + Makefile | 25 +++ Pipfile | 12 - Pipfile.lock | 51 ----- README.md | 30 +-- assets/audio/die-pygbag.ogg | Bin 0 -> 9647 bytes assets/audio/hit-pygbag.ogg | Bin 0 -> 8776 bytes assets/audio/point-pygbag.ogg | Bin 0 -> 6716 bytes assets/audio/swoosh-pygbag.ogg | Bin 0 -> 8598 bytes assets/audio/wing-pygbag.ogg | Bin 0 -> 6333 bytes flappy.py | 399 ++++++++++++++++++--------------- main.py | 1 + pyproject.toml | 32 +++ setup.py | 36 --- 17 files changed, 310 insertions(+), 308 deletions(-) create mode 100644 .flake8 create mode 100644 .pre-commit-config.yaml create mode 100644 .vscode/settings.json create mode 100644 Makefile delete mode 100644 Pipfile delete mode 100644 Pipfile.lock create mode 100644 assets/audio/die-pygbag.ogg create mode 100644 assets/audio/hit-pygbag.ogg create mode 100644 assets/audio/point-pygbag.ogg create mode 100644 assets/audio/swoosh-pygbag.ogg create mode 100644 assets/audio/wing-pygbag.ogg create mode 120000 main.py create mode 100644 pyproject.toml delete mode 100644 setup.py diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..5192df8 --- /dev/null +++ b/.flake8 @@ -0,0 +1,5 @@ +[flake8] +ignore = E203,W503,E501 +exclude = .venv,.git,__pycache__,docs/source/conf.py,old,build,dist,migrations,__init__.py,conftest.py,admin.py,.tox +max-complexity = 10 +max-line-length = 100 diff --git a/.gitignore b/.gitignore index 0d20b64..9f4f402 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ *.pyc +build +.python-version +*.egg-info diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..ecfe79d --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,22 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.3.0 + hooks: + - id: check-yaml + - id: end-of-file-fixer + - id: trailing-whitespace + - repo: https://github.com/psf/black + rev: 22.3.0 + hooks: + - id: black + exclude: ^.*\b(migrations)\b.*$ + - repo: https://github.com/PyCQA/flake8 + rev: 4.0.1 + hooks: + - id: flake8 + args: ["--config=.flake8"] + - repo: https://github.com/pycqa/isort + rev: 5.12.0 + hooks: + - id: isort + name: isort (python) diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..2c63c08 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,2 @@ +{ +} diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7daecd5 --- /dev/null +++ b/Makefile @@ -0,0 +1,25 @@ +default: + @make run + +run: + python flappy.py + +web: + pygbag flappy.py + +init: + @pip install -U pip; \ + pip install -e ".[dev]"; \ + pre-commit install; \ + +pre-commit: + pre-commit install + +pre-commit-all: + pre-commit run --all-files + +format: + black . + +lint: + flake8 --config=../.flake8 --output-file=./coverage/flake8-report --format=default diff --git a/Pipfile b/Pipfile deleted file mode 100644 index 879a373..0000000 --- a/Pipfile +++ /dev/null @@ -1,12 +0,0 @@ -[[source]] -name = "pypi" -url = "https://pypi.org/simple" -verify_ssl = true - -[dev-packages] - -[packages] -pygame = "*" - -[requires] -python_version = "3" diff --git a/Pipfile.lock b/Pipfile.lock deleted file mode 100644 index b39d6ac..0000000 --- a/Pipfile.lock +++ /dev/null @@ -1,51 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "e172a3e30534a9a0b74742497b40f2ab196a0890d1cb082eb481fb89e384456e" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "pygame": { - "hashes": [ - "sha256:06dc92ccfea33b85f209db3d49f99a2a30c88fe9fb80fa2564cee443ece787b5", - "sha256:0919a2ec5fcb0d00518c2a5fa99858ccf22d7fbcc0e12818b317062d11386984", - "sha256:0a8c92e700e0042faefa998fa064616f330201890d6ea1c993eb3ff30ab53e99", - "sha256:220a1048ebb3d11a4d48cc4219ec8f65ca62fcafd255239478677625e8ead2e9", - "sha256:315861d2b8428f7b4d56d2c98d6c1acc18f08c77af4b129211bc036774f64be2", - "sha256:3469e87867832fe5226396626a8a6a9dac9b2e21a7819dd8cd96cf0e08bbcd41", - "sha256:54c19960180626165512d596235d75dc022d38844467cec769a8d8153fd66645", - "sha256:5ba598736ab9716f53dc943a659a9578f62acfe00c0c9c5490f3aca61d078f75", - "sha256:60ddc4f361babb30ff2d554132b1f3296490f3149d6c1c77682213563f59937a", - "sha256:6a49ab8616a9de534f1bf62c98beabf0e0bb0b6ff8917576bba22820bba3fdad", - "sha256:6d4966eeba652df2fd9a757b3fc5b29b578b47b58f991ad714471661ea2141cb", - "sha256:700d1781c999af25d11bfd1f3e158ebb660f72ebccb2040ecafe5069d0b2c0b6", - "sha256:73f4c28e894e76797b8ccaf6eb1205b433efdb803c70f489ebc3db6ac9c097e6", - "sha256:786eca2bea11abd924f3f67eb2483bcb22acff08f28dbdbf67130abe54b23797", - "sha256:7bcf586a1c51a735361ca03561979eea3180de45e6165bcdfa12878b752544af", - "sha256:82a1e93d82c1babceeb278c55012a9f5140e77665d372a6d97ec67786856d254", - "sha256:9e03589bc80a21ae951fca7659a767b7cac668289937e3756c0ab3d753cf6d24", - "sha256:aa8926a4e34fb0943abe1a8bb04a0ad82265341bf20064c0862db0a521100dfc", - "sha256:aa90689b889c417d2ac571ef2bbb5f7e735ae30c7553c60fae7508404f46c101", - "sha256:c9f8cdefee267a2e690bf17d61a8f5670b620f25a981f24781b034363a8eedc9", - "sha256:d9177afb2f46103bfc28a51fbc49ce18987a857e5c934db47b4a7030cb30fbd0", - "sha256:deb0551d4bbfb8131e2463a7fe1943bfcec5beb11acdf9c4bfa27fa5a9758d62", - "sha256:e7edfe57a5972aa9130ce9a186020a0f097e7a8e4c25e292109bdae1432b77f9", - "sha256:f0ad32efb9e26160645d62ba6cf3e5a5828dc4e82e8f41f9badfe7b685b07295" - ], - "index": "pypi", - "version": "==1.9.4" - } - }, - "develop": {} -} diff --git a/README.md b/README.md index 7e0fcd0..4030cde 100644 --- a/README.md +++ b/README.md @@ -6,32 +6,15 @@ A Flappy Bird Clone made using [python-pygame][pygame] Setup (as tested on MacOS) --------------------------- -1. Install Python 3.x (recommended) 2.x from [here](https://www.python.org/download/releases/) (Or use your preffered package manager) +1. Install Python 3 from [here](https://www.python.org/download/releases/) (or use brew/apt/pyenv) -1. Install [pipenv] +1. Run `make init` (this will install pip packages, use virtualenv or something similar if you don't want to install globally) -1. _Optional_: Install PyGame 1.9.x from [here](http://www.pygame.org/download.shtml) - - On MacOS, pipenv will install PyGame, please check how to install on your Linux/Windows machines - -1. Clone the repository: - - ```bash - $ git clone https://github.com/sourabhv/FlapPyBird - ``` - - or download as zip and extract. - -1. In the root directory run - - ```bash - $ pipenv install - $ pipenv run python flappy.py - ``` +1. Run `make` to run the game. 1. Use or Space key to play and Esc to close the game. -(For x64 windows, get exe [here](http://www.lfd.uci.edu/~gohlke/pythonlibs/#pygame)) +2. Optionally run `make web` to run the game in the browser (`pygbag`). Notable forks ------------- @@ -48,8 +31,3 @@ Demo ---------- https://user-images.githubusercontent.com/2307626/130682424-9254b32d-efe0-406e-a6ea-3fb625a2df5e.mp4 - - - -[pygame]: http://www.pygame.org -[pipenv]: https://pipenv.readthedocs.io/en/latest/ diff --git a/assets/audio/die-pygbag.ogg b/assets/audio/die-pygbag.ogg new file mode 100644 index 0000000000000000000000000000000000000000..93676a0401a4d710edc56ea9bce6afa190c38a4d GIT binary patch literal 9647 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%0DGGOjV#*eUCw`x&06dd`&Rr@1 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 F_#df74~PH& literal 0 HcmV?d00001 diff --git a/assets/audio/hit-pygbag.ogg b/assets/audio/hit-pygbag.ogg new file mode 100644 index 0000000000000000000000000000000000000000..6c8abc667e2f0e2d7fb9897815a4304c001b1c8c GIT binary patch literal 8776 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 literal 0 HcmV?d00001 diff --git a/assets/audio/point-pygbag.ogg b/assets/audio/point-pygbag.ogg new file mode 100644 index 0000000000000000000000000000000000000000..0fbc95c7773964c9c5f6d00fd4d8880ef7821149 GIT binary patch literal 6716 zcmai2dpy+X_dhcRLuhhqXlN8M%4S4NCF53%`!KmR5h>P6r72z5!H|_&36(}ottqu= z)e`NdsZH6EOF}lcwq3N{cU!EA-}%t?{eJ)YeO{l>GtYCL=bY!9_c`aejNhhBfk1|j zkHA;6fv8@cd1eL4n6x8tTU1;UK_I!66F&fA2h!AUEy<7I{PYlL7|z5mW^;7nhL~-R*cpwT>?O8DFY}AIsA)!$^7QXK z7uPi;-~yN?(JfcWlk_DtLl=L8Ck&V7_~!;L-m}sfHs0ZP&64s=N@XtD;TCDFc>*99 z_Bki(N^+d3CK5Bpko*jDN33OYkQPVKXfFR2?wnZcb*36go>)6*Wc=fFrSLYYqy*TySOtE_a z_y=gXZ`D#>6&@fuGV5pg0{8!X{2=4Evzl5*p*j& zpc-q+6>$fPy{fS0QMZeK+>4+k5u&DwKN_Agq&h^i>}p-n@zw^c`Q3WjC#8KFGBs`r zWVDw{%Q~>Whef?V6w6#SE6UL3sq^TtwVGju)a8%1;n>oGWmL1mt$d#OG~i_5QcW^- z`5ylkQQ0P0^lf)oXJzR;%X4L?b8YS}y3{mPEPGylNo%CA zP;tihYr^6ys`)&YynwGhg&z~KcTq9ekQYEmj>=}8m2OkFESrXtNw@}+=}$)6ETjLt zZx}7J%mTFWwQ9GqYY%i6h+?{mM4^{c!XIV|M^k$79TTk&DT)dGpLg1H%yK3M`?Mjf zTlAzR&ai0#?O!v|p|C}F^;EohUNrT(Z+h?kBEP?E{d61yblux+1#No50Q-c1r7`Vx z!oa0M!J35NqJ;1o(W4_Vk497a|LYa1PMN3;uq=N*D}Np<|B1svQ%`Peqa|=vh>Xnn z)#Bh2hkR4de6zUxO}mQ9juk&WcEa-f)DWN%Lx?BmOmfb-uHI(4ns4gP$I2ACVhsXa#!qZh_ic;r-nX5$u}#Mj0S(?h zy=Z)N0D#8N-|60MWB6#Bb?~hOfmR)S_l`ip|M&Q`+xqAruxdoYwuU7&RUA3!JY0UH z(V|q=<;xi??P{_}mk!a`(MNCd*{&M5iku|{$_N?-0x+-GjqIC@ZOxv4n7TJ}ERE#1 zIIGWn^B{m%btY^%PhJxg|3r$-I~MXVuWyayLGkCSkO@@rn{lc&Pmjda1xVB2tFtl- zqMBJldHg$^K}J*#=d3iknPX8>aED`Fa#))s$0ln?TF~q{C_Q}N(*ol|LBh|)!ghl@ z(9B_A3&ZEIt&gsmkB+;auCPnTy`#|fo~>O#kibXRrz1!Zs_!ExGV@vDxlNJ?;#sCS7^H_^mi5jKT23B5-?i4t@L2_7xQUb}$p z!T>=`C`#TT4DAvGcc!4^jzFJoJ%OM|(A^OuC=x|%7xb>1qghCs}ll63E?{KU3!9!BKJTMN=bDdZT98}`(Nreuegv9 z{b=RI3-dKr*MGN2_-N^sdY_}B-r+f}kL|N;+rLjhdA-|(5l2M5vCppf2ML71G2Nqr z;E@zm;_*mIKV8r@ijZe@6qT$WO_9HxQ7EX%L=A?HCI~}PpA5gRx7jWVN8YNY`pOkQ zY~7x<5_4XK0YLV)P|D0Ir^2rkpi88#-p<*&8MqX%BzgRsbV(D3Wst;oVV%q5^Y_y; z@;D33l6-jI;-u`oP!|-^!Rk_ZM(!=>2 zb9up3M-#`gBmLqw(~k zq=n1Ff`T;6doq#t6cZj><&<+ad#Kcw${v&!N+yeN6{JNeH*k z3hxs2j%KdsEW5n<*_BuCZoFB38{joX6=dW&YgRk33}-Bw(h3i5=v!wGlMEVerE(gd zhifB!3m!|XPig2HUetzcj8*C(+#(MY5c?!ilNV@zI-C9Hl=eS0Wf=2wfH9|sGFFuX z_ms&(k{E?5=<=i6G?uBc>~c9OrwR%Xsr9*Rs=V|;m{(yEjpuo;`e9VLG{c|It7tHc z#+d31E3fJ-UXF3AA79RS$l%3U-3m!+zB-!6k7sv=CB3?Oq%*4AZm1`NcH?{9=*qg0 zfjzHSHDU4h9LJav|Ei9#q}Rk@azA^tC*w69FxTdChKwbzudA*H*qNaOZs`Y>l$)05 z9aNc3SW8s{CM^mbEX6!TvvD*7H)jho{uE8ig=)6pUz)ricY2(fydeMMI6Oz#49Soe zX?)%kFO{f0{X}CCC)=rBmf5G8Tteen5aSSAnhO>mQT~^f|B1%u|4TzxeH>T7_rxP8 z!vRB;?rd%#&|m+J+H7O-RQz%QbZ@Q+{HnyciM{EZ36))T&V|RyV!Pg9o?!*5n`D|K z<(QC9HLUR>ZWuHQP!R%auD`VsXlT(^sydrm2 zW!@Vi&UTYNiKFAf9%)5L14Bfid@PQro=^hu=28_{=h=L+D{+5DbN}8wvI`4mU8>qQ zeX=#gi{I_7@KB*CR)e*wR+|d}G!+kk=?Jh!?>fMZ@trFqiers+3(^q_v3#mxtd-Tp7FA@ojLTfmQ?717Xzl%uwB_f3^1((Fl6 zYe3|lv+Gp1+itNW6=y;`4%22x;kD&m2L2BrD4YZ7)vB;j}6pKDIeqWi|@ZLJG zWoK6+_r^#=ORtNDLo1}B;vMC9tsAMc#6TqRF*ZTeshU-fdRd9OS3H9E|iFaDxZAHA+j1|Ow0`onPlVG!`@+qmCsTVgsf?pQQv>lK z6w`PGgNwyg7o)-4cY44jVvU9kaSQ4V)|6uQM*@?C(i$)9kNDc@y*#n6a_yX5HlAcv zZ3TuOvsV@1;wTRwk8Db^MviR6ZUrGG9vg~l#NRZngy=7AAp}lvjK^1g_@iUr$+yDG z-$u+i^}&0iyX-?3BEuIyzdEfE4mQP@hh!ZBut;Mv(2KDG^(Yk#2wEzquQp=mwW@!3 z@^uqNN4)HAO!QDmWD&+t8FvH`WhxW5M;VcQ$+)ir7s!i@K^%b+w)nz2R-dGQrc>MP zi)W4ZjK*AA?qosU8mVH9>>i^S70->P9fy3R@6a;j8XzzkBSb}pjiJeKl$c=V zyN-Jc9jgboQRl4h|0+=9`$^2uQZqz_u)#8qu)T?3BpzeJOOQ+kjkuYRjQO|Hpzq;@ znb-RKx&z-G^G%pv=*A=u;po!KxC)36MO`MrDA>|yy_zID+7}m;rO}2WA=|MUFz%`bztc zKQ@oPTaw!TVWRcGZ{e+^G#puqNHpT*Z5q#kXUJo1jzy~gixqUzz$pIG$tb{h zrU&Evh|8=wWYV8;*cnGcXQ1hMWCBH+Iu>DsC3b`niW`d&=`Hp!G2B}laqWFp#WsG6 z(;ts24t@RVp#mD05NbCP=~c!hHq~Jk76sv0{E1c(xx&S@t8dt7j^kxocl0Pc9#Se_(H| zvOA60f+m+j_X1S%5>Z22ZLts&As2}steEm%_X;kR38`hM!{nn2n}U3wJFPyMV0tM? zg-fJLDKUQ@0hcF!;vNFR-QHLX#9>kEOV%O7%A>uF1ar965>Ah=aK8P*bmlIc9f2#r zK}rTF2WKB8)|#(~cXU3 zXB51;b5{`pfJS6u>qS&Xv1mXA#B^+m)yUAdsrNUh$;D1}@!f}idb+d8)`mu7;&BNT zCz2UhR9sT>u>!tCVf+rG859%vU#&G5(V#xqXyUKQk$AqB-}y7`4+qbYob&dg%6wc- zl?>EKpriy`0xQKm^$;1v!U`Xv;fJbom1Z8LzzKT-y#5PwtqM)|d3gpz{X|MH#i=l3 zNY;7jfV(0An_-!u^4halffHmP;l4Md$(*kMHLuH>u+F?!5-}NZZCCBv4ZD-hP4EwY zWk~TL`KOuE>5UdlfOHZh&pxWpUGMUG9|7n4O~73mNgC6UrFrVfaAR1h+AHawY`LEJ zeC-+=zc)o^Vthu?4LFpQO%>}>=3)9^L*<43=i*#+4)_{sPGA74{4xe^~X!gu&@qpv1kD zF#pn$OsW;ET&%!_U0heXN+!|DWRm)7dT!pEJefpNJ6spUKi^l`o4o3zOCI_=WTmo_ zfwC!3Z0Od@P%ifpc{8ZCW?t#}WyI>(4y9#ea*CChux`+mUmwl#y$OdHOYwx{U%SevnhN>^)f>{Pg=QMT(A!4>`wj=u~(p0Pm(j8ltx#t^g2G0nn@E|bd>NiP-^_p#EbwwdW^ zxfFCmBqRsd8f;;jBtGeTW$&}xOKAoEKY|Ibgnz7`z8;vf^WGBIK(Q$C(g8KKJm{Xk zgO!sMf7>l`BrcX=F|f)-N&P$L?_j~liR5D~=3}oCRU)j~cb(aZ1U&dw+yFUU#v8b5vrI=WRvumo3fYH{irpyzYp!P(|v4wsP#}X`{HMzGLMAB zzXA-^Au1~3;%+Nj74g0>mJXb3<`n*vztelMusQ8VFGcm5ZkVyMjWkin7$5)A_t!Z! zzmELXJ3i)fAfOI+m-0}W4H>qa5w7);lGSB_XE!}rR&rpp)2sdDy{ppkO-cK1?b#A5 zRr+CM$BP$?N?F;`C8VIEDHr-}Lp9+;k7u%9?A--kcbgW!&{bGzGuw-f_*}n8o(pri YUMg7%?H?CcQGVFC?AUU3W}KMve>$I?K>z>% literal 0 HcmV?d00001 diff --git a/assets/audio/swoosh-pygbag.ogg b/assets/audio/swoosh-pygbag.ogg new file mode 100644 index 0000000000000000000000000000000000000000..5ab9a3d726e57b87eb627cfa71a72e6ea28529e6 GIT binary patch literal 8598 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 literal 0 HcmV?d00001 diff --git a/flappy.py b/flappy.py index 57b662a..6742797 100644 --- a/flappy.py +++ b/flappy.py @@ -1,14 +1,16 @@ -from itertools import cycle +import asyncio import random import sys +from itertools import cycle + import pygame -from pygame.locals import * +from pygame.locals import K_ESCAPE, K_SPACE, K_UP, KEYDOWN, QUIT FPS = 30 -SCREENWIDTH = 288 +SCREENWIDTH = 288 SCREENHEIGHT = 512 -PIPEGAPSIZE = 100 # gap between upper and lower part of pipe -BASEY = SCREENHEIGHT * 0.79 +PIPEGAPSIZE = 100 # gap between upper and lower part of pipe +BASEY = SCREENHEIGHT * 0.79 # image, sound and hitmask dicts IMAGES, SOUNDS, HITMASKS = {}, {}, {} @@ -16,91 +18,93 @@ IMAGES, SOUNDS, HITMASKS = {}, {}, {} PLAYERS_LIST = ( # red bird ( - 'assets/sprites/redbird-upflap.png', - 'assets/sprites/redbird-midflap.png', - 'assets/sprites/redbird-downflap.png', + "assets/sprites/redbird-upflap.png", + "assets/sprites/redbird-midflap.png", + "assets/sprites/redbird-downflap.png", ), # blue bird ( - 'assets/sprites/bluebird-upflap.png', - 'assets/sprites/bluebird-midflap.png', - 'assets/sprites/bluebird-downflap.png', + "assets/sprites/bluebird-upflap.png", + "assets/sprites/bluebird-midflap.png", + "assets/sprites/bluebird-downflap.png", ), # yellow bird ( - 'assets/sprites/yellowbird-upflap.png', - 'assets/sprites/yellowbird-midflap.png', - 'assets/sprites/yellowbird-downflap.png', + "assets/sprites/yellowbird-upflap.png", + "assets/sprites/yellowbird-midflap.png", + "assets/sprites/yellowbird-downflap.png", ), ) # list of backgrounds BACKGROUNDS_LIST = ( - 'assets/sprites/background-day.png', - 'assets/sprites/background-night.png', + "assets/sprites/background-day.png", + "assets/sprites/background-night.png", ) # list of pipes PIPES_LIST = ( - 'assets/sprites/pipe-green.png', - 'assets/sprites/pipe-red.png', + "assets/sprites/pipe-green.png", + "assets/sprites/pipe-red.png", ) -try: - xrange -except NameError: - xrange = range - - -def main(): +async def flappy(): global SCREEN, FPSCLOCK pygame.init() FPSCLOCK = pygame.time.Clock() SCREEN = pygame.display.set_mode((SCREENWIDTH, SCREENHEIGHT)) - pygame.display.set_caption('Flappy Bird') + pygame.display.set_caption("Flappy Bird") # numbers sprites for score display - IMAGES['numbers'] = ( - pygame.image.load('assets/sprites/0.png').convert_alpha(), - pygame.image.load('assets/sprites/1.png').convert_alpha(), - pygame.image.load('assets/sprites/2.png').convert_alpha(), - pygame.image.load('assets/sprites/3.png').convert_alpha(), - pygame.image.load('assets/sprites/4.png').convert_alpha(), - pygame.image.load('assets/sprites/5.png').convert_alpha(), - pygame.image.load('assets/sprites/6.png').convert_alpha(), - pygame.image.load('assets/sprites/7.png').convert_alpha(), - pygame.image.load('assets/sprites/8.png').convert_alpha(), - pygame.image.load('assets/sprites/9.png').convert_alpha() + IMAGES["numbers"] = ( + pygame.image.load("assets/sprites/0.png").convert_alpha(), + pygame.image.load("assets/sprites/1.png").convert_alpha(), + pygame.image.load("assets/sprites/2.png").convert_alpha(), + pygame.image.load("assets/sprites/3.png").convert_alpha(), + pygame.image.load("assets/sprites/4.png").convert_alpha(), + pygame.image.load("assets/sprites/5.png").convert_alpha(), + pygame.image.load("assets/sprites/6.png").convert_alpha(), + pygame.image.load("assets/sprites/7.png").convert_alpha(), + pygame.image.load("assets/sprites/8.png").convert_alpha(), + pygame.image.load("assets/sprites/9.png").convert_alpha(), ) # game over sprite - IMAGES['gameover'] = pygame.image.load('assets/sprites/gameover.png').convert_alpha() + IMAGES["gameover"] = pygame.image.load( + "assets/sprites/gameover.png" + ).convert_alpha() # message sprite for welcome screen - IMAGES['message'] = pygame.image.load('assets/sprites/message.png').convert_alpha() + IMAGES["message"] = pygame.image.load( + "assets/sprites/message.png" + ).convert_alpha() # base (ground) sprite - IMAGES['base'] = pygame.image.load('assets/sprites/base.png').convert_alpha() + IMAGES["base"] = pygame.image.load( + "assets/sprites/base.png" + ).convert_alpha() # sounds - if 'win' in sys.platform: - soundExt = '.wav' + if "win" in sys.platform: + soundExt = ".wav" else: - soundExt = '.ogg' + soundExt = ".ogg" - SOUNDS['die'] = pygame.mixer.Sound('assets/audio/die' + soundExt) - SOUNDS['hit'] = pygame.mixer.Sound('assets/audio/hit' + soundExt) - SOUNDS['point'] = pygame.mixer.Sound('assets/audio/point' + soundExt) - SOUNDS['swoosh'] = pygame.mixer.Sound('assets/audio/swoosh' + soundExt) - SOUNDS['wing'] = pygame.mixer.Sound('assets/audio/wing' + soundExt) + SOUNDS["die"] = pygame.mixer.Sound("assets/audio/die" + soundExt) + SOUNDS["hit"] = pygame.mixer.Sound("assets/audio/hit" + soundExt) + SOUNDS["point"] = pygame.mixer.Sound("assets/audio/point" + soundExt) + SOUNDS["swoosh"] = pygame.mixer.Sound("assets/audio/swoosh" + soundExt) + SOUNDS["wing"] = pygame.mixer.Sound("assets/audio/wing" + soundExt) while True: # select random background sprites randBg = random.randint(0, len(BACKGROUNDS_LIST) - 1) - IMAGES['background'] = pygame.image.load(BACKGROUNDS_LIST[randBg]).convert() + IMAGES["background"] = pygame.image.load( + BACKGROUNDS_LIST[randBg] + ).convert() # select random player sprites randPlayer = random.randint(0, len(PLAYERS_LIST) - 1) - IMAGES['player'] = ( + IMAGES["player"] = ( pygame.image.load(PLAYERS_LIST[randPlayer][0]).convert_alpha(), pygame.image.load(PLAYERS_LIST[randPlayer][1]).convert_alpha(), pygame.image.load(PLAYERS_LIST[randPlayer][2]).convert_alpha(), @@ -108,31 +112,34 @@ def main(): # select random pipe sprites pipeindex = random.randint(0, len(PIPES_LIST) - 1) - IMAGES['pipe'] = ( + IMAGES["pipe"] = ( pygame.transform.flip( - pygame.image.load(PIPES_LIST[pipeindex]).convert_alpha(), False, True), + pygame.image.load(PIPES_LIST[pipeindex]).convert_alpha(), + False, + True, + ), pygame.image.load(PIPES_LIST[pipeindex]).convert_alpha(), ) # hitmask for pipes - HITMASKS['pipe'] = ( - getHitmask(IMAGES['pipe'][0]), - getHitmask(IMAGES['pipe'][1]), + HITMASKS["pipe"] = ( + getHitmask(IMAGES["pipe"][0]), + getHitmask(IMAGES["pipe"][1]), ) # hitmask for player - HITMASKS['player'] = ( - getHitmask(IMAGES['player'][0]), - getHitmask(IMAGES['player'][1]), - getHitmask(IMAGES['player'][2]), + HITMASKS["player"] = ( + getHitmask(IMAGES["player"][0]), + getHitmask(IMAGES["player"][1]), + getHitmask(IMAGES["player"][2]), ) - movementInfo = showWelcomeAnimation() - crashInfo = mainGame(movementInfo) - showGameOverScreen(crashInfo) + movementInfo = await showWelcomeAnimation() + crashInfo = await mainGame(movementInfo) + await showGameOverScreen(crashInfo) -def showWelcomeAnimation(): +async def showWelcomeAnimation(): """Shows welcome screen animation of flappy bird""" # index of player to blit on screen playerIndex = 0 @@ -141,30 +148,32 @@ def showWelcomeAnimation(): loopIter = 0 playerx = int(SCREENWIDTH * 0.2) - playery = int((SCREENHEIGHT - IMAGES['player'][0].get_height()) / 2) + playery = int((SCREENHEIGHT - IMAGES["player"][0].get_height()) / 2) - messagex = int((SCREENWIDTH - IMAGES['message'].get_width()) / 2) + messagex = int((SCREENWIDTH - IMAGES["message"].get_width()) / 2) messagey = int(SCREENHEIGHT * 0.12) basex = 0 # amount by which base can maximum shift to left - baseShift = IMAGES['base'].get_width() - IMAGES['background'].get_width() + baseShift = IMAGES["base"].get_width() - IMAGES["background"].get_width() # player shm for up-down motion on welcome screen - playerShmVals = {'val': 0, 'dir': 1} + playerShmVals = {"val": 0, "dir": 1} while True: for event in pygame.event.get(): - if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE): + if event.type == QUIT or ( + event.type == KEYDOWN and event.key == K_ESCAPE + ): pygame.quit() sys.exit() - if event.type == KEYDOWN and (event.key == K_SPACE or event.key == K_UP): + if tap(event): # make first flap sound and return values for mainGame - SOUNDS['wing'].play() + SOUNDS["wing"].play() return { - 'playery': playery + playerShmVals['val'], - 'basex': basex, - 'playerIndexGen': playerIndexGen, + "playery": playery + playerShmVals["val"], + "basex": basex, + "playerIndexGen": playerIndexGen, } # adjust playery, playerIndex, basex @@ -175,23 +184,26 @@ def showWelcomeAnimation(): playerShm(playerShmVals) # draw sprites - SCREEN.blit(IMAGES['background'], (0,0)) - SCREEN.blit(IMAGES['player'][playerIndex], - (playerx, playery + playerShmVals['val'])) - SCREEN.blit(IMAGES['message'], (messagex, messagey)) - SCREEN.blit(IMAGES['base'], (basex, BASEY)) + SCREEN.blit(IMAGES["background"], (0, 0)) + SCREEN.blit( + IMAGES["player"][playerIndex], + (playerx, playery + playerShmVals["val"]), + ) + SCREEN.blit(IMAGES["message"], (messagex, messagey)) + SCREEN.blit(IMAGES["base"], (basex, BASEY)) pygame.display.update() + await asyncio.sleep(0) FPSCLOCK.tick(FPS) -def mainGame(movementInfo): +async def mainGame(movementInfo): score = playerIndex = loopIter = 0 - playerIndexGen = movementInfo['playerIndexGen'] - playerx, playery = int(SCREENWIDTH * 0.2), movementInfo['playery'] + playerIndexGen = movementInfo["playerIndexGen"] + playerx, playery = int(SCREENWIDTH * 0.2), movementInfo["playery"] - basex = movementInfo['basex'] - baseShift = IMAGES['base'].get_width() - IMAGES['background'].get_width() + basex = movementInfo["basex"] + baseShift = IMAGES["base"].get_width() - IMAGES["background"].get_width() # get 2 new pipes to add to upperPipes lowerPipes list newPipe1 = getRandomPipe() @@ -199,64 +211,68 @@ def mainGame(movementInfo): # list of upper pipes upperPipes = [ - {'x': SCREENWIDTH + 200, 'y': newPipe1[0]['y']}, - {'x': SCREENWIDTH + 200 + (SCREENWIDTH / 2), 'y': newPipe2[0]['y']}, + {"x": SCREENWIDTH + 200, "y": newPipe1[0]["y"]}, + {"x": SCREENWIDTH + 200 + (SCREENWIDTH / 2), "y": newPipe2[0]["y"]}, ] # list of lowerpipe lowerPipes = [ - {'x': SCREENWIDTH + 200, 'y': newPipe1[1]['y']}, - {'x': SCREENWIDTH + 200 + (SCREENWIDTH / 2), 'y': newPipe2[1]['y']}, + {"x": SCREENWIDTH + 200, "y": newPipe1[1]["y"]}, + {"x": SCREENWIDTH + 200 + (SCREENWIDTH / 2), "y": newPipe2[1]["y"]}, ] - dt = FPSCLOCK.tick(FPS)/1000 + dt = FPSCLOCK.tick(FPS) / 1000 pipeVelX = -128 * dt # player velocity, max velocity, downward acceleration, acceleration on flap - playerVelY = -9 # player's velocity along Y, default same as playerFlapped - playerMaxVelY = 10 # max vel along Y, max descend speed - playerMinVelY = -8 # min vel along Y, max ascend speed - playerAccY = 1 # players downward acceleration - playerRot = 45 # player's rotation - playerVelRot = 3 # angular speed - playerRotThr = 20 # rotation threshold - playerFlapAcc = -9 # players speed on flapping - playerFlapped = False # True when player flaps - + playerVelY = -9 # player's velocity along Y, default same as playerFlapped + playerMaxVelY = 10 # max vel along Y, max descend speed + # playerMinVelY = -8 # min vel along Y, max ascend speed + playerAccY = 1 # players downward acceleration + playerRot = 45 # player's rotation + playerVelRot = 3 # angular speed + playerRotThr = 20 # rotation threshold + playerFlapAcc = -9 # players speed on flapping + playerFlapped = False # True when player flaps while True: for event in pygame.event.get(): - if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE): + if event.type == QUIT or ( + event.type == KEYDOWN and event.key == K_ESCAPE + ): pygame.quit() sys.exit() - if event.type == KEYDOWN and (event.key == K_SPACE or event.key == K_UP): - if playery > -2 * IMAGES['player'][0].get_height(): + if tap(event): + if playery > -2 * IMAGES["player"][0].get_height(): playerVelY = playerFlapAcc playerFlapped = True - SOUNDS['wing'].play() + SOUNDS["wing"].play() # check for crash here - crashTest = checkCrash({'x': playerx, 'y': playery, 'index': playerIndex}, - upperPipes, lowerPipes) + crashTest = checkCrash( + {"x": playerx, "y": playery, "index": playerIndex}, + upperPipes, + lowerPipes, + ) if crashTest[0]: return { - 'y': playery, - 'groundCrash': crashTest[1], - 'basex': basex, - 'upperPipes': upperPipes, - 'lowerPipes': lowerPipes, - 'score': score, - 'playerVelY': playerVelY, - 'playerRot': playerRot + "y": playery, + "groundCrash": crashTest[1], + "basex": basex, + "upperPipes": upperPipes, + "lowerPipes": lowerPipes, + "score": score, + "playerVelY": playerVelY, + "playerRot": playerRot, } # check for score - playerMidPos = playerx + IMAGES['player'][0].get_width() / 2 + playerMidPos = playerx + IMAGES["player"][0].get_width() / 2 for pipe in upperPipes: - pipeMidPos = pipe['x'] + IMAGES['pipe'][0].get_width() / 2 + pipeMidPos = pipe["x"] + IMAGES["pipe"][0].get_width() / 2 if pipeMidPos <= playerMidPos < pipeMidPos + 4: score += 1 - SOUNDS['point'].play() + SOUNDS["point"].play() # playerIndex basex change if (loopIter + 1) % 3 == 0: @@ -277,33 +293,36 @@ def mainGame(movementInfo): # more rotation to cover the threshold (calculated in visible rotation) playerRot = 45 - playerHeight = IMAGES['player'][playerIndex].get_height() + playerHeight = IMAGES["player"][playerIndex].get_height() playery += min(playerVelY, BASEY - playery - playerHeight) # move pipes to left for uPipe, lPipe in zip(upperPipes, lowerPipes): - uPipe['x'] += pipeVelX - lPipe['x'] += pipeVelX + uPipe["x"] += pipeVelX + lPipe["x"] += pipeVelX # add new pipe when first pipe is about to touch left of screen - if 3 > len(upperPipes) > 0 and 0 < upperPipes[0]['x'] < 5: + if 3 > len(upperPipes) > 0 and 0 < upperPipes[0]["x"] < 5: newPipe = getRandomPipe() upperPipes.append(newPipe[0]) lowerPipes.append(newPipe[1]) # remove first pipe if its out of the screen - if len(upperPipes) > 0 and upperPipes[0]['x'] < -IMAGES['pipe'][0].get_width(): + if ( + len(upperPipes) > 0 + and upperPipes[0]["x"] < -IMAGES["pipe"][0].get_width() + ): upperPipes.pop(0) lowerPipes.pop(0) # draw sprites - SCREEN.blit(IMAGES['background'], (0,0)) + SCREEN.blit(IMAGES["background"], (0, 0)) for uPipe, lPipe in zip(upperPipes, lowerPipes): - SCREEN.blit(IMAGES['pipe'][0], (uPipe['x'], uPipe['y'])) - SCREEN.blit(IMAGES['pipe'][1], (lPipe['x'], lPipe['y'])) + SCREEN.blit(IMAGES["pipe"][0], (uPipe["x"], uPipe["y"])) + SCREEN.blit(IMAGES["pipe"][1], (lPipe["x"], lPipe["y"])) - SCREEN.blit(IMAGES['base'], (basex, BASEY)) + SCREEN.blit(IMAGES["base"], (basex, BASEY)) # print score so player overlaps the score showScore(score) @@ -311,40 +330,45 @@ def mainGame(movementInfo): visibleRot = playerRotThr if playerRot <= playerRotThr: visibleRot = playerRot - - playerSurface = pygame.transform.rotate(IMAGES['player'][playerIndex], visibleRot) + + playerSurface = pygame.transform.rotate( + IMAGES["player"][playerIndex], visibleRot + ) SCREEN.blit(playerSurface, (playerx, playery)) pygame.display.update() + await asyncio.sleep(0) FPSCLOCK.tick(FPS) -def showGameOverScreen(crashInfo): +async def showGameOverScreen(crashInfo): """crashes the player down and shows gameover image""" - score = crashInfo['score'] + score = crashInfo["score"] playerx = SCREENWIDTH * 0.2 - playery = crashInfo['y'] - playerHeight = IMAGES['player'][0].get_height() - playerVelY = crashInfo['playerVelY'] + playery = crashInfo["y"] + playerHeight = IMAGES["player"][0].get_height() + playerVelY = crashInfo["playerVelY"] playerAccY = 2 - playerRot = crashInfo['playerRot'] + playerRot = crashInfo["playerRot"] playerVelRot = 7 - basex = crashInfo['basex'] + basex = crashInfo["basex"] - upperPipes, lowerPipes = crashInfo['upperPipes'], crashInfo['lowerPipes'] + upperPipes, lowerPipes = crashInfo["upperPipes"], crashInfo["lowerPipes"] # play hit and die sounds - SOUNDS['hit'].play() - if not crashInfo['groundCrash']: - SOUNDS['die'].play() + SOUNDS["hit"].play() + if not crashInfo["groundCrash"]: + SOUNDS["die"].play() while True: for event in pygame.event.get(): - if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE): + if event.type == QUIT or ( + event.type == KEYDOWN and event.key == K_ESCAPE + ): pygame.quit() sys.exit() - if event.type == KEYDOWN and (event.key == K_SPACE or event.key == K_UP): + if tap(event): if playery + playerHeight >= BASEY - 1: return @@ -357,40 +381,38 @@ def showGameOverScreen(crashInfo): playerVelY += playerAccY # rotate only when it's a pipe crash - if not crashInfo['groundCrash']: + if not crashInfo["groundCrash"]: if playerRot > -90: playerRot -= playerVelRot # draw sprites - SCREEN.blit(IMAGES['background'], (0,0)) + SCREEN.blit(IMAGES["background"], (0, 0)) for uPipe, lPipe in zip(upperPipes, lowerPipes): - SCREEN.blit(IMAGES['pipe'][0], (uPipe['x'], uPipe['y'])) - SCREEN.blit(IMAGES['pipe'][1], (lPipe['x'], lPipe['y'])) + SCREEN.blit(IMAGES["pipe"][0], (uPipe["x"], uPipe["y"])) + SCREEN.blit(IMAGES["pipe"][1], (lPipe["x"], lPipe["y"])) - SCREEN.blit(IMAGES['base'], (basex, BASEY)) + SCREEN.blit(IMAGES["base"], (basex, BASEY)) showScore(score) - - - - playerSurface = pygame.transform.rotate(IMAGES['player'][1], playerRot) - SCREEN.blit(playerSurface, (playerx,playery)) - SCREEN.blit(IMAGES['gameover'], (50, 180)) + playerSurface = pygame.transform.rotate(IMAGES["player"][1], playerRot) + SCREEN.blit(playerSurface, (playerx, playery)) + SCREEN.blit(IMAGES["gameover"], (50, 180)) FPSCLOCK.tick(FPS) pygame.display.update() + await asyncio.sleep(0) def playerShm(playerShm): """oscillates the value of playerShm['val'] between 8 and -8""" - if abs(playerShm['val']) == 8: - playerShm['dir'] *= -1 + if abs(playerShm["val"]) == 8: + playerShm["dir"] *= -1 - if playerShm['dir'] == 1: - playerShm['val'] += 1 + if playerShm["dir"] == 1: + playerShm["val"] += 1 else: - playerShm['val'] -= 1 + playerShm["val"] -= 1 def getRandomPipe(): @@ -398,55 +420,56 @@ def getRandomPipe(): # y of gap between upper and lower pipe gapY = random.randrange(0, int(BASEY * 0.6 - PIPEGAPSIZE)) gapY += int(BASEY * 0.2) - pipeHeight = IMAGES['pipe'][0].get_height() + pipeHeight = IMAGES["pipe"][0].get_height() pipeX = SCREENWIDTH + 10 return [ - {'x': pipeX, 'y': gapY - pipeHeight}, # upper pipe - {'x': pipeX, 'y': gapY + PIPEGAPSIZE}, # lower pipe + {"x": pipeX, "y": gapY - pipeHeight}, # upper pipe + {"x": pipeX, "y": gapY + PIPEGAPSIZE}, # lower pipe ] def showScore(score): """displays score in center of screen""" scoreDigits = [int(x) for x in list(str(score))] - totalWidth = 0 # total width of all numbers to be printed + totalWidth = 0 # total width of all numbers to be printed for digit in scoreDigits: - totalWidth += IMAGES['numbers'][digit].get_width() + totalWidth += IMAGES["numbers"][digit].get_width() Xoffset = (SCREENWIDTH - totalWidth) / 2 for digit in scoreDigits: - SCREEN.blit(IMAGES['numbers'][digit], (Xoffset, SCREENHEIGHT * 0.1)) - Xoffset += IMAGES['numbers'][digit].get_width() + SCREEN.blit(IMAGES["numbers"][digit], (Xoffset, SCREENHEIGHT * 0.1)) + Xoffset += IMAGES["numbers"][digit].get_width() def checkCrash(player, upperPipes, lowerPipes): """returns True if player collides with base or pipes.""" - pi = player['index'] - player['w'] = IMAGES['player'][0].get_width() - player['h'] = IMAGES['player'][0].get_height() + pi = player["index"] + player["w"] = IMAGES["player"][0].get_width() + player["h"] = IMAGES["player"][0].get_height() # if player crashes into ground - if player['y'] + player['h'] >= BASEY - 1: + if player["y"] + player["h"] >= BASEY - 1: return [True, True] else: - playerRect = pygame.Rect(player['x'], player['y'], - player['w'], player['h']) - pipeW = IMAGES['pipe'][0].get_width() - pipeH = IMAGES['pipe'][0].get_height() + playerRect = pygame.Rect( + player["x"], player["y"], player["w"], player["h"] + ) + pipeW = IMAGES["pipe"][0].get_width() + pipeH = IMAGES["pipe"][0].get_height() for uPipe, lPipe in zip(upperPipes, lowerPipes): # upper and lower pipe rects - uPipeRect = pygame.Rect(uPipe['x'], uPipe['y'], pipeW, pipeH) - lPipeRect = pygame.Rect(lPipe['x'], lPipe['y'], pipeW, pipeH) + uPipeRect = pygame.Rect(uPipe["x"], uPipe["y"], pipeW, pipeH) + lPipeRect = pygame.Rect(lPipe["x"], lPipe["y"], pipeW, pipeH) # player and upper/lower pipe hitmasks - pHitMask = HITMASKS['player'][pi] - uHitmask = HITMASKS['pipe'][0] - lHitmask = HITMASKS['pipe'][1] + pHitMask = HITMASKS["player"][pi] + uHitmask = HITMASKS["pipe"][0] + lHitmask = HITMASKS["pipe"][1] # if bird collided with upipe or lpipe uCollide = pixelCollision(playerRect, uPipeRect, pHitMask, uHitmask) @@ -457,6 +480,7 @@ def checkCrash(player, upperPipes, lowerPipes): return [False, False] + def pixelCollision(rect1, rect2, hitmask1, hitmask2): """Checks if two objects collide and not just their rects""" rect = rect1.clip(rect2) @@ -467,20 +491,29 @@ def pixelCollision(rect1, rect2, hitmask1, hitmask2): x1, y1 = rect.x - rect1.x, rect.y - rect1.y x2, y2 = rect.x - rect2.x, rect.y - rect2.y - for x in xrange(rect.width): - for y in xrange(rect.height): - if hitmask1[x1+x][y1+y] and hitmask2[x2+x][y2+y]: + for x in range(rect.width): + for y in range(rect.height): + if hitmask1[x1 + x][y1 + y] and hitmask2[x2 + x][y2 + y]: return True return False + def getHitmask(image): """returns a hitmask using an image's alpha.""" mask = [] - for x in xrange(image.get_width()): + for x in range(image.get_width()): mask.append([]) - for y in xrange(image.get_height()): - mask[x].append(bool(image.get_at((x,y))[3])) + for y in range(image.get_height()): + mask[x].append(bool(image.get_at((x, y))[3])) return mask -if __name__ == '__main__': - main() + +def tap(event): + left, _, _ = pygame.mouse.get_pressed() + return left or ( + event.type == KEYDOWN and (event.key == K_SPACE or event.key == K_UP) + ) + + +if __name__ == "__main__": + asyncio.run(flappy()) diff --git a/main.py b/main.py new file mode 120000 index 0000000..71fe1d4 --- /dev/null +++ b/main.py @@ -0,0 +1 @@ +flappy.py \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b6be597 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,32 @@ +[project] +name = "flappybird" +authors = [{name = "Sourabh Verma", email = "email@sourabh.dev"}] +version = "1.0.0" +description = "Flappy Bird in Pygame" +requires-python = ">=3.9,<4" +dependencies = [ + "pygame == 2.4.0" + ] + +[project.optional-dependencies] +dev = [ + "pygbag == 0.7.1", + "black >= 22.1.0", + "pre-commit >= 2.18.1", + "flake8 >= 4.0.1", + "isort >= 5.10.1" + ] + +[tool.black] +line-length = 80 +exclude = ''' + /( + | \.git + | build + )/ + ''' + +[tool.isort] +profile = "black" +skip = [] +skip_glob = [] diff --git a/setup.py b/setup.py deleted file mode 100644 index a551eef..0000000 --- a/setup.py +++ /dev/null @@ -1,36 +0,0 @@ -import os -import sys -from distutils.core import setup - -import py2exe - -origIsSystemDLL = py2exe.build_exe.isSystemDLL -def isSystemDLL(pathname): - dlls = ("libfreetype-6.dll", "libogg-0.dll", "sdl_ttf.dll") - if os.path.basename(pathname).lower() in dlls: - return 0 - return origIsSystemDLL(pathname) -py2exe.build_exe.isSystemDLL = isSystemDLL - -sys.argv.append('py2exe') - -setup( - name = 'Flappy Bird', - version = '1.0', - author = 'Sourabh Verma', - options = { - 'py2exe': { - 'bundle_files': 1, # doesn't work on win64 - 'compressed': True, - } - }, - - windows = [{ - 'script': "flappy.py", - 'icon_resources': [ - (1, 'flappy.ico') - ] - }], - - zipfile=None, -)