Sunday, November 15, 2026

PyGame Programming Sample

In the previous post, we checked out PyGame Programming Setup. PyGame is a free and open-source cross platform library for the development of video games using Python. Now that we have the building blocks to implement PyGame APIs we're in excellent position to port an existing video game to Python using PyGame!

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
Finally, script MyGame.pyi type stub to provide code navigation and autocompletion for coding in PyCharm!

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!