Mastermind is a classic 4-color code-breaking game where player try to crack the secret code with a limited number of attempts. It was one of my favourite game growing up, and I still remember playing it with my friends in grade school.
Recently, I wanted to relearn Java and improve my understanding of object-oriented programming(OOP). Rather than following tutorials, I thought a better way for me to learn would be to build something. Since I already have a programming foundation from working with JavaScript, I wanted to use that knowledge as a starting point while learning how Java approaches programming differently.
I thought of the Mastermind game would be perfect beginner projects. The rules are simple enough that I could focus on learning the fundamentals of Java, while the game itself still have interesting problems to solve, particularly around OOP, arrays, loops and handling duplicate colors.
The game design
Before writing the code, I broke the game down into smaller steps.
- Start the game
- Get the player's name
- Allow player to select between normal and hard mode
- Generate the secret code based on the selected game mode
- Get the player's attempts (format and validate input)
- Evaluate the player's attempt based on the game mode
- Provide feedback
- Continue step 5 - 7 until the code is cracked or running out of attempts
Breaking down the game in this way give me a high level view of what the program needed before I think about classes and methods. ==
The class design
The first class that comes into my mind is MastermindGame. It is responsible for controlling the game loop. The caller can call the start() and initiate a game session. When creating a MastermindGame, I need a Scanner to read the player's input, whether it is the player's name, the selected game mode, or their guesses.
Before generating the secret code, I needed to define the pool of six available colors. I used an enum called MastermindColor with RED, BLUE, GREEN, YELLOW, PURPLE, ORANGE.
Once the player selects a game mode, the system needs to generate a secret code based on that mode. I created a CodeGenerator class to handle this responsibility. For normal mode, duplicate colors are not allowed in the secret code. For hard mode, duplicates are allowed which make the game more challenging because there are more possible combinations.
Finally, I created an Evaluator class to evaluate the player's guess and provide feedback. The Evaluator return anEvaluatorResult which contains the number of black and white. The player can use this feedback to make a better guess on their next attempt.
MastermindGame
│
┌──────────┴──────────┐
▼ ▼
CodeGenerator Evaluator
│ │
▼ ▼
Secret Code EvaluatorResult
│
Black / White
At this point, I started to understand the benefit of OOP. Instead of having MastermindGame handle everything, I ca n create class with specific responsibility such as CodeGenerator to create secret code, and Evaluator evaluates the guess.
Main Logic
1. Game Flow
The start() method is responsible for controlling the overall game flow. When the game starts, it will prompt displayWelcome() , getPlayerName(), selectGameMode() to collect player's name and the game mode. The system then call CodeGenerator to generate a secret code based on the selected game mode and set the game status to PLAYING.
One of the challenges in the game flow was keeping track of both the number of attempts and the game state. A player can win the game before reaching the maximum number of attempts, so the game need to check the feedback of every guess.
After receiving a player's guess, the Evaluator checks the guess and return the number of black and white. If the number of black is equal to the length of secret code, the player has cracked the code and the game status changes to WON. Otherwise, the game continues and provides feedback to the player.
The loop eventually ends for one of the two reasons (1) player cracked the code (2) the maximum number of attempts is reached. If the player runs out of attempts without cracking the code, the game status changes to "LOSE" and the lose message get displayed.
The interesting part of this section is to keep track of two different conditions at the same time.
- game state - is the game still being played, has the player won?
- attempts - has the player reached the maximum number of guesses?
while(attempts < MAX_ATTEMPTS && gameStatus.equals("playing")){
// get guess
// evaluate guess
if(feedback.black == MAX_CODE_LENGTH){
// switch game state
gameStatus = 'win';
}else{
// display the results and prompt enter
}
attempts ++;
//if reach here, the attempts has run out but the status is playing
if(gameStatus.equals("playing")){
gameStatus = "lose"
}
// at the end we will end the game with the lose or win message
end(gameStatus);
}
2. Generating the Secret code
Since the game has 2 modes - Normal and Hard, the system needs to generate the secret code differently depending on the selected mode. I used Java's Random class to generate a random index for one of the six available colors.
Hard mode was relatively straightforward because duplicates are allowed. Each position can independently receive a random color.
Normal mode was more interesting because duplicate colours not allowed. After generating a random index, I need to check whether the color had already been selected. If it has, I would generate another random index and check again.
The generated color indexes is being store in an fixed-size primitive array:
int[] codeIdx = new int[MAX_CODE_LENGTH];
When the array is initialized, its elements are initialized to 0
[0,0,0,0]
This create a small problem. If I searched the entire array, I would also be checking positions that had not been assigned yet. Instead, I only needed to search the portion of the array that had already populated.
For example when i==2. The array might look like:
[2, 4, 0, 0]
At this point, only 2 and 4 are actual selections. The remaining 0s are just the default value of the array, so I only need to search indexes 0 through 1.
I used Arrays.copyOfRange() to create a search space containing only the previously selected indexes:
int[] searchSpace = Arrays.copyOfRange(codeIdx, 0,i);
Then I generated a random index and check whether it already existed in that search space:
int randomIdx = random.nextInt(0, upperBound);
while (contains(searchSpace, randomIdx)) {
randomIdx = random.nextInt(0, upperBound);
}
codeIdx[i] = randomIdx;
This process continues until four unique color indexes have been selected.
Finally those indexes can be converted into the corresponding MastermindColor values to create the secret code.
3. Evaluating a guess
The evaluator returns the number of black and white feedbacks.
- Black = correct color and correct position
- White = correct color and wrong position.
The challenging part of the evaluation is making sure that the same colour in the secret code is not counted more than once. To keep track of which colours have already been matched, I used two boolean arrays:
boolean[] secretUsed = new boolean[maxCodeLength];
boolean[] guessMatched = new boolean[maxCodeLength];
secretUsed keeps track of which positions in the secret code have already been matched, while guessMatched keeps track of which positions in the player's guess have already been matched.
Finding Black Matches
Finding black matches is relatively straightforward. I loop through each position and compare the guess with the secret code at the same index.
if (guess[i] == secretCode[i]) {
black++;
secretUsed[i] = true;
guessMatched[i] = true;
}
If the color are the same at the same position, I increment the black count and mark both positions as used. These positions will no longer be considered when looking for white matches.
Finding White Matches
Finding white matches is a little more complicated because the color needs to exists somewhere else in the secret code, but the same color cannot have already been used for another match.
For each unmatched color in the guess, I loop through the secret code and look for an unused matching color.
Guess colour
↓
Already matched?
↓ No
Search secret code
↓
Matching unused colour?
↓ Yes
White + 1
↓
Mark both as used
When a match is found, I increment the white count and mark both positions as used. I then stop searching for that particular guess color and move to the next one.
This two pass approach is important when duplicated color in Hard mode. It ensures that an exact match gets priority and that the same color is not counted multiple times.
What I would improve?
There are several things I would like to improve or explore further
- Add unit tests
- Add Game History
- Improve the console UI
- Role reverse, have the player to create code and computer to solve it.
Conclusion
Building Mastermind was a small project, but it gave me a different perspective on how I approach programming.
The biggest change for me was how I think about functionality. Coming from JavaScript, I am used to creating functions whenever I need a piece of functionality and then calling them from wherever I need them. This makes JavaScript very fast to work with, but it also means that I sometimes have to rely on my knowledge of the codebase to know whether a similar function already exists and where it belongs.
Java pushed me to think differently. Instead of immediately thinking, "I need a function for this," I started thinking, "Which object should be responsible for this?" With Mastermind, MastermindGame is responsible for controlling the game, CodeGenerator is responsible for creating the secret code, and Evaluator is responsible for evaluating guesses. The caller doesn't need to know how those responsibilities are implemented; it just needs to know which object provides the functionality.
I also found myself slowing down more before implementing something. I had to think about where variables should live, which classes should own certain data, and how different objects should interact. Concepts such as private, final, constructors, arrays, and loops became more meaningful when I had an actual problem to solve.
I also got to become more comfortable with a new IDE and revisit a language that I had previously learned but hadn't used much in my recent development work.
Overall, I didn't just relearn Java. I became more aware of my own programming habits. JavaScript has taught me to move quickly and get functionality working, while Java is teaching me to slow down and think about responsibility, structure, and how functionality will be used before I implement it.