Return to repo list

tzed

Simple story-driven open world 2D CRPG.
Return to HMagellan.com

game.py (24594B)


      1 import pygame, os, json
      2 from . import interface, images, board, entity, ui, message, dungeon, battle
      3 from .gamelib import *
      4 
      5 ###########
      6 # game.py #
      7 ###########
      8 
      9 # This file contains:
     10 #   1.  The 'Game' object, which represents the entire game application.
     11 
     12 ##############################
     13 # Section 1 - The Game Class #
     14 ##############################
     15 
     16 class Game(object):
     17     """
     18     Game is the object that represents the entire running
     19     game application. Everything is a subcomponent of and
     20     subordinate to the Game object, and there is only ever
     21     a single instance of Game.
     22     """
     23 
     24     def __init__(self):
     25         
     26         # Basic values
     27         self.on = True
     28 
     29         # Mode values
     30         self.state_mode = None
     31         self.control_mode = None
     32 
     33         # Pygame objects
     34         self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
     35         self.frame_clock = pygame.time.Clock()
     36         self.viewport = pygame.Surface((2 * (SCREEN_WIDTH // 3), 2 * (SCREEN_HEIGHT // 3)))
     37         self.viewport_rect = self.viewport.get_rect()
     38         self.viewport_rect.topleft = SCREEN_MARGINS
     39 
     40         # Game components
     41         self.interface = interface.Interface(self)
     42         self.message_board = message.MessageBoard(self, (313, 620), (696, 72)) # TODO: Hardcoded. Need to make the whole UI resolution-aware...
     43 
     44         # Gameplay objects
     45         self.player = None
     46         self.player_name = ""
     47         self.party = []
     48         self.party_group = pygame.sprite.Group()
     49 
     50         # UI elements
     51         self.ui_font = pygame.font.Font(os.path.join(FONT_PATH, MESSAGE_FONT), MESSAGE_FONT_SIZE) # TODO: Should there be a UI font option???
     52         self.small_ui_font = pygame.font.Font(os.path.join(FONT_PATH, MESSAGE_FONT), MESSAGE_FONT_SIZE - 3)
     53         self.tiny_ui_font = pygame.font.Font(os.path.join(FONT_PATH, MESSAGE_FONT), MESSAGE_FONT_SIZE - 7)
     54         self.date_string = ""
     55         self.ui_tray = None
     56         self.ui_group = pygame.sprite.Group()
     57         self.location_element = None
     58         self.location_element_pos = (0, 0)
     59         self.date_element = None
     60         self.date_element_pos = (0, 0)
     61         self.clock_element = None
     62         self.clock_element_pos = (0, 0)
     63 
     64         # Progress values
     65         self.prog_flags = { "npc" : {}, "quest" : {}, "other" : {} }
     66 
     67         # Other values
     68         self.active_scenario = "Tzed 1" # TMP!
     69         self.sheets = {}
     70         self.loaded_boards = {}
     71         self.loaded_dungeons = {}
     72         self.current_board = None
     73         self.current_dungeon = None
     74         self.current_battle = None
     75         self.gamedata = {}
     76         self.gametime = 0
     77         self.old_gametime = -1
     78         self.seconds = 0
     79         self.minutes = 0
     80         self.hours = 0
     81         self.days = 0
     82         self.months = 0
     83         self.years = 0
     84         self.day_descriptors = ["Advent", "Apex", "Verge"]
     85         self.month_names = ["Turrich's Reign", "The Rebirth", "Thrive", "Arbala's Reign", "The Long Days", "The Turning", "Mensor's Reign", "Cold Winds", "The Nights"]
     86         self.datestring = ""
     87         self.clockstring = ""
     88         self.old_location = ""
     89 
     90         # Loading
     91         self.load_sheets("sheets.json")
     92         self.load_boards("boards.json")
     93         self.load_dungeons("dungeons.json")
     94         self.build_ui()
     95 
     96         # TMP!
     97         self.load_game("savegame1")
     98 
     99     def load_sheets(self, imagefile):
    100         """
    101         Load all of the images as sheets.
    102         """
    103         # Load the sheet definition from JSON
    104         with open(os.path.join(ETC_PATH, imagefile)) as sf:
    105             sheetdef = json.load(sf)
    106 
    107         # Create the sheet objects
    108         for s in sheetdef:
    109             self.sheets[s] = images.Sheet(load_image(str(s) + ".png", sheetdef[s][2]), s, (sheetdef[s][0], sheetdef[s][1]), sheetdef[s][2])
    110 
    111     def load_boards(self, boardfile):
    112         """
    113         Load boards from a file.
    114         """
    115         # Load board from json
    116         with open(os.path.join(ETC_PATH, boardfile)) as bf:
    117             boardlist = json.load(bf)
    118 
    119         # Create board and add it to loaded boards
    120         for b in boardlist["boards"]:
    121             with open(os.path.join(BOARD_PATH, b, "board.json")) as bj:
    122                 boarddef = json.load(bj)
    123             with open(os.path.join(BOARD_PATH, b, "entities.json")) as ej:
    124                 entdef = json.load(ej)
    125             pal = { tuple(i[0]): i[1] for i in boarddef["palette"] }
    126             nb = board.Board(self, b, boarddef["display_name"], pal, (boarddef["board_width"], boarddef["board_height"]), (boarddef["cell_width"], boarddef["cell_height"]), boarddef["cells"], boarddef["scale"], entdef)
    127             self.loaded_boards[b] = nb
    128 
    129     def load_dungeons(self, dungeonfile):
    130         """
    131         Load dungeons from file.
    132         """
    133         # Load dungeons from json
    134         with open(os.path.join(ETC_PATH, dungeonfile)) as df:
    135             dungeonlist = json.load(df)
    136 
    137         # Create dungeons and add to loaded dungeons
    138         for d in dungeonlist["dungeons"]:
    139             with open(os.path.join(DUNGEON_PATH, d, "dungeon.json")) as dj:
    140                 dungeondef = json.load(dj)
    141             with open(os.path.join(DUNGEON_PATH, d, "encounters.json")) as ej:
    142                 encountdef = json.load(ej)
    143             nd = dungeon.Dungeon(self, d, dungeondef, encountdef)
    144             self.loaded_dungeons[d] = nd
    145 
    146     def build_ui(self):
    147         """
    148         Create the out-of-board game UI.
    149         """
    150         # TODO: This is totally not generic. Would it be worth it
    151         #       to abstract this in some way? That will have to be
    152         #       done for menus anyway...
    153         self.ui_tray = entity.CustomSprite(self.sheets["ui_tray"].sprites[(0, 0)], (0, 0))
    154         self.ui_group.add(self.ui_tray)
    155 
    156     def switch_board(self, boardname, cellpos):
    157         """
    158         Switch to a given board and cell, and
    159         populate its adjacents and entities.
    160         """
    161         if boardname in self.loaded_boards.keys():
    162             if self.current_board != None:
    163                 self.old_location = self.current_board.display_name
    164             self.current_board = self.loaded_boards[boardname]
    165             self.current_board.change_cell(cellpos)
    166             self.collect_game_data()
    167             self.message_board.post("$PLAYERNAME enters $CURRENTBOARDNAME.", self.gamedata)
    168         else:
    169             return 1
    170 
    171     def switch_dungeon(self, dungeonname, enter = False, direction = 0, floor = 0, cell = (0, 0)):
    172         """
    173         Switch to the given dungeon and go
    174         to the given floor, facing the given
    175         direction, and in the given cell.
    176         """
    177         if dungeonname in self.loaded_dungeons.keys():
    178             if enter:
    179                 direction = self.loaded_dungeons[dungeonname].entrance_direction
    180                 floor = 0
    181                 cell = self.loaded_dungeons[dungeonname].entrance_cell
    182             self.current_dungeon = self.loaded_dungeons[dungeonname]
    183             self.current_dungeon.enter_dungeon(direction, floor, cell)
    184             self.collect_game_data()
    185             self.generate_party_graphics()
    186             self.message_board.post("$PLAYERNAME enters $CURRENTDUNGEONNAME.", self.gamedata)
    187         else:
    188             return 1
    189 
    190     def spawn_player(self, playerchar, cellpos, tilepos):
    191         """
    192         Spawn the player in the current board at
    193         the given cellpos and tilepos. Called on load
    194         and game start. 'playerchar' is a party
    195         character dict, which should contain info
    196         needed to create a player.
    197         """
    198         if self.current_board != None:
    199             self.player = entity.PlayerEntity(self.sheets[playerchar["sheet"] + str(self.current_board.scale_factor)].sprites[tuple(playerchar["sprite"])], cellpos, tilepos, playerchar["color"])
    200             self.player_name = playerchar["name"]
    201             self.player.rect.topleft = ((SCREEN_WIDTH // 3) - 6, (SCREEN_HEIGHT // 3) - 6)# TODO: Weirdly derived
    202             self.current_board.position_offset = self.player.rect.topleft
    203             self.current_board.position_to_tile(tilepos)
    204             self.current_board.generate_tiles()
    205             self.current_board.get_playerpos()
    206         else:
    207             return 1
    208 
    209     def switch_mode(self, new_mode, data = None):
    210         """
    211         Change the current state_mode, as well as load up
    212         the elements of the new mode. This large method
    213         should take account of every possible mode in the
    214         game. Such a structure has no reason to exists e.g. 
    215         as a JSON file. The 'data' option contains the info
    216         needed as part of a mode switch. This method handles
    217         a change even if data is None.
    218         """
    219         if new_mode == None:
    220             return
    221         else:
    222             self.state_mode = new_mode
    223 
    224         # TODO: Mode-specific switch logic
    225 
    226     def generate_party_graphics(self):
    227         """
    228         Generate fresh versions of the party graphics for
    229         display in battles and dungeons.
    230         """
    231         for p in self.party:
    232             pg = entity.CustomSprite(self.sheets[self.party[p]["party_sheet"]].sprites[tuple(self.party[p]["party_sprite"])], (self.viewport_rect.left + (256 * self.party[p]["position"]), self.viewport_rect.center[1] + (48 * self.party[p]["row"])), self.party[p]["color"])
    233             self.party_group.add(pg)
    234 
    235     def move_player_on_board(self, offset):
    236         """
    237         Move the player from tile-to-tile relative to the
    238         given offset.
    239         """
    240         
    241         # First, prep messages
    242         canmove = False
    243         d = { (-1, 0) : "west", (1, 0) : "east", (0, -1) : "north", (0, 1) : "south" }
    244 
    245         # Next, calculate the new tilepos for the player if it exists.
    246         if self.player != None:
    247             np = (self.player.tilepos[0] + offset[0], self.player.tilepos[1] + offset[1])
    248 
    249             # Next, if the new tilepos is within the bounds of the current cell and is passable, simply change the players tilepos to that value.
    250             if np[1] >= 0 and np[1] < len(self.current_board.current_cell) and np[0] >= 0 and np[0] < len(self.current_board.current_cell[np[1]]):
    251                 if self.current_board.cellmap[self.player.cellpos][np[1]][np[0]][1] and (self.player.cellpos, np) not in self.current_board.npc_positions:
    252                     self.player.tilepos = np 
    253                     canmove = True
    254 
    255             # Otherwise, in case we are trying to move out of the current cell, calculate the relative offset of the new cellpos
    256             # and the absolute position of the new cellpos using the player's current absolute cellpos.
    257             else:
    258                 celloff = (-1 if offset[0] < 0 else 0 if offset[0] == 0 else 1, -1 if offset[1] < 0 else 0 if offset[1] == 0 else 1)
    259                 ncc = (self.player.cellpos[0] + celloff[0], self.player.cellpos[1] + celloff[1])
    260 
    261                 # Check if we would land in this new cell.
    262                 if self.current_board.adjacent_cells[celloff] != None and ncc in self.current_board.cellmap.keys():
    263                     ntp = (0 if celloff[0] > 0 else self.player.tilepos[0] if celloff[0] == 0 else self.current_board.cell_dimensions[0] - 1, 0 if celloff[1] > 0 else self.player.tilepos[1] if celloff[1] == 0 else self.current_board.cell_dimensions[1] - 1)
    264 
    265                     # If that tilepos is passable, move the player's cellpos and tilepos, then change the board cell.
    266                     if self.current_board.cellmap[ncc][ntp[1]][ntp[0]][1] and (ncc, ntp) not in self.current_board.npc_positions:
    267                         self.player.cellpos = ncc
    268                         self.player.tilepos = ntp
    269                         self.current_board.change_cell(self.player.cellpos)
    270                         canmove = True
    271 
    272             # Finally, post the message, reposition, and check events
    273             self.message_board.post("$PLAYERNAME moves " + d[offset] if offset in d.keys() and canmove else "$PLAYERNAME cannot move there!" if not canmove else "", self.gamedata)
    274             self.current_board.position_to_tile(self.player.tilepos)
    275             if canmove:
    276                 self.current_board.check_events_at_tilepos(self.player.tilepos)
    277                 self.pass_time()
    278 
    279     def pass_time(self, time_mod = 1, take_turn = True):
    280         """
    281         Passes time by 'time_mod', usually 1. If 'take_turn'
    282         is true, the other entities on the current board
    283         will move as well.
    284         """
    285 
    286         # First, add time and calculate into date string.
    287         self.old_gametime = self.gametime
    288         self.gametime += time_mod
    289         
    290         # Factor this time into the appropriate categories
    291         # TODO: Time should be defined externally to allow for custom time systems
    292         self.seconds = 10 * self.gametime
    293         self.minutes = self.seconds // 60
    294         self.hours = self.seconds // (60 * 60)
    295         self.days = self.seconds // (60 * 60 * 25)
    296         self.years = self.seconds // (60 * 60 * 25 * 351)
    297         self.days -= self.years * 351
    298         self.hours -= self.years * (25 * 351)
    299         self.hours -= self.days * 25
    300         self.minutes -= self.years * (60 * 25 * 351)
    301         self.minutes -= self.days * (60 * 25)
    302         self.minutes -= self.hours * 60
    303         self.seconds -= self.years * (60 * 60 * 25 * 351)
    304         self.seconds -= self.days * (60 * 60 * 25)
    305         self.seconds -= self.hours * (60 * 60)
    306         self.seconds -= self.minutes * 60
    307         self.months = self.days // 39
    308         self.days -= self.months * 39
    309 
    310         # Then, if take_turn is true, take a turn for the other entities.
    311         if self.state_mode in OVERHEAD_MODES and take_turn:
    312             self.current_board.get_playerpos()
    313             self.current_board.execute_npc_turns(time_mod)
    314 
    315     def derive_datestring(self):
    316         """
    317         Derive a datestring and a clockstring from 
    318         the current game time.
    319         """
    320         mnth = self.month_names[self.months]
    321         day_index = self.days // 13
    322         day_rem = self.days % 13
    323         if day_rem == 0:
    324             day_meas = ""
    325         else:
    326             day_meas = str(13 - day_rem) + " day" + str("s" if 13 - day_rem != 1 else "") + " ere the "
    327             if day_index < 2:
    328                 day_index += 1
    329             else:
    330                 day_index = 0
    331         self.datestring = day_meas + self.day_descriptors[day_index] + " of " + mnth
    332         self.clockstring = "Year " + str(self.years) + "  ]|[  " + str("0" if self.hours <= 9 else "") + str(self.hours) + ":" + str("0" if self.minutes <= 9 else "") + str(self.minutes) + ":" + str("0" if self.seconds <= 9 else "") + str(self.seconds) + " o'clock"
    333 
    334     def post_message(self, text):
    335         """
    336         This method is used by external objects to
    337         post messages.
    338         """
    339         self.message_board.post(text, self.gamedata)
    340 
    341     def trigger_battle(self, partydef):
    342         """
    343         Trigger a battle with the given party
    344         definition.
    345         """
    346         self.current_battle = battle.Battle(self, self.party, partydef)
    347         self.collect_game_data()
    348         self.post_message("Joe encounters $CURRENTBATTLEFORCES")
    349         self.switch_mode(STATE_MODES.Battle_Mode)
    350 
    351     def update_dynamic_ui(self, surface = None):
    352         """
    353         Update the dynamic portions of the UI.
    354         """
    355 
    356         # TODO: Lotsa hardcoding
    357         if surface != None:
    358 
    359             # To begin with, check if any elements in the dynamic UI need to be updated
    360             if self.old_gametime != self.gametime:
    361                 self.derive_datestring()
    362                 self.old_gametime = self.gametime
    363                 self.date_element = self.small_ui_font.render(self.datestring, False, (255, 255, 255))
    364                 self.date_element_pos = (696, 34)
    365                 self.clock_element = self.small_ui_font.render(self.clockstring, False, (255, 255, 255))
    366                 self.clock_element_pos = (696, 48)
    367             if self.old_location != self.current_board.display_name:
    368                 self.old_location = self.current_board.display_name
    369                 self.location_element = self.ui_font.render(self.current_board.display_name, False, (255, 255, 255))
    370                 self.location_element_pos = (696, 16)
    371 
    372             # After updating, blit the whole dynamic UI
    373             if self.date_element != None:
    374                 surface.blit(self.date_element, self.date_element_pos)
    375             if self.clock_element != None:
    376                 surface.blit(self.clock_element, self.clock_element_pos)
    377             if self.location_element != None:
    378                 surface.blit(self.location_element, self.location_element_pos)
    379 
    380             # Last is the party stat display
    381             x_mod = 0
    382             for c in self.party:
    383 
    384                 # First three meters
    385                 x2 = 0
    386                 for s in ("hp", "mp", "sp"):
    387                     if self.party[c][s] > self.party[c][s + "_mod"]:
    388                         yr = round(((self.party[c][s] - self.party[c][s + "_mod"]) / self.party[c][s]) * 38)
    389                         sq = pygame.Surface((8, yr))
    390                         sq.fill((205 if s == "hp" else 0, 205 if s == "sp" else 0, 205 if s == "mp" else 0))
    391                         surface.blit(sq, (83 + (x_mod * 137) + (x2 * 13), 587 + (38 - yr)))
    392                     x2 += 1
    393 
    394                 # XP meter
    395                 xpyr = round((self.party[c]["xp"] / calculate_xp_to_level(self.party[c]["level"])) * 38)
    396                 if xpyr > 0:
    397                     xpsq = pygame.Surface((8, xpyr))
    398                     xpsq.fill((205, 205, 0))
    399                     surface.blit(xpsq, (122 + (x_mod * 137), 587 + (38 - xpyr)))
    400 
    401                 # Character name
    402                 surface.blit(self.ui_font.render(self.party[c]["name"][0:min(len(self.party[c]["name"]), 14)], False, (0, 0, 0)), (15 + (x_mod * 137), 529))
    403 
    404                 # Character stats
    405                 x3 = 0
    406                 y3 = 0
    407                 for statpair in (("phs", "mag"), ("agi", "chr"), ["luk"]):
    408                     for st in statpair:
    409                         statmod = self.party[c][st + "_mod"]
    410                         if statmod > 0:
    411                             statstring = str(self.party[c][st]) + "(-" + str(statmod) + ")"
    412                         elif statmod < 0:
    413                             statstring = str(self.party[c][st]) + "(+" + str(-statmod) + ")"
    414                         else:
    415                             statstring = str(self.party[c][st])
    416                         rs = self.tiny_ui_font.render(statstring, False, (175, 20, 20) if statmod > 0 else (20, 235, 20) if statmod < 0 else (0, 0, 0))
    417                         rsr = rs.get_rect()
    418                         rsr.topright = (68 + (x_mod * 137) + (62 * x3), 633 + (24 * y3))
    419                         surface.blit(rs, rsr)
    420                         x3 += 1
    421                     x3 = 0
    422                     y3 += 1
    423 
    424                 # Move right to next character
    425                 x_mod += 1
    426 
    427     def collect_game_data(self):
    428         """
    429         Collect a bunch of info about the current
    430         state of the game and game objects. This can
    431         be safely called multiple times per frame.
    432         """
    433         # TODO: This will see regular expansion
    434         self.gamedata["$PLAYERNAME"] = self.player_name
    435         self.gamedata["$CURRENTBOARDNAME"] = self.current_board.display_name if self.current_board != None else "!NOWHERE!"
    436         self.gamedata["$CURRENTDUNGEONNAME"] = self.current_dungeon.display_name if self.current_dungeon != None else "!NODUNGEON!"
    437         self.gamedata["$CURRENTBATTLEFORCES"] = self.current_battle.get_enemy_forces() if self.current_battle != None else "!NOBATTLE!"
    438 
    439     def save_game(self, savename):
    440         """
    441         Save the current game state as a JSON file.
    442         """
    443         # First, determine what kind of location data we need to save
    444         if self.state_mode in OVERHEAD_MODES:
    445             at = {
    446                 "type" : "location",
    447                 "data" : {
    448                     "board" : self.current_board.name,
    449                     "cell" : self.player.cellpos,
    450                     "tile" : self.player.tilepos,
    451                 }
    452             }
    453         elif self.state_mode == STATE_MODES.Dungeon_Mode:
    454             at = {
    455                 "type" : "dungeon",
    456                 "data" : {
    457                     "dungeon" : self.current_dungeon.name,
    458                     "direction" : self.current_dungeon.direction,
    459                     "floor" : self.current_dungeon.current_floor,
    460                     "cell" : self.current_dungeon.current_cell,
    461                 }
    462             }
    463 
    464         # Next, get progress for various loaded objects
    465         dungeons = {}
    466         for d in self.loaded_dungeons:
    467             self.dungeons[d] = { "floor_factors" : self.loaded_dungeons[d].floor_factor_counters, "dungeon_flags" : self.loaded_dungeons[d].dungeon_flags }
    468 
    469         # Then, construct the savestate and write it to a json
    470         savestate = {
    471             "story" : self.active_story,
    472             "player" : self.player_name,
    473             "gametime" : self.gametime,
    474             "at" : at,
    475             "party" : self.party,
    476             "flags" : self.prog_flags,
    477             "dungeons" : dungeons,
    478             "newgame" : False
    479 
    480         }
    481         with open(os.path.join(SAVE_PATH, savename + ".json")) as sj: json.dump(savestate)
    482 
    483     def load_game(self, savename):
    484         """
    485         Load a save from JSON.
    486         """
    487         # Open the json and apply universal values
    488         if os.path.exists(os.path.join(SAVE_PATH, savename + ".json")):
    489             with open(os.path.join(SAVE_PATH, savename + ".json")) as lj: saveraw = json.load(lj)
    490             self.active_scenario = saveraw["scenario"]
    491             self.player_name = saveraw["player"]
    492             self.party = saveraw["party"]
    493 
    494             # Load savedata values into already-loaded resources
    495             for dun in self.loaded_dungeons:
    496                 self.loaded_dungeons[dun].load_from_savedata(saveraw["dungeons"][dun])
    497 
    498             # Load a board
    499             if saveraw["at"]["type"] == "location":
    500                 self.switch_board(saveraw["at"]["data"]["board"], tuple(saveraw["at"]["data"]["cell"]))
    501                 self.spawn_player(saveraw["party"][saveraw["player"]], tuple(saveraw["at"]["data"]["cell"]), tuple(saveraw["at"]["data"]["tile"]))
    502                 self.switch_mode(STATE_MODES.Overworld_Mode)
    503 
    504             # Load a dungeon
    505             elif saveraw["at"]["type"] == "dungeon":
    506                 self.switch_dungeon(saveraw["at"]["data"]["dungeon"], False, saveraw["at"]["data"]["direction"], saveraw["at"]["data"]["floor"], tuple(saveraw["at"]["data"]["cell"]))
    507 
    508             # Pass time up
    509             self.pass_time(saveraw["gametime"], False)
    510         else:
    511             return 1
    512 
    513     def shift_frames(self, framerate = FRAMERATE):
    514         """
    515         Shift to the next frame of the game at the specified
    516         framerate.
    517         """
    518         self.frame_clock.tick(framerate)
    519 
    520     def update_game(self):
    521         """
    522         Update the game elements, the screen, and any other
    523         objects.
    524         """
    525         # First, fill the screen and viewport
    526         self.screen.fill((33, 33, 33))
    527         self.viewport.fill((0, 0, 0))
    528 
    529         # Next, update the interface
    530         self.interface.update_interface()
    531 
    532         # Next, update and draw objects
    533         self.screen.blit(self.viewport, self.viewport_rect)
    534         if self.state_mode in OVERHEAD_MODES:
    535             self.current_board.update_board(self.screen, self.viewport_rect)
    536             if self.player != None:
    537                 self.player.update(self.screen, self.viewport_rect)
    538         elif self.state_mode in PARTY_MODES:
    539             if self.state_mode == STATE_MODES.Dungeon_Mode:
    540                 self.current_dungeon.update_dungeon(self.screen)
    541             elif self.state_mode == STATE_MODES.Battle_Mode:
    542                 self.current_battle.update_battle(self.screen)
    543             self.party_group.update(self.screen)
    544         self.ui_group.update(self.screen)
    545         self.message_board.update_message_board(self.screen)
    546         self.update_dynamic_ui(self.screen)
    547 
    548         # Next, get info about the game
    549         self.collect_game_data()
    550 
    551         # Last, update the screen
    552         pygame.display.update()
    553 
    554     def quit_game(self):
    555         """
    556         Safely move the game into a quitting state.
    557         """
    558         self.on = False
    559 
    560     def mainloop(self):
    561         """
    562         The main game loop. Run once to start the game. Also
    563         safely handles shutting down if the game is turned 
    564         off.
    565         """
    566         while self.on:
    567             self.shift_frames()
    568             self.interface.handle_events()
    569             self.update_game()
    570         pygame.quit()