Tier 0 — Absolute Beginner

Number Guessing Game

Build a number guessing game in JavaScript — random numbers, loops, and terminal input with readline.

🐍 Python 🟨 JavaScript

1. The Problem

Build a number guessing game where JavaScript picks a secret number between 1 and 100, and the player guesses until they find it. After each guess, the program says "too high", "too low", or "correct!" and counts the attempts.

Why this project? It's the perfect first program — it uses every fundamental idea: variables, random numbers, a loop, conditionals, and user input. In Node.js, we use readline to get input from the terminal.

2. How to Think About This

Before writing code, think through the steps a human would follow:

  1. Pick a secret number (the computer does this with Math.random())
  2. Ask the player to guess
  3. Compare guess to secret: too low? too high? correct?
  4. If correct — celebrate and show attempt count. If not — go back to step 2.

The "keep doing this until correct" pattern maps directly to a while loop. The "compare and branch" maps to if / else if / else.

3. The Build

Save this as game.mjs and run with node game.mjs:

game.mjs
// Import readline so we can ask the player for input
import * as readline from "readline/promises";
import { stdin, stdout } from "process";

// Create an interface that reads from the keyboard and writes to the terminal
const rl = readline.createInterface({ input: stdin, output: stdout });

// Pick a random secret number between 1 and 100
// Math.random() gives 0.0 to 0.9999
// * 100 gives 0.0 to 99.9999
// Math.floor rounds DOWN to nearest integer: 0 to 99
// + 1 shifts to: 1 to 100
const secret = Math.floor(Math.random() * 100) + 1;

// Track how many guesses the player has made
let attempts = 0;

console.log("I've picked a number between 1 and 100. Can you guess it?");

// Keep looping until the player guesses correctly
while (true) {
  // Ask for a guess — await means "wait for the player to type and press Enter"
  const input = await rl.question("Your guess: ");

  // Convert the typed string to a number
  const guess = Number(input);

  // Validate the input
  if (isNaN(guess) || guess < 1 || guess > 100) {
    console.log("Please enter a number between 1 and 100.");
    continue;  // skip the rest of this loop iteration, ask again
  }

  // Count this as a valid attempt
  attempts++;

  // Compare guess to secret
  if (guess < secret) {
    console.log("Too low! Try higher.");
  } else if (guess > secret) {
    console.log("Too high! Try lower.");
  } else {
    // Correct!
    console.log(`Correct! You found ${secret} in ${attempts} guess${attempts === 1 ? "" : "es"}!`);
    break;  // exit the while loop
  }
}

// Close the readline interface — otherwise Node.js keeps running
rl.close();

What each part does

  • import * as readline — bring in Node.js's readline module for terminal input
  • Math.floor(Math.random() * 100) + 1 — picks a random integer 1-100
  • while (true) — loop forever until we hit break
  • await rl.question(...) — pause and wait for the player to type
  • Number(input) — convert the typed string to a number
  • isNaN(guess) — check if conversion failed (e.g. player typed "abc")
  • continue — skip the rest of this iteration and loop again
  • break — exit the while loop when the guess is correct

4. Test & Prove

Extract the game logic into a pure function — then you can test it without needing to type at a terminal:

game.test.mjs
// ── logic.mjs ── (extract this from game.mjs)
export function checkGuess(secret, guess) {
  // Returns: "low", "high", or "correct"
  if (guess < secret) return "low";
  if (guess > secret) return "high";
  return "correct";
}

export function validateGuess(input) {
  // Returns the number, or null if invalid
  const n = Number(input);
  if (isNaN(n) || n < 1 || n > 100 || !Number.isInteger(n)) return null;
  return n;
}

// ── game.test.mjs ──
import { checkGuess, validateGuess } from "./logic.mjs";

let passed = 0; let failed = 0;
function test(desc, fn) {
  try { fn(); console.log("  ✓", desc); passed++; }
  catch(e) { console.log("  ✗", desc, "—", e.message); failed++; }
}
function expect(val) {
  return {
    toBe(exp) { if (val !== exp) throw new Error(`Expected ${exp}, got ${val}`); },
  };
}

console.log("Number Guessing Game — Logic Tests
");

test("guess lower than secret returns low",    () => expect(checkGuess(50, 30)).toBe("low"));
test("guess higher than secret returns high",  () => expect(checkGuess(50, 70)).toBe("high"));
test("correct guess returns correct",          () => expect(checkGuess(50, 50)).toBe("correct"));
test("works at boundaries: secret=1, guess=1", () => expect(checkGuess(1,  1)).toBe("correct"));
test("valid number string parses correctly",   () => expect(validateGuess("42")).toBe(42));
test("letters return null",                    () => expect(validateGuess("abc")).toBe(null));
test("0 is invalid (out of range)",            () => expect(validateGuess("0")).toBe(null));
test("101 is invalid (out of range)",          () => expect(validateGuess("101")).toBe(null));
test("3.5 is invalid (not integer)",           () => expect(validateGuess("3.5")).toBe(null));

console.log(\`
Results: \${passed} passed, \${failed} failed\`);

Run with: node game.test.mjs

5. The Interface

Input

One number per line, entered at the terminal prompt. Must be an integer between 1 and 100. Invalid input (letters, decimals, out-of-range) is rejected and the player is asked again.

Output

After each valid guess: "Too low! Try higher.", "Too high! Try lower.", or the success message. Success includes the secret number and attempt count.

Expected session

I've picked a number between 1 and 100. Can you guess it?
Your guess: 50
Too low! Try higher.
Your guess: 75
Too high! Try lower.
Your guess: 62
Too low! Try higher.
Your guess: 68
Correct! You found 68 in 4 guesses!

6. Run It

# Requires Node.js v18+ (for readline/promises and top-level await)
node game.mjs

# Run the tests:
node game.test.mjs

🎯 Try this next

  1. Limit guesses — add a maxGuesses variable; end the game if the player exceeds it
  2. Play again — after the game ends, ask "Play again? (y/n)" and loop
  3. Difficulty levels — easy (1-10), medium (1-100), hard (1-1000)
  4. Binary search strategy — always guess the midpoint; prove it finds any number in at most 7 guesses (1-100)

What you practised

Random numbers with Math.random(), the while(true)/break pattern for game loops, readline/promises for async terminal input, input validation with isNaN, pure function extraction for testability, and writing your own test runner from scratch.

Reference pages: Numbers & Math · Control Flow · User Input & Output

Try It Yourself