6
\$\begingroup\$

I am writing code to randomly place ships on a board in Python. I need help reducing the amount of Global variables I have in this code. I have thought about using a Ship class here but I'm not very comfortable with OOPS yet.

I have also thought about passing these global variables by as function arguments but that would lead to a lot of functions having arguments they never use, for e.g place_ships_helper would need ship_positions and board which it never uses.

Would really appreciate any help here. Thankyou !

import random

display_board = [['@']*10 for i in range(10)]
board = [[0]*10 for i in range(10)]
total_ships = 4
sunk_ships = 0
turns = 10
ship_positions = [[]]

#-----------------------------------------------------------------------------------------------------------------------------------
# PLACE SHIPS LOGIC
#-----------------------------------------------------------------------------------------------------------------------------------

def place_ships(start_row,end_row,start_col,end_col):
    valid = True
    for r in range(start_row,end_row):
        for c in range(start_col,end_col):
            if board[r][c] != 0:
                valid = False
                break
    if valid:
        ship_positions.append([start_row,end_row,start_col,end_col])
        for r in range(start_row,end_row):
            for c in range(start_col,end_col):
                board[r][c] = 1
    #print(board)
    return valid


def place_ships_helper(row,col,length,direction):
    start_row, end_row, start_col, end_col = row, row + 1, col, col + 1
    if direction == "left":
        if col - length < 0:
            return False
        start_col = col - length + 1

    elif direction == "right":
        if col + length >= board.size():
            return False
        end_col = col + length

    elif direction == "up":
        if row - length < 0:
            return False
        start_row = row - length + 1

    elif direction == "down":
        if row + length >= board.size():
            return False
        end_row = row + length

    return place_ships(start_row, end_row, start_col, end_col)


def generate_ships():
    ships_placed = 0
    rows,cols = board.size(),board.size()
    while ships_placed != total_ships:
        random_row = random.randint(0, rows - 1)
        random_col = random.randint(0, cols - 1)
        direction = random.choice(["left", "right", "up", "down"])
        ship_size = random.randint(3, 5)
        if place_ships_helper(random_row, random_col, ship_size, direction):
            ships_placed += 1

def print_board():
    generate_ships()
    for i in board:
        print(i)

if __name__ == "__main__":
    print_board()
\$\endgroup\$

1 Answer 1

6
\$\begingroup\$

The program had several unused items. I ignored and ultimately deleted them.

Stop using global variables and ignore OOP for now. Absolutely, stop using global variables from this point forward. They are never needed except under quite unusual circumstances -- none that you are likely to encounter at this stage of your journey. And before dealing with OOP, first develop some proficiency in building programs from small, focused, well-designed functions. Until you do that, OOP will be at best a distraction and at worst a crutch allowing you to smuggle global-variable thinking into your code while pretending that you're not.

Functions: a few best practices. Build functions that take data as input and return data as output. Whenever feasible, do not ask functions to mutate the data they are given. The basic model: information in, information out, and never mess with someone else's information.

Step 1: move the globals into print_board(). Try to run the program. You'll encounter a series of problems. For example, first we see that generate_ships() will have no access to board. The solution is to have the function return the board that it creates. While we're doing that, we also move the ship-counting logic to that function, because that's where the information needs to be managed. Run the program again: you'll see that we need to pass board down the call chain, but that's easy enough to do. After a few edits like that, you'll have a running program.

Step 2: tidy up. Define constants for empty and full. Put spaces after commas and other operators. For consistency, one should also define constants for the directions, but that topic raises other issues, so let's defer that. And rearrange the functions so that they agree with the narrative flow and structural hierarchy of the program. Here's the code after those adjustments.

import random

EMPTY = '.'
FULL = 'x'

def print_board():
    board = generate_ships(4)
    for row in board:
        print(' '.join(row))

