This program implements a dice-rolling game known as Farkle. Based on a dice roll, a score is calculated, and the player decides whether to keep rolling. With multiple players, the goal is to be the first player to reach a total score of 10,000.
The learning objectives of the program are:
Farkle is a multi-player dice game, developed in the 1980s. It is also known by other names, such as Pocket Farkel, 10000, Zilch, Squelch, Cosmic Wimpout, or Hot Dice. Each player rolls a group of dice to accumulate points, until they voluntarily end the turn, or a roll wipes out their points and ends the turn. The winner is the player who first reaches 10,000 points.
There are variations in the scoring rules, so be sure to read the rules for this version carefully. More information about Farkle can be found on Wikipedia:
https://en.wikipedia.org/wiki/Farkle
Remember: There are many variations to the game. For this program, you must follow the rules exactly as specified.
Farkle is played with two or more players. Each player takes a turn rolling dice to earn points. After any roll, the player may choose to end the turn and keep the accumulated points. If, however, the roll scores zero points, the player loses all of the points from that turn and play goes to the next person. This is called a "Farkle."
Here is a more detailed description of one player's turn:
The scoring options are shown in the following table.
Dice | Points |
1 | 100 for each |
5 | 50 for each |
Three 1's | 1000 |
Three 2's | 200 |
Three 3's | 300 |
Three 4's | 400 |
Three 5's | 500 |
Three 6's | 600 |
Straight (1-2-3-4-5-6) | 1500 |
Multiple scores can be combined from a single roll. For example, a roll of 1-1-3-3-3-4 can score 200 (two 1's) + 300 (three 3s) = 500. The player decides which dice to count toward the score by removing those dice from the group. For example, after this roll, the player could remove one 1, both 1s, the 3s and the two 1s, or just the 3s, etc.
Here are is an example of one player's turn. We are showing the dice sorted in ascending order, to make it easier to find scoring opportunities, but of course in a real game the order of the dice is irrelevant.
Here's another example.
The main function, which is provided for you, is the top-level user interface (UI) for the game. The assignment specifies several other functions that you must implement. You may also implement additional functions of your own, if you choose to. We will only directly test the functions that are explicitly listed below.
First, the program asks how many players. Usually, Farkle requires two or more players. For this program, the maximum is 4. We also allow 1 player for testing purposes - when 1 player is specified, one turn is played for that player, and then the program ends. (This is how we will test your takeTurn function, described below.)
Next, the program asks for a random number. You can enter a number using decimal or hexadecimal (0x...) notation. This is used as a seed for the random number generator, described below. For a given number, the same set of random numbers will be generated. This allows you to test your code by trying the same combination over and over, and it allows us to test/grade the code - when a certain seed is specified, your code will generate the same sequence of random numbers as the test program, and so the results should match exactly.
At the beginning of each turn, the scores for all players is printed. Then the first roll (of six dice) is performed for the proper player. From there, the main function will call a function that you must write, named takeTurn. This function will interact with the user to complete the turn: removing dice, rolling again, accumulating the score, etc. The takeTurn function returns the score for that player's turn.
When more than one player is specified, the program will continue until some player exceeds 10,000 points, and then it will announce the winner. As described above, if only one player is specified, then only one turn is played, and the score for that turn is printed.
Because we need to deal with rolling a group of dice, we need a data structure to represent a group of dice. During game play, we can roll any number of dice between 1 and 6. We could use an array of six integers, where each integer represents the value shown by a die. (We could use a value of zero for "empty" slots when fewer than six dice are rolled.)
However, we will use a different representation of a group of dice that is more flexible and makes the scoring process for this game easier.
We use an array of seven integer. The first integer (element 0) is the number of dice in the group. Elements 1 through 6 contain the number of dice showing that value. In other words, element 1 contains the number of ones, element 2 contains the number of twos, and so forth. See the example in the figure below.
Figure: see image.
The flexibility of this representation is that we can represent any number of six-side dice. Also the dice are already sorted and collated, making it easy to find sequences, pairs, triples, etc.
To declare an array to hold a group of dice, and to initialize to all zeroes, we can use the following:
int dice[7] = {0};
For convenience we define a new name for this type, using C's typedef statement:
typedef int DiceGroup[7]; // an array of seven integers
Now we can use this type to declare a variable or a parameter, and we don't have to specify the size of the array - it will always be seven:
DiceGroup dice = {0}; // declare and initialize a group of dice
You are not required to use the DiceGroup type name, if you don't want. Its perfectly fine to declare an array of seven integers each time. The typedef is a way of giving a user-friendly name to a type, so that the programmer does not need to type/remember the details every time.
To model rolling of a dice, we use a pseudorandom number generator (PRNG). The function getRandom is provided for you, with the following declaration:
unsigned int getRandom(unsigned int limit);
The function returns a pseudorandom number between 0 and limit-1. There is also a standard library function that does this, but its implementation can vary on different platforms. By implementing our own function, we can be certain that the function will be exactly the same on your local machine running CLion and the zyBook testing environment.
To initialize the PRNG, we call the seed function. You don't have to worry about this, because it is done in the main function. (You should only seed the PRNG once.) It uses the number entered after the number of users.
This PRNG uses a linear feedback shift register. You don't have to understand how it works, but if you want learn more, heres a Wikipedia page: https://en.wikipedia.org/wiki/Linear-feedback_shift_register.
The functions and global variable (!) used to implement the PRNG are located at the bottom of the main.c file. Leave them there and don't change anything about them. Define your functions between the main function and the line above the PRNG code.
You must implement the following functions in your program. They are already declared at the beginning of the main.c file. DO NOT change the declarations of any of these functions.
If you choose to implement additional functions you should also declare them at the beginning of the file.
void printDice(const DiceGroup dice);
Prints the dice as a sequence of decimal digits to stdout, with no spaces or other characters between the digits. Do not print any space or character before or after the digits. The digits must be in sorted order, which is easy using the DiceGroup data structure described above.
Example: The dice in the figure above would be printed as 113455
void rollDice(DiceGroup dice, int n);
Simulate a roll of n dice, and put the result in the dice array. This function must call getRandom. The contents of the dice array is ignored, and will be overwritten by the roll.
int setDice(DiceGroup dice, int data);
The parameter data is an integer, where each digit is the value on the face of a die. The digits are not necessarily in sorted order. If there are any digits that cannot be on the face of a die (0, 7, 8, 9), return 0 to indicate failure. The state of the dice array will be undefined in that case. Otherwise, the dice array will represent a group of dice with the specified face values, and the function returns 1.
Example: setDice(d, 451531) will set d to the array shown in the figure above and will return 1.
Example: setDice(d, 405) will return 0 because of the 0 digit in the integer. The contents of d are undefined. (This means it might be changed from its previous state, but it should no longer be assumed to contain a legal, consistent set of dice.)
int testFarkle(const DiceGroup dice);
Returns 1 if the dice array has no scoring dice. Otherwise, returns 0.
int scoreDice(const DiceGroup dice);
Returns the highest possible score generated by the group of dice in the array. See the scoring rules above. All scoring dice in the array must be counted in the best possible outcome.
int selectDice(DiceGroup dice, DiceGroup keep, int choice);
Given an original group of dice, the choice parameter indicate a set of dice to move into the keep group, removing them from the dice group. The choice integer specifies the dice by its digits, in the same manner as the setDice function.
This function can fail in two ways: (1) An illegal digit can be present, as in setDice. (2) One or more specified dice do not exist in the original group of dice. If either happens, the function returns 0, the dice array is unchanged, and the keep array is undefined.
If the call succeeds, the specified dice are present in the keep array, and are removed from the dice array, and the function returns 1.
int takeTurn();
This function implements one turn for one player and returns the score for that turn. You must follow the specified steps exactly, and your code must print exactly what is specified, including spaces, linefeeds, etc. This function will be tested by running the program with one player and a variety of seed values.
A turn consists of one or more dice rolls. The first roll uses six dice. Subsequent rolls will use between 1 and 6 dice, depending on how many dice were removed from the previous roll. For each roll, print the following, with the first blank replaced by the number of dice being rolled, and the second blank replaced by the printed dice (call printDice). There must be a linefeed printed at the end of the line.
Rolling _ dice... _____
Call testFarkle to determine if the roll scores zero points. If so, print the following message and return 0. There must be a linefeed printed at the end of the line.
FARKLE -- your turn is over.
If the roll is not a Farkle, then ask the user which dice to keep for scoring. To prompt the user for the choice, print the following text. There must be a space after the question mark, and there must be no linefeed at the end.
Which to keep?
The function must then read a decimal integer from the user. Assume that the user will type a non- negative, non-zero integer, small enough to fit into an int variable. (The user will type Enter (linefeed) after the integer.)
Call selectDice to separate the dice into keep and re-roll groups. If the selection fails (see the description of selectDice), print the following message and go back to the prompt. There must be a linefeed at the end of the line.
No match, try again.
If the selection is successful, but the keep dice score zero points, this is not legal. The user must always remove at least one scoring die. If this is the case, print the following message, restore the dice to the original group (after the roll) and return the prompt. The message must include a linefeed at the end.
Must keep scoring dice. Try again.
When a successful selection is made, call scoreDice to determine the score for the dice that were kept. Print the following message, with the first blank replaced by the dice selected for scoring, and the second blank replaced by the score for that roll. Include a linefeed at the end of the line.
Keeping _____, score = ____
Add the roll score to the player's score for this turn and print the following message, replacing the blank with the turn score. End the line with a linefeed.
Score so far = ___
Now, determine how many dice are remaining after the scoring dice are removed. If that number is zero, this is the Hot Dice condition: all six dice were used for scoring. If that is the case, print the following message. There is a space after the question mark, but NO linefeed.
HOT DICE! Roll 6 dice (y/n)?
Otherwise, tell the user how many dice are left using the following message, replacing the blank with the number of dice. Again, we ask whether the player wants to keep rolling. There is a space after the question mark, but NO linefeed.
_ dice left -- roll again (y/n)?
Read a single character from the player. Assume they will enter either 'y' for yes, or n for no. If yes, go back to the first message above and start a new roll, either with 6 dice (for Hot Dice) or the appropriate number of dice. If no, the turn is over - return the players score for this turn.
NOTE: When you read a single character, you will see the linefeed that is still on the input stream after the dice selection was entered by the user. The easiest way to skip over this linefeed is to include a space before %c in the scanf format string, like this:
scanf(" %c", ...);