├── .gitignore ├── LICENSE ├── README.md ├── config └── config.yaml ├── fonts ├── a_goblin_appears.ttf ├── asap-bold.otf ├── asap-italic.otf ├── asap.otf ├── eras_demi.ttf ├── eras_demi_bold.ttf └── pixel_caps.ttf ├── images ├── achievement_1.png ├── achievement_2.png ├── achievement_3.png ├── achievement_box_body.png ├── achievement_box_header.png ├── achievements_label.png ├── bang.png ├── earth.png ├── flag.png ├── green_alert_box_bottom.png ├── green_alert_box_middle.png ├── green_alert_box_top.png ├── instructions.png ├── large_planet.png ├── large_planet_2.png ├── moon.png ├── moonglow.png ├── planet_shadow.png ├── red_alert_box_bottom.png ├── red_alert_box_middle.png ├── red_alert_box_top.png ├── scoreboard_background.png ├── ship.png ├── small_planet.png ├── small_planet_2.png ├── small_waypoint.png ├── splash.png ├── trans_gui_back.png ├── vote_for_a_modifier.png ├── vote_label_background.png ├── waypoint.png ├── waypoint_glow.png ├── wormhole.png └── wormhole_bw.png ├── sounds ├── alert_appears.wav ├── land_on_moon.wav ├── music.wav ├── planet_explode.wav ├── ship_destroy.wav ├── solar_wind.wav ├── use_wormhole.wav ├── vote.wav ├── vote_over.wav └── waypoint_collect.wav └── src ├── __init__.py ├── achievement_row.py ├── alert_manager.py ├── constants.py ├── death_particle.py ├── error_logging.py ├── exhaust_particle.py ├── explosion.py ├── game.py ├── high_score_scene.py ├── high_score_table.py ├── level_scene.py ├── moon.py ├── nugget.py ├── nugget_explosion.py ├── particle.py ├── planet.py ├── planet_explosion.py ├── player.py ├── primitives.py ├── scene.py ├── score_manager.py ├── ship.py ├── start_scene.py ├── transition_gui.py ├── twitch_chat_stream.py ├── wind_particle.py ├── wormhole.py └── wormhole_explosion.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Error logging 10 | src/error_log.txt 11 | 12 | # Profile files 13 | *.prof 14 | 15 | # Test scores 16 | data/*.pkl 17 | 18 | # Distribution / packaging 19 | .Python 20 | build/ 21 | develop-eggs/ 22 | dist/ 23 | downloads/ 24 | eggs/ 25 | .eggs/ 26 | lib/ 27 | lib64/ 28 | parts/ 29 | sdist/ 30 | var/ 31 | wheels/ 32 | pip-wheel-metadata/ 33 | share/python-wheels/ 34 | *.egg-info/ 35 | .installed.cfg 36 | *.egg 37 | MANIFEST 38 | 39 | # PyInstaller 40 | # Usually these files are written by a python script from a template 41 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 42 | *.manifest 43 | *.spec 44 | 45 | # Installer logs 46 | pip-log.txt 47 | pip-delete-this-directory.txt 48 | 49 | # Unit test / coverage reports 50 | htmlcov/ 51 | .tox/ 52 | .nox/ 53 | .coverage 54 | .coverage.* 55 | .cache 56 | nosetests.xml 57 | coverage.xml 58 | *.cover 59 | *.py,cover 60 | .hypothesis/ 61 | .pytest_cache/ 62 | 63 | # Translations 64 | *.mo 65 | *.pot 66 | 67 | # Django stuff: 68 | *.log 69 | local_settings.py 70 | db.sqlite3 71 | db.sqlite3-journal 72 | 73 | # Flask stuff: 74 | instance/ 75 | .webassets-cache 76 | 77 | # Scrapy stuff: 78 | .scrapy 79 | 80 | # Sphinx documentation 81 | docs/_build/ 82 | 83 | # PyBuilder 84 | target/ 85 | 86 | # Jupyter Notebook 87 | .ipynb_checkpoints 88 | 89 | # IPython 90 | profile_default/ 91 | ipython_config.py 92 | 93 | # pyenv 94 | .python-version 95 | 96 | # pipenv 97 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 98 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 99 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 100 | # install all needed dependencies. 101 | #Pipfile.lock 102 | 103 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 104 | __pypackages__/ 105 | 106 | # Celery stuff 107 | celerybeat-schedule 108 | celerybeat.pid 109 | 110 | # SageMath parsed files 111 | *.sage.py 112 | 113 | # Environments 114 | .env 115 | .venv 116 | env/ 117 | venv/ 118 | ENV/ 119 | env.bak/ 120 | venv.bak/ 121 | 122 | # Spyder project settings 123 | .spyderproject 124 | .spyproject 125 | 126 | # Rope project settings 127 | .ropeproject 128 | 129 | # mkdocs documentation 130 | /site 131 | 132 | # mypy 133 | .mypy_cache/ 134 | .dmypy.json 135 | dmypy.json 136 | 137 | # Pyre type checker 138 | .pyre/ 139 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jeremy 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GameOff2020 2 | A repository for our 2020 Github Game Off entry, Launch Party! 3 | 4 | ![Launch Party logo, featuring a little rocket ship and a moon with a flag](images/splash.png) 5 | 6 | ## About 7 | Launch Party is a multiplayer ship-programming game played via Twitch stream. 8 | Write some code, send it as a comment, and watch your ship take off! Everyone 9 | is racing to be the first to land on the moon. 10 | 11 | Since it's a party game, **it's recommended you play this game with three or 12 | more people.** It's playable in any browser that supports Twitch, as well as on 13 | mobile with the Twitch app. 14 | 15 | This game was made for the 2020 Github Game Off with the theme "Moonshot." All 16 | game programming, art, music, and sound design were completed within the one-month jam. 17 | 18 | ​The entire game's source code is available in 19 | [our Github repository](https://github.com/jeremycryan/GameOff2020).​ Feel free 20 | to use and distribute it in any way permitted by the MIT license. 21 | 22 | ## How to play 23 | If the game is already being hosted in a Twitch stream, you can just jump right 24 | in! Follow the instructions on screen and use the comments to play. 25 | 26 | If you're looking to host your own stream, you can do one of the following: 27 | 28 | ### Using Python 3.8.3 29 | 30 | If you have a Python 3.8.3 environment, you can use the following command to 31 | install PyGame via pip: 32 | 33 | ``` 34 | pip install pygame 35 | ``` 36 | 37 | If you have something other than pip set up to manage your libraries, then... 38 | you probably don't need directions for it. 39 | 40 | To run the game, navigate to the `src` directory and run `game.py`. 41 | 42 | ### Using the Windows executable 43 | 44 | If you don't have Python installed, or that last section looked like gibberish, 45 | you can download the "Streamer Application Pack" from 46 | [my Itch page](plasmastarfish.itch.io.launch-party). This doesn't require any special 47 | software to run (other than Windows). 48 | 49 | To run, simply extract everything from the zip file, navigate to the `executable` 50 | directory, and run `LaunchParty.exe`. 51 | 52 | ## Credits 53 | 54 | - plasmastarfish: programming, art, music, sound effects 55 | - superduperpacman42: programming 56 | - Fonts used: 57 | - A Goblin Appears 58 | - ASAP 59 | - Eras Demi ITC 60 | - Pixel Caps 61 | -------------------------------------------------------------------------------- /config/config.yaml: -------------------------------------------------------------------------------- 1 | channel: plasmastarfish 2 | scoreboard_file: scoreboard.pkl 3 | fullscreen: False 4 | -------------------------------------------------------------------------------- /fonts/a_goblin_appears.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycryan/GameOff2020/8b84be48516afab9def65214116708a452fa349a/fonts/a_goblin_appears.ttf -------------------------------------------------------------------------------- /fonts/asap-bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycryan/GameOff2020/8b84be48516afab9def65214116708a452fa349a/fonts/asap-bold.otf -------------------------------------------------------------------------------- /fonts/asap-italic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycryan/GameOff2020/8b84be48516afab9def65214116708a452fa349a/fonts/asap-italic.otf -------------------------------------------------------------------------------- /fonts/asap.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycryan/GameOff2020/8b84be48516afab9def65214116708a452fa349a/fonts/asap.otf -------------------------------------------------------------------------------- /fonts/eras_demi.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycryan/GameOff2020/8b84be48516afab9def65214116708a452fa349a/fonts/eras_demi.ttf -------------------------------------------------------------------------------- /fonts/eras_demi_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycryan/GameOff2020/8b84be48516afab9def65214116708a452fa349a/fonts/eras_demi_bold.ttf -------------------------------------------------------------------------------- /fonts/pixel_caps.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycryan/GameOff2020/8b84be48516afab9def65214116708a452fa349a/fonts/pixel_caps.ttf -------------------------------------------------------------------------------- /images/achievement_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycryan/GameOff2020/8b84be48516afab9def65214116708a452fa349a/images/achievement_1.png -------------------------------------------------------------------------------- /images/achievement_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycryan/GameOff2020/8b84be48516afab9def65214116708a452fa349a/images/achievement_2.png -------------------------------------------------------------------------------- /images/achievement_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycryan/GameOff2020/8b84be48516afab9def65214116708a452fa349a/images/achievement_3.png -------------------------------------------------------------------------------- /images/achievement_box_body.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycryan/GameOff2020/8b84be48516afab9def65214116708a452fa349a/images/achievement_box_body.png -------------------------------------------------------------------------------- /images/achievement_box_header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycryan/GameOff2020/8b84be48516afab9def65214116708a452fa349a/images/achievement_box_header.png -------------------------------------------------------------------------------- /images/achievements_label.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycryan/GameOff2020/8b84be48516afab9def65214116708a452fa349a/images/achievements_label.png -------------------------------------------------------------------------------- /images/bang.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycryan/GameOff2020/8b84be48516afab9def65214116708a452fa349a/images/bang.png -------------------------------------------------------------------------------- /images/earth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycryan/GameOff2020/8b84be48516afab9def65214116708a452fa349a/images/earth.png -------------------------------------------------------------------------------- /images/flag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycryan/GameOff2020/8b84be48516afab9def65214116708a452fa349a/images/flag.png -------------------------------------------------------------------------------- /images/green_alert_box_bottom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycryan/GameOff2020/8b84be48516afab9def65214116708a452fa349a/images/green_alert_box_bottom.png -------------------------------------------------------------------------------- /images/green_alert_box_middle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycryan/GameOff2020/8b84be48516afab9def65214116708a452fa349a/images/green_alert_box_middle.png -------------------------------------------------------------------------------- /images/green_alert_box_top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycryan/GameOff2020/8b84be48516afab9def65214116708a452fa349a/images/green_alert_box_top.png -------------------------------------------------------------------------------- /images/instructions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycryan/GameOff2020/8b84be48516afab9def65214116708a452fa349a/images/instructions.png -------------------------------------------------------------------------------- /images/large_planet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycryan/GameOff2020/8b84be48516afab9def65214116708a452fa349a/images/large_planet.png -------------------------------------------------------------------------------- /images/large_planet_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycryan/GameOff2020/8b84be48516afab9def65214116708a452fa349a/images/large_planet_2.png -------------------------------------------------------------------------------- /images/moon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycryan/GameOff2020/8b84be48516afab9def65214116708a452fa349a/images/moon.png -------------------------------------------------------------------------------- /images/moonglow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycryan/GameOff2020/8b84be48516afab9def65214116708a452fa349a/images/moonglow.png -------------------------------------------------------------------------------- /images/planet_shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycryan/GameOff2020/8b84be48516afab9def65214116708a452fa349a/images/planet_shadow.png -------------------------------------------------------------------------------- /images/red_alert_box_bottom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycryan/GameOff2020/8b84be48516afab9def65214116708a452fa349a/images/red_alert_box_bottom.png -------------------------------------------------------------------------------- /images/red_alert_box_middle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycryan/GameOff2020/8b84be48516afab9def65214116708a452fa349a/images/red_alert_box_middle.png -------------------------------------------------------------------------------- /images/red_alert_box_top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycryan/GameOff2020/8b84be48516afab9def65214116708a452fa349a/images/red_alert_box_top.png -------------------------------------------------------------------------------- /images/scoreboard_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycryan/GameOff2020/8b84be48516afab9def65214116708a452fa349a/images/scoreboard_background.png -------------------------------------------------------------------------------- /images/ship.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycryan/GameOff2020/8b84be48516afab9def65214116708a452fa349a/images/ship.png -------------------------------------------------------------------------------- /images/small_planet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycryan/GameOff2020/8b84be48516afab9def65214116708a452fa349a/images/small_planet.png -------------------------------------------------------------------------------- /images/small_planet_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycryan/GameOff2020/8b84be48516afab9def65214116708a452fa349a/images/small_planet_2.png -------------------------------------------------------------------------------- /images/small_waypoint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycryan/GameOff2020/8b84be48516afab9def65214116708a452fa349a/images/small_waypoint.png -------------------------------------------------------------------------------- /images/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycryan/GameOff2020/8b84be48516afab9def65214116708a452fa349a/images/splash.png -------------------------------------------------------------------------------- /images/trans_gui_back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycryan/GameOff2020/8b84be48516afab9def65214116708a452fa349a/images/trans_gui_back.png -------------------------------------------------------------------------------- /images/vote_for_a_modifier.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycryan/GameOff2020/8b84be48516afab9def65214116708a452fa349a/images/vote_for_a_modifier.png -------------------------------------------------------------------------------- /images/vote_label_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycryan/GameOff2020/8b84be48516afab9def65214116708a452fa349a/images/vote_label_background.png -------------------------------------------------------------------------------- /images/waypoint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycryan/GameOff2020/8b84be48516afab9def65214116708a452fa349a/images/waypoint.png -------------------------------------------------------------------------------- /images/waypoint_glow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycryan/GameOff2020/8b84be48516afab9def65214116708a452fa349a/images/waypoint_glow.png -------------------------------------------------------------------------------- /images/wormhole.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycryan/GameOff2020/8b84be48516afab9def65214116708a452fa349a/images/wormhole.png -------------------------------------------------------------------------------- /images/wormhole_bw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycryan/GameOff2020/8b84be48516afab9def65214116708a452fa349a/images/wormhole_bw.png -------------------------------------------------------------------------------- /sounds/alert_appears.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycryan/GameOff2020/8b84be48516afab9def65214116708a452fa349a/sounds/alert_appears.wav -------------------------------------------------------------------------------- /sounds/land_on_moon.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycryan/GameOff2020/8b84be48516afab9def65214116708a452fa349a/sounds/land_on_moon.wav -------------------------------------------------------------------------------- /sounds/music.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycryan/GameOff2020/8b84be48516afab9def65214116708a452fa349a/sounds/music.wav -------------------------------------------------------------------------------- /sounds/planet_explode.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycryan/GameOff2020/8b84be48516afab9def65214116708a452fa349a/sounds/planet_explode.wav -------------------------------------------------------------------------------- /sounds/ship_destroy.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycryan/GameOff2020/8b84be48516afab9def65214116708a452fa349a/sounds/ship_destroy.wav -------------------------------------------------------------------------------- /sounds/solar_wind.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycryan/GameOff2020/8b84be48516afab9def65214116708a452fa349a/sounds/solar_wind.wav -------------------------------------------------------------------------------- /sounds/use_wormhole.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycryan/GameOff2020/8b84be48516afab9def65214116708a452fa349a/sounds/use_wormhole.wav -------------------------------------------------------------------------------- /sounds/vote.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycryan/GameOff2020/8b84be48516afab9def65214116708a452fa349a/sounds/vote.wav -------------------------------------------------------------------------------- /sounds/vote_over.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycryan/GameOff2020/8b84be48516afab9def65214116708a452fa349a/sounds/vote_over.wav -------------------------------------------------------------------------------- /sounds/waypoint_collect.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycryan/GameOff2020/8b84be48516afab9def65214116708a452fa349a/sounds/waypoint_collect.wav -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeremycryan/GameOff2020/8b84be48516afab9def65214116708a452fa349a/src/__init__.py -------------------------------------------------------------------------------- /src/achievement_row.py: -------------------------------------------------------------------------------- 1 | ##!/usr/bin/env python3 2 | 3 | import random 4 | 5 | import pygame 6 | 7 | from primitives import GameObject, Pose 8 | import constants as c 9 | 10 | class AchievementRow(GameObject): 11 | 12 | class AchievementPanel(GameObject): 13 | def __init__(self, 14 | game, 15 | container, 16 | surface, 17 | points, 18 | description, 19 | requires=None, 20 | tags=None): 21 | super().__init__(game) 22 | self.container = container 23 | self.surface = pygame.transform.scale(surface, 24 | (c.ACHIEVEMENT_WIDTH, 25 | surface.get_height())).convert() 26 | self.points = points 27 | if c.DOUBLE_POINTS_MOD in self.game.modifications: 28 | self.points *= 2 29 | self.description = description 30 | self.achieved = False 31 | self.tags = [] if tags is None else tags 32 | self.requires = {} if requires is None else requires 33 | self.blink = self.surface.copy() 34 | self.blink.fill(c.WHITE) 35 | self.blink.set_alpha(0) 36 | self.blink_alpha = 0 37 | 38 | def update(self, dt, events): 39 | self.blink_alpha -= 300 * dt 40 | if self.blink_alpha < 0: 41 | self.blink_alpha = 0 42 | pass 43 | 44 | def ship_can_score(self, ship): 45 | if self.requires.get(c.MOON, False): 46 | if not ship.has_hit_moon: 47 | return False 48 | required_nuggets = self.requires.get(c.NUGGET, 0) 49 | if len(ship.nuggets) < required_nuggets: 50 | return False 51 | return True 52 | 53 | def achieve(self, player): 54 | if self.achieved: 55 | return 56 | 57 | self.achieved = True 58 | self.game.current_scene.shake(15) 59 | 60 | base_color = self.surface.get_at((0, 0)) 61 | cover_surf = pygame.Surface((self.surface.get_width() - c.ACHIEVEMENT_POINTS_WIDTH, self.surface.get_height() - 3)) 62 | cover_surf.fill(base_color) 63 | self.surface.blit(cover_surf, (c.ACHIEVEMENT_POINTS_WIDTH, 0)) 64 | 65 | veil_surf = pygame.Surface((c.ACHIEVEMENT_WIDTH, self.surface.get_height()-3)) 66 | veil_surf.fill(c.BLACK) 67 | veil_surf.set_alpha(120) 68 | self.surface.blit(veil_surf, (0, 0)) 69 | veil_surf.fill(player.color) 70 | veil_surf.set_alpha(70) 71 | self.surface.blit(veil_surf, (0, 0)) 72 | 73 | font = self.game.small_font if len(player.name) < 15 else self.game.very_small_font 74 | font_render = font.render(player.name[:23].upper(), 0, player.color) 75 | y = self.surface.get_height()//2 - font_render.get_height()//2 76 | x = (c.ACHIEVEMENT_WIDTH - c.ACHIEVEMENT_POINTS_WIDTH)//2 + c.ACHIEVEMENT_POINTS_WIDTH - font_render.get_width()//2 77 | 78 | self.game.temp_scores[player.name] = self.game.temp_scores.get(player.name, 0) + self.points 79 | 80 | self.surface.blit(font_render, (x, y)) 81 | 82 | self.blink_alpha = 255 83 | 84 | 85 | def draw(self, surface, offset=(0, 0)): 86 | shake_offset = self.game.current_scene.apply_screenshake((0, 0)) 87 | x = self.container.pose.x + offset[0] 88 | y = self.container.pose.y + offset[1] 89 | surface.blit(self.surface, (x, y)) 90 | if self.blink_alpha > 0: 91 | self.blink.set_alpha(self.blink_alpha) 92 | surface.blit(self.blink, (x, y)) 93 | 94 | 95 | def __init__(self, game, top_left_position=(0, 0)): 96 | super().__init__(game) 97 | self.pose = Pose(top_left_position, 0) 98 | self.achievements = self.default_achievements() 99 | self.label = pygame.image.load(c.IMAGE_PATH + "/achievement_box_header.png") 100 | self.label = pygame.transform.scale(self.label, (c.ACHIEVEMENT_LABEL_WIDTH, self.label.get_height())) 101 | self.body = pygame.image.load(c.IMAGE_PATH + "/achievement_box_body.png") 102 | self.body = pygame.transform.scale(self.body, 103 | (c.ACHIEVEMENT_LABEL_WIDTH, 104 | sum([item.surface.get_height() for item in self.achievements]) + 5 * (len(self.achievements) - 1) + 8)) 105 | 106 | def default_achievements(self): 107 | achievements = [ 108 | AchievementRow.AchievementPanel(self.game, 109 | self, 110 | pygame.image.load(c.IMAGE_PATH + "/achievement_1.png"), 111 | 1000, 112 | "land on moon", 113 | requires={c.MOON:True, c.NUGGET:0}, 114 | tags=[c.MOON_ACH]), 115 | AchievementRow.AchievementPanel(self.game, 116 | self, 117 | pygame.image.load(c.IMAGE_PATH + "/achievement_2.png"), 118 | 1500, 119 | "1 thing and land on moon", 120 | requires={c.MOON:True, c.NUGGET:1}, 121 | tags=[c.MOON_1_NUGGET_ACH]), 122 | AchievementRow.AchievementPanel(self.game, 123 | self, 124 | pygame.image.load(c.IMAGE_PATH + "/achievement_3.png"), 125 | 2500, 126 | "2 things and land on moon", 127 | requires={c.MOON:True, c.NUGGET:2}, 128 | tags=[c.MOON_2_NUGGET_ACH]) 129 | ] 130 | return achievements 131 | 132 | 133 | def update(self, dt, events): 134 | for item in self.achievements: 135 | item.update(dt, events) 136 | 137 | def get_height(self): 138 | return self.label.get_height() + self.body.get_height() 139 | 140 | def draw_box(self, surface, offset=(0, 0)): 141 | x = self.pose.x + offset[0] 142 | y = self.pose.y + offset[1] 143 | surface.blit(self.label, (x, y)) 144 | y += self.label.get_height() 145 | surface.blit(self.body, (x, y)) 146 | return y - self.pose.y 147 | 148 | def draw(self, surface, offset=(0, 0)): 149 | x = self.pose.x + c.SIDE_PANEL_WIDTH//2 - c.ACHIEVEMENT_WIDTH//2 150 | y = self.draw_box(surface, offset) 151 | #surface.blit(self.label, (x, y)) 152 | for item in self.achievements: 153 | item.draw(surface, (x, y)) 154 | y += item.surface.get_height() + 5 155 | 156 | def score_ship(self, ship): 157 | for achievement in self.achievements: 158 | if achievement.ship_can_score(ship): 159 | achievement.achieve(ship.player) 160 | 161 | def all_scored(self): 162 | return all([item.achieved for item in self.achievements]) 163 | -------------------------------------------------------------------------------- /src/alert_manager.py: -------------------------------------------------------------------------------- 1 | ##!/usr/bin/env python3 2 | 3 | import pygame 4 | 5 | import constants as c 6 | from primitives import GameObject 7 | 8 | class Alert(GameObject): 9 | def __init__(self, game, message, player=None): 10 | super().__init__(game) 11 | self.message = message 12 | self.player = player 13 | self.age = 0 14 | if player: 15 | self.lines = self.split(player + ": " + message) 16 | else: 17 | self.lines = self.split(message) 18 | self.surface = self.generate_surface().convert_alpha() 19 | 20 | def update(self, dt, events): 21 | self.age += dt 22 | 23 | def split(self, message): 24 | # lines = [""] 25 | # w = [] 26 | # for char in message+" ": 27 | # if char == ' ' and sum(w) > c.ALERT_WIDTH - c.ALERT_MARGIN[c.LEFT] - c.ALERT_MARGIN[c.RIGHT]: 28 | # i = lines[-1].rfind(' ') 29 | # word = lines[-1][i:] 30 | # w = w[i:] + [self.game.small_font_render[' '].get_width()] 31 | # lines[-1] = lines[-1][:i] 32 | # lines.append(word[1:]+" ") 33 | # else: 34 | # lines[-1] += char 35 | # w.append(self.game.small_font_render[char].get_width()) 36 | # return lines 37 | lines = [] 38 | max_width = c.ALERT_WIDTH - c.ALERT_MARGIN[c.RIGHT] - c.ALERT_MARGIN[c.LEFT] 39 | cur_width = 0 40 | space_width = self.game.other_alert_font.render(" ", 1, c.WHITE).get_width() 41 | current_line = "" 42 | for word in message.split(): 43 | render = self.game.other_alert_font.render(word, 1, c.WHITE) 44 | width = render.get_width() 45 | if cur_width + width + space_width > max_width: 46 | cur_width = 0 47 | lines.append(current_line) 48 | current_line = "" 49 | current_line += word + " " 50 | cur_width += width + space_width 51 | lines.append(current_line) 52 | return lines 53 | 54 | 55 | def generate_surface(self): 56 | zero_height = self.game.other_alert_font.render("0", 0, c.WHITE).get_height() 57 | 58 | h = c.ALERT_MARGIN[c.UP] + len(self.lines)*(c.PAUL_ALERT_LINE_SPACING + zero_height) - c.PAUL_ALERT_LINE_SPACING + c.ALERT_MARGIN[c.DOWN] 59 | surface = pygame.Surface((c.ALERT_WIDTH, h)) 60 | surface.fill(c.MAGENTA) 61 | surface.set_colorkey(c.MAGENTA) 62 | pygame.draw.rect(surface, c.ALERT_BACKGROUND_COLOR, (0, 0, surface.get_width(), surface.get_height()), border_radius=7) 63 | 64 | x = c.ALERT_MARGIN[c.LEFT] 65 | for i, line in enumerate(self.lines): 66 | y = c.ALERT_MARGIN[c.UP] + (c.PAUL_ALERT_LINE_SPACING + zero_height)*i 67 | surface.blit(self.game.other_alert_font.render(line, 1, c.ALERT_TEXT_COLOR), (x, y)) 68 | if self.player: 69 | if self.player in self.game.players: 70 | color = self.game.players[self.player].color 71 | else: 72 | color = c.ALERT_TEXT_COLOR 73 | surface.blit(self.game.other_alert_font.render(self.player+": ", 1, color), (x, c.ALERT_MARGIN[c.UP])) 74 | return surface 75 | 76 | def get_alpha(self): 77 | if self.age < c.ALERT_DURATION: 78 | return c.ALERT_ALPHA 79 | elif self.age < c.ALERT_DURATION + c.ALERT_FADEOUT: 80 | through = (self.age - c.ALERT_DURATION)/(c.ALERT_FADEOUT) 81 | return int(c.ALERT_ALPHA*(1 - through)) 82 | else: 83 | return 0 84 | 85 | def draw(self, surface, offset): 86 | # TODO account for changes in position 87 | self.surface.set_alpha(self.get_alpha()) 88 | surface.blit(self.surface, (offset)) 89 | 90 | 91 | class AlertManager(GameObject): 92 | def __init__(self, game): 93 | super().__init__(game) 94 | self.font = self.game.small_font 95 | self.alerts = [] 96 | self.yoff = 0 97 | # TODO draw and update alerts 98 | 99 | def alert(self, message, player=None): 100 | self.alerts.append(Alert(self.game, message, player)) 101 | self.yoff += self.alerts[-1].surface.get_height() + c.ALERT_PADDING[c.UP] + c.ALERT_PADDING[c.DOWN] 102 | self.game.alert_appears_sound.play() 103 | 104 | def draw(self, surface): 105 | x = c.ALERT_PADDING[c.LEFT] 106 | y = c.LEVEL_HEIGHT 107 | for i, alert in enumerate(self.alerts[::-1]): 108 | y -= c.ALERT_PADDING[c.DOWN] + alert.surface.get_height() 109 | alert.draw(surface, (x, y + self.yoff)) 110 | 111 | def total_height(self, offset=0): 112 | margins = c.ALERT_PADDING[c.UP] + c.ALERT_PADDING[c.DOWN] 113 | height = sum([alert.surface.get_height() + margins for alert in self.alerts[:len(self.alerts) - offset]]) 114 | return height 115 | 116 | def update(self, dt): 117 | if self.yoff > 0: 118 | dy = self.yoff 119 | self.yoff -= dy ** dt * 15 120 | if self.yoff < 3: 121 | self.yoff = 0 122 | offset = 0 123 | index = 0 124 | while self.total_height(offset) - self.yoff > c.MAX_ALERT_HEIGHT: 125 | if self.alerts[index].age < c.ALERT_DURATION: 126 | self.alerts[index].age = c.ALERT_DURATION 127 | index += 1 128 | offset += 1 129 | for i, alert in enumerate(self.alerts[:]): 130 | alert.update(dt, []) 131 | # if len(self.alerts) - i > c.ALERT_NUM and alert.age < c.ALERT_DURATION: 132 | # alert.age = c.ALERT_DURATION 133 | if alert.age >= c.ALERT_DURATION + c.ALERT_FADEOUT: 134 | self.alerts.remove(alert) 135 | 136 | def clear(self): 137 | self.alerts = [] 138 | -------------------------------------------------------------------------------- /src/constants.py: -------------------------------------------------------------------------------- 1 | ##!/usr/bin/env python3 2 | 3 | import os 4 | 5 | WINDOW_WIDTH = 1280 6 | WINDOW_HEIGHT = 720 7 | WINDOW_SIZE = WINDOW_WIDTH, WINDOW_HEIGHT 8 | WINDOW_CAPTION = "Launch Party" 9 | 10 | LEVEL_WIDTH = int(WINDOW_WIDTH * 0.8) 11 | LEVEL_HEIGHT = int(WINDOW_HEIGHT * 1.0) 12 | 13 | SIDE_PANEL_WIDTH = WINDOW_WIDTH - LEVEL_WIDTH 14 | SIDE_PANEL_HEIGHT = WINDOW_HEIGHT 15 | SIDE_PANEL_SIZE = SIDE_PANEL_WIDTH, SIDE_PANEL_HEIGHT 16 | 17 | ACHIEVEMENT_WIDTH = 237 18 | ACHIEVEMENT_POINTS_WIDTH = 70 19 | 20 | ACHIEVEMENT_LABEL_WIDTH = SIDE_PANEL_WIDTH 21 | ACHIEVEMENT_LABEL_HEIGHT = 40 22 | ACHIEVEMENT_LABEL_SIZE = ACHIEVEMENT_LABEL_WIDTH, ACHIEVEMENT_LABEL_HEIGHT 23 | 24 | SCORE_ROW_PADDING = 15 25 | SCORE_TILE_PADDING = 6 26 | SCORE_ROW_HEIGHT = 45 + SCORE_TILE_PADDING * 2 27 | EMPTY = "Empty" 28 | SCORE_EVEN_COLOR = 140, 180, 220 29 | SCORE_ODD_COLOR = 110, 150, 195 30 | SCORE_TABLE_PADDING = 0 31 | SCORE_TABLE_COLOR = (40, 70, 90) 32 | SCORE_TABLE_WIDTH = int(WINDOW_WIDTH * 0.6) 33 | SCORE_TABLE_HEIGHT = WINDOW_HEIGHT 34 | 35 | ALERT_SIDE_PADDING = 20 36 | ALERT_LINE_SPACING = 20 37 | ALERT_BODY_SPACE = 4 38 | 39 | OPTION_A = "A" 40 | OPTION_B = "B" 41 | 42 | TIMER_POSITION = SIDE_PANEL_WIDTH//2, 40 43 | 44 | SHIP_SCALE = 0.6 45 | 46 | MAX_FPS = 65 47 | TICK_LENGTH = 1/80 48 | 49 | BLACK = 0, 0, 0 50 | WHITE = 255, 255, 255 51 | RED = 255, 0, 0 52 | GREEN = 0, 255, 0 53 | BLUE = 0, 0, 255 54 | YELLOW = 255, 255, 0 55 | CYAN = 0, 255, 255 56 | MAGENTA = 255, 0, 255 57 | GRAY = 128, 128, 128 58 | MEDIUM_DARK_GRAY = 80, 80, 80 59 | DARK_GRAY = 50, 46, 57 60 | DARKER_GRAY = 45, 42, 52 61 | LIGHT_GRAY = 192, 192, 192 62 | VERT_LIGHT_GRAY = 235, 235, 235 63 | 64 | WORMHOLE_COLORS = [(0, 255, 255), (255, 220, 80), (150, 255, 100), (255, 100, 255)] 65 | 66 | RIGHT = (1, 0) 67 | UP = (0, -1) 68 | LEFT = (-1, 0) 69 | DOWN = (0, 1) 70 | CENTER = (0, 0) 71 | 72 | TEXT_BLIT_WIDTH = 2 73 | TEXT_BLIT_OFFSETS = ((-TEXT_BLIT_WIDTH, 0), 74 | (-TEXT_BLIT_WIDTH//2, TEXT_BLIT_WIDTH//2), 75 | (0, TEXT_BLIT_WIDTH), 76 | (TEXT_BLIT_WIDTH//2, TEXT_BLIT_WIDTH//2), 77 | (TEXT_BLIT_WIDTH, 0), 78 | (TEXT_BLIT_WIDTH//2, -TEXT_BLIT_WIDTH//2), 79 | (0, -TEXT_BLIT_WIDTH), 80 | (-TEXT_BLIT_WIDTH//2, -TEXT_BLIT_WIDTH//2)) 81 | 82 | COMMANDS = {"thrust":'t', "delay":'d', "rotate":'r'} 83 | COMMANDS_LONG = {'t':"Thrust", 'd':"Delay", 'r':"Rotate"} 84 | COMMANDS_MIN = {'t':(0,), 'd':(0,), 'r':(-360,)} 85 | COMMANDS_MAX = {'t':(100,), 'd':(60000,), 'r':(360,)} 86 | THRUST = 2 87 | 88 | LOG_PATH = "error_log.txt" 89 | SCORE_SAVE_PATH = "../data" 90 | IMAGE_PATH = "../images" 91 | FONT_PATH = "../fonts" 92 | CONFIG_PATH = "../config" 93 | SOUNDS_PATH = "../sounds" 94 | 95 | GRAVITY_CONSTANT = 800 96 | 97 | MIN_PLANET_RADIUS = 25 98 | MAX_PLANET_RADIUS = 75 99 | HOME_PLANET_RADIUS = 35 100 | HOME_ANGLE_VARIATION = 20 101 | MIN_SPACING = 60 102 | 103 | SHIP_SPAWN_ALTITUDE = 25 104 | 105 | MOON = 0 106 | NUGGET = 1 107 | 108 | MOON_ACH = 0 109 | MOON_1_NUGGET_ACH = 1 110 | MOON_2_NUGGET_ACH = 2 111 | 112 | ALERT_PADDING = {RIGHT:10, LEFT:10, DOWN:10, UP:10} 113 | ALERT_MARGIN = {RIGHT:10, LEFT:10, DOWN:10, UP:10} 114 | ALERT_DURATION = 5 115 | ALERT_ALPHA = 135 116 | ALERT_FADEOUT = 0.25 117 | ALERT_WIDTH = 300 118 | ALERT_NUM = 3 119 | ALERT_BACKGROUND_COLOR = BLACK 120 | ALERT_TEXT_COLOR = WHITE 121 | MAX_ALERT_HEIGHT = WINDOW_HEIGHT*0.65 122 | PAUL_ALERT_LINE_SPACING = 0 123 | 124 | SCORE_EXPIRATION = 48 125 | 126 | JOKE_MESSAGES = ( 127 | "Help, I'm trapped in a spaceship factory! Please let me out before they send me to the moon...", 128 | "Beep boop, out of mayonnaise.", 129 | "On behalf of NeoSpace Enterprises, we apologize for any death or dismemberment.", 130 | "Press A again, I dare you." 131 | ) 132 | 133 | HINTS = ( 134 | "When reading your code, the game ignores spaces and semicolons. Feel free to use them to keep organized!", 135 | "You can type !score to view your current score.", 136 | "You can type !recolor to re-roll your player color.", 137 | "Your ship rotation is measured in degrees per second, so you can calculate precise turns with the right timing.", 138 | "In real life, spacecraft of this size would be impractical.", 139 | "You only get points for an achievement if you're the first player to score it. Speed is important!", 140 | "This game was created by plasmastarfish and superduperpacman42 for the 2020 Github Game Off.\nIts source code is at github.com/jeremycryan.", 141 | ) 142 | 143 | MULT_0_MESSAGES = ( 144 | "You can only score leaderboard points in games with three or more players.\n\nGrab some friends!", 145 | ) 146 | 147 | MULT_MESSAGES = ( 148 | "The more players in the game, the more points are scored by achievements!\n\nThis game had {num} players.", 149 | ) 150 | 151 | NO_PLANETS_MOD = "No planets" 152 | DOUBLE_POINTS_MOD = "Double points" # TODO add visual change 153 | DOUBLE_THRUST_MOD = "Double thrust" 154 | INVERTED_GRAVITY_MOD = "Inverted gravity" # TODO add visual change 155 | EXTRA_TIME_MOD = "Extra time" 156 | MANY_WORMHOLES_MOD = "Many wormholes" 157 | GIANT_PLANET_MOD = "Giant planets" 158 | SMALL_PLANETS_MOD = "Small planets" 159 | SOLAR_WIND = "Solar wind" 160 | EXPLODING_PLANETS = "Exploding planets" 161 | 162 | MODIFICATIONS = ( 163 | NO_PLANETS_MOD, 164 | DOUBLE_POINTS_MOD, 165 | DOUBLE_THRUST_MOD, 166 | INVERTED_GRAVITY_MOD, 167 | EXTRA_TIME_MOD, 168 | MANY_WORMHOLES_MOD, 169 | GIANT_PLANET_MOD, 170 | SMALL_PLANETS_MOD, 171 | SOLAR_WIND, 172 | EXPLODING_PLANETS 173 | ) 174 | 175 | WIND_STRENGTH = 50 176 | PLANET_EXPLODE_RATE = 60 177 | 178 | PARTICIPATION_POINTS = 50 179 | 180 | PARTICIPATION = 0 181 | OBJECTIVE = 1 182 | -------------------------------------------------------------------------------- /src/death_particle.py: -------------------------------------------------------------------------------- 1 | ##!/usr/bin/env python3 2 | 3 | import random 4 | 5 | import pygame 6 | 7 | from particle import Particle 8 | import constants as c 9 | from primitives import Pose 10 | 11 | class DeathParticle(Particle): 12 | def __init__(self, game, ship): 13 | super().__init__(game) 14 | self.ship = ship 15 | self.pose = ship.pose.copy() 16 | self.velocity = Pose(((random.random() * 2 - 1) * 160, 17 | (random.random() * 2 - 1) * 160), 18 | random.random() * 360) + self.ship.velocity * 0.1 19 | self.start_radius = 40 + random.random()*30 20 | self.duration = 0.6 21 | 22 | def get_scale(self): 23 | return 1 - self.through(loading=2.5) 24 | 25 | def get_alpha(self): 26 | return 255 * (1 - self.through(loading=1)) 27 | 28 | def update(self, dt, events): 29 | super().update(dt, events) 30 | self.pose += self.velocity * dt 31 | 32 | def draw(self, surface, offset=(0, 0)): 33 | radius = int(self.start_radius * self.get_scale()) 34 | surf = pygame.Surface((radius*2, radius*2)) 35 | surf.fill(c.BLACK) 36 | surf.set_colorkey(c.BLACK) 37 | pygame.draw.circle(surf, self.ship.player.color, (radius, radius), radius) 38 | x = self.pose.x - offset[0] - surf.get_width()//2 39 | y = self.pose.y - offset[1] - surf.get_height()//2 40 | surf.set_alpha(self.get_alpha()) 41 | surface.blit(surf, (x, y)) 42 | -------------------------------------------------------------------------------- /src/error_logging.py: -------------------------------------------------------------------------------- 1 | ##!/usr/bin/env python3 2 | 3 | from traceback import format_exception 4 | 5 | def error_logging(path): 6 | return ErrorLoggingContext(path) 7 | 8 | class ErrorLoggingContext: 9 | def __init__(self, path): 10 | self.path = path 11 | 12 | def __enter__(self): 13 | return self 14 | 15 | def __exit__(self, 16 | exception_type, 17 | exception_value, 18 | traceback): 19 | if not issubclass(exception_type, Exception): 20 | return 21 | tb_list = format_exception(exception_type, 22 | exception_value, 23 | traceback, 24 | 100) 25 | tb_string = "".join(tb_list) 26 | with open(self.path, "a") as f: 27 | f.write(tb_string) 28 | f.write(f"{'-'*10}\n") 29 | print(f"Exception logged to {self.path}.") 30 | -------------------------------------------------------------------------------- /src/exhaust_particle.py: -------------------------------------------------------------------------------- 1 | ##!/usr/bin/env python3 2 | 3 | import pygame 4 | 5 | from particle import Particle 6 | from primitives import Pose 7 | import constants as c 8 | 9 | class ExhaustParticle(Particle): 10 | def __init__(self, game, ship): 11 | super().__init__(game) 12 | self.ship = ship 13 | size = 18 14 | if c.DOUBLE_THRUST_MOD in self.game.modifications: 15 | size *= 1.5 16 | self.surface = pygame.Surface((size, size)) 17 | self.surface.fill(c.BLACK) 18 | self.surface.set_colorkey(c.BLACK) 19 | pygame.draw.circle(self.surface, 20 | ship.player.color, 21 | (size//2, size//2), 22 | size//2) 23 | self.position = ship.pose.copy() 24 | self.position.add_pose(Pose((-20, 0), 0), 1, self.position) 25 | self.thrust = ship.thrust.copy() 26 | self.thrust_mag = self.thrust.magnitude() 27 | self.thrust.scale_to(-100) 28 | self.duration = 0.4 29 | self.intensity = 1 - (1 - self.thrust_mag/100/c.THRUST)**3 30 | 31 | def get_alpha(self): 32 | return (255 - self.through() * 255)*self.intensity 33 | 34 | def get_scale(self): 35 | return (1 - self.through())*self.intensity 36 | 37 | def update(self, dt, events): 38 | super().update(dt, events) 39 | rotated = self.thrust.copy() 40 | rotated.rotate_position(self.position.angle) 41 | self.position += rotated * dt * 3 * self.intensity 42 | self.position += self.ship.velocity * dt 43 | 44 | def draw(self, surface, offset=(0, 0)): 45 | x, y = self.position.x, self.position.y 46 | scale = self.get_scale() 47 | x += offset[0] - self.surface.get_width() * scale/2 48 | y += offset[1] - self.surface.get_width() * scale/2 49 | w = int(self.surface.get_width()*scale) 50 | h = int(self.surface.get_height()*scale) 51 | surf_to_blit = pygame.transform.scale(self.surface, (w, h)) 52 | surf_to_blit.set_alpha(self.get_alpha()) 53 | surface.blit(surf_to_blit, (x, y)) 54 | -------------------------------------------------------------------------------- /src/explosion.py: -------------------------------------------------------------------------------- 1 | ##!/usr/bin/env python3 2 | 3 | import pygame 4 | 5 | from particle import Particle 6 | import constants as c 7 | 8 | class Explosion(Particle): 9 | def __init__(self, game, ship): 10 | super().__init__(game) 11 | self.ship = ship 12 | self.pose = ship.pose.copy() 13 | self.start_radius = 20 14 | self.duration = 0.4 15 | 16 | def get_scale(self): 17 | return 1 + self.through(loading=2) * 4 18 | 19 | def get_alpha(self): 20 | return 255 * (1 - self.through(loading=2)) 21 | 22 | def update(self, dt, events): 23 | super().update(dt, events) 24 | 25 | def draw(self, surface, offset=(0, 0)): 26 | radius = int(self.start_radius * self.get_scale()) 27 | surf = pygame.Surface((radius*2, radius*2)) 28 | surf.fill(c.BLACK) 29 | surf.set_colorkey(c.BLACK) 30 | r = 255 - self.through(loading=2.5) * (255 - self.ship.player.color[0]) 31 | g = 255 - self.through(loading=2.5) * (255 - self.ship.player.color[1]) 32 | b = 255 - self.through(loading=2.5) * (255 - self.ship.player.color[2]) 33 | 34 | pygame.draw.circle(surf, (r, g, b), (radius, radius), radius) 35 | x = self.pose.x - offset[0] - surf.get_width()//2 36 | y = self.pose.y - offset[1] - surf.get_height()//2 37 | surf.set_alpha(self.get_alpha()) 38 | surface.blit(surf, (x, y)) 39 | -------------------------------------------------------------------------------- /src/game.py: -------------------------------------------------------------------------------- 1 | ##!/usr/bin/env python3 2 | 3 | import sys 4 | import string 5 | import random 6 | 7 | import pygame 8 | import yaml 9 | 10 | import constants as c 11 | from error_logging import error_logging 12 | from start_scene import StartScene 13 | from twitch_chat_stream import Stream 14 | from level_scene import LevelScene 15 | from player import Player 16 | from score_manager import ScoreManager 17 | from high_score_scene import HighScoreScene 18 | from high_score_table import HighScoreTable 19 | from alert_manager import AlertManager 20 | 21 | class Game: 22 | def __init__(self): 23 | pygame.init() 24 | self.config = self.get_config() 25 | pygame.mixer.music.load(c.SOUNDS_PATH + "/music.wav") 26 | pygame.mixer.music.play(loops=-1) 27 | self.load_sounds() 28 | if self.config["fullscreen"]: 29 | self.screen = pygame.display.set_mode(c.WINDOW_SIZE, pygame.FULLSCREEN) 30 | else: 31 | self.screen = pygame.display.set_mode(c.WINDOW_SIZE) 32 | pygame.display.set_caption(c.WINDOW_CAPTION) 33 | self.clock = pygame.time.Clock() 34 | self.players_in_last_round = set() 35 | self.stream = Stream(channel=self.config["channel"]) 36 | self.scoreboard = ScoreManager.from_file(self.config["scoreboard_file"]) 37 | self.temp_scores = {} 38 | self.modifications = [c.EXPLODING_PLANETS] 39 | self.last_snapshot = {} 40 | self.players = {} 41 | self.player_flags = {} 42 | self.player_label_font = pygame.font.Font(c.FONT_PATH + "/pixel_caps.ttf", 12) 43 | self.timer_font = pygame.font.Font(c.FONT_PATH + "/asap-bold.otf", 55) 44 | self.small_timer_font = pygame.font.Font(c.FONT_PATH + "/asap-bold.otf", 40) 45 | self.timer_render = {digit:self.timer_font.render(digit, 1, c.WHITE) for digit in "1234567890:-"} 46 | self.red_timer_render = {digit:self.timer_font.render(digit, 1, (255, 80, 80)) for digit in "1234567890:-"} 47 | self.small_font = pygame.font.Font(c.FONT_PATH + "/a_goblin_appears.ttf", 10) 48 | self.small_font_render = {char:self.small_font.render(char, 0, c.WHITE) for char in string.printable} 49 | self.very_small_font = pygame.font.Font(c.FONT_PATH + "/a_goblin_appears.ttf", 7) 50 | self.scoreboard_font = pygame.font.Font(c.FONT_PATH + "/asap.otf", 25) 51 | self.voting_planet_font = pygame.font.Font(c.FONT_PATH + "/asap.otf", 26) 52 | self.small_scoreboard_font = pygame.font.Font(c.FONT_PATH + "/asap.otf", 16) 53 | self.scoreboard_title_font = pygame.font.Font(c.FONT_PATH + "/eras_demi_bold.ttf", 40) 54 | self.scoreboard_description_font = pygame.font.Font(c.FONT_PATH + "/eras_demi.ttf", 25) 55 | self.alert_body_font = pygame.font.Font(c.FONT_PATH + "/asap-italic.otf", 15) 56 | self.alert_header_font = pygame.font.Font(c.FONT_PATH + "/asap-bold.otf", 18) 57 | self.scoreboard_font.bold = False 58 | self.alert_large_font = pygame.font.Font(c.FONT_PATH + "/eras_demi.ttf", 50) 59 | self.other_alert_font = pygame.font.Font(c.FONT_PATH + "/asap.otf", 14) 60 | self.current_scene = HighScoreScene(self) 61 | self.fps = [0] 62 | self.alertManager = AlertManager(self) 63 | self.main() 64 | 65 | def update_globals(self): 66 | """ Update global events, like checking for game close. 67 | Returns a tuple (dt, events), where dt is the float amount of 68 | seconds since the last update_globals call and events is a list 69 | of PyGame events that have occurred since the last call. 70 | """ 71 | dt = self.clock.tick(c.MAX_FPS) 72 | dt /= 1000 # ms to seconds 73 | events = pygame.event.get() 74 | for event in events: 75 | if event.type == pygame.QUIT: 76 | self.close() 77 | if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE: 78 | self.close() 79 | if event.type == pygame.KEYDOWN and event.key == pygame.K_r: 80 | self.current_scene.lastLevel = self.current_scene.level 81 | self.current_scene.spawn_level() 82 | self.current_scene.ships = [] 83 | self.current_scene.spawn_ship("!t100", "superduperpacman42") 84 | if event.type == pygame.KEYDOWN and event.key == pygame.K_a: 85 | message = random.choice(c.JOKE_MESSAGES) 86 | self.alertManager.alert(message) 87 | self.alertManager.update(dt) 88 | return dt, events 89 | 90 | def close(self): 91 | """ Close the game. """ 92 | try: 93 | self.scoreboard.save_if_changes() 94 | except NameError: 95 | pass 96 | pygame.quit() 97 | sys.exit() 98 | 99 | def get_config(self): 100 | with open(c.CONFIG_PATH + "/config.yaml", "r") as f: 101 | return yaml.load(f.read(), Loader=yaml.Loader) 102 | 103 | def update_screen(self): 104 | """ Update the pygame display. 105 | 106 | This can also be used as a hook to add game-wide 107 | display objects, like an FPS monitor. 108 | """ 109 | # fps_text = f"FPS: {int(sum(self.fps)/len(self.fps))}" 110 | # self.screen.blit(self.small_font.render(fps_text, 0, c.BLACK), (10, 10)) 111 | # self.screen.blit(self.small_font.render(fps_text, 0, c.WHITE), (8, 9)) 112 | self.alertManager.draw(self.screen) 113 | pygame.display.flip() 114 | 115 | def main(self): 116 | while True: 117 | self.current_scene.main() 118 | self.current_scene = self.current_scene.next_scene() 119 | if self.current_scene is None: 120 | self.close() 121 | 122 | def high_score_scene(self): 123 | return HighScoreScene(self) 124 | 125 | def number_of_players_last_round(self): 126 | return len(self.players_in_last_round) 127 | 128 | def player_multiplier(self): 129 | if self.number_of_players_last_round() < 3: 130 | return 0 131 | elif self.number_of_players_last_round() < 10: 132 | return 1 133 | elif self.number_of_players_last_round() < 30: 134 | return 2 135 | else: 136 | return 3 137 | 138 | def recolor_flag(self, player_name): 139 | player = self.players[player_name] 140 | player_flag = self.player_flags[player.name] 141 | flag = pygame.image.load(c.IMAGE_PATH + "/flag.png") 142 | player_flag.blit(flag, (0, 0)) 143 | tint = pygame.Surface((flag.get_width(), flag.get_height())) 144 | tint.fill(player.color) 145 | player_flag.blit(tint, (0, 0), special_flags=pygame.BLEND_MULT) 146 | player_flag.set_colorkey(player_flag.get_at((0, 0))) 147 | 148 | def load_sounds(self): 149 | self.ship_destroy_sound = pygame.mixer.Sound(c.SOUNDS_PATH + "/ship_destroy.wav") 150 | self.ship_destroy_sound.set_volume(0.4) 151 | self.waypoint_collect_sound = pygame.mixer.Sound(c.SOUNDS_PATH + "/waypoint_collect.wav") 152 | self.waypoint_collect_sound.set_volume(0.8) 153 | self.use_wormhole_sound = pygame.mixer.Sound(c.SOUNDS_PATH + "/use_wormhole.wav") 154 | self.use_wormhole_sound.set_volume(0.4) 155 | self.vote_sound = pygame.mixer.Sound(c.SOUNDS_PATH + "/vote.wav") 156 | self.vote_sound.set_volume(0.4) 157 | self.solar_wind_sound = pygame.mixer.Sound(c.SOUNDS_PATH + "/solar_wind.wav") 158 | self.solar_wind_sound.set_volume(0.025) 159 | self.land_on_moon_sound = pygame.mixer.Sound(c.SOUNDS_PATH + "/land_on_moon.wav") 160 | self.land_on_moon_sound.set_volume(0.5) 161 | self.planet_explode_sound = pygame.mixer.Sound(c.SOUNDS_PATH + "/planet_explode.wav") 162 | self.planet_explode_sound.set_volume(0.45) 163 | self.alert_appears_sound = pygame.mixer.Sound(c.SOUNDS_PATH + "/alert_appears.wav") 164 | self.alert_appears_sound.set_volume(0.03) 165 | 166 | if __name__ == '__main__': 167 | with error_logging(c.LOG_PATH): 168 | Game() 169 | -------------------------------------------------------------------------------- /src/high_score_scene.py: -------------------------------------------------------------------------------- 1 | ##!/usr/bin/env python3 2 | 3 | import pygame 4 | 5 | import constants as c 6 | from scene import Scene 7 | from level_scene import LevelScene 8 | from high_score_table import HighScoreTable 9 | from transition_gui import TransitionGui 10 | 11 | class HighScoreScene(Scene): 12 | def __init__(self, *args, **kwargs): 13 | super().__init__(*args, **kwargs) 14 | 15 | for player in self.game.players: 16 | if player not in [item.name for item in self.game.scoreboard.scores]: 17 | self.game.scoreboard.add_score(player, 0) 18 | 19 | self.board_offset = -c.WINDOW_HEIGHT 20 | self.table = HighScoreTable(self.game) 21 | self.table_all = HighScoreTable(self.game, hours_to_display=10**9) 22 | self.table.pose.x = c.WINDOW_WIDTH * 0.3 23 | self.table_all.pose.x = self.table.pose.x 24 | self.age = 0 25 | self.shade = pygame.Surface(c.WINDOW_SIZE) 26 | self.shade.fill(c.BLACK) 27 | self.shade_alpha = 255 28 | self.scene_over = False 29 | self.side_gui = TransitionGui(self.game) 30 | pygame.mixer.music.set_volume(0.25) 31 | 32 | def next_scene(self): 33 | pygame.mixer.music.set_volume(1.0) 34 | return LevelScene(self.game) 35 | 36 | def update(self, dt, events): 37 | self.age += dt 38 | 39 | if self.age > 25 and self.board_offset < 0: 40 | speed = 4 41 | d = abs(self.board_offset) 42 | self.board_offset += min(d * dt * speed, c.WINDOW_HEIGHT*dt*2) 43 | if self.board_offset > 0: 44 | self.board_offset = 0 45 | 46 | for event in events: 47 | if event.type == pygame.KEYDOWN and event.key == pygame.K_RETURN: 48 | self.scene_over = True 49 | if self.side_gui.countdown_over(): 50 | self.scene_over = True 51 | self.table.update(dt, events) 52 | self.table_all.update(dt, events) 53 | self.side_gui.update(dt, events) 54 | 55 | for message in self.game.stream.queue_flush(): 56 | if message.text.lower() == '!recolor': 57 | if message.user in self.game.players: 58 | self.game.players[message.user].recolor() 59 | self.game.recolor_flag(message.user) 60 | elif message.text.lower() == '!score': 61 | board = self.game.scoreboard.get_total_by_player(c.SCORE_EXPIRATION) 62 | if message.user in board: 63 | score = self.game.scoreboard.get_total_by_player(c.SCORE_EXPIRATION)[message.user].score 64 | self.game.alertManager.alert("Your score is "+str(score), message.user) 65 | else: 66 | self.game.alertManager.alert("You have not played in the last " + str(c.SCORE_EXPIRATION) + " hours", message.user) 67 | elif message.text.lower()[:5] == "!vote": 68 | split = message.text.lower().split() 69 | if len(split) != 2: 70 | self.game.alertManager.alert("Invalid number of arguments for !vote", message.user) 71 | continue 72 | player_name = message.user 73 | argument = split[1] 74 | self.game.current_scene.side_gui.vote(player_name, argument) 75 | 76 | speed = 800 77 | if self.scene_over: 78 | self.shade_alpha += speed*dt 79 | else: 80 | self.shade_alpha -= speed*dt 81 | self.shade_alpha = max(0, min(255, self.shade_alpha)) 82 | 83 | if self.scene_over and self.shade_alpha == 255: 84 | self.is_running = False 85 | 86 | def draw(self, surface, offset=(0, 0)): 87 | surface.fill(c.BLACK) 88 | surface.blit(self.table.background_surface, (0, 0)) 89 | self.table.draw(surface, (offset[0], offset[1] + self.board_offset + c.WINDOW_HEIGHT)) 90 | self.table_all.draw(surface, (offset[0], offset[1] + self.board_offset)) 91 | self.side_gui.draw(surface, offset) 92 | 93 | if self.shade_alpha > 0: 94 | self.shade.set_alpha(self.shade_alpha) 95 | surface.blit(self.shade, (0, 0)) 96 | -------------------------------------------------------------------------------- /src/high_score_table.py: -------------------------------------------------------------------------------- 1 | ##!/usr/bin/env python3 2 | 3 | import math 4 | 5 | import pygame 6 | 7 | from primitives import GameObject, Pose 8 | import constants as c 9 | from player import Player 10 | 11 | class HighScoreColumn(GameObject): 12 | def __init__(self, game, parent, get_data, align=c.LEFT, width=100, small_font=False): 13 | super().__init__(game) 14 | self.align = align 15 | self.width = width 16 | self.small_font = small_font 17 | 18 | # get_data takes in a player and returns data for the column as string 19 | self.get_data = get_data 20 | # self.surface = self.get_column_surface() 21 | 22 | class HighScoreRow(GameObject): 23 | def __init__(self, game, player, columns = None, row_number=0): 24 | super().__init__(game) 25 | self.player = player 26 | self.columns = columns if columns is not None else [] 27 | self.row_number = row_number 28 | self.wiggle_radius = 2 29 | self.wiggle_offset = -self.row_number * 0.6 30 | self.wiggle_frequency = 0.7 31 | self.debug_lines = False 32 | self.age = 0 33 | self.tile = self.get_tile() 34 | self.tile_shadow.fill(c.BLACK) 35 | self.tile_shadow.set_alpha(80) 36 | 37 | def height(self): 38 | return self.tile.get_height() 39 | 40 | def tile_color(self): 41 | if self.row_number%2: 42 | return c.SCORE_ODD_COLOR 43 | else: 44 | return c.SCORE_EVEN_COLOR 45 | 46 | def get_piece(self, player, column): 47 | line_text = column.get_data(player) 48 | color = c.WHITE 49 | if line_text and line_text[0] == "@": 50 | line_text = line_text[1:] 51 | color = player.color 52 | font = self.game.scoreboard_font 53 | if column.small_font: 54 | font = self.game.small_scoreboard_font 55 | text = font.render(line_text, 1, c.BLACK) 56 | text_white = font.render(line_text, 1, color) 57 | surf = pygame.Surface((column.width, c.SCORE_ROW_HEIGHT - c.SCORE_TILE_PADDING*2)) 58 | surf.fill(self.tile_color()) 59 | surf.set_colorkey(self.tile_color()) 60 | if column.align is c.LEFT or text.get_width() > surf.get_width() - c.SCORE_ROW_PADDING*2: 61 | x = c.SCORE_ROW_PADDING 62 | elif column.align is c.RIGHT: 63 | x = surf.get_width() - text.get_width() - c.SCORE_ROW_PADDING 64 | else: 65 | x = surf.get_width()//2 - text.get_width()//2 66 | if player.name is c.EMPTY: 67 | text.set_alpha(128) 68 | text_white.set_alpha(0) 69 | else: 70 | text.set_alpha(128) 71 | white_offset = 1 72 | offsets = c.TEXT_BLIT_OFFSETS if player.name is not c.EMPTY else [c.CENTER] 73 | for offset in offsets: 74 | surf.blit(text, (x + offset[0], surf.get_height()//2 - text.get_height()//2 + offset[1])) 75 | surf.blit(text_white, (x, surf.get_height()//2 - text.get_height()//2 - white_offset)) 76 | if player.name is c.EMPTY: 77 | black = pygame.Surface((surf.get_width(), surf.get_height())) 78 | black.set_alpha(75) 79 | surf.blit(black, (0, 0)) 80 | if self.debug_lines: 81 | pygame.draw.rect(surf, c.RED, (0, 0, surf.get_width(), surf.get_height()), width=1) 82 | return surf 83 | 84 | def get_row_surface(self, player): 85 | pieces = [self.get_piece(player, column) for column in self.columns] 86 | width = sum([piece.get_width() for piece in pieces]) 87 | surf = pygame.Surface((width, c.SCORE_ROW_HEIGHT - 2*c.SCORE_TILE_PADDING)) 88 | surf.fill(self.tile_color()) 89 | x = 0 90 | for piece in pieces: 91 | surf.blit(piece, (x, 0)) 92 | x += piece.get_width() 93 | self.tile_shadow = surf.copy() 94 | return surf 95 | 96 | def get_tile(self): 97 | return(self.surf_to_tile(self.get_row_surface(self.player))) 98 | 99 | def surf_to_tile(self, surface): 100 | tile = pygame.Surface((surface.get_width() + c.SCORE_TILE_PADDING * 2, c.SCORE_ROW_HEIGHT)) 101 | tile.fill((50, 80, 110)) 102 | tile.set_colorkey((50, 80, 110)) 103 | pygame.draw.rect(tile, 104 | c.GRAY, 105 | (c.SCORE_TILE_PADDING, 106 | c.SCORE_TILE_PADDING, 107 | tile.get_width() - c.SCORE_TILE_PADDING * 2, 108 | tile.get_height() - c.SCORE_TILE_PADDING * 2)) 109 | x = tile.get_width()//2 - surface.get_width()//2 110 | y = tile.get_height()//2 - surface.get_height()//2 111 | tile.blit(surface, (x, y)) 112 | return tile 113 | 114 | def update(self, dt, events): 115 | self.age += dt 116 | 117 | def draw(self, surface, offset=(0, 0)): 118 | wx = math.sin(math.pi * 2 * self.wiggle_frequency * self.age + self.wiggle_offset) * self.wiggle_radius 119 | wy = -math.cos(math.pi * 2 * self.wiggle_frequency * self.age + self.wiggle_offset) * self.wiggle_radius 120 | x = offset[0] - self.tile.get_width()//2 + wx 121 | y = offset[1] - self.tile.get_height()//2 + wy 122 | if not self.player.name is c.EMPTY: 123 | surface.blit(self.tile_shadow, (x+9, y+11)) 124 | surface.blit(self.tile, (x, y)) 125 | 126 | class HighScoreTable(GameObject): 127 | def __init__(self, game, hours_to_display=c.SCORE_EXPIRATION): 128 | super().__init__(game) 129 | self.all_time = hours_to_display >= 10**6 130 | self.pose = Pose((c.WINDOW_WIDTH//2, c.WINDOW_HEIGHT//2), 0) 131 | self.title = f"High scores".upper() 132 | self.description = f"Last {hours_to_display} hours".upper() 133 | if self.all_time: 134 | self.description = "all time".upper() 135 | snapshot_dict = self.game.scoreboard.get_total_by_player(hours_to_display) 136 | self.rows = 10 137 | self.last_snapshot = self.game.last_snapshot.get(hours_to_display, None) 138 | self.snapshot = self.dict_to_sorted_list(snapshot_dict) 139 | self.game.last_snapshot[hours_to_display] = self.snapshot 140 | self.player_names = [item[0] for item in self.snapshot[:self.rows] if item[0] != c.EMPTY] 141 | self.add_missing_players() 142 | self.player_names += [c.EMPTY for i in range(self.rows - len(self.player_names))] 143 | self.columns = [] 144 | self.placing_calls = 0 145 | self.assemble_table() 146 | players = [self.game.players[name] for name in self.player_names] 147 | self.rows = [HighScoreRow(self.game, player, columns=self.columns, row_number=i) for i, player in enumerate(players)] 148 | self.width = c.SCORE_TABLE_WIDTH 149 | self.height = c.SCORE_TABLE_HEIGHT 150 | self.background_surface = pygame.image.load(c.IMAGE_PATH + "/scoreboard_background.png") 151 | self.background_surface = pygame.transform.scale(self.background_surface, 152 | (self.width - c.SCORE_TABLE_PADDING*2, 153 | self.height - c.SCORE_TABLE_PADDING*2)) 154 | self.title_surf, self.description_surf = self.table_title_surfaces() 155 | self.hours = hours_to_display 156 | self.age = 0 157 | 158 | def add_missing_players(self): 159 | if c.EMPTY not in self.game.players: 160 | self.game.players[c.EMPTY] = Player(self.game, c.EMPTY, c.MEDIUM_DARK_GRAY) 161 | for player_name in self.player_names: 162 | if player_name not in self.game.players: 163 | self.game.players[player_name] = Player(self.game, player_name) 164 | 165 | def placing(self, player): 166 | return self.player_names.index(player.name) + 1 167 | 168 | def render_placing(self, player): 169 | self.placing_calls += 1 170 | return f"{self.placing_calls}." 171 | 172 | def player_to_score(self, player): 173 | if player.name == c.EMPTY: 174 | return 0 175 | placing = self.placing(player) 176 | return self.snapshot[placing - 1][1].score 177 | 178 | def rank_change(self, player): 179 | if player.name == c.EMPTY: 180 | return 0 181 | if self.last_snapshot is None: 182 | return 0 183 | else: 184 | last_players = [item[0] for item in self.last_snapshot] 185 | cur_players = [item[0] for item in self.snapshot] 186 | cur_index = cur_players.index(player.name) 187 | if not player.name in last_players: 188 | return len(cur_players) - cur_index 189 | last_index = last_players.index(player.name) 190 | return cur_index - last_index 191 | 192 | def score_increase(self, player): 193 | if player.name == c.EMPTY: 194 | return "" 195 | if self.last_snapshot is None: 196 | return "-" 197 | else: 198 | last_players = [item[0] for item in self.last_snapshot] 199 | cur_players = [item[0] for item in self.snapshot] 200 | cur_index = cur_players.index(player.name) 201 | if not player.name in last_players: 202 | increase = self.snapshot[cur_index][1].score 203 | else: 204 | last_index = last_players.index(player.name) 205 | increase = self.snapshot[cur_index][1].score - self.last_snapshot[last_index][1].score 206 | increase = int(increase) 207 | plus = "+" if increase >= 0 else "" 208 | return f"{plus}{increase}" 209 | 210 | def assemble_table(self): 211 | self.columns = [ 212 | HighScoreColumn(self.game, self, lambda x: f"{self.render_placing(x)}", align=c.CENTER, width=50), 213 | HighScoreColumn(self.game, self, lambda x: f"{x.name}", align=c.CENTER, width=400), 214 | HighScoreColumn(self.game, self, lambda x: f"{self.score_increase(x)}", align=c.CENTER, width=70, small_font=True), 215 | HighScoreColumn(self.game, self, lambda x: f"{int(self.player_to_score(x))}", align=c.RIGHT, width=120) 216 | ] 217 | 218 | @staticmethod 219 | def dict_to_sorted_list(d): 220 | l = [(key, d[key]) for key in d] 221 | l.sort(reverse=True, key=lambda x: x[1].score) 222 | return l 223 | 224 | def update(self, dt, events): 225 | self.age += dt 226 | for row in self.rows: 227 | row.update(dt, events) 228 | 229 | def table_title_surfaces(self): 230 | title = self.game.scoreboard_title_font.render(self.title, 1, c.WHITE) 231 | description = self.game.scoreboard_font.render(self.description, 1, c.WHITE) 232 | return title, description 233 | 234 | def draw(self, surface, offset=(0, 0)): 235 | height = self.height - c.SCORE_TABLE_PADDING*2 236 | width = self.width - c.SCORE_TABLE_PADDING*2 237 | yoffset = 32 238 | x = width//2 + offset[0] 239 | y = height//2 - (c.SCORE_ROW_HEIGHT * len(self.rows))//2 + c.SCORE_ROW_HEIGHT//2 + yoffset 240 | y_title = y - yoffset + math.sin(self.age * 3) * 2 241 | #pygame.draw.rect(back_surf, c.SCORE_TABLE_COLOR, (x - width//2, y - c.SCORE_TABLE_PADDING - c.SCORE_ROW_HEIGHT//2, width, height)) 242 | for row in self.rows: 243 | row.draw(surface, (x, y + offset[1])) 244 | y += row.height() 245 | surface.blit(self.title_surf, (x-self.title_surf.get_width()//2, y_title - 52 - self.title_surf.get_height()//2 + offset[1])) 246 | surface.blit(self.description_surf, (x-self.description_surf.get_width()//2, y_title - 22 - self.description_surf.get_height()//2 + offset[1])) 247 | #surface.blit(back_surf, (c.SCORE_TABLE_PADDING + offset[0], c.SCORE_TABLE_PADDING + offset[1])) 248 | -------------------------------------------------------------------------------- /src/level_scene.py: -------------------------------------------------------------------------------- 1 | ##!/usr/bin/env python3 2 | 3 | import math 4 | 5 | import pygame 6 | import random 7 | 8 | import constants as c 9 | from scene import Scene 10 | from planet import Planet 11 | from moon import Moon 12 | from wormhole import Wormhole 13 | from ship import Ship 14 | from primitives import Pose 15 | from achievement_row import AchievementRow 16 | from nugget import Nugget 17 | from player import Player 18 | from wind_particle import WindParticle 19 | 20 | class LevelScene(Scene): 21 | def __init__(self, *args, lastLevel=None, **kwargs): 22 | super().__init__(*args, **kwargs) 23 | 24 | self.game.temp_scores = {} 25 | self.lastLevel = lastLevel 26 | self.game.players_in_last_round = set() 27 | self.spawn_level() 28 | 29 | self.home_planet = None 30 | for item in self.planets: 31 | if item.home: 32 | self.home_planet = item 33 | break 34 | 35 | self.ships = [] 36 | 37 | self.surface = pygame.Surface((c.LEVEL_WIDTH, c.LEVEL_HEIGHT)) 38 | self.side_panel = pygame.Surface(c.SIDE_PANEL_SIZE) 39 | self.achievement_row = AchievementRow(self.game, (0, 0)) 40 | self.achievement_row.pose.y = c.WINDOW_HEIGHT - self.achievement_row.get_height() 41 | self.alignment = c.LEFT, c.DOWN 42 | self.offset = self.get_initial_offset() 43 | self.particles = set() 44 | self.screenshake_time = 0 45 | self.screenshake_amp = 0 46 | self.exploded_planets = 0 47 | self.age = 0 48 | self.scene_over = False 49 | 50 | self.shade = pygame.Surface(c.WINDOW_SIZE) 51 | self.shade.fill(c.BLACK) 52 | self.shade_alpha = 255 53 | self.shade.set_colorkey(c.MAGENTA) 54 | #self.timer_label = self.game. 55 | 56 | self.since_sh = 99 57 | 58 | self.instructions = pygame.image.load(c.IMAGE_PATH + "/instructions.png") 59 | 60 | if self.game.modifications: 61 | lowered = [mod.lower() for mod in self.game.modifications] 62 | self.game.alertManager.alert(f"Modifications active: {', '.join(lowered)}") 63 | 64 | def shake(self, amp=15): 65 | self.screenshake_amp = max(self.screenshake_amp, amp) 66 | self.screenshake_time = 0 67 | 68 | def round_length(self): 69 | if c.EXTRA_TIME_MOD in self.game.modifications: 70 | return 12 71 | else: 72 | return 10 # minutes 73 | 74 | def apply_screenshake(self, offset): 75 | x = offset[0] + self.screenshake_amp * math.cos(self.screenshake_time * 24) 76 | y = offset[1] + self.screenshake_amp * math.cos(self.screenshake_time * 24) 77 | return (x, y) 78 | 79 | def apply_own_offset(self, offset): 80 | return offset[0] + self.offset[0], offset[1] + self.offset[1] 81 | 82 | def update(self, dt, events): 83 | self.age += dt 84 | 85 | self.since_sh += dt 86 | self.screenshake_time += dt 87 | self.screenshake_amp *= 0.001**dt 88 | self.screenshake_amp = max(0, self.screenshake_amp - 20*dt) 89 | 90 | if c.SOLAR_WIND in self.game.modifications: 91 | if self.since_sh > 0.75: 92 | self.game.solar_wind_sound.play() 93 | self.since_sh = 0 94 | self.particles.add(WindParticle(self.game)) 95 | 96 | if c.EXPLODING_PLANETS in self.game.modifications: 97 | if self.age > (self.exploded_planets+1)*c.PLANET_EXPLODE_RATE and len(self.planets) > 2: 98 | self.exploded_planets += 1 99 | i = random.randint(2, len(self.planets)-1) 100 | if not isinstance(self.planets[i], Wormhole): 101 | self.planets[i].destroy() 102 | self.game.current_scene.shake(25) 103 | 104 | for ship in self.ships[::-1]: 105 | if ship.destroyed: 106 | self.ships.remove(ship) 107 | for object_to_update in self.ships + self.planets + [self.achievement_row] + self.nuggets: 108 | object_to_update.update(dt, events) 109 | for particle in self.particles: 110 | particle.update(dt, events) 111 | self.particles = {item for item in self.particles if not item.dead} 112 | 113 | for message in self.game.stream.queue_flush(): 114 | if message.text.lower() == '!recolor': 115 | if message.user in self.game.players: 116 | self.game.players[message.user].recolor() 117 | for ship in self.ships: 118 | if ship.player.name == message.user: 119 | ship.recolor() 120 | self.game.recolor_flag(message.user) 121 | elif message.text.lower() == '!score': 122 | board = self.game.scoreboard.get_total_by_player(c.SCORE_EXPIRATION) 123 | if message.user in board: 124 | score = self.game.scoreboard.get_total_by_player(c.SCORE_EXPIRATION)[message.user].score 125 | self.game.alertManager.alert("Your score is "+str(score), message.user) 126 | else: 127 | self.game.alertManager.alert("You have not played in the last " + str(c.SCORE_EXPIRATION) + " hours", message.user) 128 | elif message.text.lower()[:5] == "!vote": 129 | pass 130 | elif message.text[0] == '!': 131 | program, info = Ship.parse_program(message.text) 132 | if not program: 133 | #print("Error: " + info) 134 | self.game.alertManager.alert(info, message.user) 135 | elif not self.scene_over: 136 | if message.user not in self.game.players: 137 | self.game.players[message.user] = Player(self.game, message.user) 138 | self.spawn_ship(message.text, message.user) 139 | 140 | shade_speed = 900 141 | if self.shade_alpha > 0 and not self.scene_over: 142 | center = (self.home_planet.pose.x, self.home_planet.pose.y) 143 | hold_rad = 100 144 | pause = 0.3 145 | radius = max( 146 | max((self.age - pause + 0.1), 0)**2.5 * c.WINDOW_WIDTH, 147 | min((1 - (1 - self.age/pause)**3)*hold_rad, hold_rad) 148 | ) 149 | if radius < c.WINDOW_WIDTH * 1.4: 150 | pygame.draw.circle(self.shade, c.MAGENTA, center, radius) 151 | else: 152 | self.shade_alpha = 0 153 | elif self.shade_alpha < 255 and self.scene_over: 154 | if not self.shade.get_at((0, 0))[:3] == c.BLACK: 155 | self.shade.fill(c.BLACK) 156 | self.shade_alpha = min(255, self.shade_alpha + shade_speed * dt) 157 | 158 | if self.age > self.round_length() * 60 and not self.scene_over: 159 | self.scene_over = True 160 | self.game.alertManager.alert("Time's up!") 161 | if self.achievement_row.all_scored(): 162 | self.scene_over = True 163 | for event in events: 164 | if event.type == pygame.KEYDOWN and event.key == pygame.K_RETURN: 165 | self.scene_over = True 166 | 167 | if self.scene_over and self.shade_alpha == 255: 168 | self.is_running = False 169 | 170 | def draw(self, surf, offset=(0, 0)): 171 | offset_with_shake = self.apply_screenshake(offset) 172 | #surf.fill(c.BLACK) 173 | if not c.SOLAR_WIND in self.game.modifications: 174 | self.surface.fill(c.DARK_GRAY) 175 | else: 176 | color = (65 - math.sin(self.age*2)*8, 35, 30) 177 | self.surface.fill(color) 178 | self.draw_lines() # TODO make background more interesting but not so laggy 179 | for planet in self.planets[:]: 180 | if planet.destroyed: 181 | self.planets.remove(planet) 182 | for planet in self.planets: 183 | planet.draw_gravity_region(self.surface, offset_with_shake) 184 | for planet in self.planets: 185 | planet.draw(self.surface, offset_with_shake) 186 | for nugget in self.nuggets: 187 | nugget.draw(self.surface, offset_with_shake) 188 | for particle in self.particles: 189 | particle.draw(self.surface, offset_with_shake) 190 | for ship in self.ships: 191 | ship.draw(self.surface, offset_with_shake) 192 | surf.blit(self.surface, self.apply_own_offset(offset)) 193 | 194 | self.side_panel.fill(c.BLACK) 195 | self.achievement_row.draw(self.side_panel) 196 | self.draw_timer(self.side_panel, c.TIMER_POSITION) 197 | surf.blit(self.side_panel, (c.LEVEL_WIDTH, 0)) 198 | surf.blit(self.instructions, (c.WINDOW_WIDTH - self.instructions.get_width(), 199 | 80)) 200 | 201 | if self.shade_alpha > 0: 202 | self.shade.set_alpha(self.shade_alpha) 203 | surf.blit(self.shade, (0, 0)) 204 | 205 | def draw_lines(self): 206 | border = 15 207 | line_period = 40 208 | line_width = 5 209 | offset = (self.age * 25) % line_period 210 | x = - c.WINDOW_HEIGHT - border + offset 211 | y_low = c.WINDOW_HEIGHT + border 212 | y_high = - border 213 | y_height = y_low - y_high 214 | color = c.DARKER_GRAY 215 | if c.SOLAR_WIND in self.game.modifications: 216 | return 217 | while x < c.WINDOW_WIDTH + border: 218 | pygame.draw.line(self.surface, 219 | c.DARKER_GRAY, 220 | (x, y_low), 221 | (x+y_height, y_high), 222 | width=line_width) 223 | x += line_period 224 | 225 | def get_initial_offset(self): 226 | x = 0 227 | if self.alignment[0] == c.RIGHT: 228 | x = c.WINDOW_WIDTH - c.LEVEL_WIDTH 229 | elif self.alignment[0] == c.CENTER: 230 | x = (c.WINDOW_WIDTH - c.LEVEL_WIDTH)//2 231 | y = 0 232 | if self.alignment[1] == c.DOWN: 233 | y = c.WINDOW_HEIGHT - c.LEVEL_HEIGHT 234 | elif self.alignment[1] == c.CENTER: 235 | y = (c.WINDOW_HEIGHT - c.LEVEL_HEIGHT)//2 236 | return (x, y) 237 | 238 | def spawn_level(self, level=None): 239 | # if not level: 240 | # levels = ["giant", "small", "wormhole", "default"] 241 | # weights = [1, 2, 1, 3] 242 | # if self.lastLevel in levels and self.lastLevel != "default": 243 | # i = levels.index(self.lastLevel) 244 | # weights[i] = 0 245 | # level = random.choices(levels, weights)[0] 246 | if c.GIANT_PLANET_MOD in self.game.modifications: 247 | level = "giant" 248 | elif c.MANY_WORMHOLES_MOD in self.game.modifications: 249 | level = "wormhole" 250 | elif c.SMALL_PLANETS_MOD in self.game.modifications: 251 | level = "small" 252 | elif c.EXPLODING_PLANETS in self.game.modifications: 253 | level = "exploding" 254 | else: 255 | level = "default" 256 | self.level = level 257 | #print("Level type: " + level) 258 | self.planets = [] 259 | self.nuggets = [] 260 | self.spawn_home_planet() 261 | self.spawn_moon() 262 | if c.NO_PLANETS_MOD in self.game.modifications: 263 | self.spawn_waypoint(2) 264 | if random.random() < 0.35: 265 | self.add_wormhole() 266 | elif level == "giant": 267 | self.add_planet(rmin=120, rmax=150, clearance=c.MIN_SPACING+50, border=300) 268 | self.spawn_waypoint(2) 269 | if random.random() < 0.35: 270 | self.add_wormhole() 271 | self.add_planet(n=7) 272 | elif level == "small": 273 | self.spawn_waypoint(2) 274 | if random.random() < 0.35: 275 | self.add_wormhole() 276 | self.add_planet(rmax=50, n=20) 277 | elif level == "wormhole": 278 | self.spawn_waypoint(2) 279 | for i in range(4): 280 | self.add_wormhole(color=i) 281 | self.add_planet(n=7) 282 | elif level == "exploding": 283 | self.spawn_waypoint(2) 284 | self.add_planet(n=12) 285 | else: 286 | self.spawn_waypoint(2) 287 | if random.random() < 0.35: 288 | self.add_wormhole() 289 | self.add_planet(n=random.randint(7,12)) 290 | 291 | def spawn_home_planet(self, home=None, clearance=c.MIN_SPACING+100): 292 | if not home: 293 | home = self.get_edge(offset=c.HOME_PLANET_RADIUS//2) 294 | spawn_angle = self.get_angle(home, (c.LEVEL_WIDTH/2, c.LEVEL_HEIGHT/2)) 295 | spawn_angle += (2 * random.random() - 1) * c.HOME_ANGLE_VARIATION * math.pi/180 296 | spawn_x = home[0] + int(math.cos(spawn_angle)*(c.HOME_PLANET_RADIUS+c.SHIP_SPAWN_ALTITUDE)) 297 | spawn_y = home[1] - int(math.sin(spawn_angle)*(c.HOME_PLANET_RADIUS+c.SHIP_SPAWN_ALTITUDE)) 298 | self.spawn_angle = math.degrees(spawn_angle) 299 | self.spawn_pos = (spawn_x, spawn_y) 300 | self.home_planet = Planet(self.game, home, angle=self.spawn_angle, radius=c.HOME_PLANET_RADIUS, home=True) 301 | self.home_planet.clearance = clearance 302 | self.planets.append(self.home_planet) 303 | 304 | def spawn_moon(self, home_clearance=400, clearance=c.MIN_SPACING + 50): 305 | for i in range(100): 306 | moon = Pose(self.get_edge(offset=100), 0) 307 | if moon.distance_to(self.home_planet.pose) > home_clearance: 308 | break 309 | self.moon = Moon(self.game, (moon.x, moon.y)) 310 | self.moon.clearance = clearance 311 | self.planets.append(self.moon) 312 | 313 | def spawn_waypoint(self, n=1, home_clearance=250, moon_clearance=250, waypoint_clearance=400, clearance=c.MIN_SPACING + 50): 314 | for i in range(n): 315 | for i in range(100): 316 | pos = self.get_viable_point(22, clearance, point=self.get_point(border=100)) 317 | if not pos: 318 | continue 319 | waypoint = Pose(pos, 0) 320 | if waypoint.distance_to(self.home_planet.pose) < home_clearance: 321 | continue 322 | if waypoint.distance_to(self.moon.pose) < moon_clearance: 323 | continue 324 | fail = False 325 | for waypoint2 in self.nuggets: 326 | if waypoint.distance_to(waypoint2.pose) < waypoint_clearance: 327 | fail = True 328 | break 329 | if not fail: 330 | break 331 | waypoint = Nugget(self.game, (waypoint.x, waypoint.y), 0) 332 | waypoint.clearance = clearance 333 | self.nuggets.append(waypoint) 334 | 335 | def get_point(self, W=c.LEVEL_WIDTH, H=c.LEVEL_HEIGHT, border=0): 336 | x = int(random.random()*(W-border*2)) + border 337 | y = int(random.random()*(H-border*2)) + border 338 | return (x, y) 339 | 340 | def get_edge(self, W=c.LEVEL_WIDTH, H=c.LEVEL_HEIGHT, offset=0): 341 | perimeter = (W+H-offset*4)*2 342 | x = int(random.random()*(W-2*offset)) 343 | y = int(random.random()*(H-2*offset)) 344 | edge = int(random.random()*4) 345 | if edge == 0: 346 | x = offset 347 | y = max(offset, y) 348 | y = min(W - offset, y) 349 | elif edge == 1: 350 | x = W - offset 351 | y = max(offset, y) 352 | y = min(W - offset, y) 353 | elif edge == 2: 354 | y = offset 355 | x = max(offset, x) 356 | x = min(H - offset, x) 357 | elif edge == 3: 358 | y = H - offset 359 | x = max(offset, x) 360 | x = min(H - offset, x) 361 | return (x, y) 362 | 363 | def get_angle(self, p1, p2): 364 | return math.atan2(p1[1]-p2[1], p2[0]-p1[0]) 365 | 366 | def get_viable_point(self, r, clearance=c.MIN_SPACING, point=None): 367 | if not point: 368 | point = self.get_point() 369 | x, y = point 370 | pose = Pose((x, y), 0) 371 | for planet in self.planets + self.nuggets: 372 | if planet.overlaps(pose, r, clearance): 373 | return False 374 | return (x, y) 375 | 376 | def add_planet(self, rmin=c.MIN_PLANET_RADIUS, rmax=c.MAX_PLANET_RADIUS, n=1, clearance=c.MIN_SPACING, border=0, edge=False): 377 | for i in range(100): 378 | r = int(random.random()*(rmax-rmin))+rmin 379 | if edge: 380 | p = self.get_edge(offset=border) 381 | else: 382 | p = self.get_point(border=border) 383 | pos = self.get_viable_point(r, clearance, point=p) 384 | if pos: 385 | p = Planet(self.game, pos, radius=r) 386 | p.clearance = clearance 387 | self.planets.append(p) 388 | n -= 1 389 | if n <= 0: 390 | break 391 | return True 392 | 393 | def add_wormhole(self, min_travel=300, clearance=c.MIN_SPACING, color=3): 394 | for i in range(100): 395 | p = self.get_point(border=100) 396 | pos1 = self.get_viable_point(20, clearance, point=p) 397 | if pos1: 398 | break 399 | if not pos1: 400 | return 401 | for i in range(100): 402 | p = self.get_point(border=100) 403 | pos2 = self.get_viable_point(20, clearance, point=p) 404 | if pos2 and Pose(pos1, 0).distance_to(Pose(pos2, 0)) > min_travel: 405 | break 406 | if not pos2: 407 | return 408 | w = Wormhole(self.game, pos1, pos2, color=color) 409 | w.clearance = clearance 410 | self.planets.append(w) 411 | 412 | def spawn_ship(self, program, name): 413 | player = self.game.players[name] 414 | new_ship = Ship(self.game, program, player, self.spawn_pos, self.spawn_angle) 415 | for existing_ship in self.ships[:]: 416 | if existing_ship.player == player: 417 | existing_ship.destroy() 418 | self.ships.append(new_ship) 419 | 420 | self.game.players_in_last_round.add(player) 421 | 422 | def draw_timer(self, surface, center, offset=(0, 0)): 423 | duration = self.round_length() * 60 424 | left = int(duration - self.age) 425 | minutes = left // 60 426 | seconds = int(left % 60) 427 | zero = "0" if seconds < 10 else "" 428 | text = f"{minutes}:{zero}{seconds}" 429 | font_dict = self.game.timer_render if minutes else self.game.red_timer_render 430 | surfaces = [font_dict[digit] for digit in text] 431 | total_width = sum([surf.get_width() for surf in surfaces]) 432 | max_height = max([surf.get_height() for surf in surfaces]) 433 | 434 | x = center[0] + offset[0] - total_width//2 435 | y = center[1] + offset[1] - max_height//2 436 | for item in surfaces: 437 | surface.blit(item, (x, y)) 438 | x += item.get_width() 439 | 440 | def next_scene(self): 441 | multiplier = self.game.player_multiplier() 442 | for player_name in self.game.temp_scores: 443 | self.game.scoreboard.add_score(player_name, self.game.temp_scores[player_name] * multiplier) 444 | for player in self.game.players_in_last_round: 445 | self.game.scoreboard.add_score(player.name, c.PARTICIPATION_POINTS * multiplier) 446 | self.game.modifications = [] 447 | #self.game.alertManager.clear() 448 | self.game.solar_wind_sound.fadeout(500) 449 | return self.game.high_score_scene() 450 | -------------------------------------------------------------------------------- /src/moon.py: -------------------------------------------------------------------------------- 1 | ##!/usr/bin/env python3 2 | 3 | import math 4 | 5 | import pygame 6 | 7 | from planet import Planet 8 | import constants as c 9 | from death_particle import DeathParticle 10 | 11 | class Moon(Planet): 12 | def __init__(self, game, position): 13 | super().__init__(game, position, 0, radius=30) 14 | self.mass *= 2 15 | self.surface = pygame.image.load(c.IMAGE_PATH + "/moon.png") 16 | self.glow = pygame.image.load(c.IMAGE_PATH + "/moonglow.png") 17 | self.glow.set_alpha(50) 18 | self.flags = [] 19 | self.flag_angles = [] 20 | 21 | def draw(self, surface, offset=(0, 0)): 22 | self.draw_flags(surface, offset) 23 | self.draw_glow(surface, offset) 24 | super().draw(surface, offset) 25 | 26 | def draw_flags(self, surface, offset=(0, 0)): 27 | for flag, angle in zip(self.flags, self.flag_angles): 28 | angle = angle + self.pose.angle 29 | flag_surf = pygame.transform.rotate(flag, angle) 30 | dist = self.radius + flag.get_width()//2 31 | xoff = math.cos(angle*math.pi/180) * dist 32 | yoff = -math.sin(angle*math.pi/180) * dist 33 | x = xoff + offset[0] + self.pose.x - flag_surf.get_width()//2 34 | y = yoff + offset[1] + self.pose.y - flag_surf.get_height()//2 35 | surface.blit(flag_surf, (x, y)) 36 | 37 | def draw_glow(self, surface, offset=(0, 0)): 38 | scale = math.sin(self.age * math.pi) * 0.08 + 0.92 39 | glow = pygame.transform.scale(self.glow, 40 | (int(self.glow.get_width() * scale), 41 | int(self.glow.get_height() * scale))) 42 | x = offset[0] + self.pose.x - glow.get_width()//2 43 | y = offset[1] + self.pose.y - glow.get_height()//2 44 | surface.blit(glow, (x, y), special_flags=pygame.BLEND_ADD) 45 | 46 | def collide_with_ship(self, ship): 47 | # TODO give points, proceed to next level, etc. 48 | ship.has_hit_moon = True 49 | self.game.current_scene.achievement_row.score_ship(ship) 50 | if ship in self.game.current_scene.ships: 51 | self.game.current_scene.ships.remove(ship) 52 | for i in range(5): 53 | self.game.current_scene.particles.add(DeathParticle(self.game, ship)) 54 | self.game.current_scene.shake(10) 55 | if not self.game.player_flags[ship.player.name] in self.flags: 56 | self.sprout_flag(ship) 57 | self.game.land_on_moon_sound.play() 58 | 59 | def sprout_flag(self, ship): 60 | self.flags.append(ship.flag_surf) 61 | 62 | dx = ship.pose.x - self.pose.x 63 | dy = ship.pose.y - self.pose.y 64 | angle = math.atan2(-dy, dx) 65 | self.flag_angles.append(angle*180/math.pi - self.pose.angle) 66 | 67 | def is_moon(self): 68 | return True 69 | -------------------------------------------------------------------------------- /src/nugget.py: -------------------------------------------------------------------------------- 1 | ##!/usr/bin/env python 2 | 3 | import math 4 | import random 5 | 6 | import pygame 7 | 8 | import constants as c 9 | from primitives import PhysicsObject 10 | from nugget_explosion import NuggetExplosion 11 | 12 | class Nugget(PhysicsObject): 13 | def __init__(self, game, position, angle): 14 | super().__init__(game, position, angle) 15 | self.radius = 22 16 | self.age = random.random() * 2 * math.pi 17 | self.glow = pygame.image.load(c.IMAGE_PATH + "/waypoint_glow.png") 18 | self.period = math.pi + 1 19 | self.surf = pygame.image.load(c.IMAGE_PATH + "/waypoint.png") 20 | self.target_alpha = 128 21 | self.alpha = 0 22 | 23 | def update(self, dt, events): 24 | super().update(dt, events) 25 | self.age += dt 26 | if self.alpha < self.target_alpha: 27 | self.alpha += 128 * dt 28 | if self.alpha > self.target_alpha: 29 | self.alpha = self.target_alpha 30 | 31 | def draw(self, surface, offset=(0, 0)): 32 | x = self.pose.x + offset[0] + math.sin(self.age * (self.period + 1)) * 3 33 | y = self.pose.y + offset[1] + math.cos(self.age * (self.period)) * 3 34 | 35 | glow_width = int((85 + math.sin(self.age * (self.period - 1.25)) * 10) * (self.alpha+1)/(self.target_alpha+1)) 36 | glow = pygame.transform.scale(self.glow, (glow_width, glow_width)) 37 | self.surf.set_colorkey(c.MAGENTA) 38 | self.surf.set_alpha((self.alpha/self.target_alpha)**2 * self.target_alpha) 39 | surface.blit(self.surf, 40 | (x - self.surf.get_width()//2, 41 | y - self.surf.get_height()//2)) 42 | surface.blit(glow, 43 | (x - glow_width//2, y - glow_width//2), 44 | special_flags=pygame.BLEND_ADD) 45 | 46 | visual_radius = 15 47 | 48 | def test_collision(self, ship): 49 | dist = (self.pose - ship.pose).magnitude() 50 | if dist < ship.radius + self.radius + 7 and self not in ship.nuggets: 51 | self.get_picked_up(ship) 52 | 53 | def get_picked_up(self, ship): 54 | ship.nuggets.add(self) 55 | self.game.current_scene.particles.add(NuggetExplosion(self.game, self)) 56 | self.alpha = 25 57 | self.game.waypoint_collect_sound.play() 58 | 59 | def overlaps(self, pose, r, clearance): 60 | return pose.distance_to(self.pose) < self.radius + r + max(clearance, self.clearance) 61 | -------------------------------------------------------------------------------- /src/nugget_explosion.py: -------------------------------------------------------------------------------- 1 | ##!/usr/bin/env python3 2 | 3 | import pygame 4 | 5 | from particle import Particle 6 | import constants as c 7 | 8 | class NuggetExplosion(Particle): 9 | def __init__(self, game, nugget): 10 | super().__init__(game) 11 | self.nugget = nugget 12 | self.pose = nugget.pose.copy() 13 | self.start_radius = 12 14 | self.duration = 0.3 15 | 16 | def get_scale(self): 17 | return 1 + self.through(loading=2.5) * 6 18 | 19 | def get_alpha(self): 20 | return 255 * (1 - self.through(loading=3)) 21 | 22 | def draw(self, surface, offset=(0, 0)): 23 | radius = int(self.start_radius * self.get_scale()) 24 | surf = pygame.Surface((radius*2, radius*2)) 25 | surf.fill(c.BLACK) 26 | surf.set_colorkey(c.BLACK) 27 | r = 255 - self.through(loading=2.5) * (255 - c.YELLOW[0]) 28 | g = 255 - self.through(loading=2.5) * (255 - c.YELLOW[1]) 29 | b = 255 - self.through(loading=2.5) * (255 - c.YELLOW[2]) 30 | pygame.draw.circle(surf, (r, g, b), (radius, radius), radius) 31 | x = self.pose.x - offset[0] - surf.get_width()//2 32 | y = self.pose.y - offset[1] - surf.get_height()//2 33 | surf.set_alpha(self.get_alpha()) 34 | surface.blit(surf, (x, y)) 35 | -------------------------------------------------------------------------------- /src/particle.py: -------------------------------------------------------------------------------- 1 | ##!/usr/bin/env python3 2 | 3 | from primitives import GameObject 4 | 5 | class Particle(GameObject): 6 | def __init__(self, *args, **kwargs): 7 | super().__init__(*args, **kwargs) 8 | self.age = 0 9 | self.duration = None 10 | self.dead = False 11 | 12 | def get_alpha(self): 13 | return 255 14 | 15 | def get_scale(self): 16 | return 1 17 | 18 | def through(self, loading=1): 19 | """ Increase loading argument to 'frontload' the animation. """ 20 | if self.duration is None: 21 | return 0 22 | else: 23 | return 1 - (1 - self.age / self.duration)**loading 24 | 25 | def update(self, dt, events): 26 | self.age += dt 27 | if self.duration and self.age > self.duration: 28 | self.destroy() 29 | 30 | def destroy(self): 31 | self.dead = True 32 | -------------------------------------------------------------------------------- /src/planet.py: -------------------------------------------------------------------------------- 1 | ##!/usr/bin/env python3 2 | 3 | import random 4 | import math 5 | 6 | import pygame 7 | 8 | import constants as c 9 | from primitives import PhysicsObject, Pose 10 | from planet_explosion import PlanetExplosion 11 | 12 | class Planet(PhysicsObject): 13 | def __init__(self, game, position, angle=None, radius=100, gravity_radius=None, mass=None, home=False, surf_det_size=None): 14 | if angle is None: 15 | angle = random.random()*360 16 | super().__init__(game, position, angle) 17 | self.graphic_pose = self.pose.copy() 18 | self.home = home 19 | self.velocity.angle = 15 20 | if self.home: 21 | self.velocity.angle = 0 22 | self.radius = radius 23 | if not surf_det_size: 24 | surf_det_size = radius 25 | self.gravity_radius = gravity_radius if gravity_radius is not None else 2.7*radius 26 | self.mass = mass if mass is not None else radius ** 2 27 | 28 | self.ship_surf = pygame.image.load(c.IMAGE_PATH + "/ship.png") 29 | self.ship_surf.set_alpha(80) 30 | self.ship_surf.set_colorkey(c.MAGENTA) 31 | 32 | if self.home: 33 | self.surface = pygame.image.load(c.IMAGE_PATH + "/earth.png") 34 | elif surf_det_size > 50: 35 | rel = random.choice(["large_planet.png", "large_planet_2.png"]) 36 | self.surface = pygame.image.load(c.IMAGE_PATH + f"/{rel}") 37 | else: 38 | rel = random.choice(["small_planet.png", "small_planet_2.png"]) 39 | self.surface = pygame.image.load(f"{c.IMAGE_PATH}/{rel}") 40 | self.surface = pygame.transform.scale(self.surface, (radius*2, radius*2)) 41 | self.surface.set_colorkey(c.BLACK) 42 | self.mask_surf = self.surface.copy() 43 | self.mask_surf.fill(c.MAGENTA) 44 | pygame.draw.circle(self.mask_surf, c.WHITE, (radius, radius), radius) 45 | self.mask_surf.set_colorkey(c.WHITE) 46 | self.shadow = pygame.image.load(c.IMAGE_PATH + "/planet_shadow.png") 47 | self.shadow = pygame.transform.scale(self.shadow, 48 | (self.surface.get_width(), 49 | self.surface.get_height())) 50 | self.shadow.set_colorkey(c.WHITE) 51 | self.shadow.set_alpha(70) 52 | self.back_shadow = pygame.Surface((self.surface.get_width(), self.surface.get_height())) 53 | self.back_shadow.fill(c.WHITE) 54 | pygame.draw.circle(self.back_shadow, 55 | c.BLACK, 56 | (self.surface.get_width()//2, 57 | self.surface.get_height()//2), 58 | self.surface.get_width()//2) 59 | self.back_shadow.set_alpha(80) 60 | self.back_shadow.set_colorkey(c.WHITE) 61 | self.age = 0 62 | self.destroyed = False 63 | 64 | def is_moon(self): 65 | """ Planets aren't moons, silly. """ 66 | return False 67 | 68 | def get_acceleration(self, ship): 69 | """ Return a Pose indicating the acceleration to apply to 70 | the Ship. 71 | """ 72 | if ship.is_frozen(): 73 | return Pose((0, 0), 0) 74 | distance = self.pose.distance_to(ship.pose) 75 | if distance > self.gravity_radius: 76 | return Pose((0, 0), 0) 77 | if distance < self.radius + ship.radius: 78 | self.collide_with_ship(ship) 79 | if self.home: 80 | return Pose((0, 0), 0) 81 | gravity_magnitude = self.mass * c.GRAVITY_CONSTANT / distance**2 82 | gravity_vector = (self.pose - ship.pose) 83 | gravity_vector.set_angle(0) 84 | gravity_vector.scale_to(gravity_magnitude) 85 | if c.INVERTED_GRAVITY_MOD in self.game.modifications: 86 | return gravity_vector * -1 87 | return gravity_vector 88 | 89 | def collide_with_ship(self, ship): 90 | ship.destroy() 91 | self.splatter(ship) 92 | 93 | def overlaps(self, pose, r, clearance): 94 | return pose.distance_to(self.pose) < self.radius + r + max(clearance, self.clearance) 95 | 96 | def draw(self, surf, offset=(0, 0)): 97 | self.draw_back_shadow(surf, offset) 98 | my_surface = pygame.transform.rotate(self.surface, self.pose.angle) 99 | ship = pygame.transform.rotate(self.ship_surf, self.pose.angle) 100 | x, y = self.graphic_pose.get_position() 101 | x += offset[0] 102 | y += offset[1] 103 | surf.blit(my_surface, (x - my_surface.get_width()//2, y - my_surface.get_height()//2)) 104 | surf.blit(self.shadow, (x - self.shadow.get_width()//2, y - self.shadow.get_height()//2)) 105 | pygame.draw.circle(surf, c.BLACK, (x, y), self.radius+2, width=2) 106 | if not self.home: 107 | pass 108 | 109 | if self.home: 110 | r = c.HOME_PLANET_RADIUS + c.SHIP_SPAWN_ALTITUDE 111 | x = self.pose.x + r * math.cos(self.pose.get_angle_radians()) + offset[0] 112 | y = self.pose.y + r * -math.sin(self.pose.get_angle_radians()) + offset[1] 113 | surf.blit(ship, (x - ship.get_width()//2, y - ship.get_height()//2)) 114 | 115 | #self.draw_gravity_region(surf, offset) 116 | # pygame.draw.circle(surf, (200, 200, 200), (x, y), self.radius) 117 | 118 | def draw_back_shadow(self, surf, offset=(0, 0)): 119 | x, y = self.pose.get_position() 120 | x += offset[0]/2 + 12 * (self.radius/100 + 0.5) - self.back_shadow.get_width()//2 121 | y += offset[1]/2 + 12 * (self.radius/100 + 0.5) - self.back_shadow.get_height()//2 122 | surf.blit(self.back_shadow, (x, y)) 123 | 124 | def update(self, dt, events): 125 | super().update(dt, events) 126 | pdiff = self.pose - self.graphic_pose 127 | self.graphic_pose += pdiff * dt * 6 128 | self.age += dt 129 | 130 | def destroy(self): 131 | self.destroyed = True 132 | if hasattr(self.game.current_scene, "particles"): 133 | self.game.current_scene.particles.add(PlanetExplosion(self.game, self)) 134 | self.game.planet_explode_sound.play() 135 | 136 | def align_graphic_pose(self): 137 | self.graphic_pose = self.pose.copy() 138 | 139 | def splatter(self, ship): 140 | d = ship.pose - self.pose 141 | self.graphic_pose -= d*(25/d.magnitude()) 142 | d.rotate_position(-self.pose.angle) 143 | vpos = 10 144 | vrad = 12 145 | for i in range(15): 146 | x = d.x + 2 * random.random()**2 * vpos - vpos + self.surface.get_width()//2 147 | y = d.y + 2 * random.random()**2 * vpos - vpos + self.surface.get_height()//2 148 | radius = 18 + 2 * random.random() * vrad - vrad 149 | surf = pygame.Surface((radius*2, radius*2)) 150 | surf.fill(c.WHITE) 151 | pygame.draw.circle(surf, c.VERT_LIGHT_GRAY, (radius, radius), radius) 152 | self.surface.blit(surf, 153 | (x - surf.get_width()//2, 154 | y - surf.get_height()//2), 155 | special_flags=pygame.BLEND_MULT) 156 | self.surface.blit(self.mask_surf, (0, 0)) 157 | self.surface.set_colorkey(c.MAGENTA) 158 | 159 | def draw_gravity_region(self, surf, offset=(0, 0)): 160 | if self.home: 161 | return 162 | radius = self.gravity_radius 163 | pixels_per_degree = math.pi * 2 * radius / 360 164 | x, y = self.pose.get_position() 165 | x += offset[0] 166 | y += offset[0] 167 | w = self.gravity_radius * 2 168 | h = self.gravity_radius * 2 169 | pixels_each = 8 170 | dots = int((360 * pixels_per_degree / pixels_each)/10 * 10) 171 | angle_offset = self.age * 15 / radius 172 | color = c.GRAY if not c.INVERTED_GRAVITY_MOD in self.game.modifications else (200, 50, 50) 173 | for i in range(dots): 174 | angle_rad = 2 * math.pi * i/dots + (angle_offset) 175 | my_radius = radius + math.sin(i) * 3 176 | pygame.draw.circle(surf, 177 | color, 178 | (x + my_radius * math.sin(angle_rad), y + my_radius * -math.cos(angle_rad)), 179 | 1) 180 | 181 | # x, y = self.pose.get_position() 182 | # x += offset[0] 183 | # y += offset[1] 184 | #pygame.draw.circle(surf, (100, 100, 100), (x, y), self.gravity_radius, 2) 185 | -------------------------------------------------------------------------------- /src/planet_explosion.py: -------------------------------------------------------------------------------- 1 | ##!/usr/bin/env python3 2 | 3 | import random 4 | 5 | import pygame 6 | 7 | from particle import Particle 8 | import constants as c 9 | from primitives import Pose 10 | 11 | class PlanetParticle(Particle): 12 | def __init__(self, game, planet): 13 | super().__init__(game) 14 | self.planet = planet 15 | self.pose = planet.pose.copy() 16 | self.velocity = Pose(((random.random() * 2 - 1)**2 * 180 * random.choice((-1, 1)), 17 | (random.random() * 2 - 1)**2 * 180 * random.choice((-1, 1))), 18 | random.random() * 360) 19 | self.start_radius = (60 + random.random()*30) * planet.radius/50 20 | self.duration = 0.8 21 | 22 | def get_scale(self): 23 | return 1 - self.through(loading=2.5) 24 | 25 | def get_alpha(self): 26 | return 255 * (1 - self.through(loading=1)) 27 | 28 | def update(self, dt, events): 29 | super().update(dt, events) 30 | self.pose += self.velocity * dt 31 | 32 | def draw(self, surface, offset=(0, 0)): 33 | radius = int(self.start_radius * self.get_scale()) 34 | surf = pygame.Surface((radius*2, radius*2)) 35 | surf.fill(c.BLACK) 36 | surf.set_colorkey(c.BLACK) 37 | pygame.draw.circle(surf, c.WHITE, (radius, radius), radius) 38 | x = self.pose.x - offset[0] - surf.get_width()//2 39 | y = self.pose.y - offset[1] - surf.get_height()//2 40 | surf.set_alpha(self.get_alpha()) 41 | surface.blit(surf, (x, y)) 42 | 43 | class PlanetExplosion(Particle): 44 | def __init__(self, game, planet): 45 | super().__init__(game) 46 | self.planet = planet 47 | self.pose = planet.pose.copy() 48 | self.start_radius = planet.radius 49 | self.duration = 1 50 | self.subparticles = {PlanetParticle(self.game, self.planet) for i in range(12)} 51 | 52 | def get_scale(self): 53 | return 1 + self.through(loading=3) * 2.5 54 | 55 | def get_alpha(self): 56 | return 150 * (1 - self.through(loading=4)) 57 | 58 | def update(self, dt, events): 59 | super().update(dt, events) 60 | for particle in self.subparticles: 61 | particle.update(dt, events) 62 | self.subparticles = {item for item in self.subparticles if not item.age > item.duration} 63 | 64 | def draw(self, surface, offset=(0, 0)): 65 | radius = int(self.start_radius * self.get_scale()) 66 | surf = pygame.Surface((radius*2, radius*2)) 67 | surf.fill(c.YELLOW) 68 | surf.set_colorkey(c.YELLOW) 69 | r = 255# - self.through(loading=2) * 255 70 | g = 255# - self.through(loading=2) * 255 71 | b = 255 - self.through(loading=0.2) * 128 72 | if self.age < 0.05: 73 | r, g, b = c.BLACK 74 | pygame.draw.circle(surf, (r, g, b), (radius, radius), radius) 75 | x = self.pose.x - offset[0] - surf.get_width()//2 76 | y = self.pose.y - offset[1] - surf.get_height()//2 77 | surf.set_alpha(self.get_alpha()) 78 | if self.age < 0.1: 79 | surf.set_alpha(255) 80 | surface.blit(surf, (x, y)) 81 | if self.age > 0.05: 82 | for particle in self.subparticles: 83 | particle.draw(surface, offset=offset) 84 | -------------------------------------------------------------------------------- /src/player.py: -------------------------------------------------------------------------------- 1 | ##!/usr/bin/env python3 2 | 3 | import random 4 | 5 | class Player: 6 | def __init__(self, game, name, color=None): 7 | self.name = name 8 | self.color = color if color is not None else self.random_color() 9 | 10 | @staticmethod 11 | def random_color(): 12 | a = 120 13 | b = 128 + random.random() * 128 14 | c = 192 + random.random() * 64 15 | if random.random() < 0.3: 16 | b = 120 + random.random() * 32 17 | 18 | rgb = [a, b, c] 19 | random.shuffle(rgb) 20 | return tuple(rgb) 21 | 22 | def recolor(self, color=None): 23 | self.color = color if color is not None else self.random_color() -------------------------------------------------------------------------------- /src/primitives.py: -------------------------------------------------------------------------------- 1 | ##!/usr/bin/env python3 2 | 3 | import math 4 | 5 | class GameObject: 6 | def __init__(self, game): 7 | self.game = game 8 | 9 | def update(self, dt, events): 10 | raise NotImplementedError() 11 | 12 | def draw(self, surf, offset=(0, 0)): 13 | raise NotImplementedError() 14 | 15 | 16 | class Pose: 17 | def __init__(self, position, angle): 18 | """ Initialize the Pose. 19 | 20 | position: two-length tuple (x, y) 21 | angle: angle, in degrees counterclockwise from right -> 22 | """ 23 | self.set_position(position) 24 | self.angle = angle 25 | 26 | def set_x(self, new_x): 27 | self.x = new_x 28 | 29 | def set_y(self, new_y): 30 | self.y = new_y 31 | 32 | def set_position(self, position): 33 | self.x, self.y = position 34 | 35 | def set_angle(self, angle): 36 | self.angle = angle 37 | 38 | def get_position(self): 39 | return self.x, self.y 40 | 41 | def get_angle_radians(self): 42 | return self.angle*math.pi/180 43 | 44 | def get_unit_vector(self): 45 | """ Return the unit vector equivalent of the Pose's angle """ 46 | # Note: y component is inverted because of indexing on displays; 47 | # negative y points up, while positive y points down. 48 | unit_x = math.cos(self.get_angle_radians()) 49 | unit_y = -math.sin(self.get_angle_radians()) 50 | return unit_x, unit_y 51 | 52 | def get_weighted_position(self, weight): 53 | return self.x*weight, self.y*weight 54 | 55 | def add_position(self, position): 56 | add_x, add_y = position 57 | self.set_x(self.x + add_x) 58 | self.set_y(self.y + add_y) 59 | 60 | def add_angle(self, angle): 61 | self.set_angle(self.angle + angle) 62 | 63 | def rotate_position(self, angle): 64 | x = self.x*math.cos(angle*math.pi/180) \ 65 | + self.y*math.sin(angle*math.pi/180) 66 | y = -self.x*math.sin(angle*math.pi/180) \ 67 | + self.y*math.cos(angle*math.pi/180) 68 | self.set_position((x, y)) 69 | 70 | def add_pose(self, other, weight=1, frame=None): 71 | if frame: 72 | other = other.copy() 73 | other.rotate_position(frame.angle) 74 | self.add_position(other.get_weighted_position(weight)) 75 | self.add_angle(other.angle*weight) 76 | 77 | def distance_to(self, other): 78 | return (self - other).magnitude() 79 | 80 | def magnitude(self): 81 | distance = math.sqrt(self.x**2 + self.y**2) 82 | return distance 83 | 84 | def clear(self): 85 | self.x = 0 86 | self.y = 0 87 | self.angle = 0 88 | 89 | def copy(self): 90 | return Pose(self.get_position(), self.angle) 91 | 92 | def scale_to(self, magnitude): 93 | """ Scale the X and Y components of the Pose to have a particular 94 | magnitude. Angle is unchanged. 95 | """ 96 | my_magnitude = self.magnitude() 97 | if my_magnitude == 0: 98 | self.x = magnitude 99 | self.y = 0 100 | return 101 | self.x *= magnitude / my_magnitude 102 | self.y *= magnitude / my_magnitude 103 | 104 | def __add__(self, other): 105 | copy = self.copy() 106 | copy.add_pose(other) 107 | return copy 108 | 109 | def __sub__(self, other): 110 | copy = self.copy() 111 | copy.add_pose(other, weight=-1) 112 | return copy 113 | 114 | def __mul__(self, other): 115 | copy = self.copy() 116 | copy.x *= other 117 | copy.y *= other 118 | copy.angle *= other 119 | return copy 120 | 121 | def __str__(self): 122 | return f"" 123 | 124 | def __repr__(self): 125 | return self.__str__() 126 | 127 | 128 | class PhysicsObject(GameObject): 129 | def __init__(self, game, position, angle): 130 | super().__init__(game) 131 | self.pose = Pose(position, angle) 132 | self.velocity = Pose(position=(0, 0), angle=0) 133 | self.acceleration = Pose(position=(0, 0), angle=0) 134 | 135 | def update(self, dt, events): 136 | self.velocity.add_pose(self.acceleration, weight=dt) 137 | self.pose.add_pose(self.velocity, weight=dt) 138 | -------------------------------------------------------------------------------- /src/scene.py: -------------------------------------------------------------------------------- 1 | ##!/usr/bin/env python3 2 | 3 | import pygame 4 | 5 | import constants as c 6 | from primitives import GameObject 7 | 8 | class Scene(GameObject): 9 | def __init__(self, *args, **kwargs): 10 | super().__init__(*args, **kwargs) 11 | self.is_running = True 12 | 13 | def main(self): 14 | lag = 0 15 | fps_queue = [] 16 | max_ticks_per_render = 5 17 | while self.is_running: 18 | dt, events = self.game.update_globals() 19 | lag += dt 20 | ticks_this_render = 0 21 | while lag > c.TICK_LENGTH: 22 | lag -= c.TICK_LENGTH 23 | self.update(c.TICK_LENGTH, events) 24 | ticks_this_render += 1 25 | if ticks_this_render >= max_ticks_per_render: 26 | lag = 0 27 | break 28 | self.draw(self.game.screen) 29 | fps_queue.append(1/dt) 30 | if len(fps_queue) > 20: 31 | self.game.fps = fps_queue + self.game.fps[:40] 32 | fps_queue = [] 33 | self.game.update_screen() 34 | 35 | def next_scene(self): 36 | raise NotImplementedError() 37 | -------------------------------------------------------------------------------- /src/score_manager.py: -------------------------------------------------------------------------------- 1 | ##!/usr/bin/env python3 2 | 3 | import time 4 | import pickle 5 | import os 6 | import random 7 | 8 | import constants as c 9 | 10 | class ScoreManager: 11 | 12 | class ScoreEntry: 13 | def __init__(self, name, score, tags=None, score_time=None): 14 | self.name = name 15 | self.score = score 16 | self.tags = tags if tags is not None else [] 17 | self.score_time = score_time if score_time is not None else time.time() 18 | 19 | def __str__(self): 20 | return f"{self.name} - {round(self.score, 2)} - " + \ 21 | f"{time_to_string(self.score_time)} - " + \ 22 | f"{self.tags if self.tags else 'no tags'}" 23 | 24 | def __repr__(self): 25 | return self.__str__() 26 | 27 | def copy(self): 28 | return ScoreManager.ScoreEntry(self.name, 29 | self.score, 30 | tags=self.tags, 31 | score_time = self.score_time) 32 | 33 | def __add__(self, other): 34 | copy = self.copy() 35 | copy.score += other.score 36 | copy.tags += other.tags 37 | return copy 38 | 39 | def __init__(self, filename=None): 40 | self.scores = [] 41 | self.path = os.path.join(c.SCORE_SAVE_PATH, filename) 42 | self.has_unsaved_changes = False 43 | 44 | def __str__(self): 45 | return "\n".join([str(score) for score in self.scores]) 46 | 47 | def add_score(self, name, score, tags=None, score_time=None): 48 | new_score = self.ScoreEntry(name, score, tags=tags, score_time=score_time) 49 | self.scores.append(new_score) 50 | self.scores.sort(reverse=True, key=lambda x:x.score_time) 51 | self.has_unsaved_changes = True 52 | 53 | def get_last_hours(self, num_hours): 54 | """ Return a list of only the scores created in the last specified 55 | number of hours. 56 | """ 57 | now = time.time() 58 | cutoff = now - num_hours*3600 59 | return [item for item in self.scores if item.score_time > cutoff] 60 | 61 | def get_total_by_player(self, num_hours=None): 62 | source = self.scores if num_hours is None else self.get_last_hours(num_hours) 63 | score_dict = {} 64 | for score_entry in source: 65 | name = score_entry.name 66 | if name not in score_dict: 67 | score_dict[name] = score_entry.copy() 68 | else: 69 | score_dict[name] += score_entry 70 | return score_dict 71 | 72 | def save_if_changes(self): 73 | if self.has_unsaved_changes: 74 | self.save_to_file() 75 | 76 | def save_to_file(self): 77 | with open(self.path, "wb") as pickle_file: 78 | pickle.dump(self, pickle_file) 79 | 80 | @staticmethod 81 | def from_file(filename): 82 | path = os.path.join(c.SCORE_SAVE_PATH, filename) 83 | if not os.path.isfile(path): 84 | return ScoreManager(filename=filename) 85 | else: 86 | with open(path, "rb") as pfile: 87 | return pickle.load(pfile) 88 | 89 | def ago_format(number, unit): 90 | number = int(number) 91 | s = "s" if number != 1 else "" 92 | return f"{number} {unit}{s} ago" 93 | 94 | def time_to_string(t): 95 | now = time.time() 96 | ago = now - t 97 | if ago < 60: 98 | return ago_format(ago, "second") 99 | elif ago < 60*60: 100 | return ago_format(ago/60, "minute") 101 | elif ago < 60*60*24*2: 102 | return ago_format(ago/3600, "hour") 103 | else: 104 | return ago_format(ago/3600*24, "day") 105 | 106 | if __name__ == '__main__': 107 | scoreboard = ScoreManager.from_file("test_scores.pkl") 108 | # print(f"\n{'-'*10} Last hour {'-'*10}") 109 | # for score in scoreboard.get_last_hours(1): 110 | # print(score) 111 | print(f"\n{'-'*10} Totals {'-'*10}") 112 | totals = scoreboard.get_total_by_player(1) 113 | totals_list = [(key, totals[key]) for key in totals] 114 | totals_list.sort(key=lambda x:x[1].score, reverse=True) 115 | for item in totals_list: 116 | print(f"{item[1]}") 117 | scoreboard.save_to_file() 118 | -------------------------------------------------------------------------------- /src/ship.py: -------------------------------------------------------------------------------- 1 | ##!/usr/bin/env python3 2 | 3 | import random 4 | 5 | import pygame 6 | 7 | import constants as c 8 | from primitives import PhysicsObject, Pose 9 | from player import Player 10 | from exhaust_particle import ExhaustParticle 11 | from explosion import Explosion 12 | from death_particle import DeathParticle 13 | from wormhole_explosion import WormholeExplosion 14 | 15 | class Ship(PhysicsObject): 16 | def __init__(self, game, program_string, player, position=(0, 0), angle=90): 17 | super().__init__(game, position, angle) 18 | self.program_string = program_string 19 | self.program, info = self.parse_program(program_string) 20 | self.player = player 21 | self.age = 0 22 | self.thrust = Pose((0,0), 0) 23 | self.commandIndex = 0 24 | self.delay = 0 25 | self.destroyed = False 26 | self.surface = self.get_surface() 27 | self.since_exhaust = 0 28 | self.radius = 10 29 | self.label = self.game.player_label_font.render(self.player.name, 30 | 0, 31 | self.player.color) 32 | self.label_back = pygame.Surface((self.label.get_width() + 10, 33 | self.label.get_height() + 10)) 34 | self.label_back.fill(c.BLACK) 35 | self.label_back.set_alpha(100) 36 | self.label_offset = Pose((0, -35), 0) 37 | self.label_pose = self.pose - self.label_offset 38 | 39 | self.way_surf = pygame.image.load(c.IMAGE_PATH + "/small_waypoint.png").convert() 40 | h = self.label_back.get_height() 41 | self.way_surf = pygame.transform.scale(self.way_surf, (h-2, h-2)) 42 | tint = self.way_surf.copy() 43 | tint.fill(self.player.color) 44 | self.way_surf.blit(tint, (0, 0), special_flags=pygame.BLEND_MULT) 45 | self.way_surf.set_colorkey(self.way_surf.get_at((0, 0))) 46 | self.way_back_surf = pygame.Surface((self.way_surf.get_width() + 5,self.label_back.get_height())) 47 | self.way_back_surf.fill(c.BLACK) 48 | self.way_back_surf.set_alpha(100) 49 | 50 | self.scale = 0 51 | self.target_scale = 1 52 | 53 | self.nuggets = set() 54 | self.has_hit_moon = False 55 | 56 | if self.player.name in self.game.player_flags: 57 | self.flag_surf = self.game.player_flags[self.player.name] 58 | else: 59 | self.flag_surf = pygame.image.load(c.IMAGE_PATH + "/flag.png").convert() 60 | tint = pygame.Surface((self.flag_surf.get_width(), self.flag_surf.get_height())) 61 | tint.fill(self.player.color) 62 | self.flag_surf.blit(tint, (0, 0), special_flags=pygame.BLEND_MULT) 63 | self.flag_surf.set_colorkey(self.flag_surf.get_at((0, 0))) 64 | self.game.player_flags[self.player.name] = self.flag_surf 65 | 66 | self.frozen_for = 0 67 | self.last = False 68 | 69 | def freeze(self, amt): 70 | self.frozen_for = amt 71 | self.game.current_scene.particles.add(WormholeExplosion(self.game, self)) 72 | 73 | def is_frozen(self): 74 | frozen = self.frozen_for > 0 75 | if not frozen and self.last: 76 | self.game.current_scene.particles.add(WormholeExplosion(self.game, self)) 77 | self.last = frozen 78 | return frozen 79 | 80 | def get_surface(self): 81 | surface = pygame.image.load(c.IMAGE_PATH + "/ship.png").convert() 82 | color_surf = pygame.Surface((surface.get_width(), surface.get_height())) 83 | color_surf.fill(self.player.color) 84 | surface.blit(color_surf, (0, 0), special_flags=pygame.BLEND_MULT) 85 | surface.set_colorkey(surface.get_at((surface.get_width()-1, surface.get_height()-1))) 86 | return surface 87 | 88 | def destroy(self): 89 | self.destroyed = True 90 | self.game.current_scene.particles.add(Explosion(self.game, self)) 91 | for i in range(8): 92 | self.game.current_scene.particles.add(DeathParticle(self.game, self)) 93 | self.game.current_scene.shake(20) 94 | self.game.ship_destroy_sound.play() 95 | 96 | def update(self, dt, events): 97 | self.age += dt 98 | self.frozen_for -= dt 99 | if self.is_frozen(): 100 | return 101 | super().update(dt, events) 102 | self.since_exhaust += dt 103 | exhaust_period = 0.05 104 | if self.since_exhaust > exhaust_period: 105 | self.since_exhaust -= exhaust_period 106 | self.game.current_scene.particles.add(ExhaustParticle(self.game, self)) 107 | if self.delay > 0: 108 | self.delay = max(0, self.delay-dt) 109 | self.runCommands(dt) 110 | self.acceleration.clear() 111 | if c.SOLAR_WIND in self.game.modifications: 112 | if not hasattr(self.game, "solar_wind_direction"): 113 | self.game.solar_wind_direction = random.choice((c.UP, c.DOWN, c.LEFT, c.RIGHT)) 114 | wind_accel = Pose((self.game.solar_wind_direction), 0) 115 | self.acceleration += wind_accel * c.WIND_STRENGTH 116 | self.acceleration.add_pose(self.thrust, 1, frame=self.pose) 117 | if c.DOUBLE_THRUST_MOD in self.game.modifications: 118 | self.acceleration.add_pose(self.thrust, 1, frame=self.pose) 119 | for planet in self.game.current_scene.planets: 120 | self.acceleration.add_pose(planet.get_acceleration(self)) 121 | for nugget in self.game.current_scene.nuggets: 122 | nugget.test_collision(self) 123 | 124 | ds = self.target_scale - self.scale 125 | if ds < 0.01: 126 | self.scale = self.target_scale 127 | self.scale += ds * dt * 5 128 | 129 | if self.pose.y < 120: 130 | self.label_offset = Pose((0, 35), 0) 131 | if self.pose.y > 150: 132 | self.label_offset = Pose((0, -35), 0) 133 | dl = self.pose - (self.label_pose - self.label_offset) 134 | self.label_pose += dl * dt * 12 135 | 136 | if self.pose.x < 0 or self.pose.x > c.LEVEL_WIDTH or \ 137 | self.pose.y < 0 or self.pose.y > c.LEVEL_HEIGHT: 138 | self.destroy() 139 | 140 | def runCommands(self, dt): 141 | while self.delay <= 0 and self.commandIndex < len(self.program): 142 | command = self.program[self.commandIndex] 143 | if command[0] == 'd': # delay 144 | self.delay += command[1]/1000 145 | if command[0] == 't': # thrust 146 | self.thrust = Pose((command[1]*c.THRUST, 0), 0) 147 | if command[0] == 'r': # rotate 148 | self.velocity.set_angle(command[1]) 149 | self.commandIndex += 1 150 | 151 | def recolor(self): 152 | self.surface = self.get_surface() 153 | self.label = self.game.player_label_font.render(self.player.name, 0, self.player.color) 154 | self.way_surf = pygame.image.load(c.IMAGE_PATH + "/small_waypoint.png").convert() 155 | h = self.label_back.get_height() 156 | self.way_surf = pygame.transform.scale(self.way_surf, (h-2, h-2)) 157 | tint = self.way_surf.copy() 158 | tint.fill(self.player.color) 159 | self.way_surf.blit(tint, (0, 0), special_flags = pygame.BLEND_MULT) 160 | self.way_surf.set_colorkey(self.way_surf.get_at((0, 0))) 161 | self.way_back_surf = pygame.Surface((self.way_surf.get_width() + 5,self.label_back.get_height())) 162 | self.way_back_surf.fill(c.BLACK) 163 | self.way_back_surf.set_alpha(100) 164 | 165 | def draw(self, surface, offset=(0, 0)): 166 | if self.destroyed: 167 | return 168 | 169 | if self.label_pose.x < self.label_back.get_width()//2 + 10: 170 | self.label_pose.x = self.label_back.get_width()//2 + 10 171 | if self.label_pose.x > c.LEVEL_WIDTH - self.label_back.get_width()//2 - 10: 172 | self.label_pose.x = c.LEVEL_WIDTH - self.label_back.get_width()//2 - 10 173 | 174 | x = self.label_pose.x + offset[0] - self.label_back.get_width()//2 - len(self.nuggets) * self.way_back_surf.get_width()//2 175 | y = self.label_pose.y + offset[1] - self.label_back.get_height()//2 176 | if not self.is_frozen(): 177 | surface.blit(self.label_back, (x, y)) 178 | x += self.label_back.get_width() 179 | if not self.is_frozen(): 180 | for item in self.nuggets: 181 | surface.blit(self.way_back_surf, (x, y)) 182 | surface.blit(self.way_surf, (x, y+1)) 183 | x += self.way_back_surf.get_width() 184 | 185 | x = self.label_pose.x + offset[0] - self.label.get_width()//2 - len(self.nuggets) * self.way_back_surf.get_width()//2 186 | y = self.label_pose.y + offset[1] - self.label.get_height()//2 187 | if not self.is_frozen(): 188 | surface.blit(self.label, (x, y)) 189 | 190 | if self.scale == 0: 191 | return 192 | 193 | ship_surf = pygame.transform.scale(self.surface, 194 | (int(self.surface.get_width() * self.scale), 195 | int(self.surface.get_height() * self.scale))) 196 | ship_surf = pygame.transform.rotate(ship_surf, self.pose.angle) 197 | x = self.pose.x + offset[0] - ship_surf.get_width()//2 198 | y = self.pose.y + offset[1] - ship_surf.get_height()//2 199 | surface.blit(ship_surf, (x, y)) 200 | 201 | @staticmethod 202 | def parse_program(program_string): 203 | program_string = program_string[1:].lower().strip() + 'A' 204 | program = [] 205 | arguments = [] 206 | key = '' 207 | number = '' 208 | isNumber = False 209 | for char in program_string: 210 | if char == '.': 211 | print("Decimals not permitted") 212 | return [], "Decimals not permitted" 213 | elif char.isnumeric() or char == '-': 214 | isNumber = True 215 | number += char 216 | elif char.isalnum(): 217 | # terminate previous number 218 | if (len(number) == 1 or number[1:].isnumeric()) and \ 219 | (number[0].isdigit() or number[0] == '-') and \ 220 | (number != "-"): 221 | arguments.append(int(number)) 222 | number = '' 223 | elif number != '': 224 | print('Invalid number, "' + number + '"') 225 | return [], 'Invalid number, "' + number + '"' 226 | # terminate previous command 227 | if isNumber or char == 'A': 228 | if key in c.COMMANDS.values(): 229 | command = key 230 | elif key in c.COMMANDS: 231 | command = c.COMMANDS[key] 232 | else: 233 | print('Invalid command, "' + key + '"') 234 | return [], 'Invalid command, "' + key + '"' 235 | if len(arguments) != len(c.COMMANDS_MIN[command]): 236 | print("Invalid number of arguments for " + c.COMMANDS_LONG[command]) 237 | return [], "Invalid number of arguments for " + c.COMMANDS_LONG[command] 238 | for i, arg in enumerate(arguments): 239 | if arg < c.COMMANDS_MIN[command][i]: 240 | print(c.COMMANDS_LONG[command] + " was smaller than minimum value") 241 | return [], c.COMMANDS_LONG[command] + " was smaller than minimum value" 242 | if arg > c.COMMANDS_MAX[command][i]: 243 | print(c.COMMANDS_LONG[command] + " was greater than maximum value") 244 | return [], c.COMMANDS_LONG[command] + " was greater than maximum value" 245 | program.append((command, *arguments)) 246 | key = '' 247 | arguments = [] 248 | isNumber = False 249 | key += char 250 | elif char in " ,;": 251 | isNumber = True 252 | if number[1:].isnumeric() and \ 253 | (number[0].isdigit() or number[0] == '-'): 254 | arguments.append(int(number)) 255 | number = '' 256 | else: 257 | print('Invalid character, "' + char + '"') 258 | return [], 'Invalid character, "' + char + '"' 259 | return program, None 260 | 261 | if __name__ == '__main__': 262 | Ship.parse_program("t100t120 t100") 263 | -------------------------------------------------------------------------------- /src/start_scene.py: -------------------------------------------------------------------------------- 1 | ##!/usr/bin/env python3 2 | 3 | import pygame 4 | 5 | from scene import Scene 6 | import constants as c 7 | 8 | class StartScene(Scene): 9 | """ Display a black screen with a white circle for two seconds. """ 10 | 11 | def __init__(self, *args, **kwargs): 12 | super().__init__(*args, **kwargs) 13 | self.age = 0 14 | 15 | def update(self, dt, events): 16 | self.age += dt 17 | if self.age > 2: 18 | self.is_running = False 19 | 20 | def draw(self, surf, offset=(0, 0)): 21 | surf.fill(c.BLACK) 22 | pygame.draw.circle(surf, 23 | c.WHITE, 24 | (c.WINDOW_WIDTH//2, c.WINDOW_HEIGHT//2), 25 | c.WINDOW_HEIGHT//4) 26 | 27 | def next_scene(self): 28 | return None 29 | -------------------------------------------------------------------------------- /src/transition_gui.py: -------------------------------------------------------------------------------- 1 | ##!/usr/bin/env python3 2 | 3 | import math 4 | import random 5 | 6 | import pygame 7 | 8 | from primitives import GameObject, Pose 9 | from planet import Planet 10 | import constants as c 11 | 12 | class AlertBox(GameObject): 13 | 14 | def __init__(self, game, position, header, message, side_surface=None): 15 | super().__init__(game) 16 | self.age = 0 17 | self.pose = Pose(position, 0) 18 | self.header = header 19 | self.message = message 20 | self.side_surface = side_surface 21 | self.generate_colors() 22 | self.load_surfs() 23 | self.max_width = self.top_surf.get_width() - c.ALERT_SIDE_PADDING * 2 24 | if self.side_surface is not None: 25 | self.max_width -= self.side_surface.get_width() + c.ALERT_SIDE_PADDING 26 | self.header_surf = self.get_header_surface() 27 | self.message_surface = self.get_message_surface() 28 | 29 | def generate_colors(self): 30 | self.header_color = (255, 200, 200) 31 | self.body_color = (190, 160, 160) 32 | 33 | def get_header_surface(self): 34 | render = self.game.alert_header_font.render(self.header, 1, self.header_color) 35 | background = pygame.transform.scale(self.middle_surf, 36 | (self.middle_surf.get_width(), 37 | render.get_height() + 8)).convert() 38 | x = background.get_width()//2 - render.get_width()//2 39 | if self.side_surface is not None: 40 | x += (self.side_surface.get_width() + c.ALERT_SIDE_PADDING)//2 41 | background.blit(render, (x, 0)) 42 | return background 43 | 44 | def get_message_surface(self): 45 | message_surfaces = [] 46 | message_lines = self.message.split("\n") 47 | for line in message_lines: 48 | message_words = line.split() 49 | this_line = [] 50 | this_width = 0 51 | for word in message_words: 52 | surface = self.game.alert_body_font.render(word, 1, self.body_color) 53 | if this_width + surface.get_width() > self.max_width: 54 | message_surfaces.append(this_line) 55 | this_line = [] 56 | this_width = 0 57 | this_line.append(surface) 58 | this_width += surface.get_width() + c.ALERT_BODY_SPACE 59 | message_surfaces.append(this_line) 60 | 61 | total_height = c.ALERT_LINE_SPACING*(len(message_surfaces)) 62 | if self.side_surface is not None and total_height < self.side_surface.get_height() - self.header_surf.get_height(): 63 | total_height = self.side_surface.get_height() - self.header_surf.get_height() 64 | background = pygame.transform.scale(self.middle_surf, 65 | (self.middle_surf.get_width(), 66 | total_height)).convert() 67 | y = 0 68 | for line in message_surfaces: 69 | line_width = sum([item.get_width() + c.ALERT_BODY_SPACE for item in line]) - c.ALERT_BODY_SPACE 70 | x = background.get_width()//2 - line_width//2 71 | if self.side_surface is not None: 72 | x += self.side_surface.get_width()//2 + c.ALERT_SIDE_PADDING//2 73 | for word in line: 74 | background.blit(word, (x, y)) 75 | x += word.get_width() + c.ALERT_BODY_SPACE 76 | y += c.ALERT_LINE_SPACING 77 | 78 | return background 79 | 80 | def load_surfs(self): 81 | self.top_surf = pygame.image.load(c.IMAGE_PATH + "/red_alert_box_top.png") 82 | self.middle_surf = pygame.image.load(c.IMAGE_PATH + "/red_alert_box_middle.png") 83 | self.bottom_surf = pygame.image.load(c.IMAGE_PATH + "/red_alert_box_bottom.png") 84 | 85 | def draw(self, surface, offset=(0, 0)): 86 | surfaces = [self.top_surf, self.header_surf, self.message_surface, self.bottom_surf] 87 | x = self.pose.x - self.top_surf.get_width()//2 + offset[0] 88 | y = self.pose.y - sum([item.get_height() for item in surfaces])//2 + offset[1] + 4 * math.sin(self.age * 2) 89 | y0 = y 90 | for piece in surfaces: 91 | surface.blit(piece, (x, y)) 92 | y += piece.get_height() 93 | 94 | if self.side_surface is not None: 95 | surface.blit(self.side_surface, 96 | (x + c.ALERT_SIDE_PADDING, 97 | y0 + self.top_surf.get_height() 98 | + self.header_surf.get_height()//2 99 | + self.message_surface.get_height()//2 100 | - self.side_surface.get_height()//2)) 101 | 102 | def update(self, dt, events): 103 | self.age += dt 104 | 105 | class GreenAlertBox(AlertBox): 106 | def generate_colors(self): 107 | self.header_color = (200, 230, 205) 108 | self.body_color = (150, 180, 160) 109 | 110 | def load_surfs(self): 111 | self.top_surf = pygame.image.load(c.IMAGE_PATH + "/green_alert_box_top.png") 112 | self.middle_surf = pygame.image.load(c.IMAGE_PATH + "/green_alert_box_middle.png") 113 | self.bottom_surf = pygame.image.load(c.IMAGE_PATH + "/green_alert_box_bottom.png") 114 | 115 | class PlayerMultiplierAlertBox(AlertBox): 116 | def __init__(self, game, position, header, message): 117 | self.background_color = (68, 35, 48) 118 | self.game = game 119 | self.generate_colors() 120 | side_surface = self.generate_multiplier_surface() 121 | super().__init__(game, position, header, message, side_surface=side_surface) 122 | self.age += 2 123 | 124 | def generate_multiplier_surface(self): 125 | text = f"x{self.game.player_multiplier()}" 126 | render = self.game.alert_large_font.render(text, 1, self.header_color) 127 | surface = pygame.Surface((render.get_width(), 70)) 128 | surface.fill(self.background_color) 129 | surface.blit(render, 130 | (surface.get_width()//2 - render.get_width()//2, 131 | surface.get_height()//2 - render.get_height()//2)) 132 | return surface 133 | 134 | class VotingObject(GameObject): 135 | def __init__(self, game, parent, position, strings): 136 | super().__init__(game) 137 | self.parent = parent 138 | self.pose = Pose(position, 0) 139 | self.option_keys = c.OPTION_A, c.OPTION_B 140 | self.option_strings = {self.option_keys[i]: strings[i] for i in range(len(self.option_keys))} 141 | self.votes = {option:set() for option in self.option_keys} 142 | self.planet_dict = {option:Planet(self.game, (0, 0), radius=75, surf_det_size=50+i) for i, option in enumerate(self.option_keys)} 143 | self.color_dict = {c.OPTION_A:(255, 225, 200), c.OPTION_B:(200, 210, 255)} 144 | self.label_dict = {option_key:self.get_label_surf(option_key) for option_key in self.option_keys} 145 | self.cover = pygame.Surface((150, 150)) 146 | self.cover.fill(c.WHITE) 147 | pygame.draw.circle(self.cover, c.BLACK, (self.cover.get_width()//2, self.cover.get_height()//2), self.cover.get_width()//2) 148 | self.cover.set_colorkey(c.WHITE) 149 | self.cover.set_alpha(80) 150 | self.not_picked_cover = self.cover.copy() 151 | self.not_picked_cover.set_alpha(128) 152 | self.picked = None 153 | self.since_vote = {option:999 for option in self.option_keys} 154 | self.vfam = pygame.image.load(c.IMAGE_PATH + "/vote_for_a_modifier.png") 155 | 156 | def vote(self, player_name, option): 157 | option = option.upper() 158 | if option not in self.option_keys: 159 | return 0 160 | for vote_option in self.votes: 161 | cur_votes = self.votes[vote_option] 162 | if player_name in cur_votes: 163 | cur_votes.remove(player_name) 164 | self.votes[option].add(player_name) 165 | self.since_vote[option] = 0 166 | for option in self.label_dict: 167 | self.label_dict[option] = self.get_label_surf(option) 168 | return 1 169 | 170 | def get_label_surf(self, option): 171 | text = f"!vote {option}" 172 | color = self.color_dict[option] 173 | render = self.game.alert_header_font.render(text, 1, color) 174 | count_text = str(len(self.votes[option])) 175 | count_render = self.game.alert_body_font.render(count_text, 1, color) 176 | background = pygame.image.load(c.IMAGE_PATH + "/vote_label_background.png").convert() 177 | tint = pygame.Surface((background.get_width(), background.get_height())) 178 | tint.fill(color) 179 | tint.set_alpha(10) 180 | background.blit(tint, (0, 0), special_flags=pygame.BLEND_MULT) 181 | background.set_colorkey(background.get_at((0, 0))) 182 | background.blit(render, 183 | (background.get_width()//2 - render.get_width()//2, 184 | background.get_height()//2 - render.get_height())) 185 | background.blit(count_render, 186 | (background.get_width()//2 - count_render.get_width()//2, 187 | background.get_height()//2)) 188 | return background 189 | 190 | def determine_winner(self): 191 | option_a_score = len(self.votes[c.OPTION_A]) 192 | option_b_score = len(self.votes[c.OPTION_B]) 193 | if option_a_score > option_b_score: 194 | self.picked = c.OPTION_A 195 | elif option_a_score < option_b_score: 196 | self.picked = c.OPTION_B 197 | else: 198 | self.picked = random.choice([c.OPTION_A, c.OPTION_B]) 199 | 200 | modification = self.option_strings[self.picked] 201 | self.game.modifications.append(modification) 202 | if modification is c.SOLAR_WIND: 203 | self.game.solar_wind_direction = random.choice([c.UP, c.DOWN, c.LEFT, c.RIGHT]) 204 | 205 | def draw_option(self, option_key, surface, offset=(0, 0)): 206 | planet = self.planet_dict[option_key] 207 | x = offset[0] 208 | y = offset[1] 209 | planet.pose.x = x 210 | planet.pose.y = y 211 | planet.align_graphic_pose() 212 | planet.draw(surface) 213 | surface.blit(self.cover, (x - self.cover.get_width()//2, y - self.cover.get_height()//2)) 214 | texts = [self.game.voting_planet_font.render(text, 0, c.BLACK) for text in self.option_strings[option_key].split()] 215 | backs = [pygame.Surface((text.get_width(), text.get_height())) for text in texts] 216 | for back in backs: 217 | back.fill(c.MAGENTA) 218 | for i, text in enumerate(texts): 219 | texts[i] = backs[i] 220 | texts[i].blit(text, (0, 0)) 221 | texts[i].set_colorkey(c.MAGENTA) 222 | texts[i].set_alpha(90) 223 | white_texts = [self.game.voting_planet_font.render(text, 1, c.WHITE) for text in self.option_strings[option_key].split()] 224 | total_height = sum([text.get_height() for text in texts]) 225 | 226 | y -= total_height//2 227 | for white, black in zip(white_texts, texts): 228 | for offset in c.TEXT_BLIT_OFFSETS: 229 | surface.blit(black, (x - black.get_width()//2 + offset[0], y + offset[1])) 230 | surface.blit(white, (x - white.get_width()//2, y - 1)) 231 | y += black.get_height() 232 | 233 | if self.picked is not None and self.picked is not option_key: 234 | surface.blit(self.not_picked_cover, (planet.pose.x - self.cover.get_width()//2, planet.pose.y - self.cover.get_height()//2)) 235 | 236 | #if self.picked is None: 237 | y = planet.pose.y 238 | label = self.label_dict[option_key] 239 | label_scale = min(1, self.since_vote[option_key]*1.5 + 0.7) 240 | if self.picked is not None and self.picked != option_key: 241 | label_scale = max(0, 1 + self.time_left()*3) 242 | if label_scale != 0: 243 | label = pygame.transform.scale(label, 244 | (int(label.get_width() * label_scale), 245 | int(label.get_height() * label_scale))) 246 | surface.blit(label, (x - label.get_width()//2, y + 95 - label.get_height()//2)) 247 | 248 | def time_left(self): 249 | return self.parent.countdown.duration - 5 250 | 251 | def draw(self, surface, offset): 252 | x = self.pose.x + offset[0] 253 | y = self.pose.y + offset[1] 254 | dist_apart = 200 255 | self.draw_option(self.option_keys[0], surface, offset=(x-dist_apart//2, y)) 256 | self.draw_option(self.option_keys[1], surface, offset=(x+dist_apart//2, y)) 257 | surface.blit(self.vfam, (x - self.vfam.get_width()//2, y - c.WINDOW_HEIGHT*0.17)) 258 | 259 | def update(self, dt, events): 260 | for key in self.planet_dict: 261 | self.planet_dict[key].update(dt, events) 262 | for option in self.since_vote: 263 | self.since_vote[option] += dt 264 | if self.time_left() <= 0 and self.picked is None: 265 | self.determine_winner() 266 | 267 | class Countdown(GameObject): 268 | def __init__(self, game, position): 269 | super().__init__(game) 270 | self.duration = 50.999 # seconds 271 | self.pose = Pose(position, 0) 272 | self.color = (100, 110, 135) 273 | 274 | def update(self, dt, events): 275 | self.duration -= dt 276 | 277 | def over(self): 278 | return self.duration < 0 279 | 280 | def to_string(self): 281 | if self.over(): 282 | return "0" 283 | else: 284 | return f"{int(self.duration)}" 285 | 286 | def draw(self, surface, offset=(0, 0)): 287 | text_surf = self.game.scoreboard_font.render("Next round in ", 1, self.color) 288 | surf = self.game.small_timer_font.render(self.to_string(), 1, self.color) 289 | width = text_surf.get_width() + surf.get_width() 290 | x = self.pose.x + offset[0] - width//2 291 | y = self.pose.y + offset[1] 292 | scale = 0.6 + abs(math.sin(self.duration * math.pi)) * 0.4 293 | scale = 1 - (1 - scale)**1.5 294 | if self.duration < 0: 295 | scale = max(0, 0.7 + self.duration) 296 | scaled_surf = pygame.transform.scale(surf, (int(surf.get_width() * scale), int(surf.get_height() * scale))) 297 | surface.blit(scaled_surf, 298 | (x + text_surf.get_width() + surf.get_width()//2 - scaled_surf.get_width()//2, 299 | y - scaled_surf.get_height()//2)) 300 | surface.blit(text_surf, (x, y - text_surf.get_height()//2)) 301 | 302 | class TransitionGui(GameObject): 303 | 304 | def __init__(self, game): 305 | super().__init__(game) 306 | self.age = 0 307 | self.width = c.WINDOW_WIDTH - c.SCORE_TABLE_WIDTH 308 | self.height = c.WINDOW_HEIGHT 309 | self.pose = Pose((c.SCORE_TABLE_WIDTH + self.width//2, c.WINDOW_HEIGHT//2), 0) 310 | self.objects = [] 311 | self.background = pygame.image.load(c.IMAGE_PATH + "/trans_gui_back.png") 312 | self.background = pygame.transform.scale(self.background, (self.width, self.height)) 313 | self.add_tip_box() 314 | self.add_player_mult_box() 315 | self.objects.append(Countdown(self.game, (0, c.WINDOW_HEIGHT*0.44))) 316 | self.countdown = self.objects[-1] 317 | mod_options = random.sample(c.MODIFICATIONS, 2) 318 | self.voting = VotingObject(self.game, self, (0, 0), mod_options) 319 | self.objects.append(self.voting) 320 | 321 | def countdown_over(self): 322 | return self.countdown.over() 323 | 324 | def add_tip_box(self): 325 | position = 0, c.WINDOW_HEIGHT*0.30 326 | header = "Helpful hint" 327 | body = random.choice(c.HINTS) 328 | #ss = pygame.image.load(c.IMAGE_PATH + "/bang.png") 329 | self.objects.append(GreenAlertBox(self.game, position, header, body)) 330 | 331 | def add_player_mult_box(self): 332 | position = 0, -c.WINDOW_HEIGHT * 0.35 333 | header = "Player party multiplier" 334 | if self.game.player_multiplier() == 0: 335 | choices = c.MULT_0_MESSAGES 336 | else: 337 | choices = c.MULT_MESSAGES 338 | body = random.choice(choices).replace("{num}", str(self.game.number_of_players_last_round())) 339 | self.objects.append(PlayerMultiplierAlertBox(self.game, position, header, body)) 340 | 341 | def vote(self, player, option): 342 | if self.voting.picked is None: 343 | self.game.vote_sound.play() 344 | return self.voting.vote(player, option) 345 | 346 | def update(self, dt, events): 347 | self.age += dt 348 | for item in self.objects: 349 | item.update(dt, events) 350 | 351 | def draw(self, surface, offset=(0, 0)): 352 | xoff = offset[0] + self.pose.x 353 | yoff = offset[1] + self.pose.y 354 | surface.blit(self.background, (xoff - self.width//2, yoff - self.height//2)) 355 | for item in self.objects: 356 | item.draw(surface, (xoff, yoff)) 357 | -------------------------------------------------------------------------------- /src/twitch_chat_stream.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import threading 3 | import time 4 | 5 | class Stream: 6 | """ Scrapes incoming chat messages from a twitch.tv stream using IRC. """ 7 | 8 | SERVER = "irc.chat.twitch.tv" 9 | PORT = 6667 10 | NICKNAME = "justinfan808655" # Anonymous user, doesn't require oauth 11 | 12 | class Message: 13 | """ Object to store a chat message. """ 14 | 15 | def __init__(self, user, text): 16 | self.user = user 17 | self.text = text 18 | self.creation_time = time.time() 19 | 20 | def __init__(self, channel=None): 21 | """ Initialize the stream object, and open a channel connection if a 22 | channel is provided. 23 | """ 24 | self.queue_mutex = threading.Lock() 25 | self.running = False 26 | if channel: 27 | self.open(channel) 28 | 29 | def open(self, channel): 30 | """ Open a socket connection to the given channel, send information 31 | over IRC, and start populating self.queue with data. 32 | """ 33 | self.channel = f"#{channel}" 34 | self._queue = [] 35 | self.running = True 36 | 37 | self.sock = socket.socket() 38 | self.sock.connect((self.SERVER, self.PORT)) 39 | self.sock.send(f"NICK {self.NICKNAME}\n".encode('utf-8')) 40 | self.sock.send(f"JOIN {self.channel}\n".encode('utf-8')) 41 | 42 | threading.Thread(target=self.stream_chat,daemon=True).start() 43 | 44 | def stream_chat(self): 45 | """ Continuously read messages from the subscribed Twitch channel 46 | and append results to queue. 47 | This method is blocking, and is an infinite loop so long as 48 | self.running is not modified. Only call with a thread. 49 | """ 50 | self.sock.recv(2048).decode('utf-8') 51 | self.sock.recv(2048).decode('utf-8') 52 | while self.running: 53 | resp = self.sock.recv(2048).decode('utf-8') 54 | for line in resp.split("\n"): 55 | if "PING :" in line[:6]: 56 | self.sock.send("PONG :Still alive\r\n".encode()) 57 | if len(line)<=2 or not "!" in line: 58 | continue 59 | user = line[1:].split("!")[0].strip() 60 | msg = ":".join(line.split(":")[2:]).strip() 61 | with self.queue_mutex: 62 | self._queue.append(self.Message(user, msg)) 63 | 64 | def close(self): 65 | """ Close the connection and empty the queue. """ 66 | self.running = False 67 | with self.queue_mutex: 68 | self._queue = [] 69 | self.sock.close() 70 | 71 | def queue_flush(self): 72 | """ Return a tuple containing all Messages in the queue, and 73 | empty the queue. 74 | """ 75 | if not self.running: 76 | return () 77 | with self.queue_mutex: 78 | result = tuple(self._queue) 79 | self._queue = [] 80 | return result 81 | 82 | 83 | if __name__ == '__main__': 84 | stream = Stream(channel="twitchplayspokemon") 85 | while True: 86 | for message in stream.queue_flush(): 87 | print(f"{message.user} says '{message.text}'!") 88 | -------------------------------------------------------------------------------- /src/wind_particle.py: -------------------------------------------------------------------------------- 1 | ##!/usr/bin/env python3 2 | 3 | import random 4 | from math import sin 5 | 6 | import pygame 7 | 8 | from particle import Particle 9 | from primitives import Pose 10 | import constants as c 11 | 12 | class WindParticle(Particle): 13 | 14 | def __init__(self, game): 15 | super().__init__(game) 16 | x = random.random() * c.LEVEL_WIDTH 17 | y = random.random() * c.LEVEL_WIDTH 18 | 19 | if not hasattr(self.game, "solar_wind_direction"): 20 | self.game.solar_wind_direction = random.choice((c.UP, c.DOWN, c.LEFT, c.RIGHT)) 21 | 22 | margin = 20 23 | if self.game.solar_wind_direction == c.UP: 24 | y = c.LEVEL_HEIGHT + margin 25 | elif self.game.solar_wind_direction == c.DOWN: 26 | y = -margin 27 | elif self.game.solar_wind_direction == c.LEFT: 28 | x = c.LEVEL_WIDTH + margin 29 | elif self.game.solar_wind_direction == c.RIGHT: 30 | x = -margin 31 | self.destroy_margin = 50 32 | 33 | self.layer = random.choice((1, 2, 3)) 34 | self.radius = 5 - self.layer 35 | self.speed = 1600/self.layer 36 | 37 | self.pose = Pose((x, y), 0) 38 | self.velocity = Pose((self.game.solar_wind_direction), 0) * self.speed 39 | 40 | self.color = random.choice([ 41 | (255, 255, 100), 42 | (255, 180, 100), 43 | (255, 100, 100) 44 | ]) 45 | 46 | self.age = random.random() * 100 47 | 48 | def update(self, dt, events): 49 | super().update(dt, events) 50 | self.pose.x += self.velocity.x * dt 51 | self.pose.y += self.velocity.y * dt 52 | if self.pose.x > c.WINDOW_WIDTH + self.destroy_margin or \ 53 | self.pose.y > c.WINDOW_HEIGHT + self.destroy_margin or \ 54 | self.pose.x < -self.destroy_margin or \ 55 | self.pose.y < -self.destroy_margin: 56 | self.destroy() 57 | 58 | def draw(self, surface, offset=(0, 0)): 59 | x, y = self.pose.get_position() 60 | if self.game.solar_wind_direction in (c.UP, c.DOWN): 61 | x += sin(self.age * 10) * 30/self.layer 62 | width = self.radius 63 | height = self.radius * 3 64 | else: 65 | width = self.radius * 3 66 | height = self.radius 67 | y += sin(self.age * 10) * 30/self.layer 68 | pygame.draw.ellipse(surface, self.color, (x - width//2, y - width//2, width, height)) 69 | -------------------------------------------------------------------------------- /src/wormhole.py: -------------------------------------------------------------------------------- 1 | ##!/usr/bin/env python3 2 | 3 | import pygame 4 | import random 5 | 6 | import constants as c 7 | from primitives import PhysicsObject, Pose 8 | from planet import Planet 9 | 10 | class Wormhole(PhysicsObject): 11 | def __init__(self, game, position1, position2, angle=0, radius=20, gravity_radius=None, mass=None, color=-1): 12 | super().__init__(game, position1, angle) 13 | self.pose2 = Pose(position2, 0) 14 | self.home = False 15 | self.destroyed = False 16 | self.velocity.angle = -100 17 | self.radius = radius 18 | self.gravity_radius = gravity_radius if gravity_radius is not None else 2.5*radius 19 | self.mass = mass if mass is not None else radius**2 20 | self.ships1 = [] 21 | self.ships2 = [] 22 | self.age = 0 23 | if color == -1: 24 | color = random.randint(0, 3) 25 | self.color = c.WORMHOLE_COLORS[color] 26 | self.surfs = self.get_surfs() 27 | 28 | def get_surfs(self): 29 | surfs = [] 30 | alpha = 40 31 | if self.color != c.WORMHOLE_COLORS[3] and self.color != c.WORMHOLE_COLORS[0]: 32 | base = pygame.image.load(c.IMAGE_PATH + "/wormhole_bw.png").convert() 33 | else: 34 | base = pygame.image.load(c.IMAGE_PATH + "/wormhole.png").convert() 35 | if self.color != c.WORMHOLE_COLORS[3]: 36 | tint = base.copy() 37 | tint.fill(self.color) 38 | base.blit(tint, (0, 0), special_flags = pygame.BLEND_MULT) 39 | base.set_colorkey(base.get_at((0, 0))) 40 | scale = 1 41 | for i in range(4): 42 | new_surf = base.copy() 43 | new_surf.set_alpha(alpha) 44 | alpha += (255 - alpha)*0.28 45 | new_surf = pygame.transform.scale(new_surf, 46 | (int(new_surf.get_width() * scale), 47 | int(new_surf.get_height() * scale))) 48 | scale *= 0.9 49 | if i%2==0: 50 | new_surf = pygame.transform.flip(new_surf, 1, 0) 51 | surfs.append(new_surf) 52 | return surfs 53 | 54 | def is_moon(self): 55 | """ Wormholes aren't moons, silly. """ 56 | return False 57 | 58 | def get_acceleration(self, ship): 59 | """ Return a Pose indicating the acceleration to apply to 60 | the Ship. 61 | """ 62 | distance1 = self.pose.distance_to(ship.pose) - ship.radius 63 | distance2 = self.pose2.distance_to(ship.pose) - ship.radius 64 | freeze_length = 0.4 65 | if distance1 < self.radius and not ship in self.ships2: 66 | ship.lastWormhole = self 67 | ship.freeze(freeze_length) 68 | self.ships1.append(ship) 69 | offset = self.pose-ship.pose 70 | offset.angle = 0 71 | ship.pose.add_pose(self.pose2 - self.pose + offset*2) 72 | ship.label_pose = ship.pose.copy() 73 | ship.scale = 0 74 | if distance2 < self.radius and not ship in self.ships1: 75 | ship.lastWormhole = self 76 | ship.freeze(freeze_length) 77 | self.ships2.append(ship) 78 | offset = self.pose2-ship.pose 79 | offset.angle = 0 80 | ship.pose.add_pose(self.pose - self.pose2 + offset*2) 81 | ship.label_pose = ship.pose.copy() 82 | ship.scale = 0 83 | if distance1 > self.radius and distance2 > self.radius: 84 | if ship in self.ships1: 85 | self.ships1.remove(ship) 86 | if ship in self.ships2: 87 | self.ships2.remove(ship) 88 | 89 | if distance1 == 0 or distance2 == 0 or ship.is_frozen(): 90 | return Pose((0, 0), 0) 91 | if distance1 < self.gravity_radius: 92 | gravity_magnitude = self.mass * c.GRAVITY_CONSTANT / distance1**2 93 | gravity_vector = (self.pose - ship.pose) 94 | elif distance2 < self.gravity_radius: 95 | gravity_magnitude = self.mass * c.GRAVITY_CONSTANT / distance2**2 96 | gravity_vector = (self.pose2 - ship.pose) 97 | else: 98 | return Pose((0, 0), 0) 99 | gravity_vector.set_angle(0) 100 | gravity_vector.scale_to(gravity_magnitude) 101 | return gravity_vector 102 | 103 | def overlaps(self, pose, r, clearance): 104 | hit1 = pose.distance_to(self.pose) < self.radius + r + max(clearance, self.clearance) 105 | hit2 = pose.distance_to(self.pose2) < self.radius + r + max(clearance, self.clearance) 106 | return hit1 or hit2 107 | 108 | def draw_gravity_region(self, surf, offset=(0, 0)): 109 | # This is a bit jank, but hey, it's a game jam 110 | Planet.draw_gravity_region(self, surf, offset) 111 | self.pose, self.pose2 = self.pose2, self.pose 112 | Planet.draw_gravity_region(self, surf, offset) 113 | self.pose, self.pose2 = self.pose2, self.pose 114 | 115 | def update(self, dt, events): 116 | self.age += dt 117 | self.pose.angle += dt * self.velocity.angle 118 | self.pose2.angle += dt * self.velocity.angle 119 | 120 | def draw(self, surf, offset=(0, 0)): 121 | 122 | x, y = self.pose.get_position() 123 | x += offset[0] 124 | y += offset[1] 125 | #pygame.draw.circle(surf, (100, 100, 100), (x, y), self.gravity_radius, 2) 126 | speed = -1 127 | for i, surface in enumerate(self.surfs): 128 | surface = pygame.transform.rotate(surface, self.pose.angle * speed) 129 | surf.blit(surface, (x - surface.get_width()//2, y - surface.get_height()//2)) 130 | speed *= -0.7 131 | 132 | #pygame.draw.circle(surf, (150, 50, 250), (x, y), self.radius) 133 | 134 | x, y = self.pose2.get_position() 135 | x += offset[0] 136 | y += offset[1] 137 | #pygame.draw.circle(surf, (100, 100, 100), (x, y), self.gravity_radius, 2) 138 | speed = -1 139 | for i, surface in enumerate(self.surfs): 140 | surface = pygame.transform.rotate(surface, self.pose.angle * speed) 141 | surf.blit(surface, (x - surface.get_width()//2, y - surface.get_height()//2)) 142 | speed *= -0.7 143 | -------------------------------------------------------------------------------- /src/wormhole_explosion.py: -------------------------------------------------------------------------------- 1 | ##!/usr/bin/env python3 2 | 3 | import pygame 4 | 5 | from particle import Particle 6 | import constants as c 7 | 8 | class WormholeExplosion(Particle): 9 | def __init__(self, game, ship): 10 | super().__init__(game) 11 | self.ship = ship 12 | self.pose = ship.pose.copy() 13 | self.start_radius = 16 14 | self.duration = 0.5 15 | self.game.use_wormhole_sound.play() 16 | self.color = ship.lastWormhole.color 17 | 18 | def get_scale(self): 19 | return 1 + self.through(loading=1.5) * 2.5 20 | 21 | def get_alpha(self): 22 | return 255 * (1 - self.through(loading=1.5)) 23 | 24 | def update(self, dt, events): 25 | super().update(dt, events) 26 | 27 | def draw(self, surface, offset=(0, 0)): 28 | radius = int(self.start_radius * self.get_scale()) 29 | surf = pygame.Surface((radius*2, radius*2)) 30 | surf.fill(c.BLACK) 31 | surf.set_colorkey(c.BLACK) 32 | # color = 198, 59, 211 33 | r = 255 - self.through(loading=2.5) * (255 - self.color[0]) 34 | g = 255 - self.through(loading=2.5) * (255 - self.color[1]) 35 | b = 255 - self.through(loading=2.5) * (255 - self.color[2]) 36 | 37 | pygame.draw.circle(surf, (r, g, b), (radius, radius), radius) 38 | x = self.pose.x - offset[0] - surf.get_width()//2 39 | y = self.pose.y - offset[1] - surf.get_height()//2 40 | surf.set_alpha(self.get_alpha()) 41 | surface.blit(surf, (x, y)) 42 | --------------------------------------------------------------------------------