main.py (12645B)
1 #!/usr/bin/env python3 2 3 """ 4 UoM COMP16321 Coursework 2 5 6 This coursework is split into multiple parts, because i could not deal with a 7 single python file that would be like 2000 lines long. The general structure 8 is as follows: 9 - main.py : initialises the game, does serialisation / deserialisation, 10 and also starts the main menu. 11 - assets/ : stores game related assets such as the boss-key image 12 - src/ : stores the games source code 13 - common.py : Stores global variables, functions common to the 14 rest of the games modules, and a few miscellaneous 15 utility classes and functions. 16 - platform.py : Provides a wrapper around the tkinter canvas, called 17 "Screen". This wrapper implemented a bunch of GUI 18 related functionality, and is a singleton class. 19 20 - ecs/ : Stores the Entity-Component-System source code 21 - ecs.py : Stores the implementation of the ECS manager, 22 which allows for the registration of systems, 23 the creation of entities, and the registration 24 of components on said entities. It also provides 25 methods to setup, process, and cleanup an ECS. 26 - components.py : Stores all the components in use by the game. 27 Provides a "formal virtual interface" that should 28 be implemented by all components. 29 - systems.py : Stores all the systems in use by the game. 30 Provides a "formal virtual interface" that should 31 be implemented by all systems. 32 """ 33 34 import os 35 import random 36 import tkinter as tk 37 38 import src 39 import src.ecs.ecs as ecs 40 import src.ecs.components as components 41 import src.ecs.systems as systems 42 import src.platform as platform 43 44 from math import pi 45 46 from src.common import * 47 48 49 def run(root, starting_score=0, starting_lives=5): 50 screen = platform.Screen(root) 51 52 manager = ecs.EcsManager() 53 54 ## register all systems that will be used in the game 55 manager.register_system(systems.Render2DSystem()) 56 manager.register_system(systems.Collider2DSystem()) 57 manager.register_system(systems.EdgeHarmSystem()) 58 manager.register_system(systems.Physics2DSystem()) 59 manager.register_system(systems.UserInputSystem()) 60 manager.register_system(systems.LifeSystem()) 61 manager.register_system(systems.LifespanSystem()) 62 manager.register_system(systems.BulletEmitterSystem()) 63 manager.register_system(systems.SpawnerSystem()) 64 manager.register_system(systems.EnemyEmitterSystem()) 65 66 ## create player 67 player = manager.create_entity() 68 manager.register_component(player, components.PlayerTag()) 69 screen.set_tracked_entity(player) 70 71 player_vertices = [-5, 10, 0, -10, 5, 10, 0, 5] 72 player_sprite = screen.draw_poly(player_vertices, fill=CYAN, tag='player') 73 manager.register_component(player, components.Transform2D(WIDTH / 2, 74 HEIGHT / 2, 0)) 75 manager.register_component(player, components.Collider2D(10, 20)) 76 manager.register_component(player, components.Velocity2D(0, 0)) 77 manager.register_component(player, components.UserInput(220)) 78 manager.register_component(player, components.ScreenElement(player_sprite, 79 player_vertices)) 80 81 manager.register_component(player, components.Score(starting_score)) 82 manager.register_component(player, components.Lives(starting_lives)) 83 player_bullet_data = components.BulletData( 84 bullet_size=10, 85 bullet_speed=-270, 86 bullet_vertices=[-5, 0, 0, 5, 5, 0, 0, -5], 87 bullet_colours=[CYAN], 88 bullet_colour_idx=0) 89 manager.register_component(player, components.LinearBulletEmitter( 90 data=player_bullet_data, direction=-1)) 91 manager.register_component(player, components.RadialBulletEmitter( 92 data=player_bullet_data, bullet_count=48, bullet_arc_offset=0)) 93 94 ## create enemy spawner 95 enemy_spawner = manager.create_entity() 96 97 enemy_patterns = [ 98 'loner', 99 'column', 100 'row_ltr', 101 'row_rtl', 102 ] 103 104 enemy_size = 20 105 enemy_padding = enemy_size / 2 106 107 def next_spawn(): 108 while True: 109 next_pattern = random.choice(enemy_patterns) 110 pattern_cooldown = 1 111 112 min_enemies, max_enemies = 2, 3 113 114 player_score = screen.get_score() 115 if player_score > 100: 116 min_enemies, max_enemies = 4, 5 117 if player_score > 150: 118 min_enemies, max_enemies = 6, 7 119 if player_score > 200: 120 pattern_cooldown = 0.75 121 122 base_px = random.randint(enemy_size, WIDTH - enemy_size) 123 enemy_count = random.randint(min_enemies, max_enemies) 124 125 if next_pattern == 'loner': 126 yield (pattern_cooldown, (base_px, enemy_size)) 127 128 elif next_pattern == 'column': 129 for i in range(enemy_count): 130 yield (0.25, (base_px, enemy_size)) 131 yield (pattern_cooldown, (base_px, enemy_size)) 132 133 elif next_pattern == 'row_ltr': 134 max_px = enemy_count * (enemy_size + enemy_padding) + enemy_size 135 base_px = random.randint(enemy_size, WIDTH - max_px) 136 curr_px = base_px 137 for i in range(enemy_count): 138 yield (0.05, (curr_px, enemy_size)) 139 curr_px += enemy_size + enemy_size / 2 140 yield (pattern_cooldown, (curr_px, enemy_size)) 141 142 elif next_pattern == 'row_rtl': 143 min_px = enemy_count * (enemy_size + enemy_padding) + enemy_size 144 base_px = random.randint(min_px, WIDTH - enemy_size) 145 curr_px = base_px 146 for i in range(enemy_count): 147 yield (0.05, (curr_px, enemy_size)) 148 curr_px -= enemy_size + enemy_size / 2 149 yield (pattern_cooldown, (curr_px, enemy_size)) 150 151 152 def create_enemy(spawn_location, manager, screen): 153 e = manager.create_entity() 154 manager.register_component(e, components.EnemyTag()) 155 156 enemy_speed = 130 157 shooter_chance = 0.125 158 min_shooter_cooldown, max_shooter_cooldown = 2, 3 159 heavy_chance = 0 160 161 player_score = screen.get_score() 162 if player_score > 100: 163 shooter_chance = 0.25 164 min_shooter_cooldown = 1 165 heavy_chance = 0.05 166 167 if player_score > 150: 168 shooter_chance = 0.5 169 heavy_chance = 0.075 170 171 if player_score > 200: 172 max_shooter_cooldown = 2 173 heavy_chance = 0.1 174 175 enemy_l_wing = [-3, 0, -5, 0, -7, 7, -10, 0] 176 enemy_r_wing = [10, 0, 7, 7, 5, 0, 3, 0] 177 enemy_vertices = [*enemy_l_wing, -7, -3, 7, -3, *enemy_r_wing, 0, 15] 178 179 sx, sy = 16, 25 180 px, py = spawn_location 181 vx, vy = 0, enemy_speed 182 183 manager.register_component(e, components.Transform2D(px, py, 0)) 184 manager.register_component(e, components.Collider2D(sx, sy)) 185 manager.register_component(e, components.Velocity2D(vx, vy)) 186 manager.register_component(e, components.EdgeHarm(player, 187 HEIGHT + 2 * sy)) 188 189 ## have some enemies shoot bullets we have to dodge 190 if random.random() <= shooter_chance: 191 enemy_bullet_data = components.BulletData( 192 bullet_size=10, 193 bullet_speed=180, 194 bullet_vertices=[-5, 0, 0, 5, 5, 0, 0, -5], 195 bullet_colours=[RED], 196 bullet_colour_idx=0) 197 198 manager.register_component(e, components.EnemyEmitterCooldown( 199 min_shooter_cooldown, max_shooter_cooldown)) 200 if random.randint(0, 1) == 1: 201 manager.register_component(e, components.LinearBulletEmitter( 202 data=enemy_bullet_data, direction=1)) 203 else: 204 manager.register_component(e, components.RadialBulletEmitter( 205 data=enemy_bullet_data, bullet_count=4, 206 bullet_arc_offset=pi / 4)) 207 208 ## have some enemies have 2 lives 209 if random.random() <= heavy_chance: 210 sprite = screen.draw_poly(enemy_vertices, fill=MAGENTA) 211 manager.register_component(e, components.ScreenElement(sprite, 212 enemy_vertices)) 213 manager.register_component(e, components.Lives(2)) 214 else: 215 sprite = screen.draw_poly(enemy_vertices, fill=YELLOW) 216 manager.register_component(e, components.ScreenElement(sprite, 217 enemy_vertices)) 218 manager.register_component(e, components.Lives(1)) 219 220 221 manager.register_component(enemy_spawner, components.Spawner(next_spawn(), 222 create_enemy)) 223 224 ## make player visible 225 screen.raise_tag(player_sprite) 226 227 ## start processing the game 228 ecs.setup(manager, screen) 229 ecs.process(manager) 230 ecs.cleanup(manager, screen) 231 232 player_score = screen.get_score() 233 player_name = screen.get_name() 234 235 screen.destroy() 236 return (player_name, player_score) 237 238 239 def load_scores(fpath): 240 scores = {} 241 242 if not os.path.isfile(fpath): 243 return {} 244 245 with open(fpath, 'r') as f: 246 for line in f: 247 segments = line.rsplit(',') 248 name, score = segments[0], int(segments[1].strip()) 249 scores[name] = score 250 251 return scores 252 253 254 def save_scores(fpath, scores): 255 with open(fpath, 'w') as f: 256 for name, score in scores.items(): 257 f.write(f'{name},{score}\n') 258 259 260 def main(): 261 root = tk.Tk() 262 root.title('Bullet Purgatory') 263 root.protocol('WM_DELETE_WINDOW', lambda: root.destroy()) 264 265 menu_elements = [] 266 267 def disable_menu(): 268 for element in menu_elements: 269 element.config(state='disabled') 270 271 272 def enable_menu(): 273 for element in menu_elements: 274 element.config(state='normal') 275 276 277 def start_game(starting_score=0, starting_lives=5): 278 disable_menu() 279 280 game_window = tk.Toplevel(root) 281 game_window.title('Bullet Purgatory') 282 283 scores = load_scores(SCORE_FPATH) 284 name, score = run(game_window, starting_score, starting_lives) 285 scores[name] = max(score, scores.get(name, 0)) 286 287 entries, highscores = len(scores), {} 288 for _ in range(entries if entries < 10 else 10): 289 keyfunc = lambda t: t[1] 290 highest_scoring = max(scores.items(), key=keyfunc)[0] 291 292 highscores[highest_scoring] = scores.pop(highest_scoring) 293 294 save_scores(SCORE_FPATH, highscores) 295 296 enable_menu() 297 298 root.protocol('WM_DELETE_WINDOW', lambda: root.destroy()) 299 300 301 start_btn = tk.Button(root, text='Start Game', command=start_game) 302 start_btn.pack() 303 menu_elements.append(start_btn) 304 305 def show_leaderboard(): 306 highscores = load_scores(SCORE_FPATH) 307 308 leaderboard_window = tk.Toplevel(root) 309 leaderboard_window.title('Bullet Purgatory Leaderboard') 310 311 header = tk.Label(leaderboard_window, text='FORMAT: <name> : <score>') 312 header.pack() 313 for name, score in highscores.items(): 314 label = tk.Label(leaderboard_window, text=f'{name} : {score}') 315 label.pack() 316 317 close_btn = tk.Button(leaderboard_window, text='Close Leaderboard', 318 command=leaderboard_window.destroy) 319 close_btn.pack() 320 321 322 leaderboard_btn = tk.Button(root, text='Show Leaderboard', 323 command=show_leaderboard) 324 leaderboard_btn.pack() 325 menu_elements.append(leaderboard_btn) 326 327 def load_game(): 328 state = load_gamestate(STATE_FPATH) 329 330 if gamestate_is_valid(state): 331 start_game(state['score'], state['lives']) 332 else: 333 start_game() 334 335 336 load_game_btn = tk.Button(root, text='Load Game', command=load_game) 337 load_game_btn.pack() 338 menu_elements.append(load_game_btn) 339 340 def show_help(): 341 help_window = tk.Toplevel(root) 342 help_window.title('Bullet Purgatory Help (psst. Gradius called ;^))') 343 344 contents = tk.Label(help_window, text=HELP_CONTENTS) 345 contents.pack() 346 347 close_btn = tk.Button(help_window, text='Close Help', 348 command=help_window.destroy) 349 close_btn.pack() 350 351 352 show_help_btn = tk.Button(root, text='Show Help', command=show_help) 353 show_help_btn.pack() 354 menu_elements.append(show_help_btn) 355 356 root.mainloop() 357 358 359 if __name__ == '__main__': 360 main() 361