Coverage for topdownengine/game.py: 68%

65 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-24 02:27 +0000

1# Copyright (c) 2026 Shaurya Sharma 

2# SPDX-License-Identifier: MIT 

3 

4import pygame as pg 

5from .scenes import GameplayScene, BaseScene 

6 

7class Game: 

8 """Acts as the central core of the game and manages the core loop and gamestate. 

9  

10 Attributes: 

11 screen (pygame.Surface): The primary display Surface. 

12 is_running (bool): Boolean flag to control execution. 

13 clock (pygame.time.Clock): Controls framerate and handles deltatime. 

14 fps (int): Integer that controls how much FPS the Game should have. 

15 game_object_group (GameObjectGroup): Stores all GameObjects. 

16 game_speed_percentage (float): Coefficient for deltatime, ranging from `0` to `1`. 

17 debug (bool): If `True`, debug rendering will be enabled. 

18 target_scale (int): The target scale for the original window size. 

19 og_width (int): Original window width. 

20 extra_features (list[str]): List of extra features to add at runtime. You MUST set it during instantiation. 

21 camera (Camera): Camera object to use when rendering. 

22 bg_color (pygame.typing.ColorLike): Color to fill the screen with at the start of every draw cycle. 

23 scenes (dict[str, BaseScene]): Dictionary of all scene objects. 

24 active_scene_key (str): The dictionary key for the current active scene. 

25 active_scene (BaseScene): The active scene. 

26 """ 

27 

28 VALID_EXTRA_FEATURES = {"resize",} 

29 

30 def __init__( 

31 self, 

32 screen_width: int, 

33 screen_height: int, 

34 window_title: str="pygame-topdownengine", 

35 window_icon_path: str|None=None, 

36 fps: int=60, 

37 debug: bool=False, 

38 target_scale: int=1, 

39 extra_features: list[str]=[] 

40 ) -> None: 

41 """Initialize the GameObject. 

42  

43 Args: 

44 screen_width (int): The initial screen width. 

45 screen_height (int): The initial screen height. 

46 window_title (str): The window title. Defaults to "pygame-topdownengine". 

47 window_icon_path (str): The window icon path. Defaults to None. 

48 fps (int): Integer that controls how much FPS the Game should have. 

49 debug (bool): If `True`, debug rendering will be enabled. Defaults to False. 

50 target_scale (int): The target scale for the original window size. Defaults to 1. 

51 extra_features (list[str]): List of extra features to add at runtime. You MUST set it during instantiation. Defaults to []. 

52 """ 

53 # Enabled features 

54 self.extra_features = extra_features 

55 for item in extra_features: 

56 if item not in self.VALID_EXTRA_FEATURES: 

57 raise ValueError( 

58 f"'{item}' is not a valid extra feature and does nothing. " 

59 f"Please choose from: {list(self.VALID_EXTRA_FEATURES)}" 

60 ) 

61 

62 # Initialize pygame-ce 

63 pg.init() 

64 

65 # Initialize display 

66 if window_icon_path is not None: 

67 pg.display.set_icon(pg.image.load(window_icon_path)) 

68 

69 self.screen = pg.display.set_mode( 

70 (screen_width, screen_height), 

71 pg.RESIZABLE if "resize" in extra_features else 0 

72 ) 

73 pg.display.set_caption(window_title) 

74 self.og_width = screen_width 

75 

76 # Clock + FPS 

77 self.clock = pg.time.Clock() 

78 self.fps = fps 

79 

80 # Is Running Boolean Flag 

81 self.is_running = True 

82 

83 # Debug Boolean Flag 

84 self.debug = debug 

85 

86 # GameObject Group (Import Here to Prevent Circular Import) 

87 from .game_object import GameObjectGroup 

88 self.game_object_group = GameObjectGroup() 

89 

90 # Game Speed Percentage 

91 self.game_speed_percentage = 1 

92 

93 # Set Target Scale 

94 from .game_object import GameObject 

95 GameObject.set_scale(target_scale, self) 

96 

97 # Camera (Import Here to Prevent Circular Import) 

98 from .camera import Camera 

99 self.camera = Camera() 

100 

101 # Background Color 

102 self.bg_color = (255, 255, 255) 

103 

104 # Scenes 

105 self.scenes = { 

106 "gameplay": GameplayScene(self) 

107 } 

108 self.active_scene_key = "gameplay" 

109 

110 # Accumalated Deltatime 

111 self._accumulated_deltatime = 0 

112 

113 @property 

114 def active_scene(self) -> BaseScene: 

115 "The active scene." 

116 return self.scenes[self.active_scene_key] 

117 

118 def handle_events(self) -> None: 

119 "Handle events." 

120 for event in pg.event.get(): 

121 if event.type == pg.QUIT: 

122 self.is_running = False 

123 break 

124 elif event.type == pg.VIDEORESIZE: 

125 # We import GameObject in handle_events to prevent a circular import. 

126 from .game_object import GameObject 

127 GameObject.set_scale(self.target_scale, self) 

128 self.active_scene.handle_event(event) 

129 

130 def set_target_scale(self, target_scale: int) -> float: 

131 """Sets the target scale. 

132  

133 Args: 

134 target_scale (int): The new target scale for the original screen dimensions. 

135  

136 Returns: 

137 float: The new scale for the current window size. 

138 """ 

139 self.target_scale = target_scale 

140 return self.target_scale * (self.screen.width / self.og_width) 

141 

142 def update(self, dt: float) -> None: 

143 """Perform the update loop. 

144  

145 Args: 

146 dt (float): The deltatime. 

147 """ 

148 # Convert dt from milliseconds to seconds. 

149 dt = dt / 1000 

150 

151 # Add a cap to one frame's dt to prevent infinite lag spirals 

152 dt = min(dt, 6 / self.fps) 

153 

154 # Add processed dt to accumulater 

155 self._accumulated_deltatime += dt 

156 

157 # Execute the update logic in steps of 1 / FPS 

158 while self._accumulated_deltatime >= 1 / self.fps: 

159 # Use 1000 / self.fps for update functions because 

160 # they still use milliseconds. 

161 self.active_scene.update(1000 / self.fps * self.game_speed_percentage) 

162 self.camera.update(1000 / self.fps * self.game_speed_percentage) 

163 

164 # Subtract from accumulated deltatime in seconds. 

165 self._accumulated_deltatime -= 1 / self.fps 

166 

167 def render(self) -> None: 

168 "Render everything to the screen." 

169 self.screen.fill(self.bg_color) 

170 self.active_scene.render() 

171 pg.display.flip() 

172 

173 def run(self) -> None: 

174 "Run the game loop." 

175 while self.is_running: 

176 dt = self.clock.tick(self.fps) 

177 self.handle_events() 

178 self.update(dt) 

179 self.render() 

180 pg.quit() 

181 exit()