Java

Practice: Build a Number Guessing Game in Java

Executive Summary

Build a number guessing game in Java that generates a secret with Random.nextInt(origin, bound), reads guesses through a Scanner using check-then-consume input handling, and answers higher or lower until the player wins or burns seven attempts. The replay loop and best-score tracking introduce loop-carried state, which is the same shape production request handlers use for counters. The build’s two headline lessons: an unread bad token causes an infinite loop, so check hasNextInt() and discard with next(); and seven guesses suffice for 1 to 100 because each guess can halve the range, which is exactly how git bisect finds bad commits. Therefore, use java.util.Random for games and SecureRandom for anything an attacker could predict. Finally, extensions add difficulty levels, streaks, and an optimal-play mode.

The Problem Statement

The computer picks a secret number from 1 to 100. The player guesses a number each turn, and the program answers higher, lower, or correct, tracking how many guesses were used. Also, the player wins by guessing the number within seven attempts. After each round the game asks to play again and remembers the best (lowest) winning score of the session.

Requirements

  1. Generate a secret in 1 to 100, inclusive, on every new round.
  2. Read a guess per turn; reject non-numeric input without crashing or looping forever.
  3. Answer higher when the guess is too low, lower when it is too high.
  4. Limit each round to 7 attempts; reveal the secret when the player runs out.
  5. Count attempts and show them on a win.
  6. After each round, offer replay; track and announce the session’s best score.
  7. JDK only: one file, no libraries.

Building the Number Guessing Game in Java, Step by Step

Step 1: Generate the Secret with Random

java.util.Random is the JDK’s generator for games, simulations, and test data, as the Random class docs define. Since Java 17, it exposes nextInt(origin, bound): the origin is included, the bound is excluded. Therefore, 1 to 100 inclusive is nextInt(1, 101).

var random = new Random();
int secret = random.nextInt(1, 101);   // 1 to 100 inclusive

Of course, the classic alternative, random.nextInt(100) + 1, works too, and you will read it in older code. However, the origin-and-bound form states the range without arithmetic, so prefer it in new code. The deeper tour of Random, including seeding for reproducible tests, comes in the utility classes article.

Step 2: Read Guesses Without Infinite Loops

However, this step contains the build’s most important bug. Wrong code first:

// WRONG: the bad token is never consumed, so the loop never ends
while (true) {
    int guess = scanner.nextInt();   // letters make it throw,
}                                    // and the letters stay in the buffer
// RIGHT: check first, then consume and discard bad tokens
static int readGuess(Scanner scanner) {
    while (true) {
        System.out.print("your guess: ");
        if (scanner.hasNextInt()) {
            return scanner.nextInt();
        }
        System.out.println("numbers only");
        scanner.next();   // discard the invalid token or the loop repeats forever
    }
}

In short, the delta: Scanner does not throw away input it cannot parse. If you never consume the bad token, every pass reads the same letters and fails identically, forever. Check-then-consume with hasNextInt() and next(), per the Scanner documentation, is the loop-safe pattern; exception-based parsing works in one-shot mode, as the calculator showed, but loops need the explicit discard.

Step 3: The Round Loop and Why Seven Guesses

Each round is a bounded loop: read a guess, count it, and answer with feedback. However, seven attempts are not an arbitrary limit. If every guess splits the remaining range in half, ten guesses cover 1 to 1000 and seven cover 1 to 100, because 2 to the seventh power is 128. In fact, that strategy is binary search, and it is the same algorithm git bisect uses to find a breaking commit.

static int playRound(Scanner scanner, int secret) {
    int attempts = 0;
    while (attempts < MAX_ATTEMPTS) {
        int guess = readGuess(scanner);
        attempts++;
        if (guess < secret) {
            System.out.println("higher");
        } else if (guess > secret) {
            System.out.println("lower");
        } else {
            System.out.println("correct in " + attempts + " guesses");
            return attempts;    // a win
        }
    }
    System.out.println("out of attempts, the number was " + secret);
    return -1;                  // a loss: -1 never wins best-score
}

Also, note the return values: attempts on a win, -1 on a loss. The sentinel makes best-score logic impossible to get wrong, because -1 never beats any real score.

Step 4: Replay and Best-Score State

The replay loop wraps the round loop, and two variables cross iteration boundaries: playAgain and bestScore. So, that is loop-carried state, and it is exactly how a request handler keeps counters across requests. The yes-or-no reader reuses the string habits from the strings article: compare content with equalsIgnoreCase, never ==.

