├── .gitattributes ├── .github └── workflows │ └── python-pep8.yml ├── .gitignore ├── BUILD.md ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── assets ├── audio │ ├── collision.ogg │ ├── fire.ogg │ ├── levelup.ogg │ ├── missile_hit.ogg │ ├── music.ogg │ ├── powerup.ogg │ └── shoot.ogg ├── fonts │ └── arcade.ttf ├── icon │ └── pybluesky.png └── images │ ├── bullet.png │ ├── cloud1.png │ ├── cloud2.png │ ├── cloud3.png │ ├── grass.png │ ├── ground.png │ ├── jet.png │ ├── missile_activated.png │ ├── missile_deactivated.png │ ├── presplash.png │ ├── sam.png │ ├── samlauncher.png │ ├── star.png │ ├── vegetation_plain.png │ └── vegetation_tree.png ├── game ├── __init__.py ├── common │ ├── __init__.py │ └── singleton.py ├── data │ ├── __init__.py │ ├── dynamic.py │ ├── enums.py │ └── static.py ├── environment.py ├── handlers │ ├── __init__.py │ ├── leaderboard.py │ ├── network.py │ └── serialize.py └── sprites │ ├── __init__.py │ ├── bullet.py │ ├── cloud.py │ ├── jet.py │ ├── missile.py │ ├── sam.py │ ├── samlauncher.py │ ├── star.py │ ├── text │ ├── __init__.py │ ├── exitmenu.py │ ├── gamemenu.py │ ├── help.py │ ├── input │ │ ├── __init__.py │ │ └── name.py │ ├── leaderboard.py │ ├── replaymenu.py │ └── score.py │ └── vegetation.py ├── main.py ├── requirements.txt ├── setup.py └── tox.ini /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/python-pep8.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python application 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up Python 3.9 20 | uses: actions/setup-python@v2 21 | with: 22 | python-version: 3.9 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install --upgrade pip 26 | pip install flake8 pytest 27 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 28 | - name: Lint with flake8 29 | run: | 30 | # stop the build if there are Python syntax errors or undefined names 31 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 32 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 33 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 34 | 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # celery beat schedule file 95 | celerybeat-schedule 96 | 97 | # SageMath parsed files 98 | *.sage.py 99 | 100 | # Environments 101 | .env 102 | .venv 103 | .venv_x86 104 | env/ 105 | venv/ 106 | ENV/ 107 | env.bak/ 108 | venv.bak/ 109 | 110 | # Spyder project settings 111 | .spyderproject 112 | .spyproject 113 | 114 | # Rope project settings 115 | .ropeproject 116 | 117 | # mkdocs documentation 118 | /site 119 | 120 | # mypy 121 | .mypy_cache/ 122 | .dmypy.json 123 | dmypy.json 124 | 125 | # Pyre type checker 126 | .pyre/ 127 | *.dat 128 | api.key 129 | **/.vscode/* 130 | **/release/* 131 | Makefile-dev -------------------------------------------------------------------------------- /BUILD.md: -------------------------------------------------------------------------------- 1 | # BUILD GUIDE 2 | 3 | ## WINDOWS 4 | 5 | Start with the installation of the cs-Freeze package for building the windows binary and package. 6 | Install the following command 7 | ``` 8 | pip install cx-Freeze==6.1 9 | ``` 10 | 11 | For creating ```msi``` setup file as the final build output, invoke the following command 12 | ``` 13 | python setup.py bdist_msi 14 | ``` 15 | 16 | ## ANDROID 17 | ### Pre-requisites 18 | * Ubuntu 19 | * Python 3.7.6 (also works withh 3.8.5) ; need to complile 20 | * virtualenv 21 | * python-for-android python package 22 | * JDK 8 23 | 24 | 25 | ### Steps 26 | 27 | Prepare a ubuntu VM with ample disk space and good number of CPU and memory. 28 | 29 | 1. Install the required packages 30 | 31 | ``` 32 | sudo dpkg --add-architecture i386 33 | sudo apt-get update 34 | sudo apt-get install -y build-essential ccache git zlib1g-dev python3 python3-dev libncurses5:i386 libstdc++6:i386 zlib1g:i386 openjdk-8-jdk unzip ant ccache autoconf libtool libssl-dev libffi-dev 35 | ``` 36 | 37 | 2. Download and compile python 38 | 39 | ``` 40 | # install required packages 41 | sudo apt-get update 42 | sudo apt-get install -y build-essential checkinstall 43 | sudo apt-get install libreadline-gplv2-dev libncursesw5-dev libssl-dev libsqlite3-dev tk-dev libgdbm-dev libc6-dev libbz2-dev 44 | 45 | # Download python sources to /usr/src 46 | cd /usr/src 47 | sudo wget https://www.python.org/ftp/python/3.7.6/Python-3.7.6.tgz 48 | 49 | # extarct downloaded python sources 50 | sudo tar xzf Python-3.7.6.tgz 51 | 52 | # compile sources 53 | cd Python-3.7.6 54 | sudo ./configure --enable-optimizations 55 | 56 | # building python 57 | sudo make altinstall 58 | 59 | # verify python 60 | python3.7 --version 61 | ``` 62 | 63 | 3. Download Andorid SDK 64 | ``` 65 | # create directory structure 66 | mkdir -p ~/Desktop/android-sdk/cmdline-tools 67 | 68 | # download archive 69 | wget https://dl.google.com/android/repository/commandlinetools-linux-7302050_latest.zip 70 | 71 | # extract archive 72 | unzip commandlinetools-linux-7302050_latest.zip 73 | 74 | # cleanup file 75 | rm commandlinetools-linux-7302050_latest.zip 76 | 77 | # rename extracted directory 78 | mv cmdline-tools tools 79 | 80 | # add sdkmanager and future android to PATH variable 81 | export PATH=~/Desktop/android-sdk/tools:~/Desktop/android-sdk/cmdline-tools/tools/bin:$PATH 82 | ``` 83 | 84 | 4. Download Android NDK 85 | ``` 86 | # change directory to user desktop 87 | cd ~/Desktop 88 | 89 | # download archive 90 | https://dl.google.com/android/repository/android-ndk-r19c-linux-x86_64.zip 91 | 92 | # extract archive 93 | unzip android-ndk-r19c-linux-x86_64.zip 94 | 95 | # cleanup file 96 | rm android-ndk-r19c-linux-x86_64.zip 97 | ``` 98 | 99 | 5. Download android platform and build-tools 100 | ``` 101 | sdkmanager "platforms;android-28" 102 | sdkmanager "platforms;android-29" 103 | sdkmanager "build-tools;28.0.2" 104 | sdkmanager "build-tools;29.0.0" 105 | ``` 106 | 107 | 6. Setup python virtual environment 108 | ``` 109 | cd ~/Desktop 110 | 111 | # create python virutual environment with python 3.7.6 112 | python3.7 -m venv p4a-env 113 | 114 | # activate python virtual environment 115 | source p4a-env/bin/activate 116 | 117 | # install required packages 118 | pip instal wheel cython python-for-android 119 | 120 | # verify the installation of python-for-android package 121 | p4a --versoin 122 | ``` 123 | 124 | 7. Build apk using p4a 125 | ``` 126 | # python project is places at ~/Desktop/PyBluesky 127 | # --requirements : all the requiremenents packages listed in the requirements.txt file is listed here 128 | # 129 | 130 | p4a apk --private ~/Desktop/PyBluesky --requirements=pygame==2.0.0-dev7,aiohttp==3.7.4.post0,multidict==5.1.0,attrs==21.2.0,async-timeout==3.0.1,chardet==4.0.0,idna==3.2,typing-extensions==3.10.0.0,yarl==1.6.3 --icon /home/ljnath/Desktop/PyBluesky/assets/images/jet.png --sdk-dir ~/Desktop/android-sdk --ndk-dir ~/Desktop/android-ndk-r19c --android-api 28 --ndk-api 21 --ignore-setup-py --package=com.ljnath.pybluesky --name "PyBluesky" --version 1.0 --bootstrap=sdl2 --dist_name=PyBluesky --orientation=landscape 131 | ``` 132 | 133 | 8. Build apk using setup.py file 134 | ``` 135 | cd ~/Desktop/PyBluesky 136 | python setup.py apk 137 | ``` -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | 4 | ## [1.0.0] - 2021-07-09 5 | - First release -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Lakhya Jyoti Nath 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for PyBluesky android project 2 | # author - ljnath (www.ljnath.com} 3 | 4 | # variables used for building project 5 | ACTIVITY_NAME = org.kivy.android.PythonActivity 6 | ADB = adb 7 | ARCHITECTURE = arm64-v8a 8 | APK_NAME = PyBluesky 9 | PACKAGE_NAME = com.ljnath.pybluesky 10 | PYTHON = python 11 | RELEASE_TYPE = debug 12 | VERSION = 1.0.0 13 | 14 | ifeq (${RELEASE_TYPE}, debug) 15 | APK_FILE = ${APK_NAME}__${ARCHITECTURE}-${RELEASE_TYPE}-${VERSION}-.apk 16 | else ifeq (${RELEASE_TYPE}, release) 17 | APK_FILE = ${APK_NAME}__${ARCHITECTURE}-${RELEASE_TYPE}-unsigned-${VERSION}-.apk 18 | endif 19 | 20 | # variables used for signing 21 | JAR_SIGNER = jarsigner 22 | SIGALG = 23 | DIGESTALG = 24 | KEYSTORE_FILE = 25 | KEYSTORE_ALIAS = 26 | 27 | # variables used for zipalign 28 | ZIPALIGN = 29 | FINAL_APK = ${APK_NAME}_${ARCHITECTURE}-${RELEASE_TYPE}-signed-${VERSION}.apk 30 | 31 | 32 | # Targets 33 | 34 | all: compile uninstall install run 35 | 36 | compile: 37 | @echo Compiling project 38 | ${PYTHON} setup.py apk 39 | 40 | reinstall: uninstall install 41 | 42 | uninstall: 43 | @echo Un-installing app with package name ${PACKAGE_NAME} from target device 44 | ${ADB} uninstall ${PACKAGE_NAME} 45 | 46 | install: 47 | @echo Installing ${APK_FILE} in target device 48 | ${ADB} install ${APK_FILE} 49 | 50 | run: 51 | @echo Starting ${APK_FILE} in target device 52 | ${ADB} shell am start -n ${PACKAGE_NAME}/${ACTIVITY_NAME} 53 | 54 | reset: clean update 55 | 56 | clean: 57 | @echo Deleting all apk files 58 | rm -f *.apk 59 | 60 | update: 61 | @echo Cleaning local changes before updating codebase 62 | git reset --hard 63 | @echo Updating codebase 64 | git pull 65 | 66 | sign: 67 | @echo Signing apk 68 | ${JAR_SIGNER} -verbose -sigalg ${SIGALG} -digestalg ${DIGESTALG} -keystore ${KEYSTORE_FILE} ${APK_FILE} ${KEYSTORE_ALIAS} 69 | 70 | zipalign: 71 | @echo Running zipalign on ${APK_FILE} 72 | ${ZIPALIGN} -f -v 4 ${APK_FILE} ${FINAL_APK} 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # PyBluesky-android 3 | ### Version : 1.0.0 4 | 5 | 6 | 7 | Author : Lakhya Jyoti Nath (ljnath)
8 | Date : June 2021
9 | Email : ljnath@ljnath.com
10 | Website : https://ljnath.com 11 | 12 | [![GitHub license](https://img.shields.io/github/license/ljnath/PyBluesky-android)](https://github.com/ljnath/PyBluesky-android/blob/master/LICENSE) 13 | [![GitHub stars](https://img.shields.io/github/stars/ljnath/PyBluesky-android)](https://github.com/ljnath/PyBluesky-android/stargazers) 14 | [![LGTM grade](https://img.shields.io/lgtm/grade/python/github/ljnath/PyBluesky-android)](https://lgtm.com/projects/g/ljnath/PyBluesky-android/) 15 | [![LGTM alerts](https://img.shields.io/lgtm/alerts/github/ljnath/PyBluesky-android)](https://lgtm.com/projects/g/ljnath/PyBluesky-android/) 16 | 17 | 18 | [![Google PlayStore](https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png)](https://play.google.com/store/apps/details?id=com.ljnath.pybluesky) 19 | 20 | 21 | 22 | ## INTRODUCTION 23 | PyBluesky is a simple 2D python game developed using the pygame framework.
24 | Based on https://realpython.com/blog/python/pygame-a-primer 25 |

26 | 27 | ## GAME MECHANICS 28 | The game is simple where the objective is to navigate and shoot your way through the sky. 29 | There are enemy missiles which travels from right-to-left with varied speed. These enemy missiles can be destroyed by shooting at them. With increase in game level, SAM launchers also moves on the ground, which can fire targeted missile at the jet. These missiles cannot be destroyed, so user needs to evade them. 30 | 31 | The gameplay has levels, which changes every 20 seconds. A level increase results in increases of enemy missiles. 32 | It also gives 50 new ammo to the jet as well as the game score is bumped up by 10 points. 33 | 34 | The game also features a power-up star which falls across the sky at each level. 35 | Catching the power-up star will destroy all the enemy bullets in the current game frame. 36 |

37 | 38 | ## LEADERBOARD 39 | The game also features a network-controlled leaderboard. User scores along with few other metadata are published to a remote server. 40 | 41 | During the game startup, the updated scores are download from the server and displayed as leaderboard. 42 |

