1 The Problem
We want a tiny game: the computer thinks of a number between 1 and 100, and the player keeps guessing. After each guess the computer says “too high” or “too low” until the player gets it right. Simple — but it teaches the three things every program does: ask, decide, and repeat.
2 How to Think About It
Before writing any code, picture the game as a loop with a decision inside it. You do not need to know Java yet — you just need to know the shape of what happens:
3 The Build — explained part by part
Here is the complete game, split into a testable playGame method and a tiny main that wires it to real stdin/stdout. Read each part’s plain-English note below it.
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.util.Random;
/**
* Number Guessing Game: the computer picks a secret number between 1 and 100,
* and the player guesses until they get it, hearing "too high" or "too low"
* after every wrong guess.
*/
public class NumberGuessingGame {
/**
* Plays one full game against a given secret number, reading guesses from
* {@code in} and writing prompts/feedback to {@code out}. Kept separate
* from {@code main} so it can be tested without a real terminal.
*
* @return the number of guesses it took to win
*/
static int playGame(int secret, BufferedReader in, PrintStream out) throws IOException {
int guesses = 0;
while (true) {
out.print("Guess a number between 1 and 100: ");
String line = in.readLine();
if (line == null) {
throw new IllegalStateException("No more input.");
}
int guess;
try {
guess = Integer.parseInt(line.trim());
} catch (NumberFormatException e) {
out.println("That's not a whole number. Try again.");
continue;
}
guesses++;
if (guess < secret) {
out.println("Too low.");
} else if (guess > secret) {
out.println("Too high.");
} else {
out.println("Correct! You got it in " + guesses + " guesses.");
return guesses;
}
}
}
public static void main(String[] args) throws IOException {
int secret = new Random().nextInt(100) + 1; // once, before the loop starts
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
playGame(secret, in, System.out);
}
}
System.in/System.out directly. That is what lets the tests below feed it fake input through a StringReader and capture its output in a ByteArrayOutputStream, with no real terminal involved.while (true) { ... } — Java has no dedicated infinite-loop keyword the way Rust does, so
while (true) is the idiomatic way to say “keep going until something inside explicitly returns.”Integer.parseInt(line.trim()) wrapped in a try/catch for
NumberFormatException — unlike Rust’s Result-returning .parse(), Java’s numeric parsing throws an unchecked exception on bad input, so catching it explicitly is what stands between a typo and a crashed program.new Random().nextInt(100) + 1 —
nextInt(100) returns 0–99, so adding 1 shifts the range to the 1–100 the game promises. Called exactly once, before the loop starts — the classic mistake below shows what happens if you get that placement wrong.
new Random().nextInt(100) inside the loop instead of once before it — the secret would change every guess and the game could never be won.playGame starts looping.if (guess = secret) by mistake instead of ==.int, so watch for it manually; it is a classic typo in every C-family language.Integer.parseInt throw uncaught — a non-numeric guess would crash the whole program with a stack trace instead of a friendly message.NumberFormatException explicitly, as playGame does, and continue the loop on a parse error.4 Test & Prove Each Part
How do we know the game works without playing it by hand? We write small tests — each one checks one rule and proves it. Java has no built-in test framework, so these use JUnit, the de facto standard covered in the course’s Testing lesson.
import org.junit.Test;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.io.StringReader;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class NumberGuessingGameTest {
private String run(int secret, String input) throws Exception {
BufferedReader in = new BufferedReader(new StringReader(input));
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
PrintStream out = new PrintStream(bytes);
NumberGuessingGame.playGame(secret, in, out);
return bytes.toString();
}
@Test
public void firstGuessCorrectTakesOneGuess() throws Exception {
String output = run(42, "42\n");
assertTrue(output.contains("Correct! You got it in 1 guesses."));
}
@Test
public void narrowsDownWithHighAndLowGuesses() throws Exception {
String output = run(37, "50\n25\n37\n");
assertTrue(output.contains("Too high."));
assertTrue(output.contains("Too low."));
assertTrue(output.contains("Correct! You got it in 3 guesses."));
}
@Test
public void rejectsNonNumericInputWithoutCrashing() throws Exception {
String output = run(10, "banana\n10\n");
assertTrue(output.contains("That's not a whole number. Try again."));
assertTrue(output.contains("Correct! You got it in 1 guesses."));
}
@Test
public void countsEveryGuessIncludingWrongOnes() throws Exception {
BufferedReader in = new BufferedReader(new StringReader("1\n2\n3\n4\n5\n"));
PrintStream out = new PrintStream(new ByteArrayOutputStream());
int guesses = NumberGuessingGame.playGame(5, in, out);
assertEquals(5, guesses);
}
}
Compile and run with javac -cp junit-4.13.2.jar and hamcrest-core-1.3.jar NumberGuessingGame.java NumberGuessingGameTest.java then java -cp .:junit-4.13.2.jar:hamcrest-core-1.3.jar org.junit.runner.JUnitCore NumberGuessingGameTest. A real project would pull JUnit in through Maven or Gradle instead of a standalone jar — see the course’s Build Tools lesson — but the two jars above are exactly what those build tools download for you under the hood, so the tests themselves are identical either way. A StringReader stands in for real stdin, which is exactly what the testable playGame(secret, in, out) signature was designed to make possible.
5 The Interface
Even a tiny program has an interface — the way a person interacts with it. Here is its contract, documented plainly, the same way a professional would describe any tool.
What it expects
A number like 42, typed and then Enter pressed.What it returns
"Too low."
"Too high."
"Correct! You got it in N guesses."6 Run It & Automate It
Save the code as NumberGuessingGame.java and compile it with javac — that turns your source into .class bytecode files, which java then runs on the JVM. No separate install step: any real JDK ships both tools.
javac NumberGuessingGame.java && java NumberGuessingGameThen type a number, press Enter, and follow the “too high / too low” hints until you win.
A CI tool like Jenkins runs the same compile-then-test steps automatically whenever the code changes — every line below has a plain explanation.
Guess a number between 1 and 100: 50
Too high.
Guess a number between 1 and 100: 25
Too low.
Guess a number between 1 and 100: 37
Too high.
Guess a number between 1 and 100: 31
Correct! You got it in 4 guesses.Integer.parseInt is meant to catch. If you see it crash the program, check that the catch block wraps the parse call, not just the comparison after it.guess to secret and not to a copy that never updates — and that the loop actually returns on a correct guess.NumberGuessingGame.java before NumberGuessingGameTest.java when compiling by hand one file at a time (compiling them together, as shown above, avoids this).// Jenkinsfile — runs the tests automatically every time the code changes.
pipeline {
agent any // run on any available machine
environment {
CP = 'junit-4.13.2.jar:hamcrest-core-1.3.jar' // JUnit + its one dependency
}
stages {
stage('Get the code') {
steps { checkout scm } // download the latest code
}
stage('Set up JDK') {
steps {
sh 'java -version' // confirm a JDK is installed
sh 'javac -cp "$CP" *.java' // compile the program and its tests together
}
}
stage('Run the tests') {
steps {
sh 'java -cp ".:$CP" org.junit.runner.JUnitCore NumberGuessingGameTest'
}
}
}
post {
success { echo 'All tests passed.' }
failure { echo 'A test failed — look above.' }
}
}
- Limit the attempts. Give the player only 7 guesses. (Teaches: counting down, and an early
returnon failure.) - Add difficulty levels. Let the player choose a range like 1–1000. (Teaches: passing parameters into
Random.nextInt.) - Switch to SecureRandom. Swap
java.util.Randomforjava.security.SecureRandom, the same type the password generator project uses. (Teaches: when predictability actually matters.)
while (true) loop-with-a-return, catching NumberFormatException instead of letting bad input crash the program, and a genuinely useful pattern — writing core logic against BufferedReader/PrintStream parameters so it can be tested without touching a real terminal. Related: Control Flow, Exception Handling.