Welcome to Chapter 4 of our Java project series! In this chapter, we’re diving into the exciting world of interactive console applications by building a classic “Number Guessing Game.” This project will teach us fundamental concepts of generating random numbers, handling user input, implementing game loops, and providing intelligent feedback.
This step is crucial for understanding how to create dynamic applications that interact with users. We’ll leverage Java’s core libraries to manage randomness and collect input, applying best practices for robust and production-ready code even in a simple console environment. By the end of this chapter, you’ll have a fully functional, playable number guessing game that demonstrates clear game logic, error handling, and basic logging.
1. Planning & Design
Before we jump into coding, let’s outline the game’s core requirements and design. This clarity helps us write structured and maintainable code.
Game Flow:
- Initialization: The game will generate a secret random number within a predefined range (e.g., 1 to 100).
- User Input Loop: The player will be prompted to guess the number.
- Input Validation: The game will ensure the input is a valid integer.
- Comparison & Feedback: The player’s guess will be compared to the secret number.
- If the guess is too high, the player is told “Too high! Try again.”
- If the guess is too low, the player is told “Too low! Try again.”
- If the guess is correct, the player is congratulated, and the game ends.
- Attempt Tracking: The game will keep track of how many attempts the player has made.
- Play Again (Optional but good practice): After a game concludes, the player should have the option to play again.
Component Architecture:
For this simple console application, we’ll keep the architecture contained within a single Java class, NumberGuessingGame. This class will encapsulate all the game’s logic, including random number generation, input handling, and the main game loop.
File Structure:
We’ll continue using our standard Maven project structure. The new game logic will reside in:
src/main/java/com/example/game/NumberGuessingGame.java
2. Step-by-Step Implementation
Let’s begin building our Number Guessing Game incrementally.
a) Setup/Configuration
First, create the necessary file and set up the basic class structure, including a logger for good measure.
Action: Create the NumberGuessingGame.java file.
File: src/main/java/com/example/game/NumberGuessingGame.java
package com.example.game;
import java.util.InputMismatchException;
import java.util.Random;
import java.util.Scanner;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* A simple console-based Number Guessing Game.
* The player tries to guess a randomly generated number within a specified range.
*/
public class NumberGuessingGame {
// Logger for logging game events and errors
private static final Logger LOGGER = Logger.getLogger(NumberGuessingGame.class.getName());
// Game configuration constants
private static final int MIN_NUMBER = 1;
private static final int MAX_NUMBER = 100;
private static final int MAX_ATTEMPTS = 10; // Maximum attempts for the player
/**
* Main method to start the Number Guessing Game.
* @param args Command line arguments (not used in this application).
*/
public static void main(String[] args) {
LOGGER.info("Starting Number Guessing Game application.");
// Game logic will go here
}
}
Explanation:
package com.example.game;: Defines the package for our class, following standard Java naming conventions.import ...: We import necessary classes:InputMismatchException: For handling cases where the user enters non-integer input.Random: To generate our secret number.Scanner: To read user input from the console.Logger,Level: For basic logging, which is critical for debugging and monitoring in production.
private static final Logger LOGGER = ...: Initializes ajava.util.logging.Loggerinstance. We’ll use this to output messages about the game’s state, user actions, and any errors.private static final int MIN_NUMBER = 1;: Defines the minimum possible number for the game. Using constants makes the code more readable and easier to configure.private static final int MAX_NUMBER = 100;: Defines the maximum possible number.private static final int MAX_ATTEMPTS = 10;: Sets a limit on how many guesses the player gets. This adds a challenge to the game.public static void main(String[] args): The entry point of our application. We’ve added an initial log message to confirm the application starts.
b) Core Implementation
Now, let’s fill in the main method with the actual game logic. We’ll break this down into several steps: generating the random number, setting up the input scanner, and implementing the game loop.
i. Generating the Secret Number
We’ll use the java.util.Random class to generate a pseudo-random integer within our defined range.
Action: Add code to generate the random number inside the main method.
File: src/main/java/com/example/game/NumberGuessingGame.java (modified section)
public static void main(String[] args) {
LOGGER.info("Starting Number Guessing Game application.");
// Initialize Random number generator
Random random = new Random();
// Generate a random number between MIN_NUMBER and MAX_NUMBER (inclusive)
// nextInt(bound) generates a number from 0 (inclusive) to bound (exclusive).
// So, nextInt(MAX_NUMBER - MIN_NUMBER + 1) generates from 0 to (MAX_NUMBER - MIN_NUMBER).
// Adding MIN_NUMBER shifts the range to [MIN_NUMBER, MAX_NUMBER].
int secretNumber = random.nextInt(MAX_NUMBER - MIN_NUMBER + 1) + MIN_NUMBER;
LOGGER.log(Level.FINE, "Secret number generated: {0}", secretNumber); // Log at FINE level for debugging
System.out.println("Welcome to the Number Guessing Game!");
System.out.printf("I have secretly chosen a number between %d and %d.%n", MIN_NUMBER, MAX_NUMBER);
System.out.printf("You have %d attempts to guess it.%n", MAX_ATTEMPTS);
// Game logic will continue here
}
Explanation:
Random random = new Random();: Creates an instance of theRandomclass. For simple applications like this,Randomis sufficient. For cryptographically secure random numbers,java.security.SecureRandomshould be used.int secretNumber = random.nextInt(MAX_NUMBER - MIN_NUMBER + 1) + MIN_NUMBER;: This is the core logic for generating a number within a specific range.MAX_NUMBER - MIN_NUMBER + 1calculates the total count of numbers in our range (e.g., for 1-100, it’s 100 numbers).random.nextInt(...)generates a number from0up to (but not including) this count.- Adding
MIN_NUMBERshifts this0-based range to our desiredMIN_NUMBER-based range.
LOGGER.log(Level.FINE, "Secret number generated: {0}", secretNumber);: We useLevel.FINEfor this log message. This means it will typically not be shown in production unless logging is configured to be very verbose. This is a good practice for debugging information that shouldn’t clutter production logs.System.out.println(...)andSystem.out.printf(...): These lines provide initial instructions to the player.printfis useful for formatted output.
ii. Handling User Input and Game Loop
Now, we’ll introduce the Scanner for input and implement the main game loop, including input validation and comparison logic. We’ll use a try-with-resources block for the Scanner to ensure it’s closed automatically, preventing resource leaks.
Action: Add the game loop, input handling, and comparison logic.
File: src/main/java/com/example/game/NumberGuessingGame.java (modified section)
public static void main(String[] args) {
LOGGER.info("Starting Number Guessing Game application.");
Random random = new Random();
int secretNumber = random.nextInt(MAX_NUMBER - MIN_NUMBER + 1) + MIN_NUMBER;
LOGGER.log(Level.FINE, "Secret number generated: {0}", secretNumber);
System.out.println("Welcome to the Number Guessing Game!");
System.out.printf("I have secretly chosen a number between %d and %d.%n", MIN_NUMBER, MAX_NUMBER);
System.out.printf("You have %d attempts to guess it.%n", MAX_ATTEMPTS);
int attempts = 0;
boolean hasGuessedCorrectly = false;
// Use try-with-resources to ensure the Scanner is closed automatically
try (Scanner scanner = new Scanner(System.in)) {
while (attempts < MAX_ATTEMPTS && !hasGuessedCorrectly) {
attempts++;
System.out.printf("Attempt %d/%d. Enter your guess: ", attempts, MAX_ATTEMPTS);
int playerGuess;
try {
playerGuess = scanner.nextInt();
LOGGER.log(Level.INFO, "Player guess: {0} (Attempt {1})", new Object[]{playerGuess, attempts});
if (playerGuess < MIN_NUMBER || playerGuess > MAX_NUMBER) {
System.out.printf("Your guess %d is outside the range [%d, %d]. Please try again.%n",
playerGuess, MIN_NUMBER, MAX_NUMBER);
// Do not count this as a valid attempt against MAX_ATTEMPTS if out of range
attempts--;
continue; // Skip to the next iteration
}
if (playerGuess < secretNumber) {
System.out.println("Too low! Try again.");
} else if (playerGuess > secretNumber) {
System.out.println("Too high! Try again.");
} else {
System.out.printf("Congratulations! You've guessed the number %d in %d attempts!%n",
secretNumber, attempts);
hasGuessedCorrectly = true;
}
} catch (InputMismatchException e) {
LOGGER.log(Level.WARNING, "Invalid input received: {0}. Please enter an integer.", scanner.next());
System.out.println("Invalid input. Please enter an integer number.");
scanner.next(); // Consume the invalid input to prevent an infinite loop
attempts--; // Don't count invalid input as an attempt
} catch (Exception e) {
LOGGER.log(Level.SEVERE, "An unexpected error occurred during input processing.", e);
System.err.println("An internal error occurred. Please restart the game.");
break; // Exit the loop on severe error
}
}
if (!hasGuessedCorrectly) {
System.out.printf("Sorry, you've run out of attempts! The secret number was %d.%n", secretNumber);
}
} catch (Exception e) {
LOGGER.log(Level.SEVERE, "Failed to initialize Scanner or unexpected error during game.", e);
System.err.println("A critical error occurred. The game cannot start or continue.");
}
LOGGER.info("Number Guessing Game application finished.");
}
Explanation:
int attempts = 0; boolean hasGuessedCorrectly = false;: Variables to track game state.try (Scanner scanner = new Scanner(System.in)) { ... }: This is a try-with-resources statement. It ensures that theScannerobject, which uses system resources, is automatically closed when thetryblock finishes, regardless of whether it completes normally or an exception occurs. This is a crucial best practice for resource management.while (attempts < MAX_ATTEMPTS && !hasGuessedCorrectly): The main game loop continues as long as the player has attempts left AND hasn’t guessed correctly.attempts++;: Increments the attempt counter at the start of each valid guess attempt.try { ... } catch (InputMismatchException e) { ... }: This innertry-catchblock is vital for robust input handling.playerGuess = scanner.nextInt();: Reads the integer input. If the user types something that isn’t an integer (e.g., “hello”), anInputMismatchExceptionis thrown.LOGGER.log(Level.INFO, ...): Logs the player’s guess at anINFOlevel, useful for tracking game progress.if (playerGuess < MIN_NUMBER || playerGuess > MAX_NUMBER): Input validation. If the guess is out of range, we inform the user, decrementattempts(as it wasn’t a valid guess against the range), andcontinueto the next loop iteration.- Comparison Logic: Standard
if-else if-elsestructure to provide “Too low”, “Too high”, or “Congratulations” feedback. hasGuessedCorrectly = true;: Sets the flag totrueto exit the loop upon a correct guess.catch (InputMismatchException e): Catches non-integer input.LOGGER.log(Level.WARNING, ...): Logs the warning about invalid input.scanner.next();: Crucially, this consumes the invalid token from theScanner’s buffer. If you don’t do this, thescanner.nextInt()call in the next loop iteration will try to read the same invalid token again, leading to an infinite loop ofInputMismatchExceptions.attempts--;: Invalid input should not count as a legitimate attempt.
catch (Exception e): A general catch-all for any other unexpected errors during input, logging it asSEVEREand exiting the loop.
if (!hasGuessedCorrectly): After the loop, this checks if the player ran out of attempts without guessing correctly.- Outer
catch (Exception e): Catches any errors that might occur during theScannerinitialization or unexpected issues with thetry-with-resourcesblock itself, ensuring the application doesn’t crash silently. LOGGER.info("Number Guessing Game application finished.");: Logs the application’s graceful exit.
c) Testing This Component
Now that we have the core logic, let’s compile and run the game to see it in action and test its various behaviors.
Action: Compile and run the NumberGuessingGame.
Open your terminal or command prompt.
Navigate to your project’s root directory (where
pom.xmlis located).Compile the project using Maven:
mvn clean compileYou should see
BUILD SUCCESSif everything is correct.Run the application:
mvn exec:java -Dexec.mainClass="com.example.game.NumberGuessingGame"
Expected Behavior & Debugging Tips:
- Game Start: You should see the welcome messages and the first prompt:
Welcome to the Number Guessing Game! I have secretly chosen a number between 1 and 100. You have 10 attempts to guess it. Attempt 1/10. Enter your guess: - Correct Guess: If you guess the number correctly (you might need to guess a few times), you should see:
Congratulations! You've guessed the number [secret number] in [X] attempts! - High/Low Feedback:
- Guess
50(if secret is75):Too low! Try again. - Guess
70(if secret is60):Too high! Try again.
- Guess
- Out-of-Range Guess: Try entering
0or101.
Notice that the attempt count (Attempt 1/10. Enter your guess: 0 Your guess 0 is outside the range [1, 100]. Please try again. Attempt 1/10. Enter your guess:1/10) doesn’t increment for out-of-range guesses, which is the desired behavior. - Invalid Input: Try entering
abcor!@#.
Again, the attempt count should not increment, and the game should prompt for input again without crashing.Attempt 1/10. Enter your guess: abc Invalid input. Please enter an integer number. Attempt 1/10. Enter your guess: - Running out of Attempts: If you make
MAX_ATTEMPTSincorrect guesses, the game should end with:Sorry, you've run out of attempts! The secret number was [secret number]. - Debugging: If you encounter unexpected behavior:
- Check logs: The
LOGGER.log(...)calls will output to the console by default. Look forSEVEREorWARNINGmessages. - Review your code: Double-check loop conditions,
if-elselogic, and variable assignments. - Use print statements (temporarily): Add
System.out.println("Debug: " + variableName);at strategic points to see variable values during execution. Remove these before committing. - IDE Debugger: If using an IDE like IntelliJ IDEA or Eclipse, set breakpoints and step through your code line by line.
- Check logs: The
3. Production Considerations
Even for a simple console game, thinking about production readiness from the start is a good habit.
- Error Handling: We’ve implemented basic
InputMismatchExceptionhandling and a generalExceptioncatch for theScannerblock. For a more complex application, you might define custom exception types or use a more structured approach to error reporting. - Performance Optimization: For this small application, performance is not a concern. However, for applications requiring high-performance random numbers, especially in multi-threaded environments, consider
ThreadLocalRandomin Java 7+ which can be more efficient thanjava.util.Random. - Security Considerations:
- Randomness: For applications requiring cryptographically secure random numbers (e.g., generating session tokens, encryption keys),
java.security.SecureRandomshould be used instead ofjava.util.Random. Our guessing game doesn’t have such requirements. - Input Validation: We’ve validated that input is an integer within a range. In web applications, this would extend to preventing SQL injection, XSS, and other attacks by sanitizing all user input.
- Randomness: For applications requiring cryptographically secure random numbers (e.g., generating session tokens, encryption keys),
- Logging and Monitoring: We’ve used
java.util.logging. For production systems, you would typically configure this logger (vialogging.propertiesor programmatically) to write to files, rotate logs, and potentially send logs to a centralized logging system (e.g., ELK stack, Splunk). For more advanced logging, libraries like SLF4J with Logback or Log4j2 are preferred due to their flexibility and performance.- Log Levels: Using
Level.FINEfor debugging details andLevel.INFOfor general progress is a good practice.WARNINGfor recoverable issues andSEVEREfor critical, unrecoverable errors.
- Log Levels: Using
4. Code Review Checkpoint
At this point, you should have a single, well-structured Java file that implements the Number Guessing Game.
Summary of what was built:
- A
NumberGuessingGame.javaclass. - Random number generation within a configurable range.
- A game loop that prompts the user for guesses.
- Robust input validation, handling non-integer input and out-of-range guesses gracefully.
- Comparison logic providing feedback (“Too high!”, “Too low!”, “Congratulations!”).
- Tracking of player attempts.
- Proper resource management for
Scannerusingtry-with-resources. - Basic logging for game events and errors.
Files created/modified:
src/main/java/com/example/game/NumberGuessingGame.java
How it integrates with existing code:
This chapter’s project, the Number Guessing Game, is a standalone console application. It does not directly integrate with the SimpleCalculator from the previous chapter. Each project in this series is designed to be a self-contained learning module, building your Java skills incrementally across different application types.
5. Common Issues & Solutions
Here are some common pitfalls developers might encounter with this game and how to address them:
Issue: Infinite Loop on Invalid Input
- Symptom: When you enter non-numeric text (e.g., “hello”), the program repeatedly prints “Invalid input. Please enter an integer number.” without waiting for new input.
- Reason: The
scanner.nextInt()method throws anInputMismatchExceptionbut does not consume the invalid token from the input stream. The next timescanner.nextInt()is called, it tries to read the same invalid token again, causing the exception to re-occur immediately. - Solution: Inside your
InputMismatchExceptioncatch block, addscanner.next();after thenextInt()call that caused the exception. This consumes the invalid token, allowing theScannerto move to the next valid input. We’ve implemented this in our code.
Issue: Off-by-One Error in Random Number Generation
- Symptom: The generated secret number is never
MAX_NUMBERor sometimesMIN_NUMBER - 1. - Reason:
Random.nextInt(bound)generates numbers from0(inclusive) tobound(exclusive). If your calculation forboundor the offset+ MIN_NUMBERis incorrect, you might miss the upper or lower bounds. - Solution: Ensure your calculation
random.nextInt(MAX_NUMBER - MIN_NUMBER + 1) + MIN_NUMBERis correct.(MAX_NUMBER - MIN_NUMBER + 1)correctly represents the count of numbers in the range[MIN_NUMBER, MAX_NUMBER]. AddingMIN_NUMBERcorrectly shifts the0-based range. Test by temporarily loggingsecretNumberatLevel.INFOto verify its range during development.
- Symptom: The generated secret number is never
Issue: Scanner Resource Leak Warning
- Symptom: Your IDE or compiler might show a warning about
Scannernot being closed. - Reason: If you create a
Scannerobject (e.g.,Scanner scanner = new Scanner(System.in);) but don’t callscanner.close()when you’re done with it, it can lead to resource leaks, especially ifSystem.inis tied to a file or network stream. - Solution: Use the
try-with-resourcesstatement:try (Scanner scanner = new Scanner(System.in)) { ... }. This automatically ensuresscanner.close()is called when thetryblock exits, even if an exception occurs. Our code already uses this best practice.
- Symptom: Your IDE or compiler might show a warning about
6. Testing & Verification
To ensure your Number Guessing Game is working as expected, perform the following verification steps:
Successful Compilation:
- Run
mvn clean compilefrom your project root. VerifyBUILD SUCCESS.
- Run
Run the Application:
- Execute the game:
mvn exec:java -Dexec.mainClass="com.example.game.NumberGuessingGame"
- Execute the game:
Test Cases:
- Correct Guess: Play until you guess the correct number. Verify the “Congratulations!” message and the attempt count.
- Too High/Too Low: Make several guesses, ensuring the feedback (“Too high!”, “Too low!”) is accurate.
- Invalid Input (Non-numeric): Enter text like “hello”. Verify the “Invalid input.” message and that the attempt count does not increment.
- Out-of-Range Input: Enter numbers like
0or101. Verify the “Your guess is outside the range…” message and that the attempt count does not increment. - Run Out of Attempts: Play a full game, making incorrect guesses until you exhaust all
MAX_ATTEMPTS. Verify the “Sorry, you’ve run out of attempts!” message and that the secret number is revealed. - Multiple Games (Manual Restart): Since we haven’t implemented a “play again” feature yet, stop the application and restart it multiple times to ensure consistent behavior and that the random number generation works correctly each time.
Check Log Output:
- Examine the console output for log messages. You should see
INFOmessages for game start/end and player guesses. If you configure your logging to a finer level, you’d also see theFINElog message revealing thesecretNumber. This confirms your logging setup is functional.
- Examine the console output for log messages. You should see
7. Summary & Next Steps
Congratulations! You’ve successfully built the core logic for a Number Guessing Game. In this chapter, you learned how to:
- Generate random numbers using
java.util.Random. - Handle user input from the console using
java.util.Scanner. - Implement a robust game loop with attempt tracking.
- Perform essential input validation and error handling for a smoother user experience.
- Apply best practices like
try-with-resourcesfor resource management and basic logging for observability.
This project, while simple, reinforces critical Java concepts that are foundational for building more complex interactive applications. You’ve now mastered basic game logic, which can be extended for many other types of interactive programs.
In Chapter 5: Temperature Converter: Data Conversion & Formatting, we will shift our focus to building a utility application that converts temperatures between Celsius and Fahrenheit. This will introduce you to different data types, mathematical operations, and precise output formatting, further enhancing your command-line application development skills. Get ready to dive into practical data manipulation!