The project was implemented on my own as part of an academic assignment and received the highest score. Originally it was developed in a separate uni branch and further imported into this repository. All commits on behalf of Maria Soloveva are mine.
Project files:
battleships.py
-- run this file to play the game without visualisation (was developed according to specifications listed below);
extension.py
-- run this file to play the game with a CLI visualisation implemented using NumPy (battleships.py
is also required in the same directory);
test_battleships.py
-- tests used in development (admittedly, some of them were blown out of proportion to cover the required number of tests).
This assignment is based on a well-known game. Battleship is usually a two-player game, where each player has a fleet and an ocean (hidden from the other player) and tries to be the first to sink the other player's fleet. We'll just do a solo version, where the computer places the ships, and the human attempts to sink them.
The Ocean is a field of 10 x 10 squares. The squares are numbered from 0 to 9 in each dimension with numbers increasing from top to bottom and from left to right.
The fleet consists of 10 ships. The fleet is made up of 4 different types of ships, each of different size as follows:
- One battleship, occupying 4 squares
- Two cruisers, each occupying 3 squares
- Three destroyers, each occupying 2 squares
- Four submarines, each occupying 1 square
To begin the game, the computer places all the 10 ships of the fleet in the ocean randomly. Each ship can be placed either horizontally (as shown in the figure above) or vertically. Moreover, no ships may be immediately adjacent to each other, either horizontally, vertically, or diagonally. Examples of legal and illegal arrangements are shown below:
The human player does not know where the ships are. The human player tries to hit the ships, by calling out a row and column number. The computer responds with one bit of information--"You have a hit!" or "You missed!" (Note that the human player can call out the same location more than once, even though it does not make sense. If that happens, 2nd, 3rd, ... calls of the same location are misses.) When a ship is hit but not sunk, the program does not provide any information about what kind of a ship was hit. However, when a ship is hit and sinks, the program prints out a message "You sank a _ship-type_!"
A ship is "sunk" when every square of the ship has been hit. Thus, it takes four hits (in four different places) to sink a battleship, three to sink a cruiser, two for a destroyer, and one for a submarine. The objective is to sink the fleet with as few shots as possible; the best possible score would be 20. (Low scores are better.) When all ships have been sunk, the program prints out a message that the game is over and tells how many shots were required.
We represent a ship as tuples
(row, column, horizontal, length, hits)
where:
row
andcolumn
are integers between 0 and 9 identifying, respectively, the row and column of the square of the top-left corner of the shiphorizontal
is a Boolean value equal toTrue
if the ship is placed horizontally andFalse
if placed verticallylength
is an integer between 1 and 4 representing the length of the shiphits
is a set of tuples of the form(row, column)
containing all the squares occupied by the ship that was hit
We represent a fleet as a list
[ship1, ship2, ....]
of ships. Note that during the game, a fleet will contain 10 ships (which may be intact, hit, or sunk), however, when the computer places the ships randomly, it is convenient to start with an empty list and iteratively expand it by adding ships.
is_sunk(ship)
-- returns Boolean value, which isTrue
ifship
is sunk andFalse
otherwiseship_type(ship)
-- returns one of the strings"battleship"
,"cruiser"
,"destroyer"
, or"submarine"
identifying the type ofship
is_open_sea(row, column, fleet)
-- checks if the square given byrow
andcolumn
neither contains nor is adjacent (horizontally, vertically, or diagonally) to some ship infleet
. Returns BooleanTrue
if so andFalse
otherwiseok_to_place_ship_at(row, column, horizontal, length, fleet)
-- checks if the addition of a ship, specified byrow, column, horizontal
, andlength
as inship
representation above, to thefleet
results in a legal arrangement (see the figure above). If so, the function returns BooleanTrue
and it returnsFalse
otherwise. This function makes use of the functionis_open_sea
place_ship_at(row, column, horizontal, length, fleet)
-- returns a new fleet that is the result of adding a ship, specified byrow, column, horizontal
, andlength
as inship
representation above, tofleet
. It may be assumed that the resulting arrangement of the new fleet is legalrandomly_place_all_ships()
-- returns a fleet that is a result of a random legal arrangement of the 10 ships in the ocean. This function makes use of the functionsok_to_place_ship_at
andplace_ship_at
check_if_hits(row, column, fleet)
-- returns a Boolean value, which isTrue
if the shot of the human player at the square represented byrow
andcolumn
hits any of the ships offleet
, andFalse
otherwisehit(row, column, fleet)
-- returns a tuple(fleet1, ship)
whereship
is the ship from the fleetfleet
that receives a hit by the shot at the square represented byrow
andcolumn
, andfleet1
is the fleet resulting from this hit. It may be assumed that shooting at the squarerow, column
results in of some ship infleet
are_unsunk_ships_left(fleet)
-- returns a Boolean value, which isTrue
if there are ships in the fleet that are still not sunk, andFalse
otherwisemain()
-- returns nothing. It prompts the user to call out rows and columns of shots and outputs the responses of the computer (see General Idea of Assignment) iteratively until the game stops. Our expectations from this function: (a) there must be an option for the human player to quit the game at any time, (b) the program must never crash (i.e., no termination with Python error messages), whatever the human player does. Note that there is an indicative implementation ofmain()
to help you start working, but it does not satisfy the expectations above and you should improve or entirely redo it.
In addition, you can implement the visualisation of the game. Our expectations of the high-quality visualisation are as follows:
- The new state of the ocean and fleet are presented to the human player each time after he/she shoots
- The rows and columns of the ocean are numbered
- The squares that have never been shot at are clearly indicated (i)
- The squares that have been shot at but not resulted in a hit (nothing was there) are clearly indicated (ii)
- The squares containing a ship (of unknown type) that has been hit but not yet sunk are clearly indicated (iii)
- The ships that were sunk are clearly indicated, as well as their type (iv)
The following is one example of high-quality visualisation:
In this example, (i) from the list above are indicated by .
, (ii) by -
, (iii) by *
and (iv) by a letter specifying the type of a ship (S - submarine, C - cruiser)
readme.md
-- this file (together with several picture files)battleships.py
contains the implementation of all the required functions (see Required functions)test_battleships.py
contains unit tests for all testable functions from the set of required functions in the Pytest format. Testable are all the required functions except forrandomly_place_all_ships()
andmain()
The files above with basic templates are provided in the repo. You must edit battleships.py
and test_battleships.py
and return them in your submission. It is required to use Pytest for tests in this project. Moreover, if you provide the visualisation extension (see Extension), add the file extension.py
to your submission. The game with visualisation must be startable by executing extension.py
.
You can add additional files if you need them, provided that the requirements above on battleships.py
, test_battleships.py
, and extension.py
are satisfied.
Your project mark according to the following criteria:
Correctness and adherence to specification | 30% |
Completeness of code, tests, and commit history | 30% |
Coding Style | 10% |
Extension | 30% |
Notes:
- You don't have to implement the extension, but your maximal mark will be 70% if you don't
- Specification of the required functions must be fully respected. In particular, their names must be as specified, and the data types/structures of the arguments and return values must be as specified
- We expect you to start working on your project just after Session 7 and make regular commits (at least two times every two weeks) in your git repo. Your commit history contributes to your mark. The submissions with weak commit history (for an extreme example, all the implementation and tests appear in one commit a day before the deadline) will lose up to 10% of the mark and will be subject to extra plagiarism checks
- Documentation is not required, but provide basic comments to your code, when it does something non-trivial, as well as the docstring (description in '''...''') before the implementation of each function. As docstrings of the required functions, you can use their descriptions above.
- Tests are essential and you must aim to provide at least 5 tests for each testable required function. The quality of the tests matters more than quantity. Try to make your tests maximally diverse so that they catch problems with various inputs. The tests contribute to your project mark.
- If the description of a testable required function above specifies an assumption about the function input (e.g., it may be assumed that input has property X), do not write tests with inputs not satisfying these assumptions (i.e., inputs not satisfying property X).
- Extension is marked according to the correctness, coding style and satisfaction of the expectations above (see Extension)