← thecodex.expert · The Codex Family of Knowledge
Tier 0 · Absolute Beginner · TypeScript Project

Number Guessing Game

Your very first program with a memory and a decision: the computer picks a secret number, and you guess until you get it. You will learn how a program asks a question, checks an answer, and repeats.

🧠 Teaches how to think spoonfed, every age Last verified:

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.

Where this shows up in real life: every time an app checks your password, validates a form, or keeps asking until you give a valid answer, it is doing exactly this — take input, compare it to something, and loop until the condition is met. Learn it here in 15 lines, and you have learned the heartbeat of almost every program.

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 TypeScript yet — you just need to know the shape of what happens:

The plan — in plain English
1. Pick a secret number once, at the start. → 2. Ask the player to guess. → 3. Compare the guess to the secret: lower, higher, or equal? → 4. If not equal, go back to step 2. If equal, celebrate and stop. That is the whole program. Everything below is just saying this in TypeScript.

Too low

Too high

Correct!

Computer picks a secret number 1-100

Ask the player to guess

Is the guess right?

Say 'too low'

Say 'too high'

Show how many guesses it took

Game over

3 The Build — explained part by part

Here is the complete game. Read each part’s note below — you should understand the whole thing from the notes alone.

TypeScriptguessing_game.ts
import * as readline from "node:readline";
import { stdin, stdout } from "node:process";
import { randomInt } from "node:crypto";

// ask prints a prompt and waits for one line of input, pulling it from the
// readline interface's own async iterator rather than question()/promises —
// the one readline API that does not drop a line when several prompts are
// answered back-to-back, typed by hand or piped in from a file. It resolves
// to null once there is nothing left to read (stdin closed), so a loop can
// stop cleanly instead of spinning on an empty answer forever.
function makeAsk(rl: readline.Interface) {
  const it = rl[Symbol.asyncIterator]();
  return async (prompt: string): Promise<string | null> => {
    stdout.write(prompt);
    const { value, done } = await it.next();
    return done ? null : value;
  };
}

// playGame is a testable version of the game: hand it the secret number and
// a list of guesses, and it returns how many tries it took (or null if the
// secret was never guessed).
export function playGame(secret: number, guesses: number[]): number | null {
  let taken = 0;
  for (const guess of guesses) {
    taken += 1;
    if (guess === secret) {
      return taken;
    }
  }
  return null;
}

async function main(): Promise<void> {
  const rl = readline.createInterface({ input: stdin, terminal: false });
  const ask = makeAsk(rl);

  // The computer secretly picks a whole number from 1 to 100.
  const secretNumber = randomInt(1, 101);
  let guessesTaken = 0;

  console.log("I am thinking of a number between 1 and 100.");

  while (true) {
    const line = await ask("Your guess: ");
    if (line === null) {
      console.log("\nNo more input — goodbye.");
      break;
    }
    const guess = Number(line.trim());
    guessesTaken += 1;

    if (guess < secretNumber) {
      console.log("Too low. Try a bigger number.");
    } else if (guess > secretNumber) {
      console.log("Too high. Try a smaller number.");
    } else {
      console.log(`Correct! You got it in ${guessesTaken} guesses.`);
      break;
    }
  }

  rl.close();
}

if (require.main === module) {
  main();
}
⚠ No in-browser playground here
Running real, type-checked TypeScript in the browser needs either a full copy of the compiler or a third-party CDN script — the same kind of external dependency this site avoids relying on for a core teaching example. Copy the code below and run it with Node on your own machine instead; the “Run It” section explains exactly how.
What each part does — in plain words
import { randomInt } from "node:crypto" — Node’s built-in cryptographically-secure random number generator. randomInt(1, 101) picks a whole number from 1 up to (but not including) 101 — i.e. 1 to 100.

function playGame(secret, guesses): number | null — a testable version of the game, pulled out of the interactive loop exactly like the Python/Go versions. Its return type, number | null, is TypeScript’s union type: “either a number of guesses, or nothing at all” — and the compiler forces every caller to handle both cases.

const it = rl[Symbol.asyncIterator]() — pulling lines from the readline interface’s own async iterator, one await it.next() per prompt, is what lets several “Your guess:” prompts in a row answer correctly even when every guess is piped in at once.

