(Updated December 5, 2022)
Table of Contents
-
So, About the Secret…
Game Play – Introduction
Checking for Validity
Checking for Accuracy – Setup
Checking for Accuracy – Right Color, Right Place
Checking for Accuracy – Right Color, Wrong Place
Report the Findings
Future Code Additions
Complete Code
The previous interlude roughed out the basic components of the CodeBreaker game. We set up the program template, selected some data types, selected the secret, set up instructions, and read a guess from the user. That is quite a bit to get done in just three chapters! Now that we have the chapters on conditionals and iterations completed, we are in a really good position to begin introducing some game logic.
So, About the Secret…
We will break with tradition and dive into the most recent topic – Loops! See, the whole copy-paste thing with the secret cannot hang around and must be dealt with immediately!
And, just before we do that, let us recap the list of variables and add a couple more.
int x, guess=0;
const int maxGuesses = 10;
char secret[5], input[5];
int size;
The secret
and input
are the same – no change there. We have added a maxGuesses
constant, a guess
variable to keep track of which guess is being read from the user, and x
, which is used for simple loop control.
Using x
, we will now handle the secret as intended.
size = strlen(COLORS);
// select 4 random colors for the secret.
for ( x = 0 ; x < 4; x++ ) {
secret[x] = COLORS[rand() % size];
}
secret[4] = '\0';
Notice that the secret size is hard-coded to 4, and the number of available colors is hard-coded to 6.
One of the things that came to mind when crafting this section is some opportunity for future options. What do we mean by that? Well, what if the number of colors for the secret was not 4? We could ask the user how many secret colors could be selected. Maybe in a range of 3 to 6? Were that an option, perhaps we could offer a range of difficulty (3=easy, 4=normal, 5=difficult, 6=what?!)
Then, what if we allowed more than 6 colors to choose from? Further, what if we allowed them to enter a string representing the color palette? They could enter "ROYBGWPVA"
for Red, Orange, Yellow, Blue, Green, White, Pink, Violet, and Aqua. While we are at it, why not allow them to pick the maxGuesses
value? Can you begin to imagine a very flexible game?
The reason we mention this is we could read a value from the user and store that in a variable called secretSize
. Then if they provide the color string, it would still be in COLORS
, but we'd have to change some design principles. Now we could do something like:
char *secret, *input;
secret = malloc((secretSize + 1) * sizeof(char));
input = malloc((secretSize + 1) * sizeof(char));
// select random colors for the secret.
for ( x = 0 ; x < secretSize; x++ ) {
secret[x] = COLORS[rand() % bsecretSize];
}
Now the code is adjusting to the values entered by the user but requires some additional design changes to accommodate this fully. Anyway, it is something to think about for a future version. Our initial draft will not have these as options, but it could, and adding them is somewhat trivial.
Game Play - Introduction
The gameplay is centered around a do-while loop. Since at least one guess needs to be made by the player, we know the loop must execute at least once. We will also write the loop such that it is bounded by the value of maxGuesses
. The outline of the loop looks much like the following:
do {
// read guess from the user
// check the guess for validity
// check the guess for accuracy
// report the result of the guess, ending the
// game if it's an exact match.
// increment the guess number
} while (guess < maxGuesses)
This layout of the basic gameplay is quite natural. We will now discuss each section individually. Note that the list of variables to be used is provided below:
int x, guess=0, l;
int correct=0, exact=0, result;
const int maxGuesses = 10;
char secret[5], input[5];
int size;
char s[5], g[5];
This is an ever-changing list, and some added now may be removed in a later interlude as we restructure the program to address newly learned concepts.
Checking for Validity
The check for validity is fairly straightforward. The idea is to make sure the user has made a guess that makes sense based on the color options. Anything outside the range of color options or guess length should be rejected.
While we could write an if
test to check the length of the string (strlen()
function) and craft a loop to check each character in the guess
is within the COLORS
string, we have chosen to use another function called strspn()
! Consider the following code.
// get the guess and make sure it is valid
puts("Enter your guess:");
scanf("%4s", input);
// upcase the input
l = strlen(input);
for ( x = 0; x < l; x++)
input[x] = toupper(input[x]);
if ( strspn(input, COLORS) != 4 ) {
printf("%s is an invalid guess. Please retry!\n", input);
continue;
}
Once we read the guess from the user, we convert it to upper case to match COLORS. The strspn()
function is then utilized since it will return the number of consecutive characters in input
that match the set in COLORS
. If the length is not 4, we print an error, and use continue
.
Recall that the continue
statement forces the next iteration of the loop to begin without executing the rest of the code within the loop's compound statement. Further, the loop outlined above shows more processing to be done: checking the accuracy, reporting the result, and incrementing the guess. By using continue
, we avoid all of this.
- If they just hit the enter key,
scanf()
will wait. - If they enter too few characters, the result of
strspn()
will not be 4. (They cannot enter too many due toscanf()
limiting to 4. Leftovers are consumed on subsequent reads.) - If they enter incorrect characters, multiple outcomes are possible. Starting with a bad character will mean the length is 0.
Could we have used an else
clause and put whatever code remains in that compound statement? Of course, we could. However, the continue
and break
statements are used to navigate what can be complicated bits of code. Sometimes it is simply easier to say, "No, try again." (continue
) or, "That is it! We are leaving!" (break
).
Checking for Accuracy - Setup
This section begins by looking at how we will determine the accuracy of the guess made by the player. Now we will create two additional variables to represent copies of the secret and the guess.
char s[5], g[5];
These variables will be used in the next two sections. Each section deals with a specific detail of checking the accuracy of input
with secret
. The first section is about exact matches - the correct color in the correct place. The second section is about being in the ballpark - the correct color but the wrong location.
Checking for Accuracy - Right Color, Right Place
The next bit of code is the first in a two-stage checking algorithm. Let us first describe the idea. Since the string is mutable, we can add and remove data. Our initial approach is to remove the exact matches from b
. This is a reduction model for processing data in multiple phases.
Why start with exact matches?
- Exact matches are easy to identify on a one-for-one basis.
- Exact matches are position sensitive and, therefore, highly influenced by the mutable nature of strings. (Making changes influences how the strings line up when comparing them.)
- Removing the exact matches guarantees they cannot influence stage two.
We will now look at the code and describe how the work is performed.
snprintf(s, sizeof s, "%s", secret);
snprintf(g, sizeof g, "%s", input);
//printf(("s is %s g is %s\n", s, g); //debug
/*
Remove all exact matches.
*/
exact = 0;
for ( x = 0; x < 4; x++ ) {
if ( s[x] == g[x] ) {
exact++;
s[x]=' ';
g[x]=' ';
}
}
//printf(("s is %s g is %s\n", s, g); //debug
First, the code at lines 3 and 16 can be uncommented to reveal debugging statements that help to show before and after views of b
and g
.
Second, we introduce the deleteChatAt()
method of the StringBuffer
class which does exactly as its name suggests while closing the gap after the character is deleted. Note that we still use charAt()
just as before.
Third, the loop that begins on line 8 operates from the end of the string rather than the beginning. This is a conscious choice due to the potential of the data changing under us. If we were to start at the beginning and the positions match, we bump the exact
count then at lines 10 and 11 we are going to remove the current character from each string. After that we would advance x
and move on to the next character. Here is what that could look like.
For the initial iteration, when x is 0. x 0 1 2 3 ----------------- s --> | R | G | W | B | ----------------- ----------------- g --> | R | W | O | B | ----------------- R matches R, so we replace it with a space. x 0 1 2 3 ----------------- s --> | | G | W | B | ----------------- ----------------- g --> | | W | O | B | ----------------- Now we advance x, which becomes 1, and our next comparison will be G and W. x 0 1 2 3 ----------------- s --> | | G | W | B | ----------------- ----------------- g --> | | W | O | B | ----------------- No change is made.
No changes will be made when x
is 1 or 2. But we do make a final change on the last character.
For the initial iteration, when x is 3. x 0 1 2 3 ----------------- s --> | | G | W | | ----------------- ----------------- g --> | | W | O | | -----------------
Approaching certain kinds of solutions often requires us to look at the problem in a new way. It is not always easy or safe to take a traditional approach to processing the string from left to right. Sometimes the logic is simply easier to write by reversing the direction and processing from right to left.
Checking for Accuracy - Right Color, Wrong Place
Phase two of the accuracy check is assessing if there are any matching colors after we have removed the exact matches. We will use the remaining data from the previous section to maintain consistency through the accuracy check discourse.
Recall that our data looks like the following:
0 1 2 3 ----------------- s --> | | G | W | | ----------------- ----------------- g --> | | W | O | | -----------------
Our stage two approach is to iterate over g
while only removing information from b
.
Why are we only removing data from b
?
- There is no reason to reduce
g
further. - It simplifies the loop which operates over the content of
g
. - We will be looking for the first occurrence of a character in
b
, so reducingb
guarantees we do not count a character more than once.
Now we will take a look at the code for this stage.
/*
Using the remaining guess, check for presence of each char.
Remove the char from the secret, if we find it, to handle repeats.
*/
correct = 0;
int l = strlen(g);
for ( x = 0; x < l; x++ ) {
if (g[x] == ' ') continue;
char* pos = strchr(s,g[x]);
if ( pos ) {
correct++;
*pos = ' ';
}
//printf(("s is %d g is %d\n", s, g); //debug
}
First, we have a debug statement at line 14 that could be uncommented to provide string information similar to the last section.
Second, since g
is not going to change, we can run the loop from the beginning of the string. Line 8 deals with spaces.
Third, line 9 handles the comparison using strchr()
to find the current color from g
in b
.
Each time we find a character from g
that exists in b
, we remove it from b
and increment correct
. The visual for why we removed it is described at the end of this section.
Now, let us begin the process.
0 1 2 3 ----------------- s --> | | G | W | | ----------------- x 0 1 2 3 ----------------- g --> | | W | O | | -----------------
In the loop, x
will iterate over g
. A search for W within b
will result in success. We then bump correct
by 1, remove the matched character from b
and increment x
by 1 to check the position in g
.
0 ----- s --> | G | ----- x 0 1 --------- g --> | W | O | ---------
We will not find a match for O in b
, so the loop completes its iterations finding only one match. As you can see there is no need to reduce g
.
This concludes the two stage approach to checking the accuracy of the players guess.
Ok, why do we remove the match from b
? Well consider the following example:
0 1 2 3 ----------------- s --> | R | O | B | W | ----------------- x 0 1 2 3 ----------------- g --> | G | R | Y | R | -----------------
There were no exact matches in the first stage and now we are in stage two. Visually we can see only one correct color in the wrong place. If we do not remove the R from position 0 of b
, which is the first match of R in g
, then when we get to the R in position 3 of g
, we will match it to the R in position 0 of b
again even though it has technically been matched. See below:
When x is 1, we will match R in position 0 of b. We bump correct by 1. 0 1 2 3 ----------------- s --> | R | O | B | W | ----------------- x 0 1 2 3 ----------------- g --> | G | R | Y | R | ----------------- When x advances to 3, we will match R, again, in position 0 of b! 0 1 2 3 ----------------- s --> | R | O | B | W | ----------------- x 0 1 2 3 ----------------- g --> | G | R | Y | R | ----------------- This results in a false report that they have 2 correct colors in the wrong place.
Report the Findings
Finally, we have a bit of code that is responsible for reporting our findings:
/*
We return the analysis as a two digit number. The tens position
represents the number of exact matches and the ones position
indicates the number of correct colors that are not exact matches.
*/
result = exact*10+correct;
if ( result == 40 ) {
System.out.println("You did it!");
return;
} else {
System.out.printf("Guess %2d: %4s exact:%d correct:%d\n", x+1, input, result/10, result%10);
guess++;
}
If result
is 40, then they have 4 exact matches, which means they have successfully guessed the secret
. That being the case, we inform the player they have won and use the return
statement to end the program.
Otherwise, we report additional details on line 12. This may seem unnecessary to combine the exact matches and correct matches into a single number. However, we will need to create our own methods very shortly to tidy up the work that is all being done by main
. This is a future design choice being implemented now.
The use of return
is a design choice similar to continue
, above. Rather than structure the logic such that all the if
s and else
s and for
s create a pathway to the end of the main
method, we simply say, "Congratulations! See you next time!," and end. Otherwise, we increase guess
and prepare for another time around the do
-while
loop.
Future Code Additions
In the final Interlude for the text-based game we will talk about the following:
- Restructuring the code in
main
to allow for building our own methods. - Introduce an array to provide the ability to keep track of previous guesses.
- Create a method to display the previous guesses and report the finding of each guess.
Complete Code
The complete code for this Iteration is shown below.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <ctype.h>
const char* COLORS = "ROYBGW"; // global list of colors
int main(int argc, char *argv[]) {
int x, guess=0, l;
int correct=0, exact=0, result;
const int maxGuesses = 10;
char secret[5], input[5];
int size;
char s[5], g[5];
srand(time(NULL));
// display instructions
puts("\n"
"The game of CodeBreaker:\n\n"
"The computer will choose 4 colors from the list RED, ORANGE, YELLOW,\n"
"BLUE, GREEN and WHITE. You will have 10 chances to guess the colors\n"
"from left to right. You will use the first letter of each color. Your\n"
"guess could be \"RWYY\". Spaces are not allowed in the guess. If\n"
"you make a mistake with color selection, you will be prompted to\n"
"reenter your guess.\n\n"
"Each guess is graded. You will be told how many colors are correct\n"
"and how many are in the correct place.\n");
size = strlen(COLORS);
// select 4 random colors for the secret.
for ( x = 0 ; x < 4; x++ ) {
secret[x] = COLORS[rand() % size];
}
secret[4] = '\0';
// loop to play the game until maxGuesses
do {
// get the guess and make sure it is valid
puts("Enter your guess:");
scanf("%4s", input);
// upcase the input
l = strlen(input);
for ( x = 0; x < l; x++)
input[x] = toupper(input[x]);
if ( strspn(input, COLORS) != 4 ) {
printf("%s is an invalid guess. Please retry!\n", input);
continue;
}
snprintf(s, sizeof s, "%s", secret);
snprintf(g, sizeof g, "%s", input);
//printf(("s is %s g is %s\n", s, g); //debug
/*
Remove all exact matches.
*/
exact = 0;
for ( x = 0; x < 4; x++ ) {
if ( s[x] == g[x] ) {
exact++;
s[x]=' ';
g[x]=' ';
}
}
//printf(("s is %s g is %s\n", s, g); //debug
/*
Using the remaining guess, check for presence of each char.
Remove the char from the secret, if we find it, to handle repeats.
*/
correct = 0;
int l = strlen(g);
for ( x = 0; x < l; x++ ) {
if (g[x] == ' ') continue;
char* pos = strchr(s,g[x]);
if ( pos ) {
correct++;
*pos = ' ';
}
//printf(("s is %s g is %s\n", s, g); //debug
}
/*
We return the analysis as a two digit number. The tens position
represents the number of exact matches and the ones position
indicates the number of correct colors that are not exact matches.
*/
result = exact*10+correct;
if ( result == 40 ) {
puts("You did it!");
return 0;
} else {
printf("Guess %2d: %4s exact:%d correct:%d\n", guess+1, input, result/10, result%10);
guess++;
}
} while (guess < maxGuesses);
puts("You ran out of guesses.");
printf("The secret was %s\n", secret);
return 0;
}