Coverage for topdownengine/game.py: 68%
65 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-24 02:27 +0000
« 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
4import pygame as pg
5from .scenes import GameplayScene, BaseScene
7class Game:
8 """Acts as the central core of the game and manages the core loop and gamestate.
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 """
28 VALID_EXTRA_FEATURES = {"resize",}
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.
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 )
62 # Initialize pygame-ce
63 pg.init()
65 # Initialize display
66 if window_icon_path is not None:
67 pg.display.set_icon(pg.image.load(window_icon_path))
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
76 # Clock + FPS
77 self.clock = pg.time.Clock()
78 self.fps = fps
80 # Is Running Boolean Flag
81 self.is_running = True
83 # Debug Boolean Flag
84 self.debug = debug
86 # GameObject Group (Import Here to Prevent Circular Import)
87 from .game_object import GameObjectGroup
88 self.game_object_group = GameObjectGroup()
90 # Game Speed Percentage
91 self.game_speed_percentage = 1
93 # Set Target Scale
94 from .game_object import GameObject
95 GameObject.set_scale(target_scale, self)
97 # Camera (Import Here to Prevent Circular Import)
98 from .camera import Camera
99 self.camera = Camera()
101 # Background Color
102 self.bg_color = (255, 255, 255)
104 # Scenes
105 self.scenes = {
106 "gameplay": GameplayScene(self)
107 }
108 self.active_scene_key = "gameplay"
110 # Accumalated Deltatime
111 self._accumulated_deltatime = 0
113 @property
114 def active_scene(self) -> BaseScene:
115 "The active scene."
116 return self.scenes[self.active_scene_key]
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)
130 def set_target_scale(self, target_scale: int) -> float:
131 """Sets the target scale.
133 Args:
134 target_scale (int): The new target scale for the original screen dimensions.
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)
142 def update(self, dt: float) -> None:
143 """Perform the update loop.
145 Args:
146 dt (float): The deltatime.
147 """
148 # Convert dt from milliseconds to seconds.
149 dt = dt / 1000
151 # Add a cap to one frame's dt to prevent infinite lag spirals
152 dt = min(dt, 6 / self.fps)
154 # Add processed dt to accumulater
155 self._accumulated_deltatime += dt
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)
164 # Subtract from accumulated deltatime in seconds.
165 self._accumulated_deltatime -= 1 / self.fps
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()
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()