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 Rock Paper Scissors Lizard Spock game, the focus shifts from user interface separation to streamlining internal logic. By applying principles like the Law of Demeter and utilizing Python features like Dataclasses and Enums, 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 Python 3.10 or later. Familiarity with Object-Oriented Programming (OOP) concepts, specifically classes and inheritance, is essential. You should also understand basic Data Structures like dictionaries and tuples, and have a surface-level understanding of Dependency Injection. Key Libraries & Tools * **Dataclasses**: A standard library module that reduces boilerplate by automatically generating special methods like `__init__` and `__repr__`. * **Enums**: Provides support for enumeration types, allowing for clearer and more type-safe constant definitions. * **Random**: Used for generating CPU moves through the `random.choice` method. * **Typing**: 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. ```python 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 Python 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 Dataclass, we define the state clearly and use Dependency Injection 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. ```python 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 Auto integers are common in Enums, 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. ```python 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 Law of Demeter 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.
Rock Paper Scissors Lizard Spock
Games
- Apr 15, 2022
- Apr 8, 2022