HiLo Guessing Game in Java: Number Guessing Program

Build the HiLo number guessing game in Java with a random secret number, safe console input and a play-again loop, and see why binary search always wins in seven tries.

Java Interview Puzzles

HiLo is a number guessing game in which the program picks a secret number between 1 and 100, and after each guess it answers HI when the guess is too high or LO when the guess is too low. The player wins by finding the number within a fixed number of tries. Writing the HiLo guessing game in Java is a classic first console program, because it combines a random number with a loop that reads and checks the player’s input.

We build the HiLo game when we practice the basics of a Java program, such as reading user input and validating it. The game is also the simplest way to see binary search at work, since the best player halves the possible numbers with every guess.

The following example creates a game with the secret number 42 and seven tries, and checks three guesses. The result of each line is in the comment.

HiLo game = new HiLo(42, 7);                    // secret 42, 7 tries
HiLo.Hint first = game.check(50);               // HI
HiLo.Hint second = game.check(25);              // LO
HiLo.Hint third = game.check(42);               // CORRECT
int used = game.attemptsUsed();                 // 3
boolean won = game.isWon();                     // true

int secret = HiLo.randomSecret(RandomGenerator.getDefault());   // a random number from 1 to 100

Notice that the HiLo class only knows the rules and never reads the console, so we can test it with fixed numbers like 42.

Next, we write the HiLo class, read the guesses from the console without crashing on bad input, and play a full round. At the end, we let the computer play all 100 secret numbers to see why seven tries are always enough.

1. The Rules of the HiLo Game

The game has two rules. The secret number is a whole number from 1 to 100, both included, and the player has seven tries to find it. After every wrong guess, the program prints one hint.

  • HI means the guess is higher than the secret number, so the next guess must be lower.
  • LO means the guess is lower than the secret number, so the next guess must be higher.

Many versions of HiLo allow only six tries. With six tries, even a perfect player cannot win every round, as we prove in section 5. Seven tries make the game fair, because a player who picks the middle of the remaining range always wins.

A guess outside 1 to 100, or text such as abc, is not a real guess. The program prints a short message and asks again, and the bad input does not cost a try.

2. Writing the HiLo Class

We keep the game rules in one class and the console code in another. The HiLo class stores the secret number and counts the tries, whereas the PlayHiLoGame class reads the guesses and prints the hints. With this split, we can check the rules with fixed numbers, and the computer player in section 5 can use the same class.

2.1. Picking the Secret Number

Since Java 17, the interface RandomGenerator is the common type of all random number generators in the JDK, including the old Random class. Its method nextInt(origin, bound) returns a number from origin (included) to bound (excluded), so we pass 101 to include 100.

RandomGenerator random = RandomGenerator.getDefault();
int secret = random.nextInt(1, 101);            // 1 to 100

int oldStyle = new Random().nextInt(100) + 1;   // 1 to 100
int offByOne = new Random().nextInt(100);       // 0 to 99 (never 100)

The last line shows a common bug in guessing games. The method nextInt(100) returns a number from 0 to 99, so the secret is sometimes 0 and never 100, and a player who follows the rules can never find a secret of 0. The two-argument form nextInt(1, 101) states both ends of the range, so the code reads like the rule. For other ranges and generator types, read generating random numbers in a range.

2.2. Checking a Guess

Every guess gets one of three answers, so we model them as an enum named Hint. The method check() compares the guess with the secret number, counts the try and returns the hint.

public class HiLo {

  public static final int MIN = 1;
  public static final int MAX = 100;

  public enum Hint { HI, LO, CORRECT }

  private final int secret;
  private final int maxAttempts;
  private int attemptsUsed;
  private boolean won;

  public HiLo(int secret, int maxAttempts) {
    if (secret < MIN || secret > MAX) {
      throw new IllegalArgumentException("Secret must be between " + MIN + " and " + MAX);
    }
    this.secret = secret;
    this.maxAttempts = maxAttempts;
  }

  public static int randomSecret(RandomGenerator random) {
    return random.nextInt(MIN, MAX + 1);   // upper bound is exclusive
  }

  public Hint check(int guess) {
    if (isOver()) {
      throw new IllegalStateException("The game is over");
    }
    if (guess < MIN || guess > MAX) {
      throw new IllegalArgumentException("Guess must be between " + MIN + " and " + MAX);
    }
    attemptsUsed++;
    if (guess > secret) {
      return Hint.HI;
    }
    if (guess < secret) {
      return Hint.LO;
    }
    won = true;
    return Hint.CORRECT;
  }