Variable Lives for Why
secret One round Generated fresh each round
attempts One round Counts that round’s guesses
bestScore The whole session Compares across rounds
playAgain The whole session Controls the replay loop
boolean playAgain = true;
int bestScore = Integer.MAX_VALUE;

while (playAgain) {
    int secret = random.nextInt(MIN, MAX + 1);
    int attempts = playRound(scanner, secret);
    if (attempts > 0 && attempts < bestScore) {
        bestScore = attempts;
        System.out.println("new best: " + bestScore + " guesses");
    }
    System.out.print("play again? (y/n): ");
    playAgain = readYesNo(scanner);
}
static boolean readYesNo(Scanner scanner) {
    while (true) {
        String answer = scanner.next().strip();
        if (answer.equalsIgnoreCase("y") || answer.equalsIgnoreCase("yes")) return true;
        if (answer.equalsIgnoreCase("n") || answer.equalsIgnoreCase("no")) return false;
        System.out.print("y or n, please: ");
    }
}

The Complete Program

Assemble it as GuessingGame.java:

import java.util.Random;
import java.util.Scanner;

public class GuessingGame {

    static final int MIN = 1;
    static final int MAX = 100;
    static final int MAX_ATTEMPTS = 7;

    public static void main(String[] args) {
        var scanner = new Scanner(System.in);
        var random = new Random();
        boolean playAgain = true;
        int bestScore = Integer.MAX_VALUE;

        System.out.println("Guess the number between " + MIN + " and " + MAX + ".");

        while (playAgain) {
            int secret = random.nextInt(MIN, MAX + 1);
            int attempts = playRound(scanner, secret);
            if (attempts > 0 && attempts < bestScore) {
                bestScore = attempts;
                System.out.println("new best: " + bestScore + " guesses");
            }
            System.out.print("play again? (y/n): ");
            playAgain = readYesNo(scanner);
        }
        System.out.println("thanks for playing, best: "
                + (bestScore == Integer.MAX_VALUE ? "-" : bestScore));
    }

    static int playRound(Scanner scanner, int secret) {
        int attempts = 0;
        while (attempts < MAX_ATTEMPTS) {
            int guess = readGuess(scanner);
            attempts++;
            if (guess < secret) {
                System.out.println("higher");
            } else if (guess > secret) {
                System.out.println("lower");
            } else {
                System.out.println("correct in " + attempts + " guesses");
                return attempts;
            }
        }
        System.out.println("out of attempts, the number was " + secret);
        return -1;
    }

    static int readGuess(Scanner scanner) {
        while (true) {
            System.out.print("your guess: ");
            if (scanner.hasNextInt()) {
                return scanner.nextInt();
            }
            System.out.println("numbers only");
            scanner.next();   // discard the invalid token
        }
    }

    static boolean readYesNo(Scanner scanner) {
        while (true) {
            String answer = scanner.next().strip();
            if (answer.equalsIgnoreCase("y") || answer.equalsIgnoreCase("yes")) return true;
            if (answer.equalsIgnoreCase("n") || answer.equalsIgnoreCase("no")) return false;
            System.out.print("y or n, please: ");
        }
    }
}

A session should look like this:

$ java GuessingGame
Guess the number between 1 and 100.
your guess: 50
higher
your guess: abc
numbers only
your guess: 75
lower
your guess: 62
correct in 3 guesses
play again? (y/n): n
thanks for playing, best: 3

Extensions to Try Yourself

  • Difficulty levels: easy (1 to 10, 4 attempts), normal (1 to 100, 7), hard (1 to 1000, 10). Then, a menu at startup turns the constants into per-round values.
  • Streak tracking: consecutive wins without a loss, shown next to the best score.
  • An optimal bot: make the program guess its own secret using the midpoint strategy and print each step. As a result, watching it win in at most 7 rounds is binary search made visible.
  • Let q quit mid-round by checking hasNextInt before each guess and handling the no-token case as an exit.
  • Warm and cold hints inside a narrow range of the secret, which forces you to think about thresholds as data, not code.

How Real Systems Do This

In fact, the feedback loop in this game is a real algorithm wearing a costume. Each guess divides the remaining range in half, which is binary search, and binary search underpins git bisect, database index lookups, and JDK array sorting. When you played well, you executed the same algorithm those systems run.

The bounded-attempt limit is also a security control in disguise. Verification codes for login and password-reset flows must reject after a fixed number of tries, or an attacker simply enumerates them. In my experience, the single most common finding in security reviews of such flows is a missing or unenforced attempt limit; the fix is the same while (attempts < MAX) shape you just wrote.

