Refactoring Game Logic: From Messy Classes to Pythonic Patterns

ArjanCodes////4 min read

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.choice method.
  • : Utilized for providing hints like Optional to 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 display logic into the Scoreboard avoids a violation, it introduces a slight coupling between the Scoreboard and the UI protocol. This is usually an acceptable trade-off for cleaner high-level logic.
Topic DensityMention share of the most discussed topics · 20 mentions across 12 distinct topics
15%· libraries
15%· libraries
15%· programming languages
10%· concepts
10%· concepts
Other topics
35%
End of Article
Source video
Refactoring Game Logic: From Messy Classes to Pythonic Patterns

Refactoring a Rock Paper Scissors Lizard Spock Game // Part 2

Watch

ArjanCodes // 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!

What they talk about
AI and Agentic Coding News
Who and what they mention most
Python
33.3%5
Python
20.0%3
Python
20.0%3
Pydantic
13.3%2
4 min read0%
4 min read