We value academic integrity very highly. Please read the Honor Code section on our course webpage to make sure you understand what is considered as plagiarism and what the penalties are. The following are some of the highlights:
The objective of this assignment is to provide you with practice on arrays, functions, and recursion. Upon completion of this assignment, you should be able to:
"Candy Crush" is an example of a Match-Three game
Source:
https://en.wikipedia.org/wiki/Candy_Crush
The goal of this assignment is to implement a text-based "match-three" game. You can check this Wikipedia link here for a historical overview of this genre of games. The rules of the game are as follows:
In this assignment, you are required to:
Please read the FAQ section for some common clarifications. You should check that a day before the deadline to make sure you don't miss any clarification, even if you have already submitted your work by then.
The map is the data structure that records the current position of items in a game. The position of items within a map is initialized from a given input file, and updated accordingly during the game. It is further divided into two regions:
In each game, the map is represented by a global 2D array int map[MAX_ROWS][WIDTH]. The
following diagram illustrates the structure and coordinate system of the map:
In this assignment, we always set WIDTH = 9, HEIGHT = 9, and MAX_ROWS =
81. Note that the origin is at the bottom-left corner, so the first row map[0]
refers to the bottommost row of the map (and the active area).
The skeleton project structure is as follows:
PA1 ├── game.cpp ├── game.h ├── main.cpp └── map.txt
main.cpp contains the game interface; game.cpp and game.h
provide functions for various game mechanisms; and map.txt is a sample map file. Your
job is to complete five functions game.cpp so that the game will function properly. The
purposes and specifications for these functions are documented in the Tasks
section.
Since you will be asked to submit the completed game.cpp only, making modifications to
game.h and main.cpp is strongly discouraged.
You are required to write a greedy solver that returns the optimal move at the current stage. The solver considers all possible next moves, and return the move that results in the biggest score gain.
When there are multiple candidate moves that lead to the biggest gain, the solver should recursively solve for the best subsequent move for each candidate move, and choose the candidate that can result in the biggest total gain.
Note that as with other greedy algorithms, such a solver do not always return the "actual" optimal move. It is possible that a move with a small gain could lead to another move with a much higher gain in the next round. Nevertheless, it is computationally infeasible to consider too many steps at once, and this greedy solution should produce reasonable results for our purposes.
During each round, the game interface displays the current active area, player score, and no. of
remaining moves. The latter two are stored in the global variables score and
remaining_moves, defined in game.h. Initially, each player has a score of 0
and a total of 12 remaining moves. The console then asks the user enter one of the below commands:
swap x_pos y_pos direction - swaps a specific tile with its
neighbour in some direction.
solve - invokes the solver to get the optimal move for this round, and execute this
move.
quit - quits the game.
This section describes the functions that you will need to implement. Note that some parameters are used for function output, i.e. the objective variable is passed to the parameter by reference, and is subsequently modified by the function.
findMatches() functionDescription - Find all tiles that belong to a match; these tiles will be removed at a later stage.
int findMatches(int map[][WIDTH], bool matches[HEIGHT][WIDTH]);
Parameters
int map[][WIDTH] - Input - game map.
bool matches[HEIGHT][WIDTH] - Output - 2D boolean array
indicating the position of all tiles that are part of some match.
Return value - Number of matched tiles.
Notes
processMatches() function
Description - Find all matches using findMatches() and remove them. Then, update
the game map by shifting tiles downwards to fill up the removed space. Since new matches may exist
after updating, recursively call this function until there are no matches on the map.
int processMatches(int map[][WIDTH]);
Parameters
int map[][WIDTH] - Input - game map.
Return value - Total number of matched tiles, including recursive calls.
Notes
swapTiles() functionDescription - Swap two tiles in the map.
void swapTiles(int map[][WIDTH], int x1, int y1, int x2, int y2);
Parameters
int map[][WIDTH] - Input - game map.
int x1 - Input - x-coordinate of the first tile to swap.
int y1 - Input - y-coordinate of the first tile to swap.
int x2 - Input - x-coordinate of the second tile to swap.
int y2 - Input - y-coordinate of the second tile to swap.
considerMoves() functionDescription - Given the current map, consider every possible move and return the candidate move(s) that increases the score by the most.
int considerMoves(int map[][WIDTH], int candidate_moves[][4], int& num_candidate_moves);
Parameters
int map[][WIDTH] - Input - game map.
int candidate_moves[][4] - Output - a 2D array containing
all candidate moves. Each row corresponds to a single move, and contains the coordinates of the two
points to be swapped, in the format x1, y1, x2, y2 (same order as Task 3).
int& num_candidate_moves - Output - the total number of
candidate moves.
Return value - The maximal score gain.
Notes
candidate_moves is intentionally left
out, as the actual number of candidate moves is not a fixed value. For this assignment, you can pass
in a 2D array with the first dimension set to (HEIGHT-1) * WIDTH + (WIDTH-1) * HEIGHT =
144, which is the number of all possible moves, and only use part of the array to store the
values.
std::vector) to store data whose size is not determined at compile-time. These
will be introduced later in the course.
candidate_moves in
the
following order:
(Updated 21/09/21) Example - Suppose we check all 144 possible moves. The highest increase in score is 9, and there are four moves that result in an increase of 9. The moves are (1,7) <-> (1,8), (5,1) <-> (6,1), (4,0) <-> (4,1) and (6,7) <-> (6,8).
candidate_moves, which is a 144 x 4 array, should be filled up according to the above
order, i.e. the first row is [5, 1, 6, 1], the second row is [4, 0, 4,
1], and so on. The top 4 rows is to be filled, and the remaining 140 rows can be left
untouched.
num_candidate_moves should be set to 4. That way, when this function is called
somewhere else (e.g. in Task 5), the calling function will know that only the first 4 rows of
candidate_moves contain useful information.
solver() function
Description - Find the optimal move using the considerMoves function. If more
than one candidate moves exist, then for each of the candidate move:
solver() on the modified map to determine the subsequent optimal move.
candidate_moves returned
from considerMoves().
int solver(int map[][WIDTH], int return_coordinates[4]);
Parameters
int map[][WIDTH] - Input - game map.
int return_coordinates[4] - Output - the coordinates of the
two tiles to be swapped in the optimal move, in the format x1, y1, x2, y2 (same order
as Tasks 3 and 4).
Return value - Total score gain of the optimal move, considering all recursive calls.
Notes
copyMap function to copy a map.
solver() can be called with a map as the only argument if
return_coordinates is not needed. This may be useful for recursive calls of
solver().
int solver(int map[][WIDTH]);
considerMoves return zero candidate moves, do not return any coordinates and return
0 as the maximal score gain.
(Updated 21/09/21) Example - Suppose we run the demo program on the default
map.txt, and keep on calling solve. At the 5th solve, the
program chooses (5,4) <-> (6,4) as the optimal move. We explain why below.
considerMoves should return two candidate moves, (5,4) <-> (6,4) and (5,1)
<-> (5,2). Both of them increase the score by 20. (5,4) <-> (6,4) is in front because it is a
right-swap.
solver() on these two moves.
solver().
considerMoves should return another two candidate moves, (1,1) <-> (2,1) and (7,3)
<-> (8,3). Both of the increase the score by 6. (1,1) <-> (2,1) is in front because it has a
smaller y-coordinate.
solver() on these two moves.
solver().
considerMoves should return one candidate move, (5,3) <-> (5,4), which increases
the score by 12.
solver() exits with return value of 12.
solver().
considerMoves should return one candidate move, (1,1) <-> (2,1), which increases
the score by 6.
solver() exits with return value of 6.
solver() exits with return value of 18.
considerMoves should return one candidate move, (2,6) <-> (2,7), which increases
the score by 13. Since there is only one candidate, solver() exits with return
value of 13.
solver() is 38. Since the candidate (5,4) <-> (6,4) has a
higher score, we set return_coordinates to [5, 4, 6, 4].
Deadline: 28 September 2021 Tuesday HKT 23:59.
You may earn 8% course grade for each PA via Automated Grading on the
ZINC Online Submission System.
Compress the single source code file game.cpp by itself as PA1.zip
for submission to ZINC.
(Updated 21/09/21) There are two revealed stdio test cases (one of them is just the
Sample I/O) on ZINC which are both worth 0 points. Their main purpose is to double-confirm that your
code can successfully compile and execute on ZINC, and also to provide you with straightforward tests.
It is a good indicator that your code is mostly correct (but not a 100% guarantee) if you can pass
both revealed stdio test cases. Please note that the Sample I/O
and the revealed stdio test cases on ZINC don't show all possible cases. It is part of
the assessment for you to design your own test cases to test your program. Please also remember to
remove or comment out any debugging message(s) that you might have added, before submitting your
code.
Download the textfiles of the two stdio test cases here:
PA1_stdio_revealed.zip
The actual grading is with Googletest C++ Unit Testing (GTest). Each test case will be worth 5% in the Grading Scheme table below, for a total of 20 test cases. The actual grading will also only be triggered, with the scores and test cases revealed, after the deadline. This hidden grading policy is for all the PAs in order to prevent reverse-engineering of the test cases, since the PAs are a significant part of the course assessment and course grade. On the other hand, the Labs have a more visible grading policy, as their primary purpose is for hands-on programming practice, with full Lab marks being the expectation.
(Updated 17/09/21) We will execute unit testing on the task functions individually, and some of the test cases may include invalid input values. In general, you can assume that:
map input arrays in all tasks are always of correct size and contains valid input
candidate_moves (Task 4), only the first N rows is used (where N is the no. of
candidate moves), and the values of the remaining rows don't matter (i.e. it can be uninitialized).
Please ensure that you submit to ZINC well before the deadline as all late submissions will be automatically rejected.
| Task Function | Grade |
|---|---|
Task 1 - findMatches() |
10% |
Task 2 - processMatches() |
20% |
Task 3 - swapTiles() |
10% |
Task 4 - considerMoves() |
30% |
Task 5 - solver() |
30% |
Q: My code doesn't work, there is an error/bug, here is the code, can you help me fix it?
A: As the assignment is a major course assessment, to be fair, you are supposed to work on it
on your own and we should not finish the tasks for you. We are happy to help with explanations and
advice, but we shall not directly debug the code for you.
Q: The demo program enters an infinite loop when given unexpected input (e.g. inputting a
character when expecting an integer). Is this a bug?
A: This is just the behavior of cin >> variable; when given input that is
not type-matched. You don't need to worry about these user-input-error cases for PA1, as all console
I/O is already handled for you by the skeleton code in main.cpp. In other words, only
your code in game.cpp is graded.
Q: What are the restrictions regarding modifying the header files, writing our own helper
functions, including extra header files, etc.?
A: The only hard restriction is that you can only submit game.cpp to ZINC, and we
will use the skeleton code versions of game.h and main.cpp. Anything else
that you do, while not strictly prohibited, will be at your own risk regarding the PA1
grading result. Please keep in mind that there is a grade penalty for all grade appeals that
include modifications to your already submitted code (no matter how trivial the modification is).
Q: Am I allowed to use local function declarations (function declaration inside an existing
function) for my helper functions?
A: You are strongly discouraged from doing so, as that "feature" is a leftover merely for
backwards compatibility with C. In C++, it is superseded with class functions and lambda functions,
which will be taught later in this course.
Q: (Updated 17/09/21) In the demo program, why can we enter invalid commands such as
swap 8 0 up or swap 4 8 right?
A: As explained in the previous email, this is caused by incorrect range checking in
main.cpp. You can re-download the skeleton code to get the correct version. The sample
programs should also be fixed now.
Q: (Updated 19/09/21) What are the difference between "cells" and "tiles"?
A: Both of them refer to a single digit in the game map. To avoid confusion, all occurences of
the word "cells" above have been replaced with "tiles".