Assignment 1 - CS Defence
Tower Defence is a specific genre of games that boomed in the late 2000s. Many innovative games were created under this genre that pushed new and exciting ideas!
The core mechanics of these types of games are actually very simple! Waves of enemies continuously spawn (often forever!) and the player has the task of placing towers that will automatically fight these enemies. Both the player and enemies increase in strength over time and the defences need to be upgraded and improved to avoid enemies reaching a particular destination that could end the game!
CS Defence is heavily inspired by this genre, and specifically, one of the more famous games under its belt - Bloons Tower Defense. In this game, as well as CS Defence, enemies spawn from some start position and try to move to some end position via a path on the map. The player's goal is to place down towers that attack these enemies as they move. As towers destroy enemies, the player earns money that they can use to either upgrade or place down new towers.
Overview
Assignment Structure
This assignment will test your ability to create, use and manipulate 2D arrays and structs to solve problems. To do this, the map used in the game has been implemented as a 2D array of tiles. These tiles are each represented by a struct tile
, which is outlined below:
struct tile
- Purpose:
- To store information about the tiles of the map.
- Contains:
enum land land_type
- The type of land this tile has.
- All land types are found in the
enum land
definition.
enum entity entity
- The type of entity that exists on top of the land for this tile.
- All entity types are found in the
enum entity
definition.
int n_enemies
- Represents the number of enemies at this tile in the map.
The provided enums are quite extensive. Definitions of each are provided below:
enum land
- Purpose:
- Represent the type of land.
- Possible values:
GRASS
- Represents a piece of grass. It is notable that towers can be placed on this land.
WATER
- Represents a block of water. It is notable that towers cannot be placed on this land, with an exception mentioned in later stages.
PATH_START
- Represents the start of the path on the map. It is notable that this is where enemies will spawn.
PATH_END
- Represents the end of the path on the map. It is notable that enemies move to this location, and when reached, the player loses lives.
PATH_UP
,PATH_RIGHT
,PATH_DOWN
,PATH_LEFT
- Represents the “flow” of a path on this piece of land. Each of these types indicates the direction that the path is heading.
TELEPORTER
- Represents a teleporter on the path that links to another teleporter. Similar use case as the path tiles mentioned, but allows for more complex path flow.
enum entity
- Purpose:
- Represent the kind of entity.
- Possible values:
EMPTY
- Represents no entity. Some tiles will only have land and no entity, this allows us to represent that.
ENEMY
- Represents single or multiple enemies. Used when enemies are spawned and will move along the path.
BASIC_TOWER
,POWER_TOWER
,FORTIFIED_TOWER
- Represents all the different types of towers that exist. Each tower has different abilities, specified in Stage 3.
Mental Model
A tile can always be thought of as a piece of land with an entity on top of it. Click through the different combination of land + entities below to build up this mental model of what a tile is!
In this assignment, you will have a 2D array of these tiles. This could represent a map like the one below:
Implementation Model
Although the mental model mentioned above is a great way to understand how the map works in this assignment, it is also important to understand how it works in the code itself.
In this assignment, you will be need to manipulate a map provided which is defined as
struct tile map[MAP_ROWS][MAP_COLUMNS];
,
where MAP_ROWS
is 6 and MAP_COLUMNS
is 12.
This means that we have a 2D array of struct tile
s, which themselves have both entity + land enums associated with them. Change attributes in the interactive below to get an understanding of how you should access each part of this map
.
Note that the indices indicate which struct in the array you are accessing, and the word after the .
indicates which member of that struct that you are accessing.
map[][].
How To Get Started
There are a few steps to getting started with CS Defence.
Create a new folder for your assignment work and move into it.
mkdir ass1 cd ass1
Download the starter code (cs_defence.c) here or use this command on your CSE account to copy the file into your current directory:
cp -n /web/cs1511/23T1/activities/cs_defence/cs_defence.c .
Run
1511 autotest cs_defence
to make sure you have correctly downloaded the file.
1511 autotest cs_defence
Read through Stage 1.
Spend a few minutes playing with the reference solution -- get a feel for how the assignment works.
1511 cs_defence
Think about your solution, draw some diagrams, write some pseudocode to help you get started.
- Start coding!
About the Starter Code
The provided starter code has done some setup for you. This is explained below.
Before the main function, the starter code has:
- Imported the standard input/output library.
- Defined some initial
#define
's and enums. - Defined the struct described above.
In the main function, the starter code:
- Creates a 2D array of
struct tile
s calledmap
. - Initialises this
map
with some default values. - Prompts you to write your own code!
Your Tasks
This assignment consists of four stages. Each stage builds on the work of the previous stage, and each stage has a higher complexity than its predecessor. You should complete the stages in order
Stage 1
Stage 1.1 - Setup
To start off the assignment, you will need to initialise some basic information for the game. The starter code currently creates a 2D array of struct tile
s and initialises them using the initialise_map()
function that we’ve provided.
Your job is to take input from the user, asking them for their initial lives and money. An example of this is shown below:
dcc cs_defence.c -o cs_defence ./cs_defence Starting Lives: 100 Starting Money($): 500
Once you have the above working, you will need to scan in the “starting” point on the map and the “ending” point on the map. We describe a point as 2 integers, indicating a row then column. The starting and ending points will indicate where enemies will spawn from, and where they need to get to respectively. Implementing this, your program should behave in the same manner as below:
dcc cs_defence.c -o cs_defence ./cs_defence Starting Lives: 100 Starting Money($): 500 Start Point: 1 2 End Point: 4 10
In the example above, the start point is at row 1, column 2 and the ending point is at row 4, column 10.
These points can be visualised on the map like so:
You will need to modify the given 2D array, map
, to account for these points.
Make sure to read through the overview of the given structs/enums to understand how they work before continuing.
Once you understand how each struct/enum works, it is important to know that the map
variable in the code is a 2D array of struct tile
s. Alternatively, it can also be thought of as a 2D array of land with optional entities on top of it, since this is what each tile represents.
Currently, all the land is simply grass. However, given the starting/ending point that was just scanned in, you should update the relevant tiles such that their land is now PATH_START
and PATH_END
respectively.
After this, you should print out the map using the provided print_map()
function. When printing out the map, the start point will be printed as S
and the ending point will be printed as E
, assuming you’ve updated the land correctly!
Assumptions/Restrictions/Clarifications
- Starting lives and money will always be given as numbers greater than 0.
- Start/End points will always be valid indexes in the
map
array. They will never be out of bounds. - Start/End points will never be the same point.
Examples
Stage 1.2 - Initial Enemies
Tower defence games aren’t quite so fun when enemies don’t exist! For Stage 1.2, you will add some initial enemies to the starting point on the map. After printing out the map in the previous section, you should scan in how many enemies to put at the starting point:
Initial Enemies: [num]
To add enemies to the starting tile, you need to do two things:
- Set the
entity
component at that tile toENEMY
. - Set the
n_enemies
component of that tile to the number scanned in.
In the case where num
is 0 or negative, the starting tile should not change.
After scanning in the enemies, you will need to print out the map again. If done correctly, the map will now show a number above the 'S' on the starting tile!
Assumptions/Restrictions/Clarifications
- You can assume that the initial enemies will never be greater than 999.
Examples
Stage 1.3 - Lake
A common way to increase the difficulty of Tower Defence games is to add environmental obstacles, rather than just bombarding the player with more enemies. For Stage 1.3, you will add the ability to create a lake on the map! Lakes restrict the types of towers that can be placed on them.
After printing out the map in the previous section, you should scan in information about this lake:
Enter Lake: [row] [column] [height] [width]
The row and column that are scanned in indicate where the lake starts. The height and width indicate how far the lake stretches out in both dimensions. The lake always stretches down and to the right with these dimensions.
For every tile that appears in the lake on the map, their land
component should be set to WATER
.
After filling in the lake, the map should be printed again.
As an example, assume we give the program the following input:
Enter Lake: 1 2 4 6
The map would then look like this (ignoring start/ending tiles and initial enemies):
Error Handling
If the row
and column
provided is outside the map, or the height
or width
would stretch the lake outside the map, the message below should be printed and no lake should be added.
"Error: Lake out of bounds, ignoring...".
Assumptions/Restrictions/Clarifications
- The given lake will never overlap with the start/end points.
- The height and width entered will always be positive integers.
- When handling the lake out-of-bounds error mentioned, the program should still continue normally. This error should only cause the lake to be ignored, and no water to be added to the map.
- The map should still be printed even if the lake out-of-bounds error occurs.
Examples