  public boolean isOver() {
    return won || attemptsUsed >= maxAttempts;
  }

  // isWon(), attemptsUsed(), attemptsLeft() and secret() return the fields
}

The method randomSecret() takes the generator as a parameter instead of creating one, so a caller can pass any RandomGenerator. The method check() also protects the game state. It throws an IllegalStateException when the round is over, or an IllegalArgumentException when the guess is outside 1 to 100.

HiLo game = new HiLo(42, 7);
HiLo.Hint hint = game.check(150);   // IllegalArgumentException: Guess must be between 1 and 100

HiLo.Hint found = game.check(42);   // CORRECT
HiLo.Hint again = game.check(10);   // IllegalStateException: The game is over

3. Reading Guesses From the Console Safely

A console program gets every guess as text, so it must handle any text the player types. The input can also end without an answer, for example when the player presses Ctrl+D on Linux or macOS (Ctrl+Z on Windows), or when a script pipes a file into the program. In that case, BufferedReader.readLine()) returns null.

A common mistake is to call input.equalsIgnoreCase(“y”) on the result of readLine() right away. When the input ends, the call throws a NullPointerException and the program stops with a stack trace. Our game checks for null first and ends with a normal message.

To turn the text into a number, we parse it inside a small helper that returns an OptionalInt. The helper calls strip() first, so a guess typed with spaces around it still counts. The String to int article explains the parsing rules in detail.

static OptionalInt tryParseInt(String text) {
  try {
    return OptionalInt.of(Integer.parseInt(text.strip()));
  } catch (NumberFormatException e) {
    return OptionalInt.empty();
  }
}
OptionalInt valid = tryParseInt("50");      // OptionalInt[50]
OptionalInt spaced = tryParseInt(" 50 ");   // OptionalInt[50]
OptionalInt text = tryParseInt("abc");      // OptionalInt.empty
OptionalInt big = tryParseInt("150");       // OptionalInt[150] (the range check comes next)

The method playRound() uses the helper for every line the player types. An empty result or a number outside 1 to 100 prints a message and asks again, without calling check(), so the try is not lost. The switch expression prints one message per hint.

// Returns false when the input ends before the round is over
static boolean playRound(HiLo game, BufferedReader in, PrintStream out) throws IOException {
  while (!game.isOver()) {
    out.print("Guess (" + game.attemptsLeft() + " left): ");
    String line = in.readLine();
    if (line == null) {
      return false;
    }
    OptionalInt guess = tryParseInt(line);
    if (guess.isEmpty() || guess.getAsInt() < HiLo.MIN || guess.getAsInt() > HiLo.MAX) {
      out.println("Please type a whole number from 1 to 100.");   // does not cost an attempt
      continue;
    }
    HiLo.Hint hint = game.check(guess.getAsInt());
    switch (hint) {
      case HI -> out.println("HI");
      case LO -> out.println("LO");
      case CORRECT -> out.println("Correct! You found it in " + game.attemptsUsed() + " tries.");
    }
  }
  if (!game.isWon()) {
    out.println("No tries left. The secret number was " + game.secret() + ".");
  }
  return true;
}

We use a BufferedReader instead of a Scanner here, because readLine() gives us the whole line as text and the null check covers the end of the input. A Scanner would need hasNextInt() and hasNextLine() calls to do the same. The ways to read input from the console are compared in a separate article.

4. Playing the HiLo Game

The method main() wraps System.in in a BufferedReader and passes the default generator to play(). The method play() starts a new round with a new secret number until the player answers n to “Play again?”, or until the input ends.

static final int MAX_ATTEMPTS = 7;

public static void main(String[] args) throws IOException {
  BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
  play(in, System.out, RandomGenerator.getDefault());
}

static void play(BufferedReader in, PrintStream out, RandomGenerator random) throws IOException {
  out.println("Guess the secret number between 1 and 100. You have " + MAX_ATTEMPTS + " tries.");
  do {
    HiLo game = new HiLo(HiLo.randomSecret(random), MAX_ATTEMPTS);
    if (!playRound(game, in, out)) {
      break;   // input ended (Ctrl+D or a closed pipe)
    }
  } while (askYesNo("Play again? (y/n): ", in, out));
  out.println("Thanks for playing!");
}