def generate_ships(total_ships):
    board = [[EMPTY]*10 for i in range(10)]
    ships_placed = 0
    rows, cols = len(board), len(board)
    while ships_placed != total_ships:
        random_row = random.randint(0, rows - 1)
        random_col = random.randint(0, cols - 1)
        direction = random.choice(["left", "right", "up", "down"])
        ship_size = random.randint(3, 5)
        ships_placed += place_ships_helper(board, random_row, random_col, ship_size, direction)
    return board

def place_ships_helper(board, row, col, length, direction):
    start_row, end_row, start_col, end_col = row, row + 1, col, col + 1
    if direction == "left":
        if col - length < 0:
            return False
        start_col = col - length + 1
    elif direction == "right":
        if col + length >= len(board):
            return False
        end_col = col + length
    elif direction == "up":
        if row - length < 0:
            return False
        start_row = row - length + 1
    elif direction == "down":
        if row + length >= len(board):
            return False
        end_row = row + length
    return place_ships(board, start_row, end_row, start_col, end_col)

def place_ships(board, start_row, end_row, start_col, end_col):
    for r in range(start_row, end_row):
        for c in range(start_col, end_col):
            if board[r][c] != EMPTY:
                return False
    for r in range(start_row, end_row):
        for c in range(start_col, end_col):
            board[r][c] = FULL
    return True

if __name__ == "__main__":
    print_board()

Step 3. assess. Things are considerably better: we have no global variables! But our functions are defined entirely around mutation, which we'd like to avoid. And our modeling of directions data seems awkward, resulting in four chunks of semi-repetitive code.

Step 4. directions as change values. The noted problems spring from a poor modeling of data. Currently, we are using English words to represent directions, but those words have no connection to the underlying data used by the program. We have a board with rows, columns, and cells. Using the language of such data, how does one represent a direction? There are various options, but one common approach is to represent a direction as pairs of (row_change, col_change).

Step 5. stop mutating. Rather than asking lower-level functions to mutate the board that we give them, we should build the board up at the top level by asking lower-level functions go give us helpful data to achieve that. One approach is to ask a lower-level function to generate a single valid ship. How do we represent a ship in terms of the data? Again, a sequence of pairs will work, specifically (row, col) pairs. So print_board() will ask generate_board() to return the board and ships that it creates. To do that, generate_board() will ask generate_ship() for a ship until there are enough of them. And generate_ship() will delegate secondary calculations to utility functions, in particular the check for whether a needed cell is both valid and empty.

import random

EMPTY = '.'
FULL = 'x'
DIRECTIONS = (
    (0, 1),  # Right
    (0, -1), # Left
    (1, 0),  # Up
    (-1, 0), # Down
)
MIN_SIZE = 3
MAX_SIZE = 5

def print_board():
    board, ships = generate_board(10, 4)
    for row in board:
        print(' '.join(row))

def generate_board(board_size, n_ships):
    # A board without ship information is ambiguous so return both.
    board = [[EMPTY] * board_size for _ in range(board_size)]
    ships = []
    while len(ships) < n_ships:
        row = random.randint(0, board_size - 1)
        col = random.randint(0, board_size - 1)
        direction = random.choice(DIRECTIONS)
        ship_size = random.randint(MIN_SIZE, MAX_SIZE)
        ship = generate_ship(board, row, col, ship_size, direction)
        if ship:
            ships.append(ship)
            for r, c in ship:
                board[r][c] = FULL
    return (board, ships)

def generate_ship(board, row, col, ship_size, direction):
    r = row
    c = col
    dr, dc = direction
    cells = []
    for _ in range(ship_size):
        if is_empty(board, r, c):
            cells.append((r, c))
            r += dr
            c += dc
        else:
            return None
    return cells

def is_empty(board, row, col):
    try:
        return min(row, col) >= 0 and board[row][col] == EMPTY
    except IndexError:
        pass
    return False

if __name__ == "__main__":
    print_board()

\$\endgroup\$
1
  • 2
    \$\begingroup\$ Thankyou so much for the well detailed answer ! Got to learn a lot \$\endgroup\$ Commented Jun 1, 2022 at 12:36

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.