Let’s check it out!
Simpsons Trivia
Simpsons Trivia is a retro fan-made quiz game built for the 2018 SMS Power! Coding Competition originally targeting the Sega Master System. The game was ported to the Sega Genesis, Windows, Android and iOS.
Simpsons Trivia
Simpsons Trivia port is Python 3.11.11 compatible. Launch PyCharm | New Project. Enter the following info:
| Location: | ~/SimpsonsTrivia |
| Interpreter type: | uv |
| Python version: | 3.11 |
| Path to uv: | ~/.local/bin/uv |
PyCharm should setup UV virtual environment and configure Python interpreter if not then enter commands:
uv venv --python 3.11 source .venv/bin/activate # OR .\.venv\Scripts\activate which python `which python` --version # Python 3.11.11 |
In the PyCharm Terminal | Enter the following commands for UV to install and sync package dependencies:
uv lock |
uv sync |
Dissection
Before jumping into the full code dissection let's document some of the support folders with their functions:
Static
Includes globally loaded game resource assets and colors definitions. Constants store core configuration with enumerations defining all strongly-typed categories as Python enums and globalize has UI text information.
Objects
Includes game data models to represent configuration, questions and text elements to rendered on-screen.
Inputs
Includes low-level input layer for game engine to convert raw PyGame input into cleaner game input APIs.
CODE
SimpsonsTrivia.py
Main game entry point similar to Program.cs: creates Game -> runs the [in]finite game loop until shutdown:
| SimpsonsTrivia.py |
def main(): try: game = AnGame() game.Run() except KeyboardInterrupt: game.Exit() finally: game.ShutDown() |
AnGame.py
Builds game infrastructure and acts as an intermediary between the game bootstrap and the service registry which delegates game init, load, update and draw game loop methods to actual game implementation code.
| AnGame.py |
class AnGame: def __init__(self): self.running: bool = True build_game() def Run(self): self.Initialize() self.LoadContent() while self.running: MyGame.Manager.EventManager.ProcessEvents() if MyGame.Manager.EventManager.QuitRequested: self.Exit() deltaTime: int = MyGame.Manager.ClockManager.Update() self.Update(deltaTime) self.Draw() |
MyGame.py
Delegates game init, load, update + draw game static methods to actual management implementation code.
| MyGame.py |
class MyGame: class _ManagerFacade: def __getattr__(self, name): return ServiceRegistry.get(name) Manager = _ManagerFacade() |
Managers
A Manager is responsible for a particular game system or service. It owns data or functionality that is shared across multiple parts of the game. BaseManager is a foundational class, common toolbox and system helper.
LogManager
Simple wrapper around Python built-in logging system that standardizes how the game prints info messages.
| LogManager.py |
class LogManager: @staticmethod def Initialize(): level = logging.INFO output: str = "[%(asctime)s] [%(module)s] %(message)s" datefmt: str = '%Y-%m-%d %H:%M:%S' logging.basicConfig(level=level, format=output, datefmt=datefmt) @staticmethod def Write(msg: str): logging.info(msg, stacklevel=2) |
RandomManager
Centralised random number generator used by the game to ensure randomness is consistent across system.
| RandomManager.py |
class RandomManager: def Initialize(self, seed: int = None): if seed is None: seed = int(time.time_ns()) & 0xFFFF self._random = random.Random(seed) def Next(self, *args) -> int: if len(args) == 1: max_val: int = args[0] return self._random.randrange(max_val) elif len(args) == 2: min_val: int = args[0] max_val: int = args[1] return self._random.randrange(min_val, max_val) |
ConfigManager
External game configuration loader and runtime object responsible for all global gaming settings and access.
| ConfigManager.py |
class ConfigManager: def LoadContent(self): file: Path = self.configRoot / "GlobalConfig.toml" text = file.read_text() data: ConfigData = tomllib.loads(text) data[ScreenType.__name__] = ScreenType[data[ScreenType.__name__]] data[OptionType.__name__] = OptionType[data[OptionType.__name__]] data[DifficultyType.__name__] = DifficultyType[data[DifficultyType.__name__]] self.ConfigData = ConfigData(**data) |
EventManager
Event polling game system collecting Pygame events each frame and here is used mainly to track quit game.
| EventManager.py |
class EventManager: def __init__(self): self.QuitRequested = False self.Events = [] def ProcessEvents(self): self.Events.clear() for event in pygame.event.get(): self.Events.append(event) if event.type == pygame.QUIT: self.QuitRequested = True if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE: self.QuitRequested = True |
ClockManager
Very small but important system manager responsible for controlling the game timing and inject FPS pacing.
| ClockManager.py |
class ClockManager: def Initialize(self): self.clock: pygame.time.Clock = pygame.time.Clock() self.fps = MyGame.Manager.ConfigManager.ConfigData.FPS def Update(self) -> int: deltaTime: int = self.clock.tick(self.fps) return deltaTime |
ScreenManager
State machine and game navigation system controlling screen activation and handling all screen transitions.
| ScreenManager.py |
class ScreenManager:
def Initialize(self):
self._import_all_screens()
screen_classes = self._build_screen_map()
self.screens: dict[ScreenType, BaseScreen] = {
st: cls()
for st, cls in screen_classes.items()
}
for screen in self.screens.values():
screen.Initialize()
def Update(self, deltaTime: int):
if self.currScreen != self.nextScreen:
self.currScreen = self.nextScreen
self.screens[self.currScreen].LoadContent()
tempScreen = self.screens[self.currScreen].Update(deltaTime)
if tempScreen:
self.nextScreen = tempScreen
def Draw(self):
MyGame.Manager.DisplayManager.Clear(self.color)
self.screens[self.currScreen].Draw()
MyGame.Manager.DisplayManager.Present(self.color)
def _import_all_screens(self):
for _, module_name, _ in pkgutil.iter_modules(Screens.__path__):
if module_name == BaseScreen.__name__:
continue
path_name: str = f"{const.GAME_DIRECTORY}.{ENGINE_SCREENS}.{module_name}"
importlib.import_module(path_name)
def _build_screen_map(self) -> dict[ScreenType, type[BaseScreen]]:
screen_map: dict[ScreenType, type[BaseScreen]] = {}
for screen_type in ScreenType:
class_name = f"{screen_type.name}{ENGINE_SCREEN}"
path_name: str = f"{const.GAME_DIRECTORY}.{ENGINE_SCREENS}.{class_name}"
module = importlib.import_module(path_name)
cls = getattr(module, class_name)
screen_map[screen_type] = cls
return screen_map
|
Resources
| ContentManager | Asset loading game system responsible for loading all game content into memory |
| DisplayManager | Low-level rendering window mgt system controlling how game appears onscreen |
| GraphicsManager | Rendering abstraction layer wrapping PyGame drawing calls as usuable functions |
| ResolutionManager | Co-ordinate transformation system for game used for all full screen conversions |
Miscellaneous
| ImageManager | Character rendering system game system responsible for image content rendering |
| SoundManager | Audio game sub-system responsible loading and playing all music + sound effects |
| SpriteManager | Central UI sprite renderer and game icons mapper using spritesheet for rendering |
| TextManager | Central UI text rendering and layout-from-file system handling all text definitions |
Finally, the QuestionManager which is the core gameplay system loading questions from files, manages quiz state, randomizes question and answers, tracks difficulty, and handles all of the question / answer rendering.
Tests
The majority of Managers have corresponding Test to code in isolation. Terminal: execute command pytest:
Screens
Includes BaseScreen to define common interface for all game screens thus ScreenManager lifecycle invokes each screen uniformly and keeps game state transitions clean and decoupled from individual screen logic.
| BaseScreen.py |
class BaseScreen(ABC): @abstractmethod def Initialize(self) -> None: self.Timer: int = None @abstractmethod def LoadContent(self) -> None: self.Timer = 0 @abstractmethod def Update(self, deltaTime: int) -> ScreenType | None: return None @abstractmethod def Draw(self) -> None: pass |
Engine
In order to fully port C#/.NET paradigms to Python we need helper scripts to register and bootstrap game:
| ServiceRegistry | Simple service locator central hub which connects all game systems together |
| Bootstrap | Automatic game management discovery using reflection via dynamic imports |
| Bootstrap.py |
def build_game():
ServiceRegistry.clear()
base_dir = Path(__file__).resolve().parent.parent
managers_path = base_dir / const.GAME_DIRECTORY / const.ENGINE_MANAGERS
for file in managers_path.glob(f"*{const.ENGINE_MANAGER}.py"):
if file.stem == "__init__":
continue
module_name = file.stem
path_name = f"{const.GAME_DIRECTORY}.{const.ENGINE_MANAGERS}.{module_name}"
module = importlib.import_module(path_name)
manager_class = getattr(module, module_name)
ServiceRegistry.register(
manager_class.__name__,
manager_class()
)
return ServiceRegistry
|
PLAY
In PyCharm hit F5 to run Python code or Terminal enter uv run SimpsonsTrivia.py to play Simpsons Trivia!
CONFIG
Update GlobalConfig.toml configuration settings like set CheatMode or FullScreen to true vary the game play!
| GlobalConfig.toml |
FPS=50 ScreenType="Splash" DifficultyType="Easy" SoundEnable=true CheatMode=true RandomQuestions=true RandomAnswers=true Fullscreen=true |
Summary
To summarize, we have now completed to port Simpsons Trivia video game from C#/.NET to Python using PyGame. This exercise provides great foundation to further explore Farama Foundation Gymnasium project in which Classic Control, ToyText and Box2D environments are all implemented using PyGame. Experience gained here should provide solid grounding for any future work in Reinforcement Learning research space!


























