├── .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 | [](https://github.com/ljnath/PyBluesky-android/blob/master/LICENSE)
13 | [](https://github.com/ljnath/PyBluesky-android/stargazers)
14 | [](https://lgtm.com/projects/g/ljnath/PyBluesky-android/)
15 | [](https://lgtm.com/projects/g/ljnath/PyBluesky-android/)
16 |
17 |
18 | [](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 |
--------------------------------------------------------------------------------