Refactoring Game Logic: From Messy Classes to Pythonic Patterns
Overview
Software evolution often requires moving beyond code that simply works to code that is maintainable and scalable. In this second phase of refactoring a game, the focus shifts from user interface separation to streamlining internal logic. By applying principles like the and utilizing features like and , we can transform a clunky, class-heavy architecture into a lean, functional design. This approach matters because it reduces cognitive load for developers and makes the codebase resilient to future changes.
Prerequisites
To follow this tutorial, you should have a solid grasp of 3.10 or later. Familiarity with Object-Oriented Programming (OOP) concepts, specifically classes and inheritance, is essential. You should also understand basic like dictionaries and tuples, and have a surface-level understanding of .
Key Libraries & Tools
- : A standard library module that reduces boilerplate by automatically generating special methods like
__init__and__repr__. - : Provides support for enumeration types, allowing for clearer and more type-safe constant definitions.
- : Used for generating CPU moves through the
random.choicemethod. - : Utilized for providing hints like
Optionalto clarify function return types.
Refactoring the Rules Engine
The original code used a bulky Rules class to determine winners. We can simplify this by replacing the class with a constant dictionary and a single function. By mapping a tuple of moves to a verb (e.g., "crushes" or "vaporizes"), we eliminate redundant data storage.
RULES = {
(Entity.ROCK, Entity.SCISSORS): "crushes",
(Entity.SPOCK, Entity.ROCK): "vaporizes",
# ... other rules
}
def get_winner(e1: Entity, e2: Entity) -> tuple[Entity | None, str]:
if e1 == e2:
return None, "It's a tie"
if (e1, e2) in RULES:
return e1, f"{e1} {RULES[(e1, e2)]} {e2}"
# Logic for reverse check here
This functional approach moves the "tie" responsibility out of the main game loop and into the rule logic where it belongs. It also allows the use of modern type hinting (like | for union types) instead of importing Union or Tuple from the typing module.
Implementing Dataclasses and Dependency Injection
The Game class initializer was previously overloaded with setup logic. By converting it to a , we define the state clearly and use to pass in the Scoreboard and UI objects. This makes the code easier to test since we can now inject "mock" versions of these objects.
from dataclasses import dataclass
@dataclass
class Game:
scoreboard: Scoreboard
ui: UI
player_name: str
cpu_name: str = "CPU"
def play(self, max_rounds: int = 5):
# Logic moved from __init__ to here
self.scoreboard.register_player(self.player_name)
self.scoreboard.register_player(self.cpu_name)
Adhering to the Law of Demeter
To avoid "reaching through" objects—such as the Game class directly modifying scoreboard.points—we implement helper methods. A win_round method in the Scoreboard class hides the implementation details of how scores are stored. Similarly, adding a to_display method to the Scoreboard ensures that the Game class doesn't need to know the internal structure of the points dictionary to show results.
Syntax Notes: String-Based Enums
While integers are common in , they are brittle if the order changes or if data is persisted in a database. Switching to string-based values provides more control over the output and improves readability when debugging.
class Entity(str, Enum):
ROCK = "Rock"
PAPER = "Paper"
def __str__(self):
return self.value
Tips & Gotchas
- The Underscore Pattern: When iterating through a loop where the index isn't needed (like a round counter), use
_to signal to other developers that the variable is intentionally unused. - Input Validation: When converting string Enums back to list indices for user selection, always subtract one from the user's input to align with zero-based indexing.
- Coupling Balance: While moving the
displaylogic into theScoreboardavoids a violation, it introduces a slight coupling between theScoreboardand theUIprotocol. This is usually an acceptable trade-off for cleaner high-level logic.
- 15%· libraries
- 15%· libraries
- 15%· programming languages
- 10%· concepts
- 10%· concepts
- Other topics
- 35%

Refactoring a Rock Paper Scissors Lizard Spock Game // Part 2
WatchArjanCodes // 25:15
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!