43 | 44 | -------------------------------------------------------------------------------- /assets/audio/collision.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ljnath/PyBluesky-android/da16acf0ff8367d3619b97428e8fd40179280784/assets/audio/collision.ogg -------------------------------------------------------------------------------- /assets/audio/fire.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ljnath/PyBluesky-android/da16acf0ff8367d3619b97428e8fd40179280784/assets/audio/fire.ogg -------------------------------------------------------------------------------- /assets/audio/levelup.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ljnath/PyBluesky-android/da16acf0ff8367d3619b97428e8fd40179280784/assets/audio/levelup.ogg -------------------------------------------------------------------------------- /assets/audio/missile_hit.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ljnath/PyBluesky-android/da16acf0ff8367d3619b97428e8fd40179280784/assets/audio/missile_hit.ogg -------------------------------------------------------------------------------- /assets/audio/music.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ljnath/PyBluesky-android/da16acf0ff8367d3619b97428e8fd40179280784/assets/audio/music.ogg -------------------------------------------------------------------------------- /assets/audio/powerup.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ljnath/PyBluesky-android/da16acf0ff8367d3619b97428e8fd40179280784/assets/audio/powerup.ogg -------------------------------------------------------------------------------- /assets/audio/shoot.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ljnath/PyBluesky-android/da16acf0ff8367d3619b97428e8fd40179280784/assets/audio/shoot.ogg -------------------------------------------------------------------------------- /assets/fonts/arcade.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ljnath/PyBluesky-android/da16acf0ff8367d3619b97428e8fd40179280784/assets/fonts/arcade.ttf -------------------------------------------------------------------------------- /assets/icon/pybluesky.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ljnath/PyBluesky-android/da16acf0ff8367d3619b97428e8fd40179280784/assets/icon/pybluesky.png -------------------------------------------------------------------------------- /assets/images/bullet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ljnath/PyBluesky-android/da16acf0ff8367d3619b97428e8fd40179280784/assets/images/bullet.png -------------------------------------------------------------------------------- /assets/images/cloud1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ljnath/PyBluesky-android/da16acf0ff8367d3619b97428e8fd40179280784/assets/images/cloud1.png -------------------------------------------------------------------------------- /assets/images/cloud2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ljnath/PyBluesky-android/da16acf0ff8367d3619b97428e8fd40179280784/assets/images/cloud2.png -------------------------------------------------------------------------------- /assets/images/cloud3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ljnath/PyBluesky-android/da16acf0ff8367d3619b97428e8fd40179280784/assets/images/cloud3.png -------------------------------------------------------------------------------- /assets/images/grass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ljnath/PyBluesky-android/da16acf0ff8367d3619b97428e8fd40179280784/assets/images/grass.png -------------------------------------------------------------------------------- /assets/images/ground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ljnath/PyBluesky-android/da16acf0ff8367d3619b97428e8fd40179280784/assets/images/ground.png -------------------------------------------------------------------------------- /assets/images/jet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ljnath/PyBluesky-android/da16acf0ff8367d3619b97428e8fd40179280784/assets/images/jet.png -------------------------------------------------------------------------------- /assets/images/missile_activated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ljnath/PyBluesky-android/da16acf0ff8367d3619b97428e8fd40179280784/assets/images/missile_activated.png -------------------------------------------------------------------------------- /assets/images/missile_deactivated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ljnath/PyBluesky-android/da16acf0ff8367d3619b97428e8fd40179280784/assets/images/missile_deactivated.png -------------------------------------------------------------------------------- /assets/images/presplash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ljnath/PyBluesky-android/da16acf0ff8367d3619b97428e8fd40179280784/assets/images/presplash.png -------------------------------------------------------------------------------- /assets/images/sam.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ljnath/PyBluesky-android/da16acf0ff8367d3619b97428e8fd40179280784/assets/images/sam.png -------------------------------------------------------------------------------- /assets/images/samlauncher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ljnath/PyBluesky-android/da16acf0ff8367d3619b97428e8fd40179280784/assets/images/samlauncher.png -------------------------------------------------------------------------------- /assets/images/star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ljnath/PyBluesky-android/da16acf0ff8367d3619b97428e8fd40179280784/assets/images/star.png -------------------------------------------------------------------------------- /assets/images/vegetation_plain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ljnath/PyBluesky-android/da16acf0ff8367d3619b97428e8fd40179280784/assets/images/vegetation_plain.png -------------------------------------------------------------------------------- /assets/images/vegetation_tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ljnath/PyBluesky-android/da16acf0ff8367d3619b97428e8fd40179280784/assets/images/vegetation_tree.png -------------------------------------------------------------------------------- /game/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ljnath/PyBluesky-android/da16acf0ff8367d3619b97428e8fd40179280784/game/__init__.py -------------------------------------------------------------------------------- /game/common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ljnath/PyBluesky-android/da16acf0ff8367d3619b97428e8fd40179280784/game/common/__init__.py -------------------------------------------------------------------------------- /game/common/singleton.py: -------------------------------------------------------------------------------- 1 | """ 2 | Class responsible for acting as metaclass for generation of singleton classes 3 | """ 4 | 5 | 6 | class Singleton(type): 7 | """ 8 | Class responsible for acting as metaclass for generation of singleton classes 9 | """ 10 | _instances = {} 11 | 12 | def __call__(cls, *args, **kwargs): 13 | if cls not in cls._instances: 14 | cls._instances[cls] = super(Singleton, cls).__call__( 15 | *args, 16 | **kwargs 17 | ) 18 | return cls._instances[cls] 19 | -------------------------------------------------------------------------------- /game/data/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ljnath/PyBluesky-android/da16acf0ff8367d3619b97428e8fd40179280784/game/data/__init__.py -------------------------------------------------------------------------------- /game/data/dynamic.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import re 4 | 5 | from pygame.mixer import Sound 6 | from pygame.sprite import Group 7 | 8 | from game.data.enums import Screen, StartChoice 9 | from game.data.static import StaticData 10 | 11 | 12 | class DynamicData(): 13 | """ Class which holds all the game variables 14 | """ 15 | def __init__(self): 16 | self.__static = StaticData() 17 | self.__collision_sound = Sound(self.__static.game_sound.get('collision')) 18 | self.__levelup_sound = Sound(self.__static.game_sound.get('levelup')) 19 | self.__shoot_sound = Sound(self.__static.game_sound.get('shoot')) 20 | self.__hit_sound = Sound(self.__static.game_sound.get('hit')) 21 | self.__powerup_sound = Sound(self.__static.game_sound.get('powerup')) 22 | self.__samfire_sound = Sound(self.__static.game_sound.get('samfire')) 23 | self.__game_start_choice = StartChoice.START 24 | self.__all_sprites = Group() 25 | self.__bullets = Group() 26 | self.__sam_missiles = Group() 27 | self.__noammo_sprite = None 28 | self.__update_available = False 29 | self.__replay = True 30 | self.__exit = False 31 | self.__update_url = None 32 | self.__player_name = '' 33 | 34 | # loading the player name from file, name can be max 20 character long 35 | if os.path.exists(self.__static.player_file): 36 | with open(self.__static.player_file) as file_reader: 37 | name = file_reader.read().strip()[: self.__static.name_length] 38 | self.__player_name = name if name and re.match(r'[a-zA-Z0-9@. ]', name) else '' 39 | 40 | self.__active_screen = Screen.NAME_INPUT if not self.__player_name else Screen.GAME_MENU 41 | self.load_defaults() 42 | 43 | def load_defaults(self): 44 | self.__ammo = 100 45 | self.__game_level = 1 46 | self.__game_score = 0 47 | self.__game_playtime = 0 48 | self.__bullet_fired = 0 49 | self.__missles_destroyed = 0 50 | self.__sam_missiles.empty() 51 | 52 | @property 53 | def collision_sound(self): 54 | return self.__collision_sound 55 | 56 | @property 57 | def levelup_sound(self): 58 | return self.__levelup_sound 59 | 60 | @property 61 | def shoot_sound(self): 62 | return self.__shoot_sound 63 | 64 | @property 65 | def hit_sound(self): 66 | return self.__hit_sound 67 | 68 | @property 69 | def powerup_sound(self): 70 | return self.__powerup_sound 71 | 72 | @property 73 | def samfire_sound(self): 74 | return self.__samfire_sound 75 | 76 | @property 77 | def game_start_choice(self): 78 | return self.__game_start_choice 79 | 80 | @game_start_choice.setter 81 | def game_start_choice(self, value): 82 | self.__game_start_choice = value 83 | 84 | @property 85 | def all_sprites(self): 86 | return self.__all_sprites 87 | 88 | @all_sprites.setter 89 | def all_sprites(self, value): 90 | self.__all_sprites = value 91 | 92 | @property 93 | def bullets(self): 94 | return self.__bullets 95 | 96 | @bullets.setter 97 | def bullets(self, value): 98 | self.__bullets = value 99 | 100 | @property 101 | def sam_missiles(self): 102 | return self.__sam_missiles 103 | 104 | @sam_missiles.setter 105 | def sam_missiles(self, value): 106 | self.__sam_missiles = value 107 | 108 | @property 109 | def ammo(self): 110 | return self.__ammo 111 | 112 | @ammo.setter 113 | def ammo(self, value): 114 | self.__ammo = value if value <= self.__static.max_ammo else self.__static.max_ammo 115 | 116 | @property 117 | def noammo_sprite(self): 118 | return self.__noammo_sprite 119 | 120 | @noammo_sprite.setter 121 | def noammo_sprite(self, value): 122 | self.__noammo_sprite = value 123 | 124 | @property 125 | def game_level(self): 126 | return self.__game_level 127 | 128 | @game_level.setter 129 | def game_level(self, value): 130 | self.__game_level = value 131 | 132 | @property 133 | def update_available(self): 134 | return self.__update_available 135 | 136 | @update_available.setter 137 | def update_available(self, value): 138 | self.__update_available = value 139 | 140 | @property 141 | def active_screen(self): 142 | return self.__active_screen 143 | 144 | @active_screen.setter 145 | def active_screen(self, value): 146 | self.__active_screen = value 147 | 148 | @property 149 | def game_score(self): 150 | return self.__game_score 151 | 152 | @game_score.setter 153 | def game_score(self, value): 154 | self.__game_score = value 155 | 156 | @property 157 | def game_playtime(self): 158 | return self.__game_playtime 159 | 160 | @game_playtime.setter 161 | def game_playtime(self, value): 162 | self.__game_playtime = value 163 | 164 | @property 165 | def replay(self): 166 | return self.__replay 167 | 168 | @replay.setter 169 | def replay(self, value): 170 | self.__replay = value 171 | 172 | @property 173 | def exit(self): 174 | return self.__exit 175 | 176 | @exit.setter 177 | def exit(self, value): 178 | self.__exit = value 179 | 180 | @property 181 | def player_name(self): 182 | return self.__player_name 183 | 184 | @player_name.setter 185 | def player_name(self, value): 186 | self.__player_name = value 187 | # saving the player name to file for future reference 188 | with open(self.__static.player_file, 'w') as file_writter: 189 | file_writter.write(self.__player_name) 190 | 191 | @property 192 | def bullets_fired(self): 193 | return self.__bullet_fired 194 | 195 | @bullets_fired.setter 196 | def bullets_fired(self, value): 197 | self.__bullet_fired = value 198 | 199 | @property 200 | def missiles_destroyed(self): 201 | return self.__missles_destroyed 202 | 203 | @missiles_destroyed.setter 204 | def missiles_destroyed(self, value): 205 | self.__missles_destroyed = value 206 | 207 | @property 208 | def accuracy(self): 209 | return 0 if self.bullets_fired == 0 else round(self.missiles_destroyed / self.bullets_fired * 100, 3) 210 | 211 | @property 212 | def update_url(self): 213 | return self.__update_url 214 | 215 | @update_url.setter 216 | def update_url(self, value): 217 | self.__update_url = value 218 | -------------------------------------------------------------------------------- /game/data/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class StartChoice(Enum): 5 | """ StartChoice enumerator which holds the game start choice (start or exit) for the game 6 | """ 7 | START = 1, 8 | EXIT = 2 9 | 10 | 11 | class Screen(Enum): 12 | """ TitleScreen enumerator which holds the available title screens 13 | """ 14 | GAME_MENU = 0, 15 | HELP = 1, 16 | LEADERBOARD = 2 17 | REPLAY_MENU = 3 18 | NAME_INPUT = 4 19 | EXIT_MENU = 5 20 | GAME_SCREEN = 6 21 | -------------------------------------------------------------------------------- /game/data/static.py: -------------------------------------------------------------------------------- 1 | from pygame import display 2 | from android.storage import app_storage_path 3 | 4 | 5 | class StaticData(): 6 | """ Class which holds all the static game values 7 | """ 8 | def __init__(self): 9 | self.__display_info = display.Info() # get current display information 10 | 11 | @property 12 | def name(self): 13 | return 'PyBluesky' 14 | 15 | @property 16 | def version(self): 17 | return '1.0.0' 18 | 19 | @property 20 | def android_app_directory(self): 21 | return f'{app_storage_path()}/app' 22 | 23 | @property 24 | def images_asset_directory(self): 25 | return f'{self.android_app_directory}/assets/images' 26 | 27 | @property 28 | def audio_asset_directory(self): 29 | return f'{self.android_app_directory}/assets/audio' 30 | 31 | @property 32 | def fonts_asset_directory(self): 33 | return f'{self.android_app_directory}/assets/fonts' 34 | 35 | @property 36 | def icon_asset_directory(self): 37 | return f'{self.android_app_directory}/assets/icon' 38 | 39 | @property 40 | def game_log_file(self): 41 | return f'{self.android_app_directory}/game.log' 42 | 43 | @property 44 | def leaders_file(self): 45 | return f'{self.android_app_directory}/leaders.dat' 46 | 47 | @property 48 | def offline_score_file(self): 49 | return f'{self.android_app_directory}/offline.dat' 50 | 51 | @property 52 | def screen_width(self): 53 | # setting fixed screen width for scaling 54 | return 1280 55 | 56 | @property 57 | def screen_height(self): 58 | # setting fixed screen height for scaling 59 | return 720 60 | 61 | @property 62 | def text_default_color(self): 63 | return (255, 0, 0) # default color is red 64 | 65 | @property 66 | def text_selection_color(self): 67 | return (0, 0, 255) # selection color is blue 68 | 69 | @property 70 | def game_font(self): 71 | return f'{self.fonts_asset_directory}/arcade.ttf' # game font file path 72 | 73 | @property 74 | def clouds(self): 75 | return ( 76 | f'{self.images_asset_directory}/cloud1.png', 77 | f'{self.images_asset_directory}/cloud2.png', 78 | f'{self.images_asset_directory}/cloud3.png' 79 | ) # all game cloud designs 80 | 81 | @property 82 | def vegetation(self): 83 | return ( 84 | f'{self.images_asset_directory}/vegetation_plain.png', 85 | f'{self.images_asset_directory}/vegetation_tree.png' 86 | ) 87 | 88 | @property 89 | def ground(self): 90 | return f'{self.images_asset_directory}/ground.png' 91 | 92 | @property 93 | def grass(self): 94 | return f'{self.images_asset_directory}/grass.png' 95 | 96 | @property 97 | def sam_launcher(self): 98 | return f'{self.images_asset_directory}/samlauncher.png' 99 | 100 | @property 101 | def sam(self): 102 | return f'{self.images_asset_directory}/sam.png' 103 | 104 | @property 105 | def missile_activated_image(self): 106 | return f'{self.images_asset_directory}/missile_activated.png' # missle image path 107 | 108 | @property 109 | def missile_deactivated_image(self): 110 | return f'{self.images_asset_directory}/missile_deactivated.png' # missle image path 111 | 112 | @property 113 | def jet_image(self): 114 | return f'{self.images_asset_directory}/jet.png' # jet image path 115 | 116 | @property 117 | def powerup_image(self): 118 | return f'{self.images_asset_directory}/star.png' # jet image path 119 | 120 | @property 121 | def bullet_image(self): 122 | return f'{self.images_asset_directory}/bullet.png' # bullet image path 123 | 124 | @property 125 | def cloud_per_sec(self): 126 | return 1 # number of cloud to be spawned per second 127 | 128 | @property 129 | def missile_per_sec(self): 130 | return 2 # number of missiles to be spawned per seconds 131 | 132 | @property 133 | def background_default(self): 134 | return (208, 244, 247) # skyblue color 135 | 136 | @property 137 | def background_special(self): 138 | return (196, 226, 255) # pale skyblue 139 | 140 | @property 141 | def fps(self): 142 | return 30 # game should run at 30 pfs 143 | 144 | @property 145 | def max_ammo(self): 146 | return 999 147 | 148 | @property 149 | def player_file(self): 150 | return f'{self.android_app_directory}/player.dat' 151 | 152 | @property 153 | def name_length(self): 154 | return 12 155 | 156 | @property 157 | def game_sound(self): 158 | return { 159 | 'music': f'{self.audio_asset_directory}/music.ogg', 160 | 'collision': f'{self.audio_asset_directory}/collision.ogg', 161 | 'levelup': f'{self.audio_asset_directory}/levelup.ogg', 162 | 'shoot': f'{self.audio_asset_directory}/shoot.ogg', 163 | 'hit': f'{self.audio_asset_directory}/missile_hit.ogg', 164 | 'powerup': f'{self.audio_asset_directory}/powerup.ogg', 165 | 'samfire': f'{self.audio_asset_directory}/fire.ogg' 166 | } 167 | -------------------------------------------------------------------------------- /game/environment.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from pygame import image 4 | from pygame.locals import ( 5 | MOUSEMOTION, 6 | MOUSEBUTTONDOWN, 7 | MOUSEBUTTONUP, 8 | FULLSCREEN, 9 | QUIT, 10 | RLEACCEL, 11 | SRCALPHA, 12 | VIDEORESIZE, 13 | KEYDOWN, 14 | TEXTINPUT 15 | ) 16 | 17 | from game.common.singleton import Singleton 18 | from game.data.dynamic import DynamicData 19 | from game.data.static import StaticData 20 | 21 | # importing here to avoid reimporting in all the sub modules 22 | 23 | 24 | class GameEnvironment(metaclass=Singleton): 25 | """ Game environment which holds the game contants, variables as well as pygame constants 26 | """ 27 | def __init__(self): 28 | self.__static_data = StaticData() 29 | self.__dynamic_data = DynamicData() 30 | 31 | def get_random_point_on_right(self): 32 | pos_x = random.randint(self.__static_data.screen_width + 20, self.__static_data.screen_width + 100) # generating random x position 33 | pos_y = random.randint(0, self.__static_data.screen_height) # generating random y position 34 | return (pos_x, pos_y) 35 | 36 | def get_random_point_on_top(self): 37 | pos_x = random.randint(0, self.__static_data.screen_width) # generating random x position 38 | pos_y = random.randint(10, 20) # generating random y position 39 | return (pos_x, pos_y * -1) 40 | 41 | def get_image_size(self, image_file): 42 | image_surf = image.load(image_file) 43 | return (image_surf.get_width(), image_surf.get_height()) 44 | 45 | def reset(self): 46 | self.__dynamic_data.load_defaults() 47 | 48 | @property 49 | def vegetation_size(self): 50 | return self.get_image_size(self.__static_data.vegetation[0]) 51 | 52 | @property 53 | def static(self): 54 | return self.__static_data 55 | 56 | @property 57 | def dynamic(self): 58 | return self.__dynamic_data 59 | 60 | @property 61 | def RLEACCEL(self): 62 | return RLEACCEL 63 | 64 | @property 65 | def SRCALPHA(self): 66 | return SRCALPHA 67 | 68 | @property 69 | def FULLSCREEN(self): 70 | return FULLSCREEN 71 | 72 | @property 73 | def QUIT(self): 74 | return QUIT 75 | 76 | @property 77 | def MOUSEBUTTONUP(self): 78 | return MOUSEBUTTONUP 79 | 80 | @property 81 | def MOUSEBUTTONDOWN(self): 82 | return MOUSEBUTTONDOWN 83 | 84 | @property 85 | def MOUSEMOTION(self): 86 | return MOUSEMOTION 87 | 88 | @property 89 | def VIDEORESIZE(self): 90 | return VIDEORESIZE 91 | 92 | @property 93 | def KEYDOWN(self): 94 | return KEYDOWN 95 | 96 | @property 97 | def TEXTINPUT(self): 98 | return TEXTINPUT 99 | -------------------------------------------------------------------------------- /game/handlers/__init__.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from game.environment import GameEnvironment 4 | 5 | 6 | class Handlers(): 7 | def __init__(self): 8 | pass 9 | 10 | def log(self, message): 11 | game_env = GameEnvironment() 12 | with open(game_env.static.game_log_file, 'a+') as file_handler: 13 | file_handler.write('\n[{:%Y-%m-%d %H:%M:%S.%f}] : {}'.format(datetime.now(), message)) 14 | -------------------------------------------------------------------------------- /game/handlers/leaderboard.py: -------------------------------------------------------------------------------- 1 | from game.environment import GameEnvironment 2 | from game.handlers import Handlers 3 | from game.handlers.network import NetworkHandler 4 | from game.handlers.serialize import SerializeHandler 5 | 6 | 7 | class LeaderBoardHandler(Handlers): 8 | def __init__(self): 9 | Handlers().__init__() 10 | self.__game_env = GameEnvironment() 11 | self.__serialize_handler = SerializeHandler(self.__game_env.static.leaders_file) 12 | 13 | def load(self): 14 | leaders = [] 15 | try: 16 | deserialized_object = self.__serialize_handler.deserialize() 17 | if deserialized_object: 18 | leaders = dict(deserialized_object) 19 | except Exception: 20 | self.log('Failed to read leaders from file {}'.format(self.__game_env.static.leaders_file)) 21 | finally: 22 | return leaders 23 | 24 | def save(self, leaders): 25 | try: 26 | if leaders is None: 27 | return 28 | 29 | self.__serialize_handler.serialize(leaders) 30 | except Exception: 31 | self.log('Failed to save leaders to file {}'.format(self.__game_env.static.leaders_file)) 32 | 33 | async def update(self, api_key): 34 | network_handler = NetworkHandler(api_key) 35 | self.save(await network_handler.get_leaders()) 36 | -------------------------------------------------------------------------------- /game/handlers/network.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from json import loads 3 | from time import time 4 | 5 | import aiohttp 6 | from game.environment import GameEnvironment 7 | from game.handlers import Handlers 8 | from game.handlers.serialize import SerializeHandler 9 | from jnius import autoclass 10 | 11 | 12 | class NetworkHandler(Handlers): 13 | def __init__(self, api_key): 14 | Handlers().__init__() 15 | game_env = GameEnvironment() 16 | self.__api_key = api_key 17 | self.__api_endpoint = 'https://app.ljnath.com/pybluesky/' 18 | self.__serialize_handler = SerializeHandler(game_env.static.offline_score_file) 19 | 20 | async def check_game_update(self): 21 | try: 22 | game_env = GameEnvironment() 23 | get_parameters = { 24 | 'action': 'getUpdate', 25 | 'apiKey': self.__api_key, 26 | 'platform': 'android' 27 | } 28 | async with aiohttp.ClientSession() as session: 29 | async with session.get(self.__api_endpoint, params=get_parameters, ssl=False, timeout=aiohttp.ClientTimeout(total=10)) as response: 30 | if response.status != 200: 31 | raise Exception() 32 | json_response = loads(await response.text()) 33 | if json_response['version'] != game_env.static.version: 34 | game_env.dynamic.update_available = True 35 | game_env.dynamic.update_url = json_response['url'] 36 | self.log('New game version {} detected'.format(json_response['version'])) 37 | except Exception: 38 | self.log('Failed to check for game update') 39 | await self.submit_result(only_sync=True) 40 | 41 | async def get_leaders(self): 42 | leaders = {} 43 | try: 44 | get_parameters = { 45 | 'action': 'getTopScores', 46 | 'apiKey': self.__api_key, 47 | 'platform': 'android' 48 | } 49 | async with aiohttp.ClientSession() as session: 50 | async with session.get(self.__api_endpoint, params=get_parameters, ssl=False, timeout=aiohttp.ClientTimeout(total=15)) as response: 51 | if response.status != 200: 52 | raise Exception() 53 | leaders = loads(await response.text()) 54 | except Exception: 55 | self.log('Failed to get game leaders from remote server') 56 | finally: 57 | return leaders 58 | 59 | async def submit_result(self, only_sync=False): 60 | payloads = [] 61 | game_env = GameEnvironment() 62 | deserialized_object = self.__serialize_handler.deserialize() 63 | if deserialized_object: 64 | payloads = list(deserialized_object) 65 | 66 | build = autoclass("android.os.Build") 67 | if not only_sync: 68 | payload = { 69 | 'apiKey': self.__api_key, 70 | 'name': f'{game_env.dynamic.player_name} ({build.MODEL})', 71 | 'score': game_env.dynamic.game_score, 72 | 'level': game_env.dynamic.game_level, 73 | 'accuracy': game_env.dynamic.accuracy, 74 | 'platform': 'android', 75 | "epoch": int(time()) 76 | } 77 | payloads.append(payload) 78 | 79 | unprocessed_payloads = [] 80 | async with aiohttp.ClientSession() as session: 81 | put_tasks = [asyncio.ensure_future(self.__post_results(session, payload)) for payload in payloads] 82 | await asyncio.gather(*put_tasks, return_exceptions=False) 83 | 84 | for task, payload in zip(put_tasks, payloads): 85 | if task._result: 86 | self.log('Successfully submitted result: score={}, name={}, level={}'.format(payload.get('score'), payload.get('name'), payload.get('level'))) 87 | else: 88 | payload.update({'apiKey': ''}) 89 | unprocessed_payloads.append(payload) 90 | self.log('Failed to submit game scrore: score={}, name={}, level={}'.format(payload.get('score'), payload.get('name'), payload.get('level'))) 91 | 92 | self.__serialize_handler.serialize(unprocessed_payloads) 93 | 94 | async def __post_results(self, session, payload): 95 | result = True 96 | try: 97 | payload['apiKey'] = self.__api_key 98 | async with session.put(self.__api_endpoint, json=payload, ssl=False, timeout=aiohttp.ClientTimeout(total=30)) as response: 99 | if response.status != 201: 100 | result = False 101 | except Exception: 102 | result = False 103 | finally: 104 | return result 105 | -------------------------------------------------------------------------------- /game/handlers/serialize.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pickle 3 | 4 | from game.handlers import Handlers 5 | 6 | 7 | class SerializeHandler(Handlers): 8 | def __init__(self, file): 9 | Handlers().__init__() 10 | self.__file = file 11 | 12 | def serialize(self, object): 13 | try: 14 | if object is None: 15 | return 16 | with open(self.__file, 'wb') as file_writter: 17 | pickle.dump(object, file_writter) 18 | except Exception: 19 | self.log('Failed to serialize object to file {}'.format(self.__file)) 20 | 21 | def deserialize(self): 22 | serialized_object = None 23 | try: 24 | if os.path.exists(self.__file): 25 | with open(self.__file, 'rb') as file_reader: 26 | serialized_object = pickle.load(file_reader) 27 | except Exception: 28 | self.log('Failed to deserialize file {}'.format(self.__file)) 29 | finally: 30 | return serialized_object 31 | -------------------------------------------------------------------------------- /game/sprites/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ljnath/PyBluesky-android/da16acf0ff8367d3619b97428e8fd40179280784/game/sprites/__init__.py -------------------------------------------------------------------------------- /game/sprites/bullet.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | from game.environment import GameEnvironment 4 | from pygame import image, sprite 5 | 6 | 7 | class Bullet(sprite.Sprite): 8 | """ Bullet sprite for create and moving bullet 9 | """ 10 | def __init__(self, x_pos, y_pos): # bullet constructur takes the position where it should be created 11 | super(Bullet, self).__init__() 12 | game_env = GameEnvironment() 13 | self.__x = x_pos 14 | self.__y = y_pos 15 | self.__speed = 8 16 | self.surf = image.load(game_env.static.bullet_image).convert() # loading bullet image from file 17 | self.surf.set_colorkey((255, 255, 255), game_env.RLEACCEL) # setting the white color as the transperant area; RLEACCEL is used for better performance on non accelerated displays 18 | self.rect = self.surf.get_rect(center=(self.__x, self.__y)) # setting the position of the bullet as the input (souce_x, y_pos) 19 | 20 | def update(self): 21 | game_env = GameEnvironment() 22 | dx = game_env.static.screen_width - self.rect.x 23 | dy = 0 24 | angle = math.atan2(dy, dx) 25 | self.rect.x += self.__speed * math.cos(angle) 26 | self.rect.y += self.__speed * math.sin(angle) 27 | 28 | if self.rect.right > game_env.static.screen_width + self.rect.width: # killing bullet if it crosses the screen completely 29 | self.kill() 30 | -------------------------------------------------------------------------------- /game/sprites/cloud.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from game.environment import GameEnvironment 4 | from pygame import image, sprite 5 | from pygame.font import Font 6 | 7 | 8 | class Cloud(sprite.Sprite): 9 | """ Cloud sprite class for creating and updating the cloud in the game screen 10 | """ 11 | def __init__(self): 12 | super(Cloud, self).__init__() # initilizing parent class pygame.sprite.Sprite 13 | game_env = GameEnvironment() 14 | self.surf = image.load(random.choice(game_env.static.clouds)).convert() # loading cloud image 15 | self.surf.set_colorkey((255, 255, 255), game_env.RLEACCEL) # setting the white color as the transperant area; RLEACCEL is used for better performance on non accelerated displays 16 | self.__speed = 5 17 | pos_x = random.randint(game_env.static.screen_width + 10, game_env.static.screen_width + 50) 18 | pos_y = random.randint(0, game_env.static.screen_height - game_env.vegetation_size[1] / 2) 19 | self.rect = self.surf.get_rect(center=(pos_x, pos_y)) # create rectange from the cloud screen 20 | 21 | if game_env.dynamic.update_available: 22 | self.update_cloud() 23 | 24 | def update(self): 25 | self.rect.move_ip(-self.__speed, 0) # move the cloud towards left at constant speed 26 | if self.rect.right < 0: # if the cloud has completly moved from the screen, the cloud is killed 27 | self.kill() 28 | 29 | def update_cloud(self): # adding 'Update avilable' text to cloud when a new version of the game is available 30 | game_env = GameEnvironment() 31 | font = Font(game_env.static.game_font, 14) 32 | txt_update_surf = font.render(' Update', 1, game_env.static.text_default_color) 33 | txt_available_surf = font.render(' Available', 1, game_env.static.text_default_color) 34 | self.surf.blit(txt_update_surf, (12, 13)) 35 | self.surf.blit(txt_available_surf, (5, 23)) 36 | self.__speed = 2 37 | -------------------------------------------------------------------------------- /game/sprites/jet.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | from game.environment import GameEnvironment 4 | from game.sprites.bullet import Bullet 5 | from pygame import image, sprite 6 | 7 | 8 | # Jet class which holds jet attributes and behaviour 9 | class Jet(sprite.Sprite): 10 | """ Jet sprite class for creating and updating the jet in the game screen 11 | """ 12 | def __init__(self): 13 | super(Jet, self).__init__() # initilizing parent class pygame.sprite.Sprite 14 | game_env = GameEnvironment() 15 | self.surf = image.load(game_env.static.jet_image).convert() # loading jet image from file 16 | self.surf.set_colorkey((255, 255, 255), game_env.RLEACCEL) # setting the white color as the transperant area; RLEACCEL is used for better performance on non accelerated displays 17 | self.rect = self.surf.get_rect(center=(50, game_env.static.screen_height / 2)) # getting rectangle from jet screen; setting the jet position as the middle of the scrren on the left 18 | 19 | def update(self, acceleration_values): 20 | if not acceleration_values or len(acceleration_values) != 3 or None in acceleration_values: 21 | return 22 | 23 | # maginfy the acceleration value factor to calculate the projected new jet position 24 | acceleration_magnify_factor = 50 25 | 26 | x_axis = acceleration_values[0] 27 | y_axis = acceleration_values[1] 28 | 29 | # calculating projected jet position based on current position and accelertation change 30 | # the values are reversed as the gameplay will be in landscape mode 31 | projected_x = self.rect.y + (y_axis * acceleration_magnify_factor) 32 | projected_y = self.rect.x + (x_axis * acceleration_magnify_factor) 33 | 34 | self.auto_move((projected_x, projected_y)) 35 | 36 | def auto_move(self, position): 37 | speed = 7 38 | dx = position[0] - self.rect.x # calculating x-coordinate difference of mouse and current jet position 39 | dy = position[1] - self.rect.y # caluclating y-coordinate difference of mouse and current jet position 40 | if (dx >= -speed and dx <= speed) and (dy >= -speed and dy <= speed): # jet will not move if the delta is less then its speed 41 | return 42 | angle = math.atan2(dy, dx) # calculating angle 43 | self.rect.x += speed * math.cos(angle) # moving the x-coordinate of jet towards the mouse cursor 44 | self.rect.y += speed * math.sin(angle) # moving the y-coordinate of jet towards the mouse cursor 45 | self.__maintain_boundary() 46 | 47 | def shoot(self): 48 | game_env = GameEnvironment() 49 | if game_env.dynamic.ammo > 0: 50 | bullet = Bullet(self.rect.x + self.rect.width + 10, self.rect.y + 22) # create a bullet where the jet is located 51 | game_env.dynamic.bullets.add(bullet) # add the bullet to bullet group 52 | game_env.dynamic.all_sprites.add(bullet) # add the bullet tp all_sprites 53 | game_env.dynamic.shoot_sound.play() # play shooting sound 54 | game_env.dynamic.ammo -= 1 55 | game_env.dynamic.bullets_fired += 1 56 | else: 57 | game_env.dynamic.all_sprites.add(game_env.dynamic.noammo_sprite) # show noammo sprite 58 | 59 | def __maintain_boundary(self): 60 | game_env = GameEnvironment() 61 | if self.rect.left < 0: 62 | self.rect.left = 0 # if the jet has moved left and have crossed the screen; the left position is set to 0 as it is the boundary 63 | if self.rect.top < 0: 64 | self.rect.top = 0 # if the jet has moved top and have crossed the screen; the top position is set to 0 as it is the boundary 65 | if self.rect.right > game_env.static.screen_width: 66 | self.rect.right = game_env.static.screen_width # if the jet has moved right and have crossed the screen; the right position is set to screen width as it is the boundary 67 | if self.rect.bottom > game_env.static.screen_height - game_env.vegetation_size[1] / 2: 68 | self.rect.bottom = game_env.static.screen_height - game_env.vegetation_size[1] / 2 # if the jet has moved bottom and have crossed the screen; the bottom position is set to screen width as it is the boundary 69 | -------------------------------------------------------------------------------- /game/sprites/missile.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from game.environment import GameEnvironment 4 | from pygame import image, sprite 5 | 6 | 7 | # Missile class which holds missile attributes and behaviour 8 | class Missile(sprite.Sprite): 9 | """ Missile sprite class for creating and updating the missile in the game screen 10 | """ 11 | def __init__(self): 12 | super(Missile, self).__init__() # initilizing parent class pygame.sprite.Sprite 13 | game_env = GameEnvironment() 14 | self.surf = image.load(game_env.static.missile_activated_image).convert() # loading missile image from file 15 | self.surf.set_colorkey((255, 255, 255), game_env.RLEACCEL) # setting the white color as the transperant area; RLEACCEL is used for better performance on non accelerated displays 16 | pos_x = random.randint(game_env.static.screen_width + 10, game_env.static.screen_width + 60) # generating random x position 17 | pos_y = random.randint(0, game_env.static.screen_height - game_env.vegetation_size[1] / 2) # generating random y position 18 | self.rect = self.surf.get_rect(center=(pos_x, pos_y)) # create rectange from the missile screen 19 | self.__activated = True # bad missiles will drop down 20 | self.__speed = random.randint(5, 20) # generating random speed for the missle 21 | boost_factor = game_env.dynamic.game_level // 10 # increasing missile speed by 5% every 10th level 22 | self.__speed += int(self.__speed * (boost_factor * 5) / 100) 23 | 24 | def update(self): 25 | game_env = GameEnvironment() 26 | if not self.__activated: 27 | self.rect.move_ip(0, 10) # missile moves down 28 | else: 29 | self.rect.move_ip(-self.__speed, 0) # missile moves towards jet 30 | 31 | if self.rect.right < 0 or self.rect.bottom > game_env.static.screen_height: # if the missile has completly moved from the screen, the missile is killed 32 | self.kill() 33 | 34 | def deactivate(self): 35 | game_env = GameEnvironment() 36 | self.__activated = False # marking the current missile as bad 37 | self.surf = image.load(game_env.static.missile_deactivated_image).convert() # updating missle image when deactivated 38 | self.surf.set_colorkey((255, 255, 255), game_env.RLEACCEL) # adding transperacny to image 39 | -------------------------------------------------------------------------------- /game/sprites/sam.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | from game.environment import GameEnvironment 4 | from pygame import image, sprite, transform 5 | 6 | 7 | class Sam(sprite.Sprite): 8 | """ SurfaceToAirMissile (SAM) sprite for create and moving bullet 9 | """ 10 | def __init__(self, source, target, flip): 11 | super(Sam, self).__init__() 12 | game_env = GameEnvironment() 13 | self.__angle = math.atan2(target[1] - source[1], target[0] - source[0]) # sam angle of fire in radian 14 | self.__speed = 5 + (1 if game_env.dynamic.game_level % 2 == 0 else 0) # default sam speed is 5 and increased each level 15 | 16 | if flip: # sam image rotational angle based on the fire position 17 | rotation_angle = 90 - self.__angle * (180 / 3.1415) 18 | else: 19 | rotation_angle = 270 + self.__angle * (180 / 3.1415) 20 | 21 | self.surf = image.load(game_env.static.sam).convert() # loading sam image 22 | self.surf = transform.rotate(self.surf, rotation_angle) # rotating the image 23 | self.surf = transform.flip(self.surf, flip, ~flip) # flipping image as necessary 24 | self.surf.set_colorkey((255, 255, 255), game_env.RLEACCEL) # setting the white color as the transperant area; RLEACCEL is used for better performance on non accelerated displays 25 | self.rect = self.surf.get_rect(center=(source[0], source[1])) # setting the position of the bullet as the input (souce_x, y_pos) 26 | 27 | def update(self): 28 | game_env = GameEnvironment() 29 | self.rect.x += self.__speed * math.cos(self.__angle) 30 | self.rect.y += self.__speed * math.sin(self.__angle) 31 | if self.rect.right < 0 or self.rect.left > game_env.static.screen_width or self.rect.bottom < 0: 32 | self.kill() 33 | -------------------------------------------------------------------------------- /game/sprites/samlauncher.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from game.environment import GameEnvironment 4 | from game.sprites.sam import Sam 5 | from pygame import image, sprite, transform 6 | 7 | 8 | class SamLauncher(sprite.Sprite): 9 | """ SamLauncher sprite class for creating and updating the vegetation in the game screen 10 | """ 11 | def __init__(self): 12 | super(SamLauncher, self).__init__() 13 | game_env = GameEnvironment() 14 | self.surf = image.load(game_env.static.sam_launcher).convert() 15 | self.surf.set_colorkey((255, 255, 255), game_env.RLEACCEL) 16 | self.__speed = random.randint(8, 12) # speed of the sam launcher 17 | self.__flip = random.choice([True, False]) # random filp choice, filp-launcher will travel from left2right, else from right2left 18 | self.__fired = False if random.choice([0, 1, 2]) == 0 else True # random choice is the launcher will fire or not; reducing launch probabity to 25% 19 | self.__min_distance = int(random.randint(10, 30) * game_env.static.screen_width / 100) # min distance to cover before the launcher can fire 20 | 21 | # flip logic 22 | if self.__flip: 23 | self.surf = transform.flip(self.surf, True, False) 24 | x_pos = 0 25 | else: 26 | x_pos = game_env.static.screen_width 27 | self.__speed *= -1 28 | self.__min_distance -= game_env.static.screen_width 29 | 30 | y_pos = game_env.static.screen_height - self.surf.get_height() - 15 31 | self.rect = self.surf.get_rect(center=(x_pos, y_pos)) 32 | 33 | def update(self, target): 34 | if not self.__fired and self.rect.x > self.__min_distance: # if not fired and if the launcher has crossed the minimum diatance 35 | self.fire(target) 36 | self.__fired = True 37 | 38 | game_env = GameEnvironment() 39 | self.rect.move_ip(self.__speed, 0) 40 | if self.rect.right < 0 or self.rect.left > game_env.static.screen_width: 41 | self.kill() 42 | 43 | def fire(self, target): 44 | game_env = GameEnvironment() 45 | sam = Sam((self.rect.x, self.rect.y), target, self.__flip) 46 | game_env.dynamic.sam_missiles.add(sam) 47 | game_env.dynamic.all_sprites.add(sam) 48 | game_env.dynamic.samfire_sound.play() 49 | -------------------------------------------------------------------------------- /game/sprites/star.py: -------------------------------------------------------------------------------- 1 | from game.environment import GameEnvironment 2 | from pygame import image, sprite 3 | 4 | 5 | # Star class which holds star attributes and behaviour 6 | class Star(sprite.Sprite): 7 | """ Powerup sprite class for creating and updating the star in the game screen 8 | """ 9 | def __init__(self): 10 | super(Star, self).__init__() 11 | game_env = GameEnvironment() 12 | self.surf = image.load(game_env.static.powerup_image).convert() 13 | self.surf.set_colorkey((255, 255, 255), game_env.RLEACCEL) 14 | self.rect = self.surf.get_rect(center=game_env.get_random_point_on_top()) # powerup stars are created on top of screen 15 | self.__current_alpha = 255 16 | self.__transperant = False 17 | flash_rate = 6 # setting the blink rate of the star 18 | self.__alpha_delta = int(255 / flash_rate) # calculating the alpha delta based on the blink rate 19 | 20 | def update(self): 21 | game_env = GameEnvironment() 22 | self.rect.move_ip(0, 4) # star moves down with speed 4 23 | if self.__current_alpha < 0 or self.__current_alpha > 255: # reversing the tranperancy value if alpha reaches threshold 24 | self.__transperant = ~self.__transperant # flicking effect on the star 25 | self.__current_alpha += self.__alpha_delta if self.__transperant else -self.__alpha_delta 26 | self.surf.set_alpha(self.__current_alpha) 27 | if self.rect.bottom > game_env.static.screen_height: # star is killed if it crosses the screens 28 | self.kill() 29 | -------------------------------------------------------------------------------- /game/sprites/text/__init__.py: -------------------------------------------------------------------------------- 1 | from game.environment import GameEnvironment 2 | from pygame.font import Font 3 | from pygame.sprite import Sprite 4 | 5 | 6 | class Text(Sprite): 7 | """ Text class for create sprite out of text 8 | """ 9 | def __init__(self, text='', size=0, color=None, pos_x=None, pos_y=None): 10 | Sprite.__init__(self) # initializing parent class 11 | self.__game_env = GameEnvironment() 12 | self.color = self.__game_env.static.text_default_color if color is None else color # storing argument color in class variable 13 | self.font = Font(self.__game_env.static.game_font, size) # loading font and creating class variable font with given size 14 | self.surf = self.font.render(text, 1, self.color) # creating surface by rendering the text 15 | pos_x = self.__game_env.static.screen_width / 2 if pos_x is None else pos_x # default position is set to center of screen 16 | pos_y = self.__game_env.static.screen_height / 2 if pos_y is None else pos_y # default position is set to center of screen 17 | self.rect = self.surf.get_rect(center=(pos_x, pos_y)) # creating rectangle from the surface 18 | self.__move_forward = True 19 | self.__move_up = True 20 | 21 | def render(self, text): 22 | self.surf = self.font.render(text, 2, self.color) # dynamically updating the surface with updated text 23 | 24 | def moveOnXaxis(self, speed): 25 | """ Method to move the text across the X axis 26 | """ 27 | if not self.__move_forward and self.rect.x <= 0: # detecting if the sprite should move forward or backward 28 | self.__move_forward = True 29 | elif self.__move_forward and self.rect.x + self.rect.width >= self.__game_env.static.screen_width: 30 | self.__move_forward = False 31 | self.rect.x += speed if self.__move_forward else (speed * -1) 32 | 33 | def moveOnYaxis(self, speed): 34 | """ Method to move the text across the Y axis 35 | """ 36 | if not self.__move_up and self.rect.y <= 0: # detecting if the sprite should move up or down 37 | self.__move_up = True 38 | elif self.__move_up and self.rect.y + self.rect.height >= self.__game_env.static.screen_height: 39 | self.__move_up = False 40 | self.rect.y += speed if self.__move_up else (speed * -1) 41 | -------------------------------------------------------------------------------- /game/sprites/text/exitmenu.py: -------------------------------------------------------------------------------- 1 | from game.environment import GameEnvironment 2 | from game.sprites.text import Text 3 | from pygame.surface import Surface 4 | 5 | 6 | class ExitMenuText(Text): 7 | """ ExitText class extended from Text class. 8 | It creates the game exit menu with confirmation sprite 9 | """ 10 | def __init__(self): 11 | Text.__init__(self, size=30) 12 | game_env = GameEnvironment() 13 | self.__title_surf = self.font.render("Do you want to quit ?", 1, self.color) 14 | 15 | self.__y_selected_surf = self.font.render("Yes", 1, game_env.static.text_selection_color) # creating surface with Yes text when highlighted 16 | self.__n_surf = self.font.render("/No", 1, self.color) # creating surface with No text 17 | self.__y_surf = self.font.render("Yes/", 1, self.color) # creating surface with Yes text 18 | self.__n_selected_surf = self.font.render("No", 1, game_env.static.text_selection_color) # creating surface with No text when highlighted 19 | 20 | self.__choices = self.__title_surf.get_width() / 2 - (self.__y_selected_surf.get_width() + self.__n_surf.get_width()) / 2 21 | self.__highlight_no() 22 | 23 | def __recreate_surf(self): 24 | game_env = GameEnvironment() 25 | self.surf = Surface((self.__title_surf.get_width(), self.__title_surf.get_height() + self.__y_surf.get_height()), game_env.SRCALPHA) 26 | self.surf.blit(self.__title_surf, (self.surf.get_width() / 2 - self.__title_surf.get_width() / 2, 0)) 27 | 28 | def update(self, acceleration_values): 29 | if not acceleration_values or len(acceleration_values) != 3 or None in acceleration_values: # validation of acceleration_values 30 | return 31 | 32 | game_env = GameEnvironment() 33 | y_axis = acceleration_values[1] 34 | if y_axis > 3: # checking if android device is tiled LEFT 35 | game_env.dynamic.exit = False # setting game pause choice to NO 36 | self.__highlight_no() # calling method to highlight No 37 | elif y_axis < -3: # checking if android device is tiled RIGHT 38 | game_env.dynamic.exit = True # setting game pause choice to YES 39 | self.__highlight_yes() # calling method to highlight Yes 40 | 41 | self.rect = self.surf.get_rect(center=(game_env.static.screen_width / 2, game_env.static.screen_height / 2 + 50)) # creating default rect and setting its position center 42 | 43 | def __highlight_yes(self): 44 | self.__recreate_surf() 45 | self.surf.blit(self.__y_selected_surf, (self.__choices, self.__title_surf.get_height())) 46 | self.surf.blit(self.__n_surf, (self.__choices + self.__y_selected_surf.get_width(), self.__title_surf.get_height())) 47 | 48 | def __highlight_no(self): 49 | self.__recreate_surf() 50 | self.surf.blit(self.__y_surf, (self.__choices, self.__title_surf.get_height())) 51 | self.surf.blit(self.__n_selected_surf, (self.__choices + self.__y_surf.get_width(), self.__title_surf.get_height())) 52 | -------------------------------------------------------------------------------- /game/sprites/text/gamemenu.py: -------------------------------------------------------------------------------- 1 | from game.data.enums import StartChoice 2 | from game.environment import GameEnvironment 3 | from game.sprites.jet import Jet 4 | from game.sprites.text import Text 5 | from pygame.surface import Surface 6 | 7 | 8 | class GameMenuText(Text): 9 | """ GameMenuText class extended from Text class. 10 | It creates the game start choice menu sprite 11 | """ 12 | def __init__(self): 13 | Text.__init__(self, size=30) # initilizing parent class with default text color as red 14 | game_env = GameEnvironment() 15 | 16 | exit_text = 'Exit' 17 | start_game_text = 'Start Game' 18 | 19 | self.__jet = Jet() # creating a of jet 20 | self.__prefix_surf = self.font.render("What do you plan ?", 1, self.color) # creating surface with the prefix text 21 | self.__exit = self.font.render(f' {exit_text}', 1, self.color) # creating surface with Exit text 22 | self.__play = self.font.render(f' {start_game_text}', 1, self.color) # creating surface with Play Game text 23 | 24 | start_game_surface = self.font.render(f' {start_game_text}', 1, game_env.static.text_selection_color) # creating surface with Start Game text when highlighted 25 | self.__start_game_selected = Surface((self.__jet.surf.get_width() + start_game_surface.get_width(), start_game_surface.get_height()), game_env.SRCALPHA) # creating surface for jet and highlighted StartGame text 26 | self.__start_game_selected.blit(self.__jet.surf, (0, 0)) # drawing the jet 27 | self.__start_game_selected.blit(start_game_surface, (self.__jet.surf.get_width(), 0)) # drawing the highligted StartGame text after the jet image 28 | 29 | exit_surface = self.font.render(f' {exit_text}', 1, game_env.static.text_selection_color) # creating surface with Exit text when highlighted 30 | self.__exit_selected = Surface((self.__jet.surf.get_width() + exit_surface.get_width(), exit_surface.get_height()), game_env.SRCALPHA) # creating surface for jet and highlighted Exit text 31 | self.__exit_selected.blit(self.__jet.surf, (0, 0)) # drawing the jet 32 | self.__exit_selected.blit(exit_surface, (self.__jet.surf.get_width(), 0)) # drawing the highligted Exit text after the jet image 33 | 34 | self.__left_padding = self.__prefix_surf.get_width() / 2 - self.__start_game_selected.get_width() / 2 35 | self.__highlight_start_game() # calling method to highlight StartGame (the default choice) 36 | 37 | def __recreate_surface(self): 38 | game_env = GameEnvironment() 39 | self.surf = Surface((self.__prefix_surf.get_width(), self.__prefix_surf.get_height() * 3), game_env.SRCALPHA) # creating default surface of combinted expected length 40 | self.surf.blit(self.__prefix_surf, (0, 0)) # drawing the prefix text 41 | 42 | def update(self, acceleration_values): 43 | if not acceleration_values or len(acceleration_values) != 3 or None in acceleration_values: # validation of acceleration_values 44 | return 45 | 46 | game_env = GameEnvironment() 47 | x_axis = acceleration_values[0] 48 | if x_axis < 0: # when device accleration is moved UP 49 | game_env.dynamic.game_start_choice = StartChoice.START # StartChoice 'Start Game'is selected 50 | self.__highlight_start_game() # StartChoice 'Start Game'is highlighted 51 | elif x_axis > 5: # when device accleration is moved DOWN 52 | game_env.dynamic.game_start_choice = StartChoice.EXIT # StartChoice 'Exit'is selected 53 | self.__highlight_exit() # StartChoice 'Exit'is highlighted 54 | 55 | self.rect = self.surf.get_rect(center=(game_env.static.screen_width / 2, game_env.static.screen_height / 2 + 50)) # creating default rect and setting its position center 56 | 57 | def __highlight_start_game(self): 58 | self.__recreate_surface() # recreating the surface, as we will re-draw 59 | self.surf.blit(self.__start_game_selected, (self.__left_padding, self.__prefix_surf.get_height())) # drawing the jet+StartGame text 60 | self.surf.blit(self.__exit, (self.__left_padding + self.__jet.surf.get_width(), self.__prefix_surf.get_height() * 2)) # drawing the Exit text 61 | 62 | def __highlight_exit(self): 63 | self.__recreate_surface() # recreating the surface, as we will re-draw 64 | self.surf.blit(self.__play, (self.__left_padding + self.__jet.surf.get_width(), self.__prefix_surf.get_height())) # drawing StartGame text 65 | self.surf.blit(self.__exit_selected, (self.__left_padding, self.__prefix_surf.get_height() * 2)) # drawing jet+Exit text 66 | -------------------------------------------------------------------------------- /game/sprites/text/help.py: -------------------------------------------------------------------------------- 1 | from game.environment import GameEnvironment 2 | from game.sprites.text import Text 3 | from pygame.surface import Surface 4 | 5 | 6 | class HelpText(Text): 7 | """ HelpText class extended from Text class. 8 | It creates the game help sprite 9 | """ 10 | def __init__(self): 11 | Text.__init__(self, size=22) 12 | game_env = GameEnvironment() 13 | seperator = self.font.render(' ', 1, self.color) 14 | header = self.font.render('=== HELP ===', 1, self.color) 15 | footer = self.font.render('=== GOOD LUCK ===', 1, self.color) 16 | all_surfaces = [] 17 | all_surfaces.append(seperator) 18 | all_surfaces.append(self.font.render('Your objective should you choose to accept is to navigate your jet without getting hit by', 1, self.color)) 19 | all_surfaces.append(self.font.render('the incoming missiles. For self-defence you can shoot down the enemy missiles. You are', 1, self.color)) 20 | all_surfaces.append(self.font.render('armed with 100 special missiles. Level-up awards you another 50 special missiles and a', 1, self.color)) 21 | all_surfaces.append(self.font.render('power-up star which will instantly deactivate all the enemy missiles.', 1, self.color)) 22 | all_surfaces.append(self.font.render('Your jet can carry maximum 999 special missiles.', 1, self.color)) 23 | all_surfaces.append(seperator) 24 | all_surfaces.append(self.font.render('During menu screen, move your device horizontally and vertically to select options in menu', 1, self.color)) 25 | all_surfaces.append(self.font.render('and tap on screen to confirm. During gameplay, move your device horizontally and vertically', 1, self.color)) 26 | all_surfaces.append(self.font.render('to control the jet and tap on the screen to shoot', 1, self.color)) 27 | all_surfaces.append(self.font.render(' ', 1, self.color)) 28 | all_surfaces.append(self.font.render('POINTS: Destroy Missle -> 10 pts. Power-up Star -> 100 pts. Level-up -> 10 pts.', 1, self.color)) 29 | all_surfaces.append(seperator) 30 | 31 | self.surf = Surface((all_surfaces[1].get_width(), all_surfaces[0].get_height() * (len(all_surfaces) + 2)), game_env.SRCALPHA) 32 | 33 | self.surf.blit(header, (self.surf.get_width() / 2 - header.get_width() / 2, 0)) 34 | for index, temp_surf in enumerate(all_surfaces): 35 | self.surf.blit(temp_surf, (0, header.get_height() + index * temp_surf.get_height())) 36 | self.surf.blit(footer, (self.surf.get_width() / 2 - footer.get_width() / 2, self.surf.get_height() - footer.get_height())) 37 | 38 | self.rect = self.surf.get_rect(center=(game_env.static.screen_width / 2, game_env.static.screen_height / 2)) 39 | -------------------------------------------------------------------------------- /game/sprites/text/input/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ljnath/PyBluesky-android/da16acf0ff8367d3619b97428e8fd40179280784/game/sprites/text/input/__init__.py -------------------------------------------------------------------------------- /game/sprites/text/input/name.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from game.environment import GameEnvironment 4 | from game.sprites.text import Text 5 | from pygame.surface import Surface 6 | from pygame.key import start_text_input, stop_text_input 7 | 8 | 9 | class NameInputText(Text): 10 | """ NameInputText class extended from Text class. 11 | Class is responsible for creating the sprite for taking player name as input 12 | """ 13 | def __init__(self): 14 | Text.__init__(self, size=32) # initilizing parent class with default text color as red 15 | game_env = GameEnvironment() 16 | self.__is_rect_updated = False 17 | self.__player_name = '' 18 | self.__header = Text("=== ENTER YOUR NAME ===", 36) 19 | self.__footer = Text("===============================", 36) 20 | self.__seperator = Text(' ', 36) 21 | self.__clear = Text("< CLEAR >", 36) 22 | self.__ok = Text("< OK >", 36) 23 | 24 | max_surface_height = self.__footer.surf.get_height() 25 | max_surface_width = self.__footer.surf.get_width() 26 | 27 | # creating a single bottom surface to hold the footer, seperator and user buttons 28 | # width of the bottom surface is same as the width of the header text and height is same as footer + seperator + choice-texts (CLEAR & OK) 29 | self.__bottom_surface = Surface((max_surface_width, max_surface_height * 3), game_env.SRCALPHA) 30 | self.__bottom_surface.blit(self.__footer.surf, (0, 0)) 31 | self.__bottom_surface.blit(self.__seperator.surf, (0, max_surface_height)) 32 | self.__bottom_surface.blit(self.__clear.surf, (0, max_surface_height * 2)) 33 | self.__bottom_surface.blit(self.__ok.surf, (max_surface_width - self.__ok.surf.get_width(), max_surface_height * 2)) 34 | 35 | self.__render() 36 | 37 | def update(self, key): 38 | game_env = GameEnvironment() 39 | if key and re.match(r'[a-zA-Z0-9@. ]', key): # basic input validation; user cannot enter rubbish 40 | if len(self.__player_name) <= game_env.static.name_length: # to avoid longer name 41 | self.__player_name += key 42 | 43 | # re-rendering the surface after updating the input text 44 | self.__render() 45 | 46 | def check_for_touch(self, position): 47 | start_keyboard = False 48 | 49 | if self.__header.rect.collidepoint(position): 50 | start_keyboard = True 51 | elif self.__input.rect.collidepoint(position): 52 | start_keyboard = True 53 | elif self.__footer.rect.collidepoint(position): 54 | start_keyboard = True 55 | elif self.__clear.rect.collidepoint(position): 56 | self.__player_name = '' 57 | self.__render() 58 | elif self.__ok.rect.collidepoint(position): 59 | if len(self.__player_name.strip()) > 0: # to avoid spaces as name 60 | game_env = GameEnvironment() 61 | game_env.dynamic.player_name = self.__player_name.strip() 62 | else: 63 | self.__player_name.strip() 64 | 65 | if start_keyboard: 66 | start_text_input() 67 | else: 68 | stop_text_input() 69 | 70 | def __render(self): 71 | game_env = GameEnvironment() 72 | # self.__input = self.font.render(self.__player_name, 1, self.color) 73 | self.__input = Text(self.__player_name, 36) 74 | 75 | max_surface_height = self.__footer.surf.get_height() 76 | max_surface_width = self.__footer.surf.get_width() 77 | 78 | # creating a new surface to stitch all header, input and footer surface together 79 | self.surf = Surface((max_surface_width, max_surface_height * 5), game_env.SRCALPHA) 80 | 81 | # stitching the header, input and footer surface into one 82 | self.surf.blit(self.__header.surf, (self.surf.get_width() / 2 - self.__header.surf.get_width() / 2, 0)) 83 | self.surf.blit(self.__input.surf, (self.surf.get_width() / 2 - self.__input.surf.get_width() / 2, max_surface_height)) 84 | self.surf.blit(self.__bottom_surface, (self.surf.get_width() / 2 - self.__bottom_surface.get_width() / 2, max_surface_height * 2)) 85 | 86 | # creating rect from the stitched surface 87 | self.rect = self.surf.get_rect(center=(game_env.static.screen_width / 2, game_env.static.screen_height / 2)) 88 | 89 | # updating positions if it hasn't been done yet; the default x and y for the header rect is 0,0 90 | # so comparing against that 91 | if not self.__is_rect_updated: 92 | self.__is_rect_updated = True 93 | 94 | # updating position of header rect 95 | pos_x = self.rect.left 96 | pos_y = self.rect.top 97 | self.__header.rect.update(pos_x, pos_y, max_surface_width, max_surface_height) 98 | 99 | # updating position of the input rect, the width and height is considered to be same as the header surface 100 | pos_y += max_surface_height 101 | self.__input.rect.update(pos_x, pos_y, max_surface_width, max_surface_height) 102 | 103 | # updating position of footer rect; pos_x will be same; need to re-calculate pos_y 104 | pos_y += max_surface_height 105 | self.__footer.rect.update(pos_x, pos_y, max_surface_width, max_surface_height) 106 | 107 | # updating position of CLEAR rect; pos_x will be same, only the pos_y will be different 108 | pos_y += max_surface_height * 2 109 | self.__clear.rect.update(pos_x, pos_y, self.__clear.surf.get_width(), max_surface_height) 110 | 111 | # updating the position for OK rect, pos_x will be differnet as OK text is to the right of CLEAR text; pos_y will be unchanged 112 | pos_x += self.surf.get_width() - self.__ok.surf.get_width() 113 | self.__ok.rect.update(pos_x, pos_y, self.__ok.surf.get_width(), max_surface_height) 114 | -------------------------------------------------------------------------------- /game/sprites/text/leaderboard.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from game.environment import GameEnvironment 4 | from game.handlers.leaderboard import LeaderBoardHandler 5 | from game.sprites.text import Text 6 | from pygame.surface import Surface 7 | 8 | 9 | class LeaderBoardText(Text): 10 | """ LeaderBoardText class extended from Text class. 11 | It creates the the leaderboard table sprite 12 | """ 13 | def __init__(self): 14 | Text.__init__(self, size=22) 15 | game_env = GameEnvironment() 16 | name_length = game_env.static.name_length * 2 17 | leaders = LeaderBoardHandler().load() 18 | seperator = self.font.render('===================================================================================================', 1, self.color) 19 | header = self.font.render('=== HALL OF FAME ===', 1, self.color) 20 | all_surfaces = [] 21 | all_surfaces.append(seperator) 22 | all_surfaces.append(self.font.render(f"{'RANK'.ljust(5)} {'NAME'.ljust(name_length)} {'SCORE'.ljust(10)} {'LEVEL'.ljust(5)} {'ACCURACY'.ljust(8)} {'TIME'.rjust(21)}", 1, self.color)) 23 | all_surfaces.append(seperator) 24 | try: 25 | if len(leaders) == 0: 26 | all_surfaces.append(self.font.render('No records, make sure you have working internet connectivity', 1, self.color)) 27 | 28 | for index, score in enumerate(leaders['scores']): 29 | all_surfaces.append(self.font.render(f"{str(index+1).ljust(5)} {score['name'][:name_length].ljust(name_length)} {str(score['score']).ljust(10)} {str(score['level']).ljust(5)} {str(score['accuracy'] + '%').ljust(8)} {str(time.ctime(int(score['epoch']))).rjust(25)}", 1, self.color)) 30 | except Exception: 31 | pass 32 | all_surfaces.append(seperator) 33 | 34 | self.surf = Surface((all_surfaces[2].get_width(), all_surfaces[0].get_height() * (len(all_surfaces) + 1)), game_env.SRCALPHA) 35 | 36 | self.surf.blit(header, (self.surf.get_width() / 2 - header.get_width() / 2, 0)) 37 | for index, temp_surf in enumerate(all_surfaces): 38 | self.surf.blit(temp_surf, (0, header.get_height() + index * temp_surf.get_height())) 39 | 40 | self.rect = self.surf.get_rect(center=(game_env.static.screen_width / 2, game_env.static.screen_height / 2)) 41 | -------------------------------------------------------------------------------- /game/sprites/text/replaymenu.py: -------------------------------------------------------------------------------- 1 | from game.environment import GameEnvironment 2 | from game.sprites.text import Text 3 | from pygame.surface import Surface 4 | 5 | 6 | class ReplayMenuText(Text): 7 | """ ReplayText class extended from Text class. 8 | It creates the game replay menu sprite 9 | """ 10 | def __init__(self): 11 | Text.__init__(self, size=30) # initilizing parent class with default text color as red 12 | game_env = GameEnvironment() 13 | self.__gameover = Text("GAME OVER", 60) 14 | 15 | self.__replaytext_surf = self.font.render("Replay ", 1, self.color) # creating surface with the Replay text 16 | 17 | self.__y_selected_surf = self.font.render("Yes", 1, game_env.static.text_selection_color) # creating surface with Yes text when highlighted 18 | self.__n_surf = self.font.render("/No", 1, self.color) # creating surface with No text 19 | self.__y_surf = self.font.render("Yes/", 1, self.color) # creating surface with Yes text 20 | self.__n_selected_surf = self.font.render("No", 1, game_env.static.text_selection_color) # creating surface with No text when highlighted 21 | 22 | self.__replaytext_pos_x = self.__gameover.surf.get_width() / 2 - (self.__replaytext_surf.get_width() + self.__y_selected_surf.get_width() + self.__n_surf.get_width()) / 2 23 | 24 | self.__highlight_yes() # calling method to highlight Yes (the default choice) 25 | 26 | def __recreate_surf(self): 27 | game_env = GameEnvironment() 28 | # creating default surface of combination of expected length 29 | self.surf = Surface((self.__gameover.surf.get_width(), self.__gameover.surf.get_height() + self.__replaytext_surf.get_height()), game_env.SRCALPHA) 30 | self.surf.blit(self.__gameover.surf, (self.surf.get_width() / 2 - self.__gameover.surf.get_width() / 2, 0)) # updating the surface by drawing the prefex surface 31 | self.surf.blit(self.__replaytext_surf, (self.__replaytext_pos_x, self.__gameover.surf.get_height())) # updating the surface by drawing the prefex surface 32 | 33 | def update(self, acceleration_values): 34 | if not acceleration_values or len(acceleration_values) != 3 or None in acceleration_values: # validation of acceleration_values 35 | return 36 | 37 | game_env = GameEnvironment() 38 | y_axis = acceleration_values[1] 39 | if y_axis > 3: # checking if android device is tiled LEFT 40 | game_env.dynamic.replay = False # setting game replay choice as False 41 | self.__highlight_no() # calling method to highlight Nos 42 | elif y_axis < -3: # checking if android device is tiled RIGHT 43 | game_env.dynamic.replay = True # setting game replay choice as True 44 | self.__highlight_yes() # calling method to highlight Yes 45 | 46 | self.rect = self.surf.get_rect(center=(game_env.static.screen_width / 2, game_env.static.screen_height / 2 + 10)) # creating default rect and setting its position center below the GAME OVER text 47 | 48 | def __highlight_yes(self): 49 | self.__recreate_surf() 50 | self.surf.blit(self.__y_selected_surf, (self.__replaytext_pos_x + self.__replaytext_surf.get_width(), self.__gameover.surf.get_height())) # updating the surface by drawing the highlighted Yes after the prefix 51 | self.surf.blit(self.__n_surf, (self.__replaytext_pos_x + self.__replaytext_surf.get_width() + self.__y_selected_surf.get_width(), self.__gameover.surf.get_height())) # updating the surface by drawing the No after the highlighted Yes 52 | 53 | def __highlight_no(self): 54 | self.__recreate_surf() 55 | self.surf.blit(self.__y_surf, (self.__replaytext_pos_x + self.__replaytext_surf.get_width(), self.__gameover.surf.get_height())) # updating the surface by drawing the Yes after the prefix 56 | self.surf.blit(self.__n_selected_surf, (self.__replaytext_pos_x + self.__replaytext_surf.get_width() + self.__y_surf.get_width(), self.__gameover.surf.get_height())) # updating the surface by drawing the highlighted No after the Yes 57 | -------------------------------------------------------------------------------- /game/sprites/text/score.py: -------------------------------------------------------------------------------- 1 | from game.environment import GameEnvironment 2 | from game.sprites.text import Text 3 | 4 | 5 | # Define a ScoreText class object by Text class 6 | class ScoreText(Text): 7 | """ ScoreText class extended from Text class. 8 | It creates the game score sprite 9 | """ 10 | def __init__(self): 11 | Text.__init__(self, text="LEVEL 00 TIME 0 AMMO 0 SCORE 0", size=28, color=(103, 103, 103)) # initializing parent class with defautl text and color 12 | game_env = GameEnvironment() 13 | self.rect = self.surf.get_rect(topright=(game_env.static.screen_width - self.surf.get_width() / 2 + 30, 2)) # creating rectangle from text surface 14 | 15 | def update(self): 16 | game_env = GameEnvironment() 17 | # updating scoreboard score and time 18 | self.surf = self.font.render(f"LEVEL {str(game_env.dynamic.game_level).zfill(2)} TIME {str(game_env.dynamic.game_playtime).zfill(5)} AMMO {str(game_env.dynamic.ammo).zfill(3)} SCORE {str(game_env.dynamic.game_score).zfill(8)}", 1, self.color) 19 | -------------------------------------------------------------------------------- /game/sprites/vegetation.py: -------------------------------------------------------------------------------- 1 | import math 2 | import random 3 | 4 | from game.environment import GameEnvironment 5 | from pygame import image, sprite 6 | 7 | 8 | class Vegetation(sprite.Sprite): 9 | """ Vegetation sprite class for creating and updating the vegetation in the game screen 10 | """ 11 | def __init__(self, x_pos=None, y_pos=None): 12 | super(Vegetation, self).__init__() 13 | game_env = GameEnvironment() 14 | self.surf = image.load(random.choice(game_env.static.vegetation)).convert() 15 | ground = image.load(game_env.static.ground).convert() 16 | grass = image.load(game_env.static.grass).convert() 17 | grass.set_colorkey((255, 255, 255), game_env.RLEACCEL) 18 | 19 | pos = 0 20 | for _ in range(math.ceil(self.surf.get_width() / ground.get_width())): 21 | self.surf.blit(ground, (pos, self.surf.get_height() - 40)) 22 | if random.choice([True, False]): 23 | self.surf.blit(grass, (pos - 38, self.surf.get_height() - 40 - 28)) 24 | pos += ground.get_width() 25 | x_pos = game_env.static.screen_width if x_pos is None else x_pos 26 | y_pos = game_env.static.screen_height - self.surf.get_height() / 2 if y_pos is None else y_pos 27 | self.rect = self.surf.get_rect(center=(x_pos, y_pos)) 28 | self.__speed = 6 29 | 30 | def update(self): 31 | self.rect.move_ip(-self.__speed, 0) 32 | if self.rect.right < 0: 33 | self.kill() 34 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2021 Lakhya Jyoti Nath 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | 24 | PyBluesky - A simple python game to navigate your jet and fight though a massive missiles attack based on pygame framework. 25 | 26 | Version: 1.0.0 (based on desktop release 1.0.5 ; changed version number for android release) 27 | Author: Lakhya Jyoti Nath (ljnath) 28 | Email: ljnath@ljnath.com 29 | Website: https://ljnath.com 30 | """ 31 | 32 | import asyncio 33 | import math 34 | import random 35 | import webbrowser 36 | 37 | import pygame 38 | from android import loadingscreen 39 | from plyer import accelerometer, orientation, vibrator 40 | 41 | from game.data.enums import Screen, StartChoice 42 | from game.environment import GameEnvironment 43 | from game.handlers.leaderboard import LeaderBoardHandler 44 | from game.handlers.network import NetworkHandler 45 | from game.sprites.cloud import Cloud 46 | from game.sprites.jet import Jet 47 | from game.sprites.missile import Missile 48 | from game.sprites.samlauncher import SamLauncher 49 | from game.sprites.star import Star 50 | from game.sprites.text.input.name import NameInputText 51 | from game.sprites.text import Text 52 | from game.sprites.text.exitmenu import ExitMenuText 53 | from game.sprites.text.gamemenu import GameMenuText 54 | from game.sprites.text.help import HelpText 55 | from game.sprites.text.leaderboard import LeaderBoardText 56 | from game.sprites.text.replaymenu import ReplayMenuText 57 | from game.sprites.text.score import ScoreText 58 | from game.sprites.vegetation import Vegetation 59 | 60 | API_KEY = '' 61 | 62 | 63 | def check_update() -> None: 64 | """ 65 | Method to check for game update 66 | """ 67 | network_handler = NetworkHandler(API_KEY) 68 | asyncio.get_event_loop().run_until_complete(network_handler.check_game_update()) 69 | asyncio.get_event_loop().run_until_complete(LeaderBoardHandler().update(API_KEY)) 70 | 71 | 72 | def submit_result() -> None: 73 | """ 74 | Method to submit game score to remote server 75 | """ 76 | game_env = GameEnvironment() 77 | if game_env.dynamic.game_score > 0: 78 | network_handler = NetworkHandler(API_KEY) 79 | asyncio.get_event_loop().run_until_complete(network_handler.submit_result()) 80 | asyncio.get_event_loop().run_until_complete(LeaderBoardHandler().update(API_KEY)) 81 | 82 | 83 | def create_vegetation(vegetations) -> None: 84 | """ 85 | Method to create vegetation 86 | """ 87 | game_env = GameEnvironment() 88 | vegetations.empty() 89 | for i in range(math.ceil(game_env.static.screen_width / game_env.vegetation_size[0])): # drawing the 1st vegetations required to fill the 1st sceen (max is the screen width) 90 | vegetation = Vegetation(x_pos=i * game_env.vegetation_size[0] + game_env.vegetation_size[0] / 2) # creating a new vegetation 91 | vegetations.add(vegetation) # just adding sprite to vegetations group, to updating on screen for now 92 | 93 | 94 | def notify_user_of_update() -> None: 95 | """ 96 | Method to open the webbrowser when an new update is available 97 | """ 98 | game_env = GameEnvironment() 99 | if game_env.dynamic.update_url: 100 | try: 101 | webbrowser.open(game_env.dynamic.update_url) 102 | except Exception: 103 | pass 104 | 105 | 106 | def get_hint_sprite(hint_message: str) -> None: 107 | """ 108 | Method to create hint text 109 | """ 110 | game_env = GameEnvironment() 111 | return Text(f'HINT: {hint_message}', 26, pos_x=game_env.static.screen_width / 2, pos_y=game_env.static.screen_height - 30) # creating game hint message 112 | 113 | 114 | def play(): 115 | pygame.mixer.init() # initializing same audio mixer with default settings 116 | pygame.init() # initializing pygame 117 | game_env = GameEnvironment() # initializing game environment 118 | 119 | game_env.dynamic.collision_sound.set_volume(1.5) 120 | game_env.dynamic.levelup_sound.set_volume(1.5) 121 | game_env.dynamic.shoot_sound.set_volume(1.5) 122 | game_env.dynamic.hit_sound.set_volume(3) 123 | game_env.dynamic.powerup_sound.set_volume(10) 124 | game_env.dynamic.samfire_sound.set_volume(5) 125 | 126 | # setting main game background musicm 127 | # lopping the main game music and setting game volume 128 | pygame.mixer.music.load(game_env.static.game_sound.get('music')) 129 | pygame.mixer.music.play(loops=-1) 130 | pygame.mixer.music.set_volume(.2) 131 | 132 | # settings flags to create screen in fullscreen, use HW-accleration and DoubleBuffer 133 | flags = pygame.FULLSCREEN | pygame.DOUBLEBUF | pygame.HWSURFACE | pygame.SCALED | pygame.RESIZABLE 134 | 135 | # creating game screen with custom width and height 136 | screen = pygame.display.set_mode((game_env.static.screen_width, game_env.static.screen_height), flags) 137 | 138 | pygame.display.set_caption('{} version. {}'.format(game_env.static.name, game_env.static.version)) # setting name of game window 139 | pygame.mouse.set_visible(False) # hiding the mouse pointer from the game screen 140 | 141 | gameclock = pygame.time.Clock() # setting up game clock to maintain constant fps 142 | check_update() 143 | 144 | ADD_CLOUD = pygame.USEREVENT + 1 # creating custom event to automatically add cloud in the screen 145 | pygame.time.set_timer(ADD_CLOUD, int(1000 / game_env.static.cloud_per_sec)) # setting event to auto-trigger every 1s; 1 cloud will be created every second 146 | 147 | ADD_MISSILE = pygame.USEREVENT + 2 # creating custom event to automatically add missiles in the screen 148 | pygame.time.set_timer(ADD_MISSILE, int(1000 / game_env.static.missile_per_sec)) # setting event to auto-trigger every 500ms; 2 missiles will be created every second 149 | 150 | ADD_SAM_LAUNCHER = pygame.USEREVENT + 3 # creating custom event to automatically add SAM-LAUNCHER in the screen 151 | pygame.time.set_timer(ADD_SAM_LAUNCHER, 5000) # setting event to auto-trigger every 5s; 1 level can have 4 sam launcher 152 | 153 | running = True # game running variable 154 | gameover = False # no gameover by default 155 | game_started = False # game is not started by default 156 | game_pause = False 157 | star_shown = False 158 | user_has_swipped = False 159 | screen_color = game_env.static.background_default if game_started else game_env.static.background_special 160 | 161 | # blocking all the undesired events 162 | pygame.event.set_blocked(pygame.FINGERMOTION) 163 | pygame.event.set_blocked(pygame.FINGERUP) 164 | pygame.event.set_blocked(pygame.FINGERDOWN) 165 | pygame.event.set_blocked(pygame.MOUSEBUTTONDOWN) 166 | pygame.event.set_blocked(pygame.MOUSEMOTION) 167 | pygame.event.set_blocked(pygame.KEYUP) 168 | pygame.event.set_blocked(ADD_MISSILE) 169 | pygame.event.set_blocked(ADD_SAM_LAUNCHER) 170 | 171 | backgrounds = pygame.sprite.Group() # creating seperate group for background sprites 172 | stars = pygame.sprite.GroupSingle() # group of stars with max 1 sprite 173 | vegetations = pygame.sprite.Group() # creating cloud group for storing all the clouds in the game 174 | clouds = pygame.sprite.Group() # creating cloud group for storing all the clouds in the game 175 | missiles = pygame.sprite.Group() # creating missile group for storing all the missiles in the game 176 | deactivated_missile = pygame.sprite.Group() # creating missile group for storing all the deactivated missiles in the game 177 | samlaunchers = pygame.sprite.GroupSingle() # creating missile group for storing all the samlaunchers in the game 178 | title_sprites = pygame.sprite.Group() 179 | 180 | hint_sprite = get_hint_sprite("Swipe your finger to know more") # creating game hint message 181 | title_banner_sprite = Text("{} {}".format(game_env.static.name, game_env.static.version), 100, pos_x=game_env.static.screen_width / 2, pos_y=100) # creating title_banner_sprite text sprite with game name 182 | title_author_sprite = Text("By Lakhya Jyoti Nath aka ljnath", 28, pos_x=game_env.static.screen_width / 2, pos_y=150) # creating game author 183 | swipe_navigated_menus = { 184 | Screen.GAME_MENU: GameMenuText(), 185 | Screen.HELP: HelpText(), 186 | Screen.LEADERBOARD: LeaderBoardText() 187 | } 188 | selected_menu_index = 0 189 | 190 | # showing regular game menus if user has entered the player name 191 | if game_env.dynamic.player_name: 192 | game_env.dynamic.all_sprites.add(hint_sprite) 193 | active_sprite = swipe_navigated_menus[Screen.GAME_MENU] 194 | pygame.event.set_allowed(pygame.MOUSEMOTION) 195 | else: 196 | # else showing the screen for user to enter the player name 197 | active_sprite = NameInputText() 198 | game_env.dynamic.active_screen = Screen.NAME_INPUT 199 | 200 | [title_sprites.add(sprite) for sprite in (active_sprite, title_banner_sprite, title_author_sprite)] # adding all the necessary sprites to title_sprites 201 | [game_env.dynamic.all_sprites.add(sprite) for sprite in title_sprites] # adding all title_sprites sprite to all_sprites 202 | 203 | jet = Jet() # creating jet sprite 204 | scoretext_sprite = ScoreText() # creating scoreboard sprite 205 | game_env.dynamic.noammo_sprite = Text("NO AMMO !!!", 30) # creating noammo-sprite 206 | 207 | create_vegetation(vegetations) 208 | menu_screens = {Screen.REPLAY_MENU, Screen.GAME_MENU, Screen.EXIT_MENU} 209 | last_active_sprite = (game_env.dynamic.active_screen, active_sprite) 210 | 211 | def start_gameplay(): 212 | nonlocal gameover, jet, star_shown, screen_color, game_started, ADD_MISSILE, ADD_SAM_LAUNCHER 213 | pygame.event.set_blocked(game_env.MOUSEMOTION) 214 | pygame.event.set_allowed(ADD_MISSILE) 215 | pygame.event.set_allowed(ADD_SAM_LAUNCHER) 216 | screen_color = game_env.static.background_default # restoring screen color 217 | [sprite.kill() for sprite in title_sprites] # kill all the title_sprites sprite sprite 218 | jet = Jet() # re-creating the jet 219 | missiles.empty() # empting the missle group 220 | game_env.dynamic.all_sprites = pygame.sprite.Group() # re-creating group of sprites 221 | [game_env.dynamic.all_sprites.remove(sprite) for sprite in (active_sprite, hint_sprite)] # removing active sprite and hint sprite 222 | [game_env.dynamic.all_sprites.add(sprite) for sprite in (jet, scoretext_sprite)] # adding the jet and scoreboard to all_sprites 223 | game_env.reset() # reseting game data 224 | pygame.time.set_timer(ADD_MISSILE, int(1000 / game_env.static.missile_per_sec)) # resetting missile creation event timer 225 | create_vegetation(vegetations) # creating vegetation 226 | [backgrounds.add(sprite) for sprite in vegetations.sprites()] # adding vegetation to background 227 | game_env.dynamic.active_screen = Screen.GAME_SCREEN # setting gamescreen as the active sprite 228 | game_started = True # game has started 229 | gameover = False # game is not over yet 230 | star_shown = False # no star is displayed 231 | 232 | # enabling acclerometer sensor to get accleration sensor data 233 | accelerometer.enable() 234 | 235 | # Main game loop 236 | while running: 237 | 238 | # getting the accleration sensor data from accelerometer 239 | # acceleration_sensor_values is a tuple of (x, y, z) sensor data 240 | acceleration_sensor_values = accelerometer.acceleration 241 | 242 | # this variable is updated in case of a MOUSEMOTION; in subsequent MOUSEBUTTONUP event, 243 | # it is checked if the position of both these events are the same. 244 | # if yes, this indicates that these are part of same motion and the MOUSEBUTTONUP event can be discarded 245 | last_touch_position = (0, 0) 246 | 247 | # Look at every event in the queue 248 | for event in pygame.event.get(): 249 | 250 | # checking for VIDEORESIZE event, this event is used to prevent auto-rotate in android device 251 | # if any change in the screensize is detected, then the orienatation is forcefully re-applied 252 | if event.type == game_env.VIDEORESIZE: 253 | orientation.set_landscape(reverse=False) 254 | 255 | # handling keydown event to show the pause menu 256 | elif event.type == game_env.KEYDOWN: 257 | if game_env.dynamic.active_screen != Screen.EXIT_MENU and pygame.key.name(event.key) == 'AC Back': 258 | pygame.mixer.music.pause() 259 | last_active_screen = game_env.dynamic.active_screen 260 | last_active_sprite = active_sprite 261 | game_started, game_pause = game_pause, game_started 262 | [game_env.dynamic.all_sprites.remove(sprite) for sprite in (active_sprite, hint_sprite)] 263 | active_sprite = ExitMenuText() 264 | game_env.dynamic.all_sprites.add(active_sprite) 265 | game_env.dynamic.active_screen = Screen.EXIT_MENU 266 | 267 | # handling the textinput event to allow user to type 268 | elif event.type == game_env.TEXTINPUT and game_env.dynamic.active_screen == Screen.NAME_INPUT: 269 | active_sprite.update(event.text) 270 | 271 | # handling menu navigation via finger swipe; menu navigation is not allowed during NAME_INPUT screen 272 | elif event.type == game_env.MOUSEMOTION and not game_pause and not game_started and not gameover: 273 | 274 | # saving current interaction position; this will be later used for discarding MOUSEBUTTONUP event if the position is same 275 | last_touch_position = event.pos 276 | 277 | if user_has_swipped: 278 | continue 279 | 280 | is_valid_swipe = False 281 | 282 | if event.rel[0] < -40: 283 | user_has_swipped = True 284 | is_valid_swipe = True 285 | selected_menu_index += 1 286 | if selected_menu_index == len(swipe_navigated_menus): 287 | selected_menu_index = 0 288 | elif event.rel[0] > 40: 289 | user_has_swipped = True 290 | is_valid_swipe = True 291 | selected_menu_index -= 1 292 | if selected_menu_index < 0: 293 | selected_menu_index = len(swipe_navigated_menus) - 1 294 | 295 | if not is_valid_swipe: 296 | continue 297 | 298 | pygame.event.clear() 299 | 300 | # settings the current swipe_navigated_menus as the active one for it to be rendered 301 | # and refreshing the active_sprite in game_env.dynamic.all_sprites for re-rendering 302 | game_env.dynamic.active_screen = list(swipe_navigated_menus.keys())[selected_menu_index] 303 | 304 | game_env.dynamic.all_sprites.remove(active_sprite) 305 | active_sprite = swipe_navigated_menus[game_env.dynamic.active_screen] 306 | game_env.dynamic.all_sprites.add(active_sprite) 307 | 308 | # mouse based interaction to simulate finger based interaction 309 | elif event.type == game_env.MOUSEBUTTONUP: 310 | # handling single finger only for now 311 | if event.button == 1 and event.pos != last_touch_position: 312 | # resume or exit game based on user interaction with the EXIT-MENU 313 | if game_env.dynamic.active_screen == Screen.EXIT_MENU: 314 | if game_env.dynamic.exit: 315 | running = False 316 | elif not game_env.dynamic.exit: 317 | pygame.mixer.music.unpause() 318 | game_started, game_pause = game_pause, game_started 319 | game_env.dynamic.all_sprites.remove(active_sprite) 320 | game_env.dynamic.active_screen = last_active_screen 321 | active_sprite = last_active_sprite 322 | 323 | if game_env.dynamic.active_screen != Screen.GAME_SCREEN: 324 | [game_env.dynamic.all_sprites.add(sprite) for sprite in (active_sprite, hint_sprite)] 325 | 326 | # handling interaction oin the NAME-INPUT menu like button click and show/hide of keyboard 327 | elif game_env.dynamic.active_screen == Screen.NAME_INPUT: 328 | 329 | # if playername is not defined; this screen is shown to the user for getting the username 330 | # once the username is entered, user can touch either of CLEAR or OK surface. 331 | # we are check this touch activity here 332 | if game_env.dynamic.player_name.strip() == '': 333 | active_sprite.check_for_touch(event.pos) 334 | 335 | # jet can shoot at use touch and when the game is running 336 | elif game_started and not gameover: 337 | jet.shoot() 338 | 339 | # start the game when user has selected 'Start Game' in GAME_MENU or 'Yes' in REPLAY_MENT 340 | elif (game_env.dynamic.active_screen == Screen.GAME_MENU and game_env.dynamic.game_start_choice == StartChoice.START) or (game_env.dynamic.active_screen == Screen.REPLAY_MENU and game_env.dynamic.replay): 341 | start_gameplay() 342 | 343 | # exit the game when user has selected 'Exit' in GAME_MENU or 'No' in REPLAY_MENT 344 | elif game_env.dynamic.active_screen == Screen.GAME_MENU and game_env.dynamic.game_start_choice == StartChoice.EXIT or (game_env.dynamic.active_screen == Screen.REPLAY_MENU and not game_env.dynamic.replay): 345 | running = False 346 | 347 | # adding of clouds, backgroud, vegetation and power-up star is handled inside this 348 | # the reset of user swip is also handled in this; this a user is allowed to make 1 swipe every second 349 | elif event.type == ADD_CLOUD: 350 | user_has_swipped = False 351 | if game_pause: 352 | continue 353 | 354 | last_sprite = vegetations.sprites()[-1] # storing the last available vegetation for computation 355 | if last_sprite.rect.x + last_sprite.rect.width / 2 - game_env.static.screen_width < 0: # checking if the last vegetation has appeared in the screen, if yes a new vegetation will be created and appended 356 | vegetation = Vegetation(x_pos=last_sprite.rect.x + last_sprite.rect.width + last_sprite.rect.width / 2) # position of the new sprite is after the last sprite 357 | vegetations.add(vegetation) # adding sprite to groups for update and display 358 | backgrounds.add(vegetation) 359 | 360 | new_cloud = Cloud() # is event to add cloud is triggered 361 | clouds.add(new_cloud) # create a new cloud 362 | backgrounds.add(new_cloud) # adding the cloud to all_sprites group 363 | if not gameover and game_started: 364 | game_env.dynamic.game_playtime += 1 # increasing playtime by 1s as this event is triggered every second; just reusing existing event instead of recreating a new event 365 | if not star_shown and random.randint(0, 30) % 3 == 0: # probabity of getting a star is 30% 366 | star = Star() 367 | stars.add(star) 368 | game_env.dynamic.all_sprites.add(star) 369 | star_shown = True 370 | if game_env.dynamic.game_playtime % 20 == 0: # changing game level very 20s 371 | star_shown = False 372 | game_env.dynamic.levelup_sound.play() # playing level up sound 373 | game_env.dynamic.game_level += 1 # increasing the game level 374 | pygame.time.set_timer(ADD_MISSILE, int(1000 / (game_env.static.missile_per_sec + int(game_env.dynamic.game_level / 2)))) # updating timer of ADD_MISSLE for more missiles to be added 375 | game_env.dynamic.ammo += 50 # adding 50 ammo on each level up 376 | game_env.dynamic.game_score += 10 # increasing game score by 10 after each level 377 | game_env.dynamic.all_sprites.remove(game_env.dynamic.noammo_sprite) # removing no ammo sprite when ammo is refilled 378 | 379 | # # add missile and sam-launcher if the game has started and not gameover 380 | elif event.type == ADD_MISSILE: # is event to add missile is triggered; missles are not added during gameover 381 | new_missile = Missile() # create a new missile 382 | missiles.add(new_missile) # adding the missile to missle group 383 | game_env.dynamic.all_sprites.add(new_missile) # adding the missile to all_sprites group as well 384 | 385 | elif event.type == ADD_SAM_LAUNCHER and not samlaunchers.sprites() and game_env.dynamic.game_level > 5: 386 | samlauncher = SamLauncher() 387 | samlaunchers.add(samlauncher) 388 | game_env.dynamic.all_sprites.add(samlauncher) 389 | 390 | # if the active screen is NAME-INPUT and if the playername is available 391 | # this means that user has entered the playername in the NAME-INPNUT screen; removing the screen now 392 | if game_env.dynamic.active_screen == Screen.NAME_INPUT and game_env.dynamic.player_name.strip() != '': 393 | pygame.key.stop_text_input() 394 | game_env.dynamic.all_sprites.remove(active_sprite) 395 | active_sprite = swipe_navigated_menus[Screen.GAME_MENU] 396 | [game_env.dynamic.all_sprites.add(sprite) for sprite in (active_sprite, hint_sprite)] 397 | game_env.dynamic.active_screen = Screen.GAME_MENU 398 | pygame.event.set_allowed(pygame.MOUSEMOTION) 399 | 400 | screen.fill(screen_color) # Filling screen with sky blue color 401 | [screen.blit(sprite.surf, sprite.rect) for sprite in backgrounds] # drawing all backgrounds sprites 402 | [screen.blit(sprite.surf, sprite.rect) for sprite in game_env.dynamic.all_sprites] # drawing all sprites in the screen 403 | 404 | if game_started and not gameover: 405 | # missile hit 406 | if pygame.sprite.spritecollideany(jet, missiles) or pygame.sprite.spritecollideany(jet, game_env.dynamic.sam_missiles): # Check if any missiles have collided with the player; if so 407 | pygame.event.clear() # clearing all existing events in queue once game is over 408 | vibrator.vibrate(1) # vibrating device for 1s on game-over 409 | pygame.event.set_blocked(ADD_MISSILE) # blocking event to add missile due to gameover 410 | pygame.event.set_blocked(ADD_SAM_LAUNCHER) # blocking event to add new sam launcher due to gameover 411 | gameover = True # setting gameover to true to prevent new missiles from spawning 412 | active_sprite = ReplayMenuText() 413 | game_env.dynamic.active_screen = Screen.REPLAY_MENU 414 | jet.kill() # killing the jet 415 | [sam_missile.kill() for sam_missile in game_env.dynamic.sam_missiles] # killing the SAM missile 416 | game_env.dynamic.collision_sound.play() 417 | hint_sprite = get_hint_sprite("Move your device to change selection and tap to confirm") # updating game hint message 418 | [game_env.dynamic.all_sprites.add(sprite) for sprite in (active_sprite, hint_sprite)] # adding the gameover and the hint sprite 419 | game_env.dynamic.all_sprites.remove(game_env.dynamic.noammo_sprite) 420 | submit_result() 421 | 422 | # bullet hit 423 | collision = pygame.sprite.groupcollide(missiles, game_env.dynamic.bullets, True, True) # checking for collision between bullets and missiles, killing each one of them on collision 424 | if len(collision) > 0: 425 | game_env.dynamic.hit_sound.play() # play missile destroyed sound 426 | game_env.dynamic.game_score += len(collision) * 10 # 1 missle destroyed = 10 pts. 427 | game_env.dynamic.missiles_destroyed += len(collision) # to calulate player accuracy 428 | 429 | # powerup hit 430 | if pygame.sprite.spritecollideany(jet, stars): # collition between jet and star (powerup) 431 | game_env.dynamic.powerup_sound.play() 432 | [game_env.dynamic.all_sprites.remove(s) for s in stars.sprites()] # removing the star from all_sprites to hide from screen 433 | game_env.dynamic.game_score += 100 * game_env.dynamic.game_level # increasing game score by 100 434 | stars.empty() # removing star from stars group 435 | for missile in missiles.sprites(): 436 | missile.deactivate() # making missile as deactivated 437 | deactivated_missile.add(missile) # adding missile to deactivated_missile group 438 | missiles.remove(missile) # remove missiles from missles group to avoid collision with jet 439 | 440 | if not game_pause and game_started and not gameover: 441 | jet.update(acceleration_sensor_values) 442 | elif game_env.dynamic.active_screen in menu_screens: 443 | active_sprite.update(acceleration_sensor_values) # handling menu interactions for all the possible interactive screens 444 | 445 | pygame.display.flip() # updating display to the screen 446 | gameclock.tick(game_env.static.fps) # ticking game clock at 30 to maintain 30fps 447 | 448 | if game_pause: 449 | continue 450 | 451 | if game_started: 452 | vegetations.update() # vegetations will move only after the game starts 453 | 454 | game_env.dynamic.bullets.update() 455 | game_env.dynamic.sam_missiles.update() 456 | missiles.update() # update the position of the missiles 457 | deactivated_missile.update() 458 | clouds.update() # update the postition of the clouds 459 | stars.update() 460 | samlaunchers.update((jet.rect.x + jet.rect.width / 2, jet.rect.y + jet.rect.height)) 461 | scoretext_sprite.update() # update the game score 462 | 463 | pygame.mixer.music.stop() # stopping game music 464 | pygame.mixer.quit() # stopping game sound mixer 465 | notify_user_of_update() 466 | 467 | 468 | if __name__ == '__main__': 469 | # hide loading screen as the game has been loaded 470 | loadingscreen.hide_loading_screen() 471 | 472 | # start the game 473 | play() 474 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | appdirs==1.4.4 2 | colorama==0.4.4 3 | Cython==0.29.23 4 | importlib-metadata==4.6.0 5 | Jinja2==3.0.1 6 | MarkupSafe==2.0.1 7 | pep517==0.6.0 8 | python-for-android==2020.6.2 9 | pytoml==0.1.21 10 | sh==1.14.2 11 | six==1.16.0 12 | toml==0.10.2 13 | typing-extensions==3.10.0.0 14 | zipp==3.5.0 15 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2021 Lakhya Jyoti Nath 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | 24 | Version: 1.0.0 25 | Author: Lakhya Jyoti Nath (ljnath) 26 | Email: ljnath@ljnath.com 27 | Website: https://ljnath.com 28 | """ 29 | 30 | from distutils.core import setup 31 | from setuptools import find_packages 32 | 33 | 34 | setup( 35 | name="PyBluesky", 36 | version='1.0.0', 37 | author="Lakhya Jyoti Nath (ljnath)", 38 | author_email='ljnath@ljnath.com', 39 | description='A simple python game to navigate your jet and fight \ 40 | though a massive missiles attack based on pygame framework', 41 | url='https://github.com/ljnath/PyBluesky-android', 42 | packages=find_packages(), 43 | package_data={ 44 | '.': ['main.py'], 45 | './*': ['*.py'], 46 | '*/*': ['*.py', '*.ogg', '*.ttf', '*.png', '*.ico'], 47 | '*/*/*': ['*.py'], 48 | '*/*/*/*': ['*.py'] 49 | }, 50 | options={ 51 | 'apk': { 52 | 'ignore-setup-py': None, 53 | # 'release': None, 54 | 'arch': 'arm64-v8a', 55 | 'package': 'com.ljnath.pybluesky', 56 | 'requirements': 'pygame==2.0.1,aiohttp==3.7.4.post0,multidict==5.1.0,\ 57 | attrs==21.2.0,async-timeout==3.0.1,chardet==4.0.0,idna==3.2,\ 58 | typing-extensions==3.10.0.0,yarl==1.6.3,Plyer', 59 | 'sdk-dir': '../android-sdk', 60 | 'ndk-dir': '../android-ndk-r19c', 61 | 'presplash': 'assets/images/presplash.png', 62 | 'presplash-color': '#C4E2FF', 63 | 'icon': 'assets/icon/pybluesky.png', 64 | 'dist-name': 'PyBluesky', 65 | 'android-api': 29, 66 | 'bootstrap': 'sdl2', 67 | 'orientation': 'landscape', 68 | 'wakelock': None, 69 | 'permissions': 70 | [ 71 | 'VIBRATE', 72 | 'INTERNET' 73 | ] 74 | } 75 | } 76 | ) 77 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E501 3 | --------------------------------------------------------------------------------