The helper askYesNo() reads one line, strips it and accepts only y or n in either case. It repeats the question for any other answer and returns false when the input ends.

The game needs Java 17 or later for RandomGenerator, and the code here uses Java 25. In the following round, the player first types abc and 150, which cost no tries. After that, the player picks the middle of the remaining numbers each time and finds the secret number 14 with the last try.

Guess the secret number between 1 and 100. You have 7 tries.
Guess (7 left): abc
Please type a whole number from 1 to 100.
Guess (7 left): 150
Please type a whole number from 1 to 100.
Guess (7 left): 50
HI
Guess (6 left): 25
HI
Guess (5 left): 12
LO
Guess (4 left): 18
HI
Guess (3 left): 15
HI
Guess (2 left): 13
LO
Guess (1 left): 14
Correct! You found it in 7 tries.
Play again? (y/n): maybe
Please type y or n.
Play again? (y/n): n
Thanks for playing!

When a player guesses badly, the round ends after the seventh hint and the program shows the secret number.

Guess (1 left): 70
LO
No tries left. The secret number was 78.
Play again? (y/n): n
Thanks for playing!

5. Why Seven Tries Are Always Enough

The best strategy for HiLo is binary search. The player guesses the middle of the numbers that are still possible, and each hint removes the half on the wrong side of the guess. The diagram follows the winning round from section 4, where seven guesses shrink the range from 100 numbers to one.

Binary search for the secret number 14 in seven steps. Range 1 to 100, guess 50, HI. Range 1 to 49, guess 25, HI. Range 1 to 24, guess 12, LO. Range 13 to 24, guess 18, HI. Range 13 to 17, guess 15, HI. Range 13 to 14, guess 13, LO. Range 14 to 14, guess 14, CORRECT.
Each hint cuts the remaining range in half, so seven guesses are enough for 100 numbers.

With one guess, a player can check one number. With every extra guess, the number of values that a player can always find doubles and grows by one, so n guesses cover up to 2 to the power of n, minus 1, numbers.

GuessesNumbers a perfect player can always find
531
663
7127
101023

Six guesses cover only 63 numbers, which is less than 100, so the six-try version of the game cannot be won for every secret number. Seven guesses cover 127 numbers, which is more than enough. Binary search is not only for games, because git bisect uses the same halving to find the commit that introduced a bug. For example, among 100 commits since the last good release, git bisect finds the bad commit after about seven test runs.

To prove the table for our game, the class HiLoSolver plays all 100 secret numbers with binary search and counts the guesses. The method solve() keeps the lowest and highest possible values and moves one of them after every hint.

// Always guesses the middle of the numbers that are still possible
static int solve(HiLo game) {
  int low = HiLo.MIN;
  int high = HiLo.MAX;
  while (true) {
    int guess = (low + high) >>> 1;
    HiLo.Hint hint = game.check(guess);
    switch (hint) {
      case CORRECT -> {
        return game.attemptsUsed();
      }
      case HI -> high = guess - 1;
      case LO -> low = guess + 1;
    }
  }
}

The expression (low + high) >>> 1 divides the sum by two with an unsigned shift, which gives the right middle value even when the sum is larger than Integer.MAX_VALUE. For 1 to 100, a plain division gives the same result. The main() method of HiLoSolver runs solve() for every secret number and prints the results.

Most guesses needed: 7
Secrets found in 6 guesses or fewer: 63
Guesses for 1: 6
Guesses for 76: 6

A perfect player needs at most seven guesses, whereas with only six tries, binary search misses 37 of the 100 secret numbers. The last line shows how much the strategy matters. A player who guesses 40, 60, 80, 70, 75 and 77 misses the secret number 76 after six tries, whereas binary search finds 76 with the sixth guess.

6. Conclusion

The HiLo game picks a secret number from 1 to 100 and answers HI or LO after each guess. We create the secret number with RandomGenerator.nextInt(1, 101), because the upper bound is excluded and nextInt(100) never returns 100.

The game rules stay in the HiLo class, and the console code stays in PlayHiLoGame. The console code parses each line with a helper that returns an OptionalInt, and it treats a null line as the end of the input, so bad input never crashes the game or costs a try.

Binary search finds any of the 100 numbers within seven guesses. That is why the game gives seven tries, and why a six-try limit makes some rounds impossible to win even for a perfect player.

7. References

Happy Learning !!

Source Code on Github

Leave a Comment

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.