├── .flake8 ├── .github └── workflows │ └── deploy.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── .replit ├── LICENSE ├── Makefile ├── README.md ├── assets ├── audio │ ├── die-pygbag.ogg │ ├── die.ogg │ ├── die.wav │ ├── hit-pygbag.ogg │ ├── hit.ogg │ ├── hit.wav │ ├── point-pygbag.ogg │ ├── point.ogg │ ├── point.wav │ ├── swoosh-pygbag.ogg │ ├── swoosh.ogg │ ├── swoosh.wav │ ├── wing-pygbag.ogg │ ├── wing.ogg │ └── wing.wav └── sprites │ ├── 0.png │ ├── 1.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ ├── 6.png │ ├── 7.png │ ├── 8.png │ ├── 9.png │ ├── background-day.png │ ├── background-night.png │ ├── base.png │ ├── bluebird-downflap.png │ ├── bluebird-midflap.png │ ├── bluebird-upflap.png │ ├── gameover.png │ ├── message.png │ ├── pipe-green.png │ ├── pipe-red.png │ ├── redbird-downflap.png │ ├── redbird-midflap.png │ ├── redbird-upflap.png │ ├── yellowbird-downflap.png │ ├── yellowbird-midflap.png │ └── yellowbird-upflap.png ├── flappy.ico ├── main.py ├── pyproject.toml ├── screenshot1.png └── src ├── __init__.py ├── entities ├── __init__.py ├── background.py ├── entity.py ├── floor.py ├── game_over.py ├── pipe.py ├── player.py ├── score.py └── welcome_message.py ├── flappy.py └── utils ├── __init__.py ├── constants.py ├── game_config.py ├── images.py ├── sounds.py ├── utils.py └── window.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E203,W503,E501 3 | exclude = .venv,.git,__pycache__,docs/source/conf.py,old,build,dist,migrations,__init__.py,conftest.py,admin.py,.tox 4 | max-complexity = 10 5 | max-line-length = 100 6 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yaml: -------------------------------------------------------------------------------- 1 | name: Deploy to GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - '**.py' 9 | 10 | jobs: 11 | deploy: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v2 17 | 18 | - name: Set up Python 19 | uses: actions/setup-python@v2 20 | with: 21 | python-version: 3.x 22 | 23 | - name: Install dependencies 24 | run: make init 25 | 26 | - name: Build web version 27 | run: make web-build 28 | 29 | - name: Deploy to GitHub Pages 30 | uses: peaceiris/actions-gh-pages@v3 31 | with: 32 | github_token: ${{ secrets.GITHUB_TOKEN }} 33 | publish_dir: ./build/web 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | build 3 | .python-version 4 | *.egg-info 5 | .vscode 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v2.3.0 4 | hooks: 5 | - id: check-yaml 6 | - id: end-of-file-fixer 7 | - id: trailing-whitespace 8 | - repo: https://github.com/psf/black 9 | rev: 22.3.0 10 | hooks: 11 | - id: black 12 | exclude: ^.*\b(migrations)\b.*$ 13 | - repo: https://github.com/PyCQA/flake8 14 | rev: 4.0.1 15 | hooks: 16 | - id: flake8 17 | args: ["--config=.flake8"] 18 | - repo: https://github.com/pycqa/isort 19 | rev: 5.12.0 20 | hooks: 21 | - id: isort 22 | name: isort (python) 23 | -------------------------------------------------------------------------------- /.replit: -------------------------------------------------------------------------------- 1 | language = "python3" 2 | run = "pipenv install ; pipenv run python flappy.py" 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | default: 2 | @make run 3 | 4 | run: 5 | python main.py 6 | 7 | web: 8 | pygbag main.py 9 | 10 | web-build: 11 | pygbag --build main.py 12 | 13 | init: 14 | @pip install -U pip; \ 15 | pip install -e ".[dev]"; \ 16 | pre-commit install; \ 17 | 18 | pre-commit: 19 | pre-commit install 20 | 21 | pre-commit-all: 22 | pre-commit run --all-files 23 | 24 | format: 25 | black . 26 | 27 | lint: 28 | flake8 --config=../.flake8 --output-file=./coverage/flake8-report --format=default 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [FlapPyBird](https://sourabhv.github.io/FlapPyBird) 2 | =============== 3 | 4 | A Flappy Bird Clone made using [python-pygame][pygame] 5 | 6 | > If you are in interested in the old one-file code for the game, you can [find it here][one-file-game] 7 | 8 | [pygame]: http://www.pygame.org 9 | [one-file-game]: https://github.com/sourabhv/FlapPyBird/blob/038359dc6122f8d851e816ddb3e7d28229d585e5/flappy.py 10 | 11 | 12 | Setup (as tested on MacOS) 13 | --------------------------- 14 | 15 | 1. Install Python 3 from [here](https://www.python.org/download/releases/) (or use brew/apt/pyenv) 16 | 17 | 2. Run `make init` (this will install pip packages, use virtualenv or something similar if you don't want to install globally) 18 | 19 | 3. Run `make` to run the game. Run `DEBUG=True make` to see rects and coords 20 | 21 | 4. Use or Space key to play and Esc to close the game. 22 | 23 | 5. Optionally run `make web` to run the game in the browser (`pygbag`). 24 | 25 | Notable forks 26 | ------------- 27 | - [FlapPyBlink Blink to control the bird](https://github.com/sero583/FlappyBlink) 28 | - [FlappyBird Fury Mode](https://github.com/Cc618/FlapPyBird) 29 | - [FlappyBird Model Predictive Control](https://github.com/philzook58/FlapPyBird-MPC) 30 | - [FlappyBird OpenFrameworks Port](https://github.com/TheLogicMaster/ofFlappyBird) 31 | - [FlappyBird On Quantum Computing](https://github.com/WingCode/QuFlapPyBird) 32 | 33 | Made something awesome from FlapPyBird? Add it to the list :) 34 | 35 | 36 | Demo 37 | ---------- 38 | 39 | https://user-images.githubusercontent.com/2307626/130682424-9254b32d-efe0-406e-a6ea-3fb625a2df5e.mp4 40 | -------------------------------------------------------------------------------- /assets/audio/die-pygbag.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourabhv/FlapPyBird/95af73307f7c05f2ba8f208e911097cbb56b689a/assets/audio/die-pygbag.ogg -------------------------------------------------------------------------------- /assets/audio/die.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourabhv/FlapPyBird/95af73307f7c05f2ba8f208e911097cbb56b689a/assets/audio/die.ogg -------------------------------------------------------------------------------- /assets/audio/die.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourabhv/FlapPyBird/95af73307f7c05f2ba8f208e911097cbb56b689a/assets/audio/die.wav -------------------------------------------------------------------------------- /assets/audio/hit-pygbag.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourabhv/FlapPyBird/95af73307f7c05f2ba8f208e911097cbb56b689a/assets/audio/hit-pygbag.ogg -------------------------------------------------------------------------------- /assets/audio/hit.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourabhv/FlapPyBird/95af73307f7c05f2ba8f208e911097cbb56b689a/assets/audio/hit.ogg -------------------------------------------------------------------------------- /assets/audio/hit.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourabhv/FlapPyBird/95af73307f7c05f2ba8f208e911097cbb56b689a/assets/audio/hit.wav -------------------------------------------------------------------------------- /assets/audio/point-pygbag.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourabhv/FlapPyBird/95af73307f7c05f2ba8f208e911097cbb56b689a/assets/audio/point-pygbag.ogg -------------------------------------------------------------------------------- /assets/audio/point.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourabhv/FlapPyBird/95af73307f7c05f2ba8f208e911097cbb56b689a/assets/audio/point.ogg -------------------------------------------------------------------------------- /assets/audio/point.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourabhv/FlapPyBird/95af73307f7c05f2ba8f208e911097cbb56b689a/assets/audio/point.wav -------------------------------------------------------------------------------- /assets/audio/swoosh-pygbag.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourabhv/FlapPyBird/95af73307f7c05f2ba8f208e911097cbb56b689a/assets/audio/swoosh-pygbag.ogg -------------------------------------------------------------------------------- /assets/audio/swoosh.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourabhv/FlapPyBird/95af73307f7c05f2ba8f208e911097cbb56b689a/assets/audio/swoosh.ogg -------------------------------------------------------------------------------- /assets/audio/swoosh.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourabhv/FlapPyBird/95af73307f7c05f2ba8f208e911097cbb56b689a/assets/audio/swoosh.wav -------------------------------------------------------------------------------- /assets/audio/wing-pygbag.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourabhv/FlapPyBird/95af73307f7c05f2ba8f208e911097cbb56b689a/assets/audio/wing-pygbag.ogg -------------------------------------------------------------------------------- /assets/audio/wing.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourabhv/FlapPyBird/95af73307f7c05f2ba8f208e911097cbb56b689a/assets/audio/wing.ogg -------------------------------------------------------------------------------- /assets/audio/wing.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourabhv/FlapPyBird/95af73307f7c05f2ba8f208e911097cbb56b689a/assets/audio/wing.wav -------------------------------------------------------------------------------- /assets/sprites/0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourabhv/FlapPyBird/95af73307f7c05f2ba8f208e911097cbb56b689a/assets/sprites/0.png -------------------------------------------------------------------------------- /assets/sprites/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourabhv/FlapPyBird/95af73307f7c05f2ba8f208e911097cbb56b689a/assets/sprites/1.png -------------------------------------------------------------------------------- /assets/sprites/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourabhv/FlapPyBird/95af73307f7c05f2ba8f208e911097cbb56b689a/assets/sprites/2.png -------------------------------------------------------------------------------- /assets/sprites/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourabhv/FlapPyBird/95af73307f7c05f2ba8f208e911097cbb56b689a/assets/sprites/3.png -------------------------------------------------------------------------------- /assets/sprites/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourabhv/FlapPyBird/95af73307f7c05f2ba8f208e911097cbb56b689a/assets/sprites/4.png -------------------------------------------------------------------------------- /assets/sprites/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourabhv/FlapPyBird/95af73307f7c05f2ba8f208e911097cbb56b689a/assets/sprites/5.png -------------------------------------------------------------------------------- /assets/sprites/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourabhv/FlapPyBird/95af73307f7c05f2ba8f208e911097cbb56b689a/assets/sprites/6.png -------------------------------------------------------------------------------- /assets/sprites/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourabhv/FlapPyBird/95af73307f7c05f2ba8f208e911097cbb56b689a/assets/sprites/7.png -------------------------------------------------------------------------------- /assets/sprites/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourabhv/FlapPyBird/95af73307f7c05f2ba8f208e911097cbb56b689a/assets/sprites/8.png -------------------------------------------------------------------------------- /assets/sprites/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourabhv/FlapPyBird/95af73307f7c05f2ba8f208e911097cbb56b689a/assets/sprites/9.png -------------------------------------------------------------------------------- /assets/sprites/background-day.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourabhv/FlapPyBird/95af73307f7c05f2ba8f208e911097cbb56b689a/assets/sprites/background-day.png -------------------------------------------------------------------------------- /assets/sprites/background-night.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourabhv/FlapPyBird/95af73307f7c05f2ba8f208e911097cbb56b689a/assets/sprites/background-night.png -------------------------------------------------------------------------------- /assets/sprites/base.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourabhv/FlapPyBird/95af73307f7c05f2ba8f208e911097cbb56b689a/assets/sprites/base.png -------------------------------------------------------------------------------- /assets/sprites/bluebird-downflap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourabhv/FlapPyBird/95af73307f7c05f2ba8f208e911097cbb56b689a/assets/sprites/bluebird-downflap.png -------------------------------------------------------------------------------- /assets/sprites/bluebird-midflap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourabhv/FlapPyBird/95af73307f7c05f2ba8f208e911097cbb56b689a/assets/sprites/bluebird-midflap.png -------------------------------------------------------------------------------- /assets/sprites/bluebird-upflap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourabhv/FlapPyBird/95af73307f7c05f2ba8f208e911097cbb56b689a/assets/sprites/bluebird-upflap.png -------------------------------------------------------------------------------- /assets/sprites/gameover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourabhv/FlapPyBird/95af73307f7c05f2ba8f208e911097cbb56b689a/assets/sprites/gameover.png -------------------------------------------------------------------------------- /assets/sprites/message.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourabhv/FlapPyBird/95af73307f7c05f2ba8f208e911097cbb56b689a/assets/sprites/message.png -------------------------------------------------------------------------------- /assets/sprites/pipe-green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourabhv/FlapPyBird/95af73307f7c05f2ba8f208e911097cbb56b689a/assets/sprites/pipe-green.png -------------------------------------------------------------------------------- /assets/sprites/pipe-red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourabhv/FlapPyBird/95af73307f7c05f2ba8f208e911097cbb56b689a/assets/sprites/pipe-red.png -------------------------------------------------------------------------------- /assets/sprites/redbird-downflap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourabhv/FlapPyBird/95af73307f7c05f2ba8f208e911097cbb56b689a/assets/sprites/redbird-downflap.png -------------------------------------------------------------------------------- /assets/sprites/redbird-midflap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourabhv/FlapPyBird/95af73307f7c05f2ba8f208e911097cbb56b689a/assets/sprites/redbird-midflap.png -------------------------------------------------------------------------------- /assets/sprites/redbird-upflap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourabhv/FlapPyBird/95af73307f7c05f2ba8f208e911097cbb56b689a/assets/sprites/redbird-upflap.png -------------------------------------------------------------------------------- /assets/sprites/yellowbird-downflap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourabhv/FlapPyBird/95af73307f7c05f2ba8f208e911097cbb56b689a/assets/sprites/yellowbird-downflap.png -------------------------------------------------------------------------------- /assets/sprites/yellowbird-midflap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourabhv/FlapPyBird/95af73307f7c05f2ba8f208e911097cbb56b689a/assets/sprites/yellowbird-midflap.png -------------------------------------------------------------------------------- /assets/sprites/yellowbird-upflap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourabhv/FlapPyBird/95af73307f7c05f2ba8f208e911097cbb56b689a/assets/sprites/yellowbird-upflap.png -------------------------------------------------------------------------------- /flappy.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourabhv/FlapPyBird/95af73307f7c05f2ba8f208e911097cbb56b689a/flappy.ico -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from src.flappy import Flappy 4 | 5 | if __name__ == "__main__": 6 | asyncio.run(Flappy().start()) 7 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "flappybird" 3 | authors = [{name = "Sourabh Verma", email = "email@sourabh.dev"}] 4 | version = "1.0.0" 5 | description = "Flappy Bird in Pygame" 6 | requires-python = ">=3.9,<4" 7 | dependencies = [ 8 | "pygame == 2.4.0" 9 | ] 10 | 11 | [project.optional-dependencies] 12 | dev = [ 13 | "pygbag == 0.7.1", 14 | "black >= 22.1.0", 15 | "pre-commit >= 2.18.1", 16 | "flake8 >= 4.0.1", 17 | "isort >= 5.10.1" 18 | ] 19 | 20 | [tool.black] 21 | line-length = 80 22 | exclude = ''' 23 | /( 24 | | \.git 25 | | build 26 | )/ 27 | ''' 28 | 29 | [tool.isort] 30 | profile = "black" 31 | skip = [] 32 | skip_glob = [] 33 | -------------------------------------------------------------------------------- /screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourabhv/FlapPyBird/95af73307f7c05f2ba8f208e911097cbb56b689a/screenshot1.png -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourabhv/FlapPyBird/95af73307f7c05f2ba8f208e911097cbb56b689a/src/__init__.py -------------------------------------------------------------------------------- /src/entities/__init__.py: -------------------------------------------------------------------------------- 1 | from .background import Background 2 | from .entity import Entity 3 | from .floor import Floor 4 | from .game_over import GameOver 5 | from .pipe import Pipe, Pipes 6 | from .player import Player, PlayerMode 7 | from .score import Score 8 | from .welcome_message import WelcomeMessage 9 | 10 | __all__ = [ 11 | "Background", 12 | "Floor", 13 | "Pipe", 14 | "Pipes", 15 | "Player", 16 | "Score", 17 | "Entity", 18 | "WelcomeMessage", 19 | ] 20 | -------------------------------------------------------------------------------- /src/entities/background.py: -------------------------------------------------------------------------------- 1 | from ..utils import GameConfig 2 | from .entity import Entity 3 | 4 | 5 | class Background(Entity): 6 | def __init__(self, config: GameConfig) -> None: 7 | super().__init__( 8 | config, 9 | config.images.background, 10 | 0, 11 | 0, 12 | config.window.width, 13 | config.window.height, 14 | ) 15 | -------------------------------------------------------------------------------- /src/entities/entity.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | import pygame 4 | 5 | from ..utils import GameConfig, get_hit_mask, pixel_collision 6 | 7 | 8 | class Entity: 9 | def __init__( 10 | self, 11 | config: GameConfig, 12 | image: Optional[pygame.Surface] = None, 13 | x=0, 14 | y=0, 15 | w: int = None, 16 | h: int = None, 17 | **kwargs, 18 | ) -> None: 19 | self.config = config 20 | self.x = x 21 | self.y = y 22 | if w or h: 23 | self.w = w or config.window.ratio * h 24 | self.h = h or w / config.window.ratio 25 | self.image = pygame.transform.scale(image, (self.w, self.h)) 26 | else: 27 | self.image = image 28 | self.w = image.get_width() if image else 0 29 | self.h = image.get_height() if image else 0 30 | 31 | self.hit_mask = get_hit_mask(image) if image else None 32 | self.__dict__.update(kwargs) 33 | 34 | def update_image( 35 | self, image: pygame.Surface, w: int = None, h: int = None 36 | ) -> None: 37 | self.image = image 38 | self.hit_mask = get_hit_mask(image) 39 | self.w = w or (image.get_width() if image else 0) 40 | self.h = h or (image.get_height() if image else 0) 41 | 42 | @property 43 | def cx(self) -> float: 44 | return self.x + self.w / 2 45 | 46 | @property 47 | def cy(self) -> float: 48 | return self.y + self.h / 2 49 | 50 | @property 51 | def rect(self) -> pygame.Rect: 52 | return pygame.Rect(self.x, self.y, self.w, self.h) 53 | 54 | def collide(self, other) -> bool: 55 | if not self.hit_mask or not other.hit_mask: 56 | return self.rect.colliderect(other.rect) 57 | return pixel_collision( 58 | self.rect, other.rect, self.hit_mask, other.hit_mask 59 | ) 60 | 61 | def tick(self) -> None: 62 | self.draw() 63 | rect = self.rect 64 | if self.config.debug: 65 | pygame.draw.rect(self.config.screen, (255, 0, 0), rect, 1) 66 | # write x and y at top of rect 67 | font = pygame.font.SysFont("Arial", 13, True) 68 | text = font.render( 69 | f"{self.x:.1f}, {self.y:.1f}, {self.w:.1f}, {self.h:.1f}", 70 | True, 71 | (255, 255, 255), 72 | ) 73 | self.config.screen.blit( 74 | text, 75 | ( 76 | rect.x + rect.w / 2 - text.get_width() / 2, 77 | rect.y - text.get_height(), 78 | ), 79 | ) 80 | 81 | def draw(self) -> None: 82 | if self.image: 83 | self.config.screen.blit(self.image, self.rect) 84 | -------------------------------------------------------------------------------- /src/entities/floor.py: -------------------------------------------------------------------------------- 1 | from ..utils import GameConfig 2 | from .entity import Entity 3 | 4 | 5 | class Floor(Entity): 6 | def __init__(self, config: GameConfig) -> None: 7 | super().__init__(config, config.images.base, 0, config.window.vh) 8 | self.vel_x = 4 9 | self.x_extra = self.w - config.window.w 10 | 11 | def stop(self) -> None: 12 | self.vel_x = 0 13 | 14 | def draw(self) -> None: 15 | self.x = -((-self.x + self.vel_x) % self.x_extra) 16 | super().draw() 17 | -------------------------------------------------------------------------------- /src/entities/game_over.py: -------------------------------------------------------------------------------- 1 | from ..utils import GameConfig 2 | from .entity import Entity 3 | 4 | 5 | class GameOver(Entity): 6 | def __init__(self, config: GameConfig) -> None: 7 | super().__init__( 8 | config=config, 9 | image=config.images.game_over, 10 | x=(config.window.width - config.images.game_over.get_width()) // 2, 11 | y=int(config.window.height * 0.2), 12 | ) 13 | -------------------------------------------------------------------------------- /src/entities/pipe.py: -------------------------------------------------------------------------------- 1 | import random 2 | from typing import List 3 | 4 | from ..utils import GameConfig 5 | from .entity import Entity 6 | 7 | 8 | class Pipe(Entity): 9 | def __init__(self, *args, **kwargs) -> None: 10 | super().__init__(*args, **kwargs) 11 | self.vel_x = -5 12 | 13 | def draw(self) -> None: 14 | self.x += self.vel_x 15 | super().draw() 16 | 17 | 18 | class Pipes(Entity): 19 | upper: List[Pipe] 20 | lower: List[Pipe] 21 | 22 | def __init__(self, config: GameConfig) -> None: 23 | super().__init__(config) 24 | self.pipe_gap = 120 25 | self.top = 0 26 | self.bottom = self.config.window.viewport_height 27 | self.upper = [] 28 | self.lower = [] 29 | self.spawn_initial_pipes() 30 | 31 | def tick(self) -> None: 32 | if self.can_spawn_pipes(): 33 | self.spawn_new_pipes() 34 | self.remove_old_pipes() 35 | 36 | for up_pipe, low_pipe in zip(self.upper, self.lower): 37 | up_pipe.tick() 38 | low_pipe.tick() 39 | 40 | def stop(self) -> None: 41 | for pipe in self.upper + self.lower: 42 | pipe.vel_x = 0 43 | 44 | def can_spawn_pipes(self) -> bool: 45 | last = self.upper[-1] 46 | if not last: 47 | return True 48 | 49 | return self.config.window.width - (last.x + last.w) > last.w * 2.5 50 | 51 | def spawn_new_pipes(self): 52 | # add new pipe when first pipe is about to touch left of screen 53 | upper, lower = self.make_random_pipes() 54 | self.upper.append(upper) 55 | self.lower.append(lower) 56 | 57 | def remove_old_pipes(self): 58 | # remove first pipe if its out of the screen 59 | for pipe in self.upper: 60 | if pipe.x < -pipe.w: 61 | self.upper.remove(pipe) 62 | 63 | for pipe in self.lower: 64 | if pipe.x < -pipe.w: 65 | self.lower.remove(pipe) 66 | 67 | def spawn_initial_pipes(self): 68 | upper_1, lower_1 = self.make_random_pipes() 69 | upper_1.x = self.config.window.width + upper_1.w * 3 70 | lower_1.x = self.config.window.width + upper_1.w * 3 71 | self.upper.append(upper_1) 72 | self.lower.append(lower_1) 73 | 74 | upper_2, lower_2 = self.make_random_pipes() 75 | upper_2.x = upper_1.x + upper_1.w * 3.5 76 | lower_2.x = upper_1.x + upper_1.w * 3.5 77 | self.upper.append(upper_2) 78 | self.lower.append(lower_2) 79 | 80 | def make_random_pipes(self): 81 | """returns a randomly generated pipe""" 82 | # y of gap between upper and lower pipe 83 | base_y = self.config.window.viewport_height 84 | 85 | gap_y = random.randrange(0, int(base_y * 0.6 - self.pipe_gap)) 86 | gap_y += int(base_y * 0.2) 87 | pipe_height = self.config.images.pipe[0].get_height() 88 | pipe_x = self.config.window.width + 10 89 | 90 | upper_pipe = Pipe( 91 | self.config, 92 | self.config.images.pipe[0], 93 | pipe_x, 94 | gap_y - pipe_height, 95 | ) 96 | 97 | lower_pipe = Pipe( 98 | self.config, 99 | self.config.images.pipe[1], 100 | pipe_x, 101 | gap_y + self.pipe_gap, 102 | ) 103 | 104 | return upper_pipe, lower_pipe 105 | -------------------------------------------------------------------------------- /src/entities/player.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from itertools import cycle 3 | 4 | import pygame 5 | 6 | from ..utils import GameConfig, clamp 7 | from .entity import Entity 8 | from .floor import Floor 9 | from .pipe import Pipe, Pipes 10 | 11 | 12 | class PlayerMode(Enum): 13 | SHM = "SHM" 14 | NORMAL = "NORMAL" 15 | CRASH = "CRASH" 16 | 17 | 18 | class Player(Entity): 19 | def __init__(self, config: GameConfig) -> None: 20 | image = config.images.player[0] 21 | x = int(config.window.width * 0.2) 22 | y = int((config.window.height - image.get_height()) / 2) 23 | super().__init__(config, image, x, y) 24 | self.min_y = -2 * self.h 25 | self.max_y = config.window.viewport_height - self.h * 0.75 26 | self.img_idx = 0 27 | self.img_gen = cycle([0, 1, 2, 1]) 28 | self.frame = 0 29 | self.crashed = False 30 | self.crash_entity = None 31 | self.set_mode(PlayerMode.SHM) 32 | 33 | def set_mode(self, mode: PlayerMode) -> None: 34 | self.mode = mode 35 | if mode == PlayerMode.NORMAL: 36 | self.reset_vals_normal() 37 | self.config.sounds.wing.play() 38 | elif mode == PlayerMode.SHM: 39 | self.reset_vals_shm() 40 | elif mode == PlayerMode.CRASH: 41 | self.stop_wings() 42 | self.config.sounds.hit.play() 43 | if self.crash_entity == "pipe": 44 | self.config.sounds.die.play() 45 | self.reset_vals_crash() 46 | 47 | def reset_vals_normal(self) -> None: 48 | self.vel_y = -9 # player's velocity along Y axis 49 | self.max_vel_y = 10 # max vel along Y, max descend speed 50 | self.min_vel_y = -8 # min vel along Y, max ascend speed 51 | self.acc_y = 1 # players downward acceleration 52 | 53 | self.rot = 80 # player's current rotation 54 | self.vel_rot = -3 # player's rotation speed 55 | self.rot_min = -90 # player's min rotation angle 56 | self.rot_max = 20 # player's max rotation angle 57 | 58 | self.flap_acc = -9 # players speed on flapping 59 | self.flapped = False # True when player flaps 60 | 61 | def reset_vals_shm(self) -> None: 62 | self.vel_y = 1 # player's velocity along Y axis 63 | self.max_vel_y = 4 # max vel along Y, max descend speed 64 | self.min_vel_y = -4 # min vel along Y, max ascend speed 65 | self.acc_y = 0.5 # players downward acceleration 66 | 67 | self.rot = 0 # player's current rotation 68 | self.vel_rot = 0 # player's rotation speed 69 | self.rot_min = 0 # player's min rotation angle 70 | self.rot_max = 0 # player's max rotation angle 71 | 72 | self.flap_acc = 0 # players speed on flapping 73 | self.flapped = False # True when player flaps 74 | 75 | def reset_vals_crash(self) -> None: 76 | self.acc_y = 2 77 | self.vel_y = 7 78 | self.max_vel_y = 15 79 | self.vel_rot = -8 80 | 81 | def update_image(self): 82 | self.frame += 1 83 | if self.frame % 5 == 0: 84 | self.img_idx = next(self.img_gen) 85 | self.image = self.config.images.player[self.img_idx] 86 | self.w = self.image.get_width() 87 | self.h = self.image.get_height() 88 | 89 | def tick_shm(self) -> None: 90 | if self.vel_y >= self.max_vel_y or self.vel_y <= self.min_vel_y: 91 | self.acc_y *= -1 92 | self.vel_y += self.acc_y 93 | self.y += self.vel_y 94 | 95 | def tick_normal(self) -> None: 96 | if self.vel_y < self.max_vel_y and not self.flapped: 97 | self.vel_y += self.acc_y 98 | if self.flapped: 99 | self.flapped = False 100 | 101 | self.y = clamp(self.y + self.vel_y, self.min_y, self.max_y) 102 | self.rotate() 103 | 104 | def tick_crash(self) -> None: 105 | if self.min_y <= self.y <= self.max_y: 106 | self.y = clamp(self.y + self.vel_y, self.min_y, self.max_y) 107 | # rotate only when it's a pipe crash and bird is still falling 108 | if self.crash_entity != "floor": 109 | self.rotate() 110 | 111 | # player velocity change 112 | if self.vel_y < self.max_vel_y: 113 | self.vel_y += self.acc_y 114 | 115 | def rotate(self) -> None: 116 | self.rot = clamp(self.rot + self.vel_rot, self.rot_min, self.rot_max) 117 | 118 | def draw(self) -> None: 119 | self.update_image() 120 | if self.mode == PlayerMode.SHM: 121 | self.tick_shm() 122 | elif self.mode == PlayerMode.NORMAL: 123 | self.tick_normal() 124 | elif self.mode == PlayerMode.CRASH: 125 | self.tick_crash() 126 | 127 | self.draw_player() 128 | 129 | def draw_player(self) -> None: 130 | rotated_image = pygame.transform.rotate(self.image, self.rot) 131 | rotated_rect = rotated_image.get_rect(center=self.rect.center) 132 | self.config.screen.blit(rotated_image, rotated_rect) 133 | 134 | def stop_wings(self) -> None: 135 | self.img_gen = cycle([self.img_idx]) 136 | 137 | def flap(self) -> None: 138 | if self.y > self.min_y: 139 | self.vel_y = self.flap_acc 140 | self.flapped = True 141 | self.rot = 80 142 | self.config.sounds.wing.play() 143 | 144 | def crossed(self, pipe: Pipe) -> bool: 145 | return pipe.cx <= self.cx < pipe.cx - pipe.vel_x 146 | 147 | def collided(self, pipes: Pipes, floor: Floor) -> bool: 148 | """returns True if player collides with floor or pipes.""" 149 | 150 | # if player crashes into ground 151 | if self.collide(floor): 152 | self.crashed = True 153 | self.crash_entity = "floor" 154 | return True 155 | 156 | for pipe in pipes.upper: 157 | if self.collide(pipe): 158 | self.crashed = True 159 | self.crash_entity = "pipe" 160 | return True 161 | for pipe in pipes.lower: 162 | if self.collide(pipe): 163 | self.crashed = True 164 | self.crash_entity = "pipe" 165 | return True 166 | 167 | return False 168 | -------------------------------------------------------------------------------- /src/entities/score.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | 3 | from ..utils import GameConfig 4 | from .entity import Entity 5 | 6 | 7 | class Score(Entity): 8 | def __init__(self, config: GameConfig) -> None: 9 | super().__init__(config) 10 | self.y = self.config.window.height * 0.1 11 | self.score = 0 12 | 13 | def reset(self) -> None: 14 | self.score = 0 15 | 16 | def add(self) -> None: 17 | self.score += 1 18 | self.config.sounds.point.play() 19 | 20 | @property 21 | def rect(self) -> pygame.Rect: 22 | score_digits = [int(x) for x in list(str(self.score))] 23 | images = [self.config.images.numbers[digit] for digit in score_digits] 24 | w = sum(image.get_width() for image in images) 25 | x = (self.config.window.width - w) / 2 26 | h = max(image.get_height() for image in images) 27 | return pygame.Rect(x, self.y, w, h) 28 | 29 | def draw(self) -> None: 30 | """displays score in center of screen""" 31 | score_digits = [int(x) for x in list(str(self.score))] 32 | images = [self.config.images.numbers[digit] for digit in score_digits] 33 | digits_width = sum(image.get_width() for image in images) 34 | x_offset = (self.config.window.width - digits_width) / 2 35 | 36 | for image in images: 37 | self.config.screen.blit(image, (x_offset, self.y)) 38 | x_offset += image.get_width() 39 | -------------------------------------------------------------------------------- /src/entities/welcome_message.py: -------------------------------------------------------------------------------- 1 | from ..utils import GameConfig 2 | from .entity import Entity 3 | 4 | 5 | class WelcomeMessage(Entity): 6 | def __init__(self, config: GameConfig) -> None: 7 | image = config.images.welcome_message 8 | super().__init__( 9 | config=config, 10 | image=image, 11 | x=(config.window.width - image.get_width()) // 2, 12 | y=int(config.window.height * 0.12), 13 | ) 14 | -------------------------------------------------------------------------------- /src/flappy.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import sys 3 | 4 | import pygame 5 | from pygame.locals import K_ESCAPE, K_SPACE, K_UP, KEYDOWN, QUIT 6 | 7 | from .entities import ( 8 | Background, 9 | Floor, 10 | GameOver, 11 | Pipes, 12 | Player, 13 | PlayerMode, 14 | Score, 15 | WelcomeMessage, 16 | ) 17 | from .utils import GameConfig, Images, Sounds, Window 18 | 19 | 20 | class Flappy: 21 | def __init__(self): 22 | pygame.init() 23 | pygame.display.set_caption("Flappy Bird") 24 | window = Window(288, 512) 25 | screen = pygame.display.set_mode((window.width, window.height)) 26 | images = Images() 27 | 28 | self.config = GameConfig( 29 | screen=screen, 30 | clock=pygame.time.Clock(), 31 | fps=30, 32 | window=window, 33 | images=images, 34 | sounds=Sounds(), 35 | ) 36 | 37 | async def start(self): 38 | while True: 39 | self.background = Background(self.config) 40 | self.floor = Floor(self.config) 41 | self.player = Player(self.config) 42 | self.welcome_message = WelcomeMessage(self.config) 43 | self.game_over_message = GameOver(self.config) 44 | self.pipes = Pipes(self.config) 45 | self.score = Score(self.config) 46 | await self.splash() 47 | await self.play() 48 | await self.game_over() 49 | 50 | async def splash(self): 51 | """Shows welcome splash screen animation of flappy bird""" 52 | 53 | self.player.set_mode(PlayerMode.SHM) 54 | 55 | while True: 56 | for event in pygame.event.get(): 57 | self.check_quit_event(event) 58 | if self.is_tap_event(event): 59 | return 60 | 61 | self.background.tick() 62 | self.floor.tick() 63 | self.player.tick() 64 | self.welcome_message.tick() 65 | 66 | pygame.display.update() 67 | await asyncio.sleep(0) 68 | self.config.tick() 69 | 70 | def check_quit_event(self, event): 71 | if event.type == QUIT or ( 72 | event.type == KEYDOWN and event.key == K_ESCAPE 73 | ): 74 | pygame.quit() 75 | sys.exit() 76 | 77 | def is_tap_event(self, event): 78 | m_left, _, _ = pygame.mouse.get_pressed() 79 | space_or_up = event.type == KEYDOWN and ( 80 | event.key == K_SPACE or event.key == K_UP 81 | ) 82 | screen_tap = event.type == pygame.FINGERDOWN 83 | return m_left or space_or_up or screen_tap 84 | 85 | async def play(self): 86 | self.score.reset() 87 | self.player.set_mode(PlayerMode.NORMAL) 88 | 89 | while True: 90 | if self.player.collided(self.pipes, self.floor): 91 | return 92 | 93 | for i, pipe in enumerate(self.pipes.upper): 94 | if self.player.crossed(pipe): 95 | self.score.add() 96 | 97 | for event in pygame.event.get(): 98 | self.check_quit_event(event) 99 | if self.is_tap_event(event): 100 | self.player.flap() 101 | 102 | self.background.tick() 103 | self.floor.tick() 104 | self.pipes.tick() 105 | self.score.tick() 106 | self.player.tick() 107 | 108 | pygame.display.update() 109 | await asyncio.sleep(0) 110 | self.config.tick() 111 | 112 | async def game_over(self): 113 | """crashes the player down and shows gameover image""" 114 | 115 | self.player.set_mode(PlayerMode.CRASH) 116 | self.pipes.stop() 117 | self.floor.stop() 118 | 119 | while True: 120 | for event in pygame.event.get(): 121 | self.check_quit_event(event) 122 | if self.is_tap_event(event): 123 | if self.player.y + self.player.h >= self.floor.y - 1: 124 | return 125 | 126 | self.background.tick() 127 | self.floor.tick() 128 | self.pipes.tick() 129 | self.score.tick() 130 | self.player.tick() 131 | self.game_over_message.tick() 132 | 133 | self.config.tick() 134 | pygame.display.update() 135 | await asyncio.sleep(0) 136 | -------------------------------------------------------------------------------- /src/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .game_config import GameConfig 2 | from .images import Images 3 | from .sounds import Sounds 4 | from .utils import clamp, get_hit_mask, pixel_collision 5 | from .window import Window 6 | -------------------------------------------------------------------------------- /src/utils/constants.py: -------------------------------------------------------------------------------- 1 | # list of all possible players (tuple of 3 positions of flap) 2 | PLAYERS = ( 3 | # red bird 4 | ( 5 | "assets/sprites/redbird-upflap.png", 6 | "assets/sprites/redbird-midflap.png", 7 | "assets/sprites/redbird-downflap.png", 8 | ), 9 | # blue bird 10 | ( 11 | "assets/sprites/bluebird-upflap.png", 12 | "assets/sprites/bluebird-midflap.png", 13 | "assets/sprites/bluebird-downflap.png", 14 | ), 15 | # yellow bird 16 | ( 17 | "assets/sprites/yellowbird-upflap.png", 18 | "assets/sprites/yellowbird-midflap.png", 19 | "assets/sprites/yellowbird-downflap.png", 20 | ), 21 | ) 22 | 23 | # list of backgrounds 24 | BACKGROUNDS = ( 25 | "assets/sprites/background-day.png", 26 | "assets/sprites/background-night.png", 27 | ) 28 | 29 | # list of pipes 30 | PIPES = ( 31 | "assets/sprites/pipe-green.png", 32 | "assets/sprites/pipe-red.png", 33 | ) 34 | -------------------------------------------------------------------------------- /src/utils/game_config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pygame 4 | 5 | from .images import Images 6 | from .sounds import Sounds 7 | from .window import Window 8 | 9 | 10 | class GameConfig: 11 | def __init__( 12 | self, 13 | screen: pygame.Surface, 14 | clock: pygame.time.Clock, 15 | fps: int, 16 | window: Window, 17 | images: Images, 18 | sounds: Sounds, 19 | ) -> None: 20 | self.screen = screen 21 | self.clock = clock 22 | self.fps = fps 23 | self.window = window 24 | self.images = images 25 | self.sounds = sounds 26 | self.debug = os.environ.get("DEBUG", False) 27 | 28 | def tick(self) -> None: 29 | self.clock.tick(self.fps) 30 | -------------------------------------------------------------------------------- /src/utils/images.py: -------------------------------------------------------------------------------- 1 | import random 2 | from typing import List, Tuple 3 | 4 | import pygame 5 | 6 | from .constants import BACKGROUNDS, PIPES, PLAYERS 7 | 8 | 9 | class Images: 10 | numbers: List[pygame.Surface] 11 | game_over: pygame.Surface 12 | welcome_message: pygame.Surface 13 | base: pygame.Surface 14 | background: pygame.Surface 15 | player: Tuple[pygame.Surface] 16 | pipe: Tuple[pygame.Surface] 17 | 18 | def __init__(self) -> None: 19 | self.numbers = list( 20 | ( 21 | pygame.image.load(f"assets/sprites/{num}.png").convert_alpha() 22 | for num in range(10) 23 | ) 24 | ) 25 | 26 | # game over sprite 27 | self.game_over = pygame.image.load( 28 | "assets/sprites/gameover.png" 29 | ).convert_alpha() 30 | # welcome_message sprite for welcome screen 31 | self.welcome_message = pygame.image.load( 32 | "assets/sprites/message.png" 33 | ).convert_alpha() 34 | # base (ground) sprite 35 | self.base = pygame.image.load("assets/sprites/base.png").convert_alpha() 36 | self.randomize() 37 | 38 | def randomize(self): 39 | # select random background sprites 40 | rand_bg = random.randint(0, len(BACKGROUNDS) - 1) 41 | # select random player sprites 42 | rand_player = random.randint(0, len(PLAYERS) - 1) 43 | # select random pipe sprites 44 | rand_pipe = random.randint(0, len(PIPES) - 1) 45 | 46 | self.background = pygame.image.load(BACKGROUNDS[rand_bg]).convert() 47 | self.player = ( 48 | pygame.image.load(PLAYERS[rand_player][0]).convert_alpha(), 49 | pygame.image.load(PLAYERS[rand_player][1]).convert_alpha(), 50 | pygame.image.load(PLAYERS[rand_player][2]).convert_alpha(), 51 | ) 52 | self.pipe = ( 53 | pygame.transform.flip( 54 | pygame.image.load(PIPES[rand_pipe]).convert_alpha(), 55 | False, 56 | True, 57 | ), 58 | pygame.image.load(PIPES[rand_pipe]).convert_alpha(), 59 | ) 60 | -------------------------------------------------------------------------------- /src/utils/sounds.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import pygame 4 | 5 | 6 | class Sounds: 7 | die: pygame.mixer.Sound 8 | hit: pygame.mixer.Sound 9 | point: pygame.mixer.Sound 10 | swoosh: pygame.mixer.Sound 11 | wing: pygame.mixer.Sound 12 | 13 | def __init__(self) -> None: 14 | if "win" in sys.platform: 15 | ext = "wav" 16 | else: 17 | ext = "ogg" 18 | 19 | self.die = pygame.mixer.Sound(f"assets/audio/die.{ext}") 20 | self.hit = pygame.mixer.Sound(f"assets/audio/hit.{ext}") 21 | self.point = pygame.mixer.Sound(f"assets/audio/point.{ext}") 22 | self.swoosh = pygame.mixer.Sound(f"assets/audio/swoosh.{ext}") 23 | self.wing = pygame.mixer.Sound(f"assets/audio/wing.{ext}") 24 | -------------------------------------------------------------------------------- /src/utils/utils.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | from typing import List 3 | 4 | import pygame 5 | 6 | HitMaskType = List[List[bool]] 7 | 8 | 9 | def clamp(n: float, minn: float, maxn: float) -> float: 10 | """Clamps a number between two values""" 11 | return max(min(maxn, n), minn) 12 | 13 | 14 | def memoize(func): 15 | cache = {} 16 | 17 | @wraps(func) 18 | def wrapper(*args, **kwargs): 19 | key = (args, frozenset(kwargs.items())) 20 | if key not in cache: 21 | cache[key] = func(*args, **kwargs) 22 | return cache[key] 23 | 24 | return wrapper 25 | 26 | 27 | @memoize 28 | def get_hit_mask(image: pygame.Surface) -> HitMaskType: 29 | """returns a hit mask using an image's alpha.""" 30 | return list( 31 | ( 32 | list( 33 | ( 34 | bool(image.get_at((x, y))[3]) 35 | for y in range(image.get_height()) 36 | ) 37 | ) 38 | for x in range(image.get_width()) 39 | ) 40 | ) 41 | 42 | 43 | def pixel_collision( 44 | rect1: pygame.Rect, 45 | rect2: pygame.Rect, 46 | hitmask1: HitMaskType, 47 | hitmask2: HitMaskType, 48 | ): 49 | """Checks if two objects collide and not just their rects""" 50 | rect = rect1.clip(rect2) 51 | 52 | if rect.width == 0 or rect.height == 0: 53 | return False 54 | 55 | x1, y1 = rect.x - rect1.x, rect.y - rect1.y 56 | x2, y2 = rect.x - rect2.x, rect.y - rect2.y 57 | 58 | for x in range(rect.width): 59 | for y in range(rect.height): 60 | if hitmask1[x1 + x][y1 + y] and hitmask2[x2 + x][y2 + y]: 61 | return True 62 | return False 63 | -------------------------------------------------------------------------------- /src/utils/window.py: -------------------------------------------------------------------------------- 1 | class Window: 2 | def __init__(self, width, height): 3 | self.width = width 4 | self.height = height 5 | self.ratio = width / height 6 | self.w = width 7 | self.h = height 8 | self.r = width / height 9 | self.viewport_width = width 10 | self.viewport_height = height * 0.79 11 | self.vw = width 12 | self.vh = height * 0.79 13 | self.viewport_ratio = self.vw / self.vh 14 | self.vr = self.vw / self.vh 15 | --------------------------------------------------------------------------------