if (line === null) break; — when the input runs out (end of file), the iterator reports “done” and we exit the loop instead of spinning forever on an empty answer.
Common mistakes — and how to avoid them
✗ Using Math.random() for the secret number.
✓ Math.random() is fine for a game, but it is not reproducibly uniform across its whole range and is never appropriate once “random” matters for security (see the password generator project). crypto.randomInt costs nothing extra and builds the right habit.
✗ Looping on input forever, even after the input source has run out.
✓ Check whether the async iterator reports done and break out of the loop — otherwise a piped-in file with too few lines causes an infinite loop with no new prompts, since an exhausted iterator keeps resolving instead of throwing.
✗ Writing the return type as just number when the function can also return “no answer”.
✓ Use the union number | null (or number | undefined) so the compiler reminds every caller to check before using the result.

4 Test & Prove Each Part

How do we know this works? We pull the real logic into small, plain functions and check each one against cases we already know the answer to.

If the player guesses the secret immediately, it takes 1 try
If the right answer is the third guess, it returns 3
If the player never guesses it, the result is null, not a crash
The secret number is always between 1 and 100, across 1000 trials
TypeScriptguessing_game.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import { randomInt } from "node:crypto";
import { playGame } from "./number-guessing-game";

test("correct on the first try takes 1 guess", () => {
  assert.equal(playGame(42, [42]), 1);
});

test("correct on the third guess returns 3", () => {
  assert.equal(playGame(50, [10, 90, 50]), 3);
});

test("never guessing it returns null, not a crash", () => {
  assert.equal(playGame(7, [1, 2, 3]), null);
});

test("the secret number is always between 1 and 100", () => {
  for (let i = 0; i < 1000; i++) {
    const n = randomInt(1, 101);
    assert.ok(n >= 1 && n <= 100);
  }
});

Compile with npx tsc then run node --test guessing_game.test.js. Because playGame takes the secret and the list of guesses as plain arguments, the tests never have to deal with real randomness or real typed input — they hand it fixed numbers and check the return value.

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.

INPUTYour guessa whole number 1–100, typed by the player
What it expects
A number like 42, typed and then Enter pressed.
OUTPUTFeedbackone of three replies
What it returns
"Too low. Try a bigger number."
"Too high. Try a smaller number."
"Correct! You got it in N guesses."

6 Run It & Automate It

Save the code as guessing_game.ts, compile with npx tsc, and run with node guessing_game.js — or run it directly with npx tsx guessing_game.ts while you are experimenting.

Run it locally
npx tsc guessing_game.ts && node guessing_game.js
Keep guessing until you find the secret number between 1 and 100.

A CI tool like Jenkins runs the type-checker and tests automatically whenever the code changes — every line below has a plain explanation.

What you should see when it works
Terminala real run
I am thinking of a number between 1 and 100.
Your guess: 50
Too low. Try a bigger number.
Your guess: 75
Too high. Try a smaller number.
Your guess: 63
Correct! You got it in 3 guesses.
If it breaks — how to fix it
🚨 The game loops forever printing “Too low” after I pipe in a short list of guesses
That means the program is not checking for the end of input. Make sure you break out of the loop when the async iterator reports done (see the code above).
🚨 NaN is never equal to the secret number, so the game never ends
Typing something that is not a number makes guess become NaN, which is never <, >, or === anything — including itself. Type a whole number.
GroovyJenkinsfile
// Jenkinsfile &mdash; runs the type-checker and tests automatically every time the code changes.
pipeline {
    agent any                                  // run on any available machine

    stages {
        stage('Get the code') {
            steps { checkout scm }             // download the latest code
        }
        stage('Set up Node') {
            steps {
                sh 'node --version'                              // confirm Node is installed
                sh 'npm install -D typescript @types/node'       // zero runtime deps &mdash; just the compiler and its Node types
            }
        }
        stage('Type-check and test') {
            steps {
                sh 'npx tsc --noEmit'                 // catch type errors before anything runs
                sh 'npx tsc'                           // compile to plain JavaScript
                sh 'node --test guessing_game.test.js'            // Node's built-in test runner, no extra install needed
            }
        }
    }

    post {
        success { echo 'All tests passed.' }
        failure { echo 'A test failed &mdash; look above.' }
    }
}
🎯 Try this next — make it yours

You have a working number guessing game. Extend it:

  1. Limit the attempts. End the game after 10 wrong guesses. (Teaches: a counter plus an early exit.)
  2. Add difficulty levels. Let the player choose a range like 1–1000. (Teaches: parameterising randomInt.)
  3. Give a “warmer/colder” hint. Compare how close each guess is to the last one. (Teaches: keeping extra state between loop iterations.)
  4. Play again. After a win, ask “Play again? (y/n)” and restart. (Teaches: wrapping the whole game in an outer loop.)
What you learned
You learned a testable-core pattern (playGame takes plain data, not live input), TypeScript’s union types for “a value or nothing”, and crypto.randomInt for real randomness. Related reference: Union & Intersection Types, Type Narrowing.