# You must make sure to run all cells in sequence using shift + enter or you might encounter errors
from pykubegrader.initialize import initialize_assignment

responses = initialize_assignment("Lab_4_Battleship", "week_4", "lab", assignment_points = 17.0, assignment_tag = 'week4-lab')

# Initialize Otter
import otter
grader = otter.Notebook("Lab_4_Battleship.ipynb")

๐Ÿงช ๐ŸŽฎLAB 4 : BATTLESHIP๐ŸŽฎ#

Complete the following 4 functions and corresponding function calls which facilitate playing battleship in the Jupyter console.

Battleship Header

๐Ÿ™พ 1: Set up game board ๐Ÿ™ฟ#

  • Add the following doc string to initialize_board(). It goes between the pair of """ at the top of the function (replacing ...).

    """
    Initialize a game board for tracking ships or shots.

    :param size: The size of the game board (size x size).
    :return: A grid filled with '"๐ŸŸฆ"' to represent water.
    """

None of the next functions are called in the cells in which they are defined by default. You may add calls to run the functions inside their cells if you like, or use the ones in 5: Play the game below.

initialize_board() is given to you in its entirety (minus the doc string above). The operations it performs use a for loop to execute a program designed to generate data for a game board.

We set up an object called board which is populated with an empty list by board = []. This gives us something to which we can add symbols representing water.

The for loop in initialize_board() uses an iterator _ generated by range(size), an iterable that produces values from the functionโ€™s input size. The _ acts as a placeholder. _ will automatically be replaced with "๐ŸŸฆ" as two lines inside the for loop are executed.

Each time the loop runs, a row of "๐ŸŸฆ" symbols is created and appended to the object board. The function returns the complete size ร— size grid, called board.

def initialize_board(size):
    #doc string goes below here, replacing ...
    """
    ...
    """
    board = []
    for _ in range(size):
        row = ["๐ŸŸฆ"] * size
        board.append(row)
    return board
grader.check("task1-initialize_board")

Question 2 (Points: 4.0): Question 1 (Points: 2.0): ๐Ÿšข2: Place ships on board๐Ÿšข#

Now that we have a game board filled with "๐ŸŸฆ" to represent water, we want to replace some of those "๐ŸŸฆ" with "๐Ÿšข" to represent ships placed by a player.

1: Extract row and column indices from coordinates#

To iterate through the list of ship coordinates, we want to unpack each tupleโ€™s values to two variables, row and col. These variables will represent the row and column indices for each part of the shipโ€™s placement.

  • Write a for loop around coordinates to replace the first ... below

  • The for loop should use two iterators row, col, generated by an iterable coordinates, which is made of a list of tuples containing row and column coordinates for the desired ship location

Example:
If [(1, 2), (3, 4)] is input to place_ship(coordinates):

  • On the 1st iteration: row will be assigned 1 and col will be assigned 2

  • On the 2nd iteration: row will be assigned 3 and col will be assigned 4

2: Adjust row and column for pythonโ€™s indexing#

We want our game to accept human input for ship placement (1-based). This means if a person wants to place part of a ship in the upper leftmost element they should input (1,1). Python, by default, uses 0-based input, so any time that we accept human input we need to subtract 1 from each component to correspond to correct placement on the board.

Inside the for loop, do the following to replace ...:

  • Use the compound assignment operator -= to subtract 1 from row and update its value

  • Do the same for col on the next line (still inside the for loop)

3: Place the ship on the board#

Now that row and col have been adjusted for Pythonโ€™s indexing, the last line inside the for loop, board[row][col] = "๐Ÿšข" looks into the 2D list board at an index specified by [row][col] and replaces whatever is there with "๐Ÿšข".

Since the board starts out as all water after running initialize_board(), that means the existing "๐ŸŸฆ" gets replaced wite "๐Ÿšข" to represent part of a ship.

def place_ship(board, coordinates):
    """
    Place ships on the board.

    :param board: The game board (list of list of str).
    :param coordinates: List of (row, col) tuples for ship positions (1-based indexing).
    """
    # Add a for loop below this line
    ...
        # Convert user input to 0-based index to make Python happy
        ...
        board[row][col] = "๐Ÿšข"
grader.check("task2-place_ship")

Question 3 (Points: 6.0): ๐ŸŸฆ3: Display board๐ŸŸฆ#

Now that we have a function to generate data for board and another function to place ships on board, we want to display board in the console. If we tried to directly print board like this:

    attempt_to_display_board = (initialize_board(5))
    print(attempt_to_display_board)

the result would be:

    [['๐ŸŸฆ', '๐ŸŸฆ', '๐ŸŸฆ', '๐ŸŸฆ', '๐ŸŸฆ'], ['๐ŸŸฆ', '๐ŸŸฆ', '๐ŸŸฆ', '๐ŸŸฆ', '๐ŸŸฆ'], ['๐ŸŸฆ', '๐ŸŸฆ', '๐ŸŸฆ', '๐ŸŸฆ', '๐ŸŸฆ'], ['๐ŸŸฆ', '๐ŸŸฆ', '๐ŸŸฆ', '๐ŸŸฆ', '๐ŸŸฆ'], ['๐ŸŸฆ', '๐ŸŸฆ', '๐ŸŸฆ', '๐ŸŸฆ', '๐ŸŸฆ']]

That doesnโ€™t look like a board at all! We want to remove all the extra characters like [, ,, and '. We also want to make sure the board displays as a grid with rows stacked evenly on top of each other instead of presented end-to-end in one row.

To accomplish this goal, we will use the display_board() function below.

1: Set up a for loop to access row data#

We want to iterate though every row in board and keep only the ๐ŸŸฆ for display. To do this, we can use the built-in Python string method "".join() inside a for loop.

  • Write a for loop around the board to replace the first ... below

  • The for loop should use one iterator row, generated by the iterable board

The 1st time the loop runs, row will be equal to the entire first row of board.

The 2nd time the loops runs, row will be equal to the entire 2nd row of board.

This still includes the [, ,, and ' symbols.

2: Remove extra characters from display#

Inside the for loop:

  • Use the "".join() method to join all entries in row into one string

    • The syntax is: "".join(row)

      • "" is used to indicate what comes between the entries as they are joined โ€” in this case we want nothing, so leave it empty

    • Save the result to a variable row_content

Now [ and , have been removed from the display.

  • On the next line, wrap row_content in a print statement

    • print(row_content)

    • print() removes the ' or " that would normally be wrapped around a string in the console

Now, if we were to run:

    display_board(attempt_to_display_board))

the result would be:

๐ŸŸฆ๐ŸŸฆ๐ŸŸฆ๐ŸŸฆ๐ŸŸฆ
๐ŸŸฆ๐ŸŸฆ๐ŸŸฆ๐ŸŸฆ๐ŸŸฆ
๐ŸŸฆ๐ŸŸฆ๐ŸŸฆ๐ŸŸฆ๐ŸŸฆ
๐ŸŸฆ๐ŸŸฆ๐ŸŸฆ๐ŸŸฆ๐ŸŸฆ
๐ŸŸฆ๐ŸŸฆ๐ŸŸฆ๐ŸŸฆ๐ŸŸฆ

That looks like a good board, but because the ๐ŸŸฆ are in such a close grid it may result in a grid illusion, which can cause eye strain and make it tough to quickly count rows and columns. Due to differences in how Mac and Windows display unicode characters, this may only occur when using a Windows operating system.

To fix this, we can adjust the spacing between rows and columns.

3: Optionally reduce eye strain#

  • Change "".join() to "  ".join() to add two spaces between ๐ŸŸฆ in each row

  • Add a second print statement one line before the existing one to print an empty line between each row

    • The syntax is print()

  • This part isnโ€™t graded, so if you donโ€™t find the dot illusion straining feel free to skip

def display_board(board, otter = False):
    """
    Displays a 2D board in a visually appealing format.
   
    :param board: The game board (list of list of str).
    """
    #Add a for loop below this line
    ...
        #Define row_content below this line 
        ...
        #Add print statement(s) below this line
        ...
        
    #Do not edit below this line 
    if otter:
        return row, row_content
grader.check("task3-display_board")

Question 4 (Points: 5.0): ๐Ÿ’ฅ4: Fire away๐Ÿ’ฅ#

Now we want to manipulate board using indices provided from a tuple, target, which will be input to a function, fire_shot().

1: Extract row and column from target#

We want to unpack the tuple, target, which should always be input by players as (x,y) where x and y are two integers corresponding to a single space on board. Since we know it will always be one tuple with two values inside we donโ€™t need to use a for loop this time.

  • Define two variables, row and col, to store the two values contained in target.

    • The syntax for this is row,col = target

2: Adjust for human-friendly input#

To make our game user friendly we want target to use 1-based indexing, just like we did for place_ship()

  • Use a compound assignment operator to decrease the value of row and col by 1.

3: Determine action based on current content of target#

Next comes the core implementation of logic in our game. We need to determine what happens when any given spot on board is targeted.

We will do this by checking the current content of the index called by target then deciding happens to that content based on getting hit by a shot. The first step of the logic if otter: is just for grading (donโ€™t change it!).

We want to apply the following game logic:

  • If "๐Ÿšข" is present we want to replace it with "๐Ÿ’ฅ" to represent a hit

    • We also want to return the string "Hit"

  • If "๐ŸŸฆ" is present we want to replace it with "๐Ÿ’ฆ" to represent a miss

    • We also want to return the string "Miss"

  • If "๐Ÿ’ฅ" is present we want to keep it "๐Ÿ’ฅ"

    • We also want to return the string "Already fired"

The input tuple shouldnโ€™t be able to target anything else, so we finish the series of logical tests with an else: statement that returns "Invalid Target"

Since we already initiated an if statement to allow for grading, you can start with elif for the game logic. Then, the final else cleans up any other possible outcomes.

Replace the three sets of ... to perform logical statements where indicated below

def fire_shot(board, target, otter = False):
    """
    Fire a shot at the board and return the result.

    :param board: The game board.
    :param target: A tuple (row, col) where the shot is fired (1-based indexing).
    :return: The result of the shot: "Hit", "Miss", or "Already Fired".
    """
    # Unpack the tuple on the next line
    ...
    
    # Convert user input to 0-based index to make Python happy
    ...

    if otter:
        return row,col
    # Logical statement that should return "Hit"
    ...
    # Logical statement that should return "Miss"
    ...
    # Logical statement that should return "Already Targeted"
    ...
    # Logical statement that should return "Invalid Target"
    else:
        return "Invalid Target" 
grader.check("task4-fire_shot")

๐ŸŽฎ5: Play game๐ŸŽฎ#

The variable player_board is populated using initialize_board(10) to produce a 10 x 10 game board filled with water.

To play battleship, you should place 5 ships on your board. These ships are:

  • Carrier (occupies 5 spaces)

  • Battleship (occupies 4)

  • Submarine (occupies 3)

  • Destroyer (occupies 3)

  • Patrol boat (occupies 2)

The destroyer and patrol boat have already been placed for you with place_ship().

Keep your screen hidden from your opponent so they donโ€™t know where your ships are!

  • Edit the placement of the destroyer and patrol boat so your opponent doesnโ€™t know where they are

    • Or try to be sneaky and keep them in their default position as the one place your opponent wonโ€™t check?

  • Add 3 new calls to place_ship() to place the remaining 3 ships

  • Run the cell to save your ship placement and view the board

You can run the cell as many times as you like before the game begins to help make sure youโ€™re placing ships where you think youโ€™re placing them, but donโ€™t re-initialize the board once youโ€™ve started to play or youโ€™ll lose all progress.

Ships should be continuous and not overlap. They can run on diagonals.

# Initialize boards
player_board = initialize_board(10)

# Place destroyer and patrol boat on board
place_ship(player_board, [(2, 2), (2, 3), (2, 4)]) #destroyer
place_ship(player_board, [(7, 7), (8, 8)]) #patrol boat

# Place remaining 3 ships on board

# Display board
display_board(player_board)

Once you and your opponent have boards set up you can use the next cell to target spaces on their board (and vice versa). You can also use the tracking function below to record your shots on your opponentโ€™s board (unless you can keep track of all that in your head?!)

  • Player 1 says: โ€œ7,7โ€

  • Player 2 runs: result = fire_shot(player_board, (7, 7)) and display_board(player_board)

  • Player 2 says: โ€œHit!โ€

  • Optional: Player 1 runs: record_opponent_feedback(tracking_board) and provides input to the box, then runs display_board(tracking_board)

If your opponent hits all parts of a ship, you need to tell them which ship they sank.

# Fire shots 
result = fire_shot(player_board, (7, 7))
print(result)  # Should print "Hit" if ships are placed in default positions 

# Display the updated board
display_board(player_board)

Track shots on your opponentโ€™s board#

You want to make sure youโ€™re not aiming at the same place twice.

Run the following cell to initialize an editable tracking board.

def record_opponent_feedback(tracking_board):
    """
    Prompt the user for a target and result, then update the tracking board.

    :param tracking_board: The board tracking your shots on the opponent's board.
    """
    try:
        # Get input for target coordinates as a tuple
        target_input = input("row,col: ")
        target = tuple(map(int, target_input.split(",")))

        # Get input for result ("Hit" or "Miss")
        result = input("hit or miss?: ").capitalize()
        
        # Update the tracking board
        row, col = target
        row -= 1  # Convert to 0-based index
        col -= 1  # Convert to 0-based index

        if result == "Hit":
            tracking_board[row][col] = "๐Ÿ’ฅ"  # Mark hit
        elif result == "Miss":
            tracking_board[row][col] = "๐Ÿ’ฆ"  # Mark miss
        else:
            print("Invalid result. Please enter 'hit' or 'miss'.")
    except ValueError:
        print("Invalid input. Please ensure coordinates are in the format row,col.")

tracking_board = initialize_board(size=10)
#Running this cell will prompt for input. 

#First, tell your opponent your shot and input coordinates as a comma separated pair of integers, then hit enter. 

#Input "hit" or "miss" (no quotes) depending on what your opponent says.

record_opponent_feedback(tracking_board)
display_board(tracking_board)

Submitting Assignment#

Please run the following block of code using shift + enter to submit your assignment, you should see your score.

from pykubegrader.submit.submit_assignment import submit_assignment

submit_assignment("week4-lab", "Lab_4_Battleship")