Refactoring Python: Building a Scalable Game Engine Architecture
Overview of Modern Refactoring
Code refactoring often feels like untangling a massive knot. In this exploration, we tackle a Tower Defense Game written in Python that, while functional and entertaining, suffers from common architectural pitfalls: global variables, wildcard imports, and extreme coupling. These issues make the code brittle and nearly impossible to test or extend. By restructuring the logic into a generic game engine, we can separate the core loop mechanics from the specific rules of a tower defense scenario, creating a more professional and maintainable codebase.
Prerequisites for Architectural Improvement

To follow this refactor, you should possess a solid grasp of Python fundamentals, including classes and inheritance. Familiarity with Tkinter for basic GUI rendering is helpful, though the goal is to make the engine agnostic of specific libraries. You should also understand the concept of a game loop—the continuous cycle of updating logic and rendering frames that keeps a simulation running.
Key Libraries & Tools
- Tkinter: Python's standard GUI library used here for the canvas and main event loop.
- Enum: Part of the standard library used to define discrete game states.
- Typing: Used for structural subtyping, allowing us to define interfaces for game objects without strict inheritance.
- Tab9: An AI-driven code completion tool that assists in writing boilerplate and suggesting patterns during the refactoring process.
Code Walkthrough: Modularizing the Engine
1. The Generic Game Class
We start by stripping the Tower Defense Game of its specific logic to create a reusable Game base class. This class manages the window, canvas, and the timing of the loop.
import tkinter as tk
class Game:
def __init__(self, title: str, width: int, height: int, time_step: int = 50):
self.root = tk.Tk()
self.root.title(title)
self.canvas = tk.Canvas(self.root, width=width, height=height)
self.canvas.pack()
self.time_step = time_step
self.running = False
self.objects = []
def add_object(self, obj):
self.objects.append(obj)
def _run(self):
if not self.running: return
self.update()
self.paint()
self.root.after(self.time_step, self._run)
def run(self):
self.running = True
self._run()
self.root.mainloop()
By moving the title, width, and height into the initializer, we remove the need for global constants. The _run method handles the recursion safely, checking the running boolean before scheduling the next frame.
2. Defining Game Objects with Protocols
Instead of forcing every object to inherit from a massive base class, we use Typing to define what a game object looks like. This is structural subtyping; if a class has update and paint methods, the engine accepts it.
from typing import Protocol
class GameObject(Protocol):
def update(self) -> None: ...
def paint(self, canvas: tk.Canvas) -> None: ...
3. Decoupling with Game States
Direct communication between objects—like a button telling a wave generator to start—creates spaghetti code. We solve this by introducing an Enum to track the state of the Tower Defense Game.
from enum import Enum, auto
class GameState(Enum):
IDLE = auto()
WAITING_FOR_SPAWN = auto()
SPAWNING = auto()
# In the specific Tower Defense subclass
class TowerDefenseGame(Game):
def __init__(self, ...):
super().__init__(...)
self.state = GameState.IDLE
def set_state(self, new_state: GameState):
self.state = new_state
Now, the button simply updates the state to WAITING_FOR_SPAWN. The WaveGenerator watches this state during its own update cycle. Neither object needs to know the other exists.
Syntax Notes: Pythonic Enhancements
Python offers unique syntax that can significantly clean up conditional logic. For instance, coordinate checking for buttons can be written as a chained comparison: self.x <= x <= self.x2. This mimics mathematical notation and is far more readable than multiple and statements. Furthermore, in Python 3, super() calls no longer require explicit class names, and classes do not need to inherit from object explicitly. These small changes reduce boilerplate and modernize the feel of the codebase.
Practical Examples
This engine architecture is applicable far beyond tower defense. Any simulation requiring a fixed update rate—such as a physics engine, a cellular automata visualization (like Conway's Game of Life), or a simple RPG—can use the Game base class. By swapping the GameObject list, you can change the entire behavior of the application without touching the core loop logic. This is the foundation of professional game development: the engine provides the "how," while the game objects provide the "what."
Tips & Gotchas
One common mistake when refactoring is neglecting the order of operations. In the paint method, the order in which you iterate through self.objects determines the Z-index (layering) on the screen. Always add background elements like the map first, and UI elements like the mouse cursor last.
Another "gotcha" involves modifying lists while iterating over them. If a projectile removes itself from the game during its update call, a standard index-based loop will skip the next item or crash. Use a list comprehension or a copy of the list for safer iteration: for obj in self.objects[:] or use a specialized removal queue to be processed at the end of the frame.
- Python
- 23%· languages
- Tower Defense Game
- 23%· products
- Tkinter
- 15%· libraries
- Typing
- 15%· libraries
- Enum
- 8%· libraries
- Other topics
- 15%

Refactoring A Tower Defense Game In Python // CODE ROAST
WatchArjanCodes // 36:49
On this channel, I post videos about programming and software design to help you take your coding skills to the next level. I'm an entrepreneur and a university lecturer in computer science, with more than 20 years of experience in software development and design. If you're a software developer and you want to improve your development skills, and learn more about programming in general, make sure to subscribe for helpful videos. I post a video here every Friday. If you have any suggestion for a topic you'd like me to cover, just leave a comment on any of my videos and I'll take it under consideration. Thanks for watching!