(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 class 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 conditional 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.
String secret="", input, colors="ROYBGW";
final int maxGuesses = 10;
int x, guess=0;
The secret
, input
, and colors
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 the way it was intended.
// select 4 random colors for the secret.
for ( x = 0 ; x < 4; x++ ) {
secret += colors.charAt((int)(Math.random() * 6));
}
Notice that the number of secret colors 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 that represents 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 now we could do something like:
// select random colors for the secret.
for ( x = 0 ; x < secretSize; x++ ) {
secret += colors.charAt((int)(Math.random() * colors.length()));
}
Now the code is adjusting to the values entered by the user. 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 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 user
// check the guess for validity
// check the guess for accuracy
// report the result of the guess, ending the
// game if it an exact match.
// increment the guess number
} while (guess < maxGuesses)
This layout of basic gameplay is quite natural. We will now discuss each section individually. Note that the list of variables to be used is provided below:
String secret="", input, colors="ROYBGW";
final int maxGuesses = 10;
int x, guess=0;
int correct=0, exact=0, result;
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 pretty 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. (See Interlude 2 for how we read the guess and removed whitespace.)
While we could write an if
test to check the length of the String
(length()
method) and craft a loop to check each character in the guess
is within the colors
String (indexOf()
method) we have chosen to revisit another mechanism that allows for this type of inspection and validation - regular expressions!
Recall that regular expressions allow us to describe the criteria of what makes a valid string by selecting a series of options available in the regular expression language. It is a limited but powerful language. Let us begin with the if
test.
if ( ! input.matches("^[ROYGBW]{4}$") ) {
System.out.println(input + " is an invalid guess. Please retry!\n");
continue;
}
This test uses the matches()
method of the String
class which allows us to specify a regular expression. We will not go into how regular expressions work but will identify the purpose and meaning of each component.
RegEX Component Meaning ^ Match beginning of string. [ROYGBW] Match one character in the list ROYGBW. {4} Match exactly 4 times. $ Match end of string.
This method of checking the guess is elegant and precise.
The continue
statement allows the if
test to be simple and complete in its handling of a bad guess. If the user makes an improperly formatted guess, we tell them and then force the loop back to the top.
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 there is more processing to be done: checking the accuracy, reporting the result, and incrementing the guess. By using continue
, we avoid all of this.
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 takes the standard String
variables, which are immutable, and converts them to StringBuffer
, which are mutable and can be modified. The choice of StringBuffer
is a good one because:
- The strings representing the secret and the user's guess are fixed.
- They are also the data values that must remain unchanged for the loop iteration.
- We need to perform a series of matches that require us to reduce the content of the strings as we match components.
- Having methods that closely match those of
String
make the code easier to understand.
Let us begin with StringBuffer.
// Convert String to StringBuffer so we can modify it.
StringBuffer s = new StringBuffer(secret);
StringBuffer g = new StringBuffer(input);
The code uses the current, immutable strings as arguments to create new StringBuffer
objects. So, b
and g
will represent the mutable equivalents of secret
and input
(their guess), respectively.
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 StringBufffer
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 of processing.
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
StringBuffer
. (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.
// System.out.println("s is " + s + " g is " + g);
/*
Working from the far end, remove all exact matches.
Working from the far end handles position changes at the beginning
of the array.
*/
exact = 0;
for ( x = 3; x >= 0; x-- ) {
if ( b.charAt(x) == g.charAt(x) ) {
exact++;
s.deleteCharAt(x);
g.deleteCharAt(x);
}
}
//System.out.println("s is " + s + " g is " + g);
First, the code at lines 1 and 15 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 will remove the current character from each string. Afterward, 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 zero. x 0 1 2 3 ----------------- s --> | R | G | W | B | ----------------- ----------------- g --> | R | W | O | B | ----------------- We find that R matches R, so we remove it. The side effect is every character's position has now changed! Remember that deleteCharAt() removes the character and closes the gap! x 0 1 2 ------------- s --> | G | W | B | ------------- ------------- g --> | W | O | B | ------------- Now we advance x, which becomes 1 and our next comparison will be W and O. x 0 1 2 ------------- s --> | G | W | B | ------------- ------------- g --> | W | O | B | ------------- We will have skipped right past the test of G and W! It does not matter with this data, but if they were a match, we would miss it!
Could we add more code to avoid this? Yes, of course. We could add a boolean
to see if we had a match, but if we do that we may as well change the loop entirely to a while
to be more in line with a flag-based loop. Then we would be conditionally advancing x
which may require more bounds checking tests. Everything just becomes more complicated.
The better and safer approach is to simply make modifications to strings in a way that have us moving away from the affected area rather than through it.
For the initial iteration, when x is 3.
x
0 1 2 3
-----------------
s --> | R | G | W | B |
-----------------
-----------------
g --> | R | W | O | B |
-----------------
We increment exact and remove the match, then x becomes 2 before heading back to the top of the loop.
x
0 1 2
-------------
s --> | R | G | W |
-------------
-------------
g --> | R | W | O |
-------------
When x is 2 and 1, there are no changes to the strings until x reaches 0 when we increment exact once more and remove the last match between R and R.
x
0 1
---------
b --> | G | W |
---------
---------
g --> | W | O |
---------
We are not concerned with position changes since x
will be decremented one last time and become -1 and the loop terminates when the final test is applied.
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 --------- 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 then 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;
for ( x = 0; x < g.length(); x++ ) {
String t = "" + g.charAt(x);
int pos = s.indexOf(t);
if ( pos != -1 ) {
correct++;
b.deleteCharAt(pos);
}
//System.out.println("s is " + s + " g is " + g);
}
First, we have a debug statement at line 13 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.
Third, StringBuffer
has indexOf()
similar to that of String
, except the argument must be of type String
. We cannot simply send the char
, so we have to build a String
. Line 7 takes care of that.
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 remove it is described at the end of this section.
Now, let us begin the process.
0 1 --------- s --> | G | W | --------- x 0 1 --------- 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 seen 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.
import java.util.Scanner;
public class CodeBreakerI3 {
private static Scanner kb = new Scanner(System.in);
private static void displayInstructions() {
System.out.println("\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\" or \"R W Y Y\". Spaces are not an issue. 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");
}
private static String selectSecret(String colors) {
int x;
String secret = "";
for ( x = 0 ; x < 4; x++ ) {
secret += colors.charAt((int)(Math.random() * colors.length()));
}
return secret;
}
private static boolean validGuess(String g) {
return g.matches("^[ROYGBW]{4}$");
}
private static int checkGuess(String secret, String guess) {
int x, correct=0, exact=0;
// Convert String to StringBuffer so we can modify it.
StringBuffer s = new StringBuffer(secret);
StringBuffer g = new StringBuffer(guess);
// System.out.println("s is " + s + " g is " + g);
/*
Working from the far end, remove all exact matches.
Working from the far end handles position changes at the beginning
of the array.
*/
for ( x = 3; x >= 0; x-- ) {
if ( s.charAt(x) == g.charAt(x) ) {
exact++;
s.deleteCharAt(x);
g.deleteCharAt(x);
}
}
//System.out.println("s is " + s + " g is " + g);
/*
Using the remaining guess, check for presence of each char.
Remove the char from the secret, if we find it, to handle repeats.
*/
for ( x = 0; x < g.length(); x++ ) {
String t = "" + g.charAt(x);
int pos = s.indexOf(t);
if ( pos != -1 ) {
correct++;
s.deleteCharAt(pos);
}
//System.out.println("s is " + s + " g is " + g);
}
/*
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.
*/
return exact*10+correct;
}
private static void displayGameProgress(String secret, String[] guesses, int guess) {
int x, check;
if ( guess == 0 )
return;
System.out.println();
for ( x = 0; x < guess ; x++ ) {
check = checkGuess(secret, guesses[x]);
System.out.printf("Guess %2d: %4s exact:%d correct:%d\n", x+1, guesses[x], check/10, check%10);
}
}
public static void main(String[] args) {
final int maxGuesses = 10;
String secret, input="", guessed[] = new String[maxGuesses];
int guess=0;
displayInstructions();
secret = selectSecret("ROYBGW");
//System.out.println(secret);
do {
displayGameProgress(secret, guessed, guess);
System.out.println("Enter your guess:");
input = kb.nextLine().toUpperCase().replace(" ", "");
if ( !validGuess(input) ) {
System.out.println(input + " is an invalid guess. Please retry!\n");
continue;
}
if ( checkGuess(secret, input) == 40 ) {
System.out.println("You did it!");
return;
} else {
guessed[guess] = input;
guess++;
}
} while (guess < maxGuesses);
System.out.println("You ran out of guesses.");
System.out.println("The secret was " + secret);
}
}