Finally, random numbers split into two worlds in production. java.util.Random is fast and fine for games and sampling; SecureRandom, its cryptographically strong sibling, backs tokens, session identifiers, and one-time codes. Guessing which one a system needs is a security decision, and the Spring Security article in Part 9 revisits it in a web context.

Decision Framework

  1. What are you generating? Games and test data take Random; tokens, codes, and anything an attacker might predict take SecureRandom.
  2. nextInt(origin, bound) or Math.random()? Generally, prefer nextInt for integers: it states the range exactly. Math.random() returns a double that needs scaling and casting, per the Math class docs, and casting truncates.
  3. Check-then-consume or try-catch in a loop? Check-then-consume. After all, exceptions as control flow in a loop hide the exit and burn the reader’s patience.
  4. Bounded or unbounded rounds? Bound them. Indeed, fairness in games, safety in verification flows, and predictability in tests all point the same way.
  5. Do you need a class yet? Not at this size. Part 2 will wrap this game’s state in a Game object, and seeing that diff teaches more than adding structure early.

When NOT to Use This

  • Do not use java.util.Random for security-sensitive values. After all, its output is predictable from observations, which is a feature for tests and a vulnerability for tokens; use SecureRandom.
  • Do not use Math.random() plus a cast for integer ranges. The cast truncates, the range arithmetic is easy to get wrong, and nextInt(origin, bound) says exactly what you mean.
  • Finally, do not over-structure the game with classes and interfaces now. The procedural version is honest at this scale; Part 2 introduces objects and refactors this game on purpose.

Common Mistakes

  • The infinite input loop: nextInt throws on letters and leaves the token in the buffer, so every retry reads the same bad input. Check hasNextInt() and discard with next().
  • Off-by-one ranges: nextInt(100) yields 0 to 99, so 1 to 100 needs nextInt(1, 101) or an explicit +1.
  • Comparing the replay answer with ==. Runtime-built strings fail reference equality; use equalsIgnoreCase.
  • Counting the winning guess as free, or counting invalid tokens as attempts. So, decide both policies once and apply them everywhere.
  • Creating a new Scanner over System.in inside a method. Multiple scanners over the same stream race for bytes; create one and pass it around.
  • Unbounded rounds in any code that guards a secret. After all, the attempt limit is the control, and removing it converts a game into a brute-force oracle.

Key Takeaways

  • Random.nextInt(origin, bound) gives inclusive-origin, exclusive-bound integers; 1 to 100 is nextInt(1, 101).
  • Check-then-consume is the loop-safe input pattern: hasNextInt() to ask, next() to discard what fails.
  • Each higher-or-lower answer halves the range, so seven guesses cover 1 to 100: binary search, the algorithm behind git bisect.
  • Similarly, loop-carried state like bestScore is how production handlers keep counters across iterations.
  • Bounded attempts are a security control, not just game fairness; verification flows need the same limit.
  • java.util.Random for games, SecureRandom for tokens and codes; choosing wrong is a vulnerability, not a style point.
  • One Scanner over System.in, created once and passed everywhere.

FAQ

How do I generate random numbers in Java?

Create a java.util.Random and call nextInt(1, 101) for integers in a range, or nextDouble() for fractions. Seed the constructor for reproducible sequences in tests. For concurrent work, ThreadLocalRandom.current() avoids contention, and Part 5 covers it.

What is the difference between Random and SecureRandom in Java?

Random is fast and predictable given enough output, which suits games and sampling. SecureRandom uses entropy sources that resist prediction, which tokens, session IDs, and one-time codes require. The choice is a security decision, not a performance one.

Why does my Scanner input loop run forever in Java?

Because the invalid token is never consumed. nextInt() fails without advancing, so the loop rereads the same letters. Call hasNextInt() first and discard bad tokens with next().

How many guesses guarantee a win between 1 and 100?

Seven. Each guess can halve the remaining range, and 2 to the seventh power is 128, which covers 100. Guess the midpoint every turn and you cannot lose.

What does git bisect have to do with a guessing game?

Both are binary search. bisect halves the commit range using good or bad feedback the same way the game halves the number range using higher or lower feedback.

Conclusion

You built a conversation loop with state that survives across iterations, input that cannot trap you in an infinite cycle, and a bound that turns out to be an algorithm and a security control at once. Overall, these are small habits with large echoes, from request counters to rate limiting to git bisect.

Part 1 of the course ends here, and Part 2 begins with classes, objects, and constructors. Watch what happens to this game’s loose variables when they finally get a home.

Feedback loops, bounded attempts, and check-then-consume input: the game is small, but the habits are production-sized.

Last updated on 5 September 2026.

Share this article

Leave a Reply

Your email address will not be published. Required fields are marked *