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:

  1. Initialization: The game will generate a secret random number within a predefined range (e.g., 1 to 100).
  2. User Input Loop: The player will be prompted to guess the number.
  3. Input Validation: The game will ensure the input is a valid integer.
  4. 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.
  5. Attempt Tracking: The game will keep track of how many attempts the player has made.
  6. 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 a java.util.logging.Logger instance. 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 the Random class. For simple applications like this, Random is sufficient. For cryptographically secure random numbers, java.security.SecureRandom should 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 + 1 calculates the total count of numbers in our range (e.g., for 1-100, it’s 100 numbers).
    • random.nextInt(...) generates a number from 0 up to (but not including) this count.
    • Adding MIN_NUMBER shifts this 0-based range to our desired MIN_NUMBER-based range.
  • LOGGER.log(Level.FINE, "Secret number generated: {0}", secretNumber);: We use Level.FINE for 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(...) and System.out.printf(...): These lines provide initial instructions to the player. printf is 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 the Scanner object, which uses system resources, is automatically closed when the try block 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 inner try-catch block 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”), an InputMismatchException is thrown.
    • LOGGER.log(Level.INFO, ...): Logs the player’s guess at an INFO level, 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, decrement attempts (as it wasn’t a valid guess against the range), and continue to the next loop iteration.
    • Comparison Logic: Standard if-else if-else structure to provide “Too low”, “Too high”, or “Congratulations” feedback.
    • hasGuessedCorrectly = true;: Sets the flag to true to 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 the Scanner’s buffer. If you don’t do this, the scanner.nextInt() call in the next loop iteration will try to read the same invalid token again, leading to an infinite loop of InputMismatchExceptions.
      • 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 as SEVERE and 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 the Scanner initialization or unexpected issues with the try-with-resources block 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.

  1. Open your terminal or command prompt.

  2. Navigate to your project’s root directory (where pom.xml is located).

  3. Compile the project using Maven:

    mvn clean compile
    

    You should see BUILD SUCCESS if everything is correct.

  4. 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 is 75): Too low! Try again.
    • Guess 70 (if secret is 60): Too high! Try again.
  • Out-of-Range Guess: Try entering 0 or 101.
    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:
    
    Notice that the attempt count (1/10) doesn’t increment for out-of-range guesses, which is the desired behavior.
  • Invalid Input: Try entering abc or !@#.
    Attempt 1/10. Enter your guess: abc
    Invalid input. Please enter an integer number.
    Attempt 1/10. Enter your guess:
    
    Again, the attempt count should not increment, and the game should prompt for input again without crashing.
  • Running out of Attempts: If you make MAX_ATTEMPTS incorrect 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 for SEVERE or WARNING messages.
    • Review your code: Double-check loop conditions, if-else logic, 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.

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 InputMismatchException handling and a general Exception catch for the Scanner block. 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 ThreadLocalRandom in Java 7+ which can be more efficient than java.util.Random.
  • Security Considerations:
    • Randomness: For applications requiring cryptographically secure random numbers (e.g., generating session tokens, encryption keys), java.security.SecureRandom should be used instead of java.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.
  • Logging and Monitoring: We’ve used java.util.logging. For production systems, you would typically configure this logger (via logging.properties or 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.FINE for debugging details and Level.INFO for general progress is a good practice. WARNING for recoverable issues and SEVERE for critical, unrecoverable errors.

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.java class.
  • 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 Scanner using try-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:

  1. 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 an InputMismatchException but does not consume the invalid token from the input stream. The next time scanner.nextInt() is called, it tries to read the same invalid token again, causing the exception to re-occur immediately.
    • Solution: Inside your InputMismatchException catch block, add scanner.next(); after the nextInt() call that caused the exception. This consumes the invalid token, allowing the Scanner to move to the next valid input. We’ve implemented this in our code.
  2. Issue: Off-by-One Error in Random Number Generation

    • Symptom: The generated secret number is never MAX_NUMBER or sometimes MIN_NUMBER - 1.
    • Reason: Random.nextInt(bound) generates numbers from 0 (inclusive) to bound (exclusive). If your calculation for bound or the offset + MIN_NUMBER is incorrect, you might miss the upper or lower bounds.
    • Solution: Ensure your calculation random.nextInt(MAX_NUMBER - MIN_NUMBER + 1) + MIN_NUMBER is correct. (MAX_NUMBER - MIN_NUMBER + 1) correctly represents the count of numbers in the range [MIN_NUMBER, MAX_NUMBER]. Adding MIN_NUMBER correctly shifts the 0-based range. Test by temporarily logging secretNumber at Level.INFO to verify its range during development.
  3. Issue: Scanner Resource Leak Warning

    • Symptom: Your IDE or compiler might show a warning about Scanner not being closed.
    • Reason: If you create a Scanner object (e.g., Scanner scanner = new Scanner(System.in);) but don’t call scanner.close() when you’re done with it, it can lead to resource leaks, especially if System.in is tied to a file or network stream.
    • Solution: Use the try-with-resources statement: try (Scanner scanner = new Scanner(System.in)) { ... }. This automatically ensures scanner.close() is called when the try block exits, even if an exception occurs. Our code already uses this best practice.

6. Testing & Verification

To ensure your Number Guessing Game is working as expected, perform the following verification steps:

  1. Successful Compilation:

    • Run mvn clean compile from your project root. Verify BUILD SUCCESS.
  2. Run the Application:

    • Execute the game: mvn exec:java -Dexec.mainClass="com.example.game.NumberGuessingGame"
  3. 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 0 or 101. 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.
  4. Check Log Output:

    • Examine the console output for log messages. You should see INFO messages for game start/end and player guesses. If you configure your logging to a finer level, you’d also see the FINE log message revealing the secretNumber. This confirms your logging setup is functional.

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-resources for 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!