Refactoring Python: Clean Game Logic and Class Design in Battleship

Overview

Refactoring isn't just about making code look pretty; it's about making it maintainable and clear. In this second phase of our

refactor, we focus on moving beyond basic input handling to address the structural decay that often plagues growing projects. By separating the board's internal representation from the game's orchestration logic, we create a system where each component has one job. This approach eliminates the "God Class" problem where a single object knows too much about everything, making the game easier to scale, test, and debug.

Prerequisites

To get the most out of this tutorial, you should be comfortable with

basics, specifically classes and methods. You should understand the difference between instance variables and local variables, and have a passing familiarity with list comprehensions and the modulo operator.

Key Libraries & Tools

  • Python 3.x: The core programming language used for the implementation.
  • Random Module: A standard
    Python
    library used to generate the hidden ship's coordinates.
  • List Comprehensions: A concise
    Python
    syntax used for initializing the game grid efficiently.
Refactoring Python: Clean Game Logic and Class Design in Battleship
Refactoring a Battleship Game in Python // Code Roast Part 2 of 2

Moving Magic Numbers to Constants

Hardcoded values, or "magic numbers," are a maintenance nightmare. If you decide to change your board from 5x5 to 10x10, you don't want to hunt through twenty lines of code to update every instance of the number five. We start by defining clear constants at the top of our module.

GUESSES_COUNT = 5
BOARD_SIZE_X = 5
BOARD_SIZE_Y = 5

HIDDEN = "O"
SHIP = "S"
GUESS = "X"

By using uppercase naming conventions, we signal to other developers that these values are configuration points, not variables that should change during runtime. This immediately makes the code more readable—seeing SHIP is far more descriptive than seeing a random string "S" buried in an if statement.

Encapsulating the BattleshipBoard

The original code mixed game rules with grid management. We solve this by creating a dedicated BattleshipBoard class. This class acts as the single source of truth for the game state. Instead of the game logic manually checking nested lists, it asks the board questions like "Is this a ship?" or "Has this spot been guessed?"

class BattleshipBoard:
    def __init__(self, size_x: int, size_y: int):
        self.grid = [[HIDDEN for _ in range(size_x)] for _ in range(size_y)]
        self.ship_row = random.randint(0, size_x - 1)
        self.ship_col = random.randint(0, size_y - 1)
        self.grid[self.ship_row][self.ship_col] = SHIP

    def is_ship(self, row: int, col: int) -> bool:
        return self.grid[row][col] == SHIP

    def to_string(self, show_ship: bool = False) -> str:
        rows_string = []
        for row in self.grid:
            row_repr = [HIDDEN if col == SHIP and not show_ship else col for col in row]
            rows_string.append(" ".join(row_repr))
        return "\n".join(rows_string)

The to_string method is a key design choice. By allowing a show_ship toggle, we separate the internal data (the actual grid) from the visual representation (what the player sees). This keeps the logic clean and avoids the need for maintaining two separate "visible" and "hidden" boards.

Simplifying Game Logic with Math

Managing player turns often involves messy index tracking. We can replace complex conditional blocks with simple math using the modulo operator (%). This allows us to rotate through any number of players seamlessly within a single while loop.

def play_game(player_count: int, board: BattleshipBoard):
    total_guesses = 0
    max_guesses = GUESSES_COUNT * player_count

    while total_guesses < max_guesses:
        current_player = (total_guesses % player_count) + 1
        # ... turn logic ...
        total_guesses += 1

This math ensures that current_player always cycles correctly (1, 2, 1, 2 for a two-player game) without needing to manually reset a counter. It’s a robust pattern that reduces the surface area for bugs.

Syntax Notes

  • List Comprehensions: Used [[HIDDEN for _ in range(x)] for _ in range(y)] to avoid reference copying issues that occur with simple list multiplication.
  • Double Slash Division (//): Used for integer division to ensure guess counts remain whole numbers.
  • Dunder Methods: We avoided __str__ in favor of to_string because we needed to pass the show_ship argument, which standard string conversion doesn't support.

Tips & Gotchas

Avoid the temptation to put print statements inside your core logic classes. Notice how BattleshipBoard returns strings but doesn't print them. This makes the class portable; you could theoretically use the same board class in a web app or a GUI without changing a line of code because it doesn't care how its data is displayed.

Refactoring Python: Clean Game Logic and Class Design in Battleship

Fancy watching it?

Watch the full video and context

4 min read