In this problem set, you will be implementing mancala, a classic two player board game played worldwide. Mancala is one of the oldest games to still be widely played today, and can be traced back over 1,800 years! Although it is traditionally played on a physical board, we will help bring it into the 21st century!
A mancala board consists of two rows of six "buckets", along with one "store" on each side of the board. Players take turns moving stones between these buckets and placing them in their stores when they score points. The object of the game is to end with the most stones in your store. A finished version of the game is found below:
Before moving forward, play the finished game a few times, and make sure you understand how different rules are affecting the state of the board. If you are already familiar with mancala, you can still do this to refresh your memory! There are many different variations of mancala, so for this assignment, we'll all be following the same ruleset, found here:Mancala Ruleset Final
As with in lecture, begin by opening up VSCode, ensuring your project is still open, and then running the following two commands in the VSCode Integrated Terminal:
The first command "pulls" the latest files needed for COMP110 into your repo, if any. The second command starts the development environment and will open your web browser to the local server.
You should see a folder labeled ps03-mancala. Inside are 3 files, named board-script.ts, index-script.ts, and index.html (stone.png, and board.png are just images for the graphics). You will be working exclusively in the index-script.ts file. The board-script.ts and index.html files are responsible for rendering the visual components of the game. Feel free to look around in them, but DO NOT MODIFY EITHER OF THESE FILES.
Note: It will appear as if board-script.ts has an error in it, making the folder and file red. That can be safely ignored, and your code will still work correctly!
1.0 Honor Code Header
All problem sets begin with an Honor Code pledge. You can copy/paste the pledge below and fill in your name and ONYEN.
/* * * Author: * * ONYEN: * * UNC Honor Pledge: I certify that no unauthorized assistance has been received * or given in the completion of this work. I certify that I understand and * could now rewrite on my own, without assistance from course staff, * the problem set code I am submitting. */
When you first get into index-script.ts, the first line you'll see is a weird looking statement:
declare let drawBoard: () => void;
drawBoard is responsible for the entire graphics element of the game (don't worry, it is completely done for you!), and the function is defined in board-script.ts. It gets called in main to render all of the graphics you'll see in the browser. The main function is provided for you inside index-script.ts.
You should see the board on the screen (assuming you’ve done npm run start to open a browser and navigated to the Mancala page).
1.2 Exported Functions and Variables
In this problem set all of the functions and global variables that you currently see inside index-script.ts are exported. This is because the files that render the Mancala board rely on changes to these variables and calls to these functions in order to make changes to the board you see on the screen. Also, by exporting functions and variables, we are able to test them. Defining an exported function is the same as any other function, except that it begins with the keyword export.
One exported variable has a special value that you've probably never seen before:
export let winner: number = Number.NaN;
It may appear odd at first, but Number.NaN just stands for "Not a Number", and it simply signals that the game isn't finished yet (later, you'll set the value of winner to be the number of the player who won the game).
You may have noticed that one of the exported global variables you copied over into index-script.ts was a 2D number array called model. This model is a way of representing the number of stones in each bucket for each of the two rows of the board.
This is how we'll represent a mancala board in our code. The changes you make to the model throughout the assignment will mimic how an actual game of mancala would progress. Another variable, named player, will track whose turn it is at various points in the game.
Although we declared it for you, it's your job to initialize the values in this array, which you will do below.
2.1 Initializing the Model
2.1 a) initModel takes in no parameters and returns nothing. Using a nested for loop, initialize model to have two rows with six columns. Each index should be given a value of 4, to indicate that 4 stones are in that bucket. Note: Before being able to index into the model at some row, col value, you'll need to initialize the specific row you'll be indexing into. Where would you need to do this?
The main function makes a call to initModel. Note that this call is made before main calls drawBoard, so that drawBoard knows there are stones in the buckets when it renders the screen.
After refreshing, you should now be able to see four stones in each of the buckets on the screen. When main called drawBoard, it saw that there were four stones in each index of your model, and it changed the board to accommodate that.
2.2 Helper Functions
A helper function is a small, focused function that performs a task. It's useful because you can implement it once, and then use it later throughout your code without getting bogged down in the details. This problem set will make use of two such functions, clearRow and sumRow.
Both sumRow and clearRow only take in one number parameter, designating the row to perform the function on. clearRow is a void function (returning nothing), and sumRow returns a number.
2.2 a) Inside clearRow, write a for loop that traverses every index in the model on the specified row and sets its value to 0.
2.2 b) Inside sumRow, write a for loop that traverses every index in the model on the specified row, and returns the total number of stones in that row.
In a real game of mancala, players move stones by picking them up from buckets and placing them in neighboring buckets. In our game, we'll model this behavior with a click; when a player clicks on a bucket, you will update the game model to reflect that all of the stones have been moved to their respective locations, and we will update the board based on the state of the model. You will implement all of this logic in a function called onClick.
3.1 Defining onClick
When you click on a bucket, one of our functions that is used to update the board will call your onClick function, passing in as arguments the row and the column of the bucket that was clicked
(Fun Fact: we detect the row and column by checking the exact location of the click, and then checking to see which of the bucket's boundaries that click occurred in. Notice that, if you click on a location with no bucket, nothing happens! The exact code that does this is inside ourOnClick, a function in board-script.ts, if you want to see the inner workings).
onClick takes in two number parameters named row and col and has a return type of boolean. You'll return true when the bucket that was clicked on was a valid move and false otherwise. Note: Since this will only return false in specific circumstances that are outlined below, the initial implementation of this function returns true. Steps 3.2-3.4 are all a part of the onClick function, so all of this work should go within the function.
3.2 "Picking up the stones"
3.2 a) When a player clicks on a bucket, we need to know how many stones were in the bucket. Using the row and col parameters, access the correct index from the model and store the number at that index into a local variable named stonesInHand.
3.2 b) After you have stored the element from the model into stonesInHand, you need to remove the stones from that bucket by setting the value at the previously mentioned index to 0.
3.3 Player 0
The following logic in onClick is dependent on which row the bucket that was clicked on is in. Player 0 can pick up and moves stones from row 0 in the model, which corresponds to the top row on the board.
3.3 a) All of the logic in section 3.3 should only occur when the bucket that was clicked on is in row 0.
3.3 b) Next we need to make sure that the bucket that was clicked on is a valid move. On row 0, there are two conditions that would make a move invalid: if it is player 1's turn (Rule 2) or there are no stones in the bucket. If either is true, then the move is invalid. When a move is invalid, you should restore the number of stones in the model at the given row and col and return false. Hint: What global variables could help you in this case?
3.3 c) Now that we know we're on row 0 and the bucket that the player clicked on was a legal move, we can start moving stones. But how will we know which way to move the stones? We'll use a variable that can either be -1 (moving left) or 1 (moving right). Declare a variable called direction and set its initial value to -1. Note that the direction may change within one player's turn as stones are deposited around the board. Another thing we'll want to keep track of is if the player gets to go again (See Rule 6 for when this happens). Declare a boolean variable called goAgain and set its value to false.
3.3 d) Next, we need to provide a repetitive system for moving around our board and depositing stones. Declare a loop that will run while the variable stonesInHand is greater than 0. Inside this loop we'll handle all the logic to move stones and adjust player scores.
3.3 e) Moving in the model simply means changing the value of the row and/or col variables (i.e. model is to the left of model, model is below model, etc.). Thus, to move to the left of the bucket we clicked on (counterclockwise on row 0), simply add the value of direction to the col variable.
3.3 f) There are several cases you'll want to handle inside the loop. For example, when we add direction to col each time we move, we'll potentially reach the "edge" of the board, where player 0's store is (moving left, this would happen when col = -1, since model[-1] is not a valid index but model is). What do we want to do when this happens?
3.3 g) There is also a possibility when we're moving right that player 0 will reach the opposing player's store.
3.3 h) The last edge case we need to handle is outlined in Rule 7. Namely, that if the last stone player 0 places is going into an empty bucket on row 0, they capture the stone in that bucket and all the stones in the bucket across from it on the other player's row.
3.3 i) If none of the above conditions were true, increment the model at the current row and col value, and decrement stonesInHand. This case is the most common since you're just moving from one bucket to the next.
3.3 j) Outside the while loop (after all of the actions for player 0's turn have been processed), we need to check if player 0 gets to go again. Check if the goAgain variable is true; if so, player stays the same, but otherwise, the value of player becomes 1, since it's now player 1's turn.
3.4 Player 1
Player 1's logic is very similar to player 0's logic. the only differences lie in what direction player 1 is moving in at various times, when they should deposit into a store vs. skipping it, and how to handle Rule 7. Follow the same ideas outlined in 3.3, but modify it to work with how player 1 should be moving around the board. You can do it!
You may have noticed that although the stones now move correctly, the game never actually ends.
4 a) The only function you haven't touched yet is a function called checkIfGameOver. It takes in no parameters and returns nothing. You may have guessed that it will be responsible for checking if the game is over at the end of each turn.
4 b) Take a look at the rules governing the end of the game, namely rules 9-11. Rule 9 states that the game is over when one player's side of the board is empty. Use the sumRow helper function you implemented at the beginning of this problem set to see how many stones are in row 0 and row 1, and store those values in two variables, sum0 and sum1. If either of the two sums are 0, then the game should end. Otherwise, none of the logic below should happen.
4 c) Assuming that the above condition is true, you need to handle what happens at the end of the game. Rule 10 says that whoever has stones left on their side of the board when the other side is empty should deposit all of the remaining stones into their store. You can achieve this by simply adding the sums of both rows to their respective player's score, since adding the empty row has no effect on score.
4 d) Now, use the clearRow helper function to clear out both rows (one of them is already empty in the model, but this saves you some time dealing with more if-else logic).
4 e) Next you need to determine who won the game. Using the winner variable declared globally, set winner to be the correct player number based on their scores. If the game is a tie, set the winner variable to -1.
4 f) Lastly, inside the onClick function you wrote earlier, you should call checkIfGameOver right before returning true at the end of the function. That way, each time a player clicks on a stone, you check to make sure the game didn't end, and handle it if it does.