Lab Exercise 2 (Battleship)
Note
Please see the patch notes at /lab02patch.
Context
Battleship is a game that typically involves two players, each having a \(10 \times 10\) square grid. Before the game begins, each player places their ships on their grid. Each ship can occupy some consecutive number of cells horizontally or vertically.
Each player does not know the locations of the other player's ships. The players take turns "shooting" one cell of the other player's grid. If all of the cells occupied by a ship are hit, that ship sinks. The player who has their battleship sink loses.
For this Lab Exercise, you are to implement a terminal-based simplified version of Battleship, making use of separation of concerns as well as subtyping.
Overview
For the purposes of these specs, we will assume that:
- There are two players, which we call "you" and "the opponent".
- Each player plays on a \(6 \times 6\) grid. The rows are numbered \(0\) to \(5\) from top to bottom, and the columns are numbered \(0\) to \(5\) from left to right. The cell at row \(i\) and column \(j\) is represented as \((i, j)\).
- The sizes of the ships are \((4, 3, 2, 2)\).
- The ships for each player are placed randomly.
- Shooting a cell that has already been shot has no effect, but still consumes a turn.
- A ship sinks if any of the cells it occupies is hit.
- The goal of each player is to sink all of the other player's ships.
You will be structuring your code using the Model-View-Controller design pattern. The view and the controller will be provided in the git repo; your job is to create the model, as well as come up with unit tests for it.
Battleship is played in turns; each turn will prompt you to enter a row and column number:
CC.... ??????
BBB... ??????
..AAAA ??????
...... ??????
....DD ??????
...... ??????
Choose a row [0-5]: 1
Choose a column [0-5]: 1
Once (valid) row and column numbers are entered, the following things happen:
- The cell in the designated row and column of the opponent's grid is shot.
- The opponent randomly chooses one of cells in your grid to attack.
- If the game has not ended, you will be asked to choose a row and column to shoot in the opponent's grid again.
The following characters are used to represent your grid and the opponent's grid:
| Character | Your Grid | Opponent's Grid |
|---|---|---|
. |
A cell without a ship | A cell that you have shot but is empty. |
A, B, etc. |
A ship that has not sunk. | |
a, b, etc. |
A ship that has sunk. | A cell that you have shot and is not empty. |
? |
A cell you have not shot yet. |
After the move above (shooting the cell at \((1, 1)\)), the next few lines shown in the terminal are as follows:
(2,1) was shot!
CC.... ??????
BBB... ?.????
..AAAA ??????
...... ??????
....DD ??????
...... ??????
Here, \((2, 1)\) is the cell (randomly) chosen by the opponent. This doesn't contain a ship in your grid, so your grid is still the same.
Here are the last few interactions for the same game above:
CC.... ??????
bbb... ?.?d??
..aaaa ??.???
...... ???.??
....dd ????a?
...... ?????.
Choose a row [0-5]: 2
Choose a column [0-5]: 5
(0,0) was shot!
cc.... B.....
bbb... B.dd..
..aaaa B....c
...... .....c
....dd ..aaaa
...... ......
You lose...
As shown, at the end of the game, both your grid and the opponent's grids are fully shown. The message at the end changes depending on whether you win or not.
Task
Ensure that you read the Notes at the end of this section!
When we say "formal argument", we mean the x and y in def f(x: int, y: int) -> int:.
Your model should be called BattleshipModel, and it should have the following attributes and methods:
n(attribute)- An
intdenoting the number of rows of the grid. - For our purposes, \(n = 6\).
- This must also be a formal argument in the initializer.
- An
ship_sizes(attribute)- A sequence of integers denoting the ship sizes.
- For our purposes, the ship sizes are \((4, 3, 2, 2)\).
- This must also be a formal argument in the initializer.
rng(attribute)- A
Randomobject denoting the random number generator used for the game. - This must also be a formal argument in the initializer.
- A
turn(attribute)- An
intdenoting whose player's turn it is. - This must be \(0\) if and only if it is your turn.
- An
is_game_over(method)- Returns a
booldenoting whether the game has ended or not. - The game has ended if you no longer have any ships remaining.
- Returns a
grids(method)- Returns a sequence of lists of strings; for Phases 1-3, the first list of strings corresponds to your grid, while the second list of strings corresponds to the opponent's grid. Each string in each list should correspond to a row of the grid they represent.
winner(method)- Returns an
intdenoting whether you win or not. - A player wins if (1) they still have at least one ship remaining, and (2) all other players have no ships remaining.
- In particular, this means that it is possible for all players end up losing (from e.g., shooting each other's ships simultaneously).
- The return value must be \(0\) if and only if you win.
- This should raise an
AssertionErrorif accessed before the game has ended.
- Returns an
go_to_next_turn(method)- Advances to the next turn of the game.
get_random_ij(method)- Returns a pair of random integers that form a valid \((\text{row}, \text{column})\) pair.
- Must use
rng.
shoot(method)- Takes in two integer arguments \(i\) and \(j\).
- Uses
turnto determine which grid is being shot; must not modifyturn - Shoots the opposing player's grid at row \(i\) and column \(j\). This "opposing player" could be you, depending on whose turn it is now.
You must have unit tests (runnable via pytest) for the following aspects of the BattleshipModel:
turnandgo_to_next_turngridsandshootis_game_overandwinner
There must be unit tests that verify the state of the model by using both aspects in each pair above. For example, there must be unit tests verifying the value of turn after some number of calls to go_to_next_turn.
There must also be unit tests that simulate an entire game's worth of play. You should use a fixed random seed for this (Random(seed) where seed is your seed). You may set n and ship_sizes to smaller/shorter values to make testing this easier.
Note
You may use the function generate_grid in utils.py to randomly generate ship positions in a grid.
Note
To make testing elements that involve randomness easier, you may use a fixed random number generator by making a Random object (passing in an integer as the seed).
For example, the following block of code will always print out the same five lines:
from random import Random
rng = Random(12)
for _ in range(5):
print(rng.randint(1, 10))
Meanwhile, the following block of code will (likely) have different outputs every time you run it:
from random import randint
for _ in range(5):
print(randint(1, 10))
Scoring
This lab exercise will be scored in phases. You can get \(0/20/50/80/100 \%\) of the points in a phase depending on how far/close you are from meeting the phase's requirements. You must get \(\ge 80 \%\) of the points in each of the previous phases to get nonzero points for a certain phase.
For example, if you get Phases 1, 2, \(80\%\) of Phase 3, and \(20\%\) of Phase 4, you get a total of \(30 + 60 + 30 \times 0.8 + 30 \times 0.2 = 120\) points for this lab.
This lab will be scored over \(100\) 🔴.
Note
For all phases, your program should be runnable using the command python3 battleship.py.
Indicate the farthest phase you (think you) got in a README.md file.
Phase 1 (\(30\) 🔴)
For this phase, you only need to implement a working version of our modified Battleship game. You do not need to follow the Model-View-Controller design pattern or make unit tests.
You may ignore BattleshipView and BattleshipController or use some of their contents if you want to.
This phase is skippable; if you get \(\ge 80\%\) of the points for Phase 2, you immediately get full points for this phase.
Phase 2 (\(60\) 🔴)
For this phase, you must follow everything outlined in the Tasks section; that is, you must follow the Model-View-Controller design pattern, and you must have unit tests.
Phase 3 (\(30\) 🔴)
For this phase, you must have obtained \(\ge 80\%\) of the points in Phase 2, as well as support the following additional features:
- Move ship. Each player, during their turn, may choose to move one of their ships in the direction of its orientation. For example, a ship oriented vertically may be moved one cell upwards or downwards. Each player may do this at most once. Ship movement is only allowed if the resulting position is entirely within bounds and does not overlap another ship; sunk ships cannot be moved.
- Square scanner. Each player, during their turn, may choose to examine a \(k \times k\) square area of their liking. This does not shoot the cells in that area; it only reveals the cells that are there. Each player may do this at most once. For our purposes, \(k = 2\).
Your modifications must still properly adhere to the Model-View-Controller design pattern. How the UI for these additional features will work is up to you; at the very least, the tester should have little to no difficulty making use of these additional features.
You must also have unit tests for these additional features.
Phase 4 (\(30\) 🔴)
For this phase:
- The user should be able to set \(n\) and \(k\) at the start of the game.
- There should be more than one opponent.
- Each player can choose one of the opposing players to shoot during their turn; this should be an additional prompt before asking for the row/column to shoot.
- Each player can use
Move shipandSquare scannerat most \(3\) times now.
Your modifications must still properly adhere to the Model-View-Controller design pattern. How the UI for these additional features will work is up to you; at the very least, the tester should have little to no difficulty making use of these additional features.
You must also have unit tests for these additional features.
Submission
Via GitHub Classroom.
GitHub Classroom link: https://classroom.github.com/a/Ak08FU-C
Deadline: February 19, 2026 (Th) 11:12 pm
Warning
The deadline of Lab 3 will likely be the same as the deadline for Lab 2. Please manage your time well.