pyecs

pyecs.git
git clone git://git.lenczewski.org/pyecs.git
Log | Files | Refs | README | LICENSE

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