<!-- canonical: https://efficientnewlanguage.org/eml-p/projects/P008-number-guessing/ | updated: 2026-09-29 -->

# P008 Number guessing

Guess the number at three levels, with higher/lower hints that narrow to a range, a limited number of guesses and a best score per level; the secret numbers come from a random generator written in EML - from a text menu.

EML-P project `projects/number-guessing` in the EML language repo: 3 module(s), entry `main.eml`, terminal UI. There, `eml project run projects/number-guessing` runs it and `eml project verify projects/number-guessing` replays every session under CPython (two hash seeds) and in the interpreter; the site build replays every session in the interpreter again and publishes a session only if its screen matches.

Built on verified corpus cases: mvp-number-guessing-game (https://efficientnewlanguage.org/cases/006-mvp-number-guessing-game/).

## Sessions

### bad-input - interpreter: byte-equal to the golden

Input:

```text
9
4
5
abc
5
7
1
abc
0
21

5.5
10
3
5
1
7
4
6
```

Screen:

```text

== Number guessing (seed 2026) ==
1) easy 1-20  2) normal 1-100  3) hard 1-1000  4) best scores  5) new seed  6) quit
choice> 9
Pick a number from 1 to 6.

== Number guessing (seed 2026) ==
1) easy 1-20  2) normal 1-100  3) hard 1-1000  4) best scores  5) new seed  6) quit
choice> 4

-- best scores --
  easy    -
  normal  -
  hard    -

== Number guessing (seed 2026) ==
1) easy 1-20  2) normal 1-100  3) hard 1-1000  4) best scores  5) new seed  6) quit
choice> 5
seed> abc
A seed is a whole number.

== Number guessing (seed 2026) ==
1) easy 1-20  2) normal 1-100  3) hard 1-1000  4) best scores  5) new seed  6) quit
choice> 5
seed> 7
Seed set to 7.

== Number guessing (seed 7) ==
1) easy 1-20  2) normal 1-100  3) hard 1-1000  4) best scores  5) new seed  6) quit
choice> 1
I am thinking of a number from 1 to 20. You have 5 guesses.
guess 1/5> abc
Guess a whole number from 1 to 20.
guess 1/5> 0
Guess a whole number from 1 to 20.
guess 1/5> 21
Guess a whole number from 1 to 20.
guess 1/5> 
Guess a whole number from 1 to 20.
guess 1/5> 5.5
Guess a whole number from 1 to 20.
guess 1/5> 10
Too high.
guess 2/5> 3
Too low.
guess 3/5> 5
Right! 5 in 3 guesses.
New best for easy: 3 guesses.

== Number guessing (seed 7) ==
1) easy 1-20  2) normal 1-100  3) hard 1-1000  4) best scores  5) new seed  6) quit
choice> 1
I am thinking of a number from 1 to 20. You have 5 guesses.
guess 1/5> 7
Right! 7 in 1 guess.
New best for easy: 1 guess.

== Number guessing (seed 7) ==
1) easy 1-20  2) normal 1-100  3) hard 1-1000  4) best scores  5) new seed  6) quit
choice> 4

-- best scores --
  easy    1 guess
  normal  -
  hard    -

== Number guessing (seed 7) ==
1) easy 1-20  2) normal 1-100  3) hard 1-1000  4) best scores  5) new seed  6) quit
choice> 6
Bye.
```

### basic - interpreter: byte-equal to the golden

Input:

```text
2
50
25
12
18
21
1
10
15
19
2
10
20
30
40
50
60
70
3
500
750
625
562
593
577
585
581
583
4
6
```

Screen:

```text

== Number guessing (seed 2026) ==
1) easy 1-20  2) normal 1-100  3) hard 1-1000  4) best scores  5) new seed  6) quit
choice> 2
I am thinking of a number from 1 to 100. You have 7 guesses.
guess 1/7> 50
Too high.
guess 2/7> 25
Too high.
guess 3/7> 12
Too low. It is between 13 and 24.
guess 4/7> 18
Too low. It is between 19 and 24.
guess 5/7> 21
Right! 21 in 5 guesses.
New best for normal: 5 guesses.

== Number guessing (seed 2026) ==
1) easy 1-20  2) normal 1-100  3) hard 1-1000  4) best scores  5) new seed  6) quit
choice> 1
I am thinking of a number from 1 to 20. You have 5 guesses.
guess 1/5> 10
Too low.
guess 2/5> 15
Too low.
guess 3/5> 19
Right! 19 in 3 guesses.
New best for easy: 3 guesses.

== Number guessing (seed 2026) ==
1) easy 1-20  2) normal 1-100  3) hard 1-1000  4) best scores  5) new seed  6) quit
choice> 2
I am thinking of a number from 1 to 100. You have 7 guesses.
guess 1/7> 10
Too low.
guess 2/7> 20
Too low.
guess 3/7> 30
Too low. It is between 31 and 100.
guess 4/7> 40
Too low. It is between 41 and 100.
guess 5/7> 50
Too low. It is between 51 and 100.
guess 6/7> 60
Too low. It is between 61 and 100.
guess 7/7> 70
Too low. It is between 71 and 100.
Out of guesses - it was 88.

== Number guessing (seed 2026) ==
1) easy 1-20  2) normal 1-100  3) hard 1-1000  4) best scores  5) new seed  6) quit
choice> 3
I am thinking of a number from 1 to 1000. You have 10 guesses.
guess 1/10> 500
Too low.
guess 2/10> 750
Too high.
guess 3/10> 625
Too high. It is between 501 and 624.
guess 4/10> 562
Too low. It is between 563 and 624.
guess 5/10> 593
Too high. It is between 563 and 592.
guess 6/10> 577
Too low. It is between 578 and 592.
guess 7/10> 585
Too high. It is between 578 and 584.
guess 8/10> 581
Too low. It is between 582 and 584.
guess 9/10> 583
Right! 583 in 9 guesses.
New best for hard: 9 guesses.

== Number guessing (seed 2026) ==
1) easy 1-20  2) normal 1-100  3) hard 1-1000  4) best scores  5) new seed  6) quit
choice> 4

-- best scores --
  easy    3 guesses
  normal  5 guesses
  hard    9 guesses

== Number guessing (seed 2026) ==
1) easy 1-20  2) normal 1-100  3) hard 1-1000  4) best scores  5) new seed  6) quit
choice> 6
Bye.
```

## Modules

### main.eml

```eml
# P008 number guessing: pick a level and guess the number. The best score of
# each level is kept while the program runs, and the seed of the random
# generator can be changed - the same seed always gives the same games.
import game
import rng

def plural(n):
    if n == 1:
        return "1 guess"
    return str(n) + " guesses"

def show_best(best):
    "" ^0
    "-- best scores --" ^0
    for i in [0:len(game.levels) - 1]:
        ("  " + ("%-8s" % game.levels[i][0])) => line
        if best[i] == 0:
            line + "-" => line
        else:
            line + plural(best[i]) => line
        line ^0

2026 => seed
rng.Rng(seed) => r
[0 for level in game.levels] => best
True => running
while running:
    "" ^0
    ("== Number guessing (seed " + str(seed) + ") ==") ^0
    "1) easy 1-20  2) normal 1-100  3) hard 1-1000  4) best scores  5) new seed  6) quit" ^0
    game.trim(input("choice> ")) => choice
    if choice == "1" or choice == "2" or choice == "3":
        int(choice) - 1 => i
        game.play(r, game.levels[i]) => used
        if used > 0 and (best[i] == 0 or used < best[i]):
            used => best[i]
            ("New best for " + game.levels[i][0] + ": " + plural(used) + ".") ^0
    elif choice == "4":
        show_best(best)
    elif choice == "5":
        game.number(game.trim(input("seed> "))) => s
        if s < 0:
            "A seed is a whole number." ^0
        else:
            s => seed
            rng.Rng(seed) => r
            ("Seed set to " + str(seed) + ".") ^0
    elif choice == "6":
        False => running
    else:
        "Pick a number from 1 to 6." ^0
"Bye." ^0
```

Python projection of main.eml:

```python
import game
import rng

def plural(n):
    if n == 1:
        return "1 guess"
    return str(n) + " guesses"

def show_best(best):
    print("")
    print("-- best scores --")
    for i in range(0, len(game.levels)):
        line = "  " + "%-8s" % game.levels[i][0]
        if best[i] == 0:
            line = line + "-"
        else:
            line = line + plural(best[i])
        print(line)

seed = 2026
r = rng.Rng(seed)
best = [0 for level in game.levels]
running = True
while running:
    print("")
    print("== Number guessing (seed " + str(seed) + ") ==")
    print("1) easy 1-20  2) normal 1-100  3) hard 1-1000  4) best scores  5) new seed  6) quit")
    choice = game.trim(input("choice> "))
    if choice == "1" or choice == "2" or choice == "3":
        i = int(choice) - 1
        used = game.play(r, game.levels[i])
        if used > 0 and (best[i] == 0 or used < best[i]):
            best[i] = used
            print("New best for " + game.levels[i][0] + ": " + plural(used) + ".")
    elif choice == "4":
        show_best(best)
    elif choice == "5":
        s = game.number(game.trim(input("seed> ")))
        if s < 0:
            print("A seed is a whole number.")
        else:
            seed = s
            r = rng.Rng(seed)
            print("Seed set to " + str(seed) + ".")
    elif choice == "6":
        running = False
    else:
        print("Pick a number from 1 to 6.")
print("Bye.")
```

### game.eml

```eml
# P008 number guessing - one game. The player has a limited number of
# guesses; a guess that is not a whole number in range does not use one.
# After every miss the game says higher or lower, and from the third miss on
# it also names the range the number must be in, which it keeps narrowing.

# [name, highest number, guesses allowed]
[
    ["easy", 20, 5],
    ["normal", 100, 7],
    ["hard", 1000, 10],
] => levels

def trim(s):
    0 => i
    len(s) => j
    while i < j and s[i] == " ":
        i + 1 => i
    while j > i and s[j - 1] == " ":
        j - 1 => j
    return s[i:j]

def number(s):
    # The value of s if it is digits only (at least one), otherwise -1.
    if s == "":
        return -1
    0 => n
    for c in s:
        if not (c in "0123456789"):
            return -1
        n * 10 + int(c) => n
    return n

def play(rng, level):
    # One game at this level. Returns the number of guesses it took, or 0 if
    # the guesses ran out.
    level[1] => top
    level[2] => allowed
    1 + rng.below(top) => secret
    ("I am thinking of a number from 1 to " + str(top) + ". You have " + str(allowed) + " guesses.") ^0
    1 => low
    top => high
    0 => used
    while used < allowed:
        number(trim(input("guess " + str(used + 1) + "/" + str(allowed) + "> "))) => g
        if g < 1 or g > top:
            ("Guess a whole number from 1 to " + str(top) + ".") ^0
        else:
            used + 1 => used
            if g == secret:
                if used == 1:
                    ("Right! " + str(secret) + " in 1 guess.") ^0
                else:
                    ("Right! " + str(secret) + " in " + str(used) + " guesses.") ^0
                return used
            if g < secret:
                "Too low." => hint
                if g + 1 > low:
                    g + 1 => low
            else:
                "Too high." => hint
                if g - 1 < high:
                    g - 1 => high
            if used >= 3:
                hint + " It is between " + str(low) + " and " + str(high) + "." => hint
            hint ^0
    ("Out of guesses - it was " + str(secret) + ".") ^0
    return 0
```

Python projection of game.eml:

```python
levels = [["easy", 20, 5], ["normal", 100, 7], ["hard", 1000, 10]]

def trim(s):
    i = 0
    j = len(s)
    while i < j and s[i] == " ":
        i = i + 1
    while j > i and s[j - 1] == " ":
        j = j - 1
    return s[i:j]

def number(s):
    if s == "":
        return -1
    n = 0
    for c in s:
        if not c in "0123456789":
            return -1
        n = n * 10 + int(c)
    return n

def play(rng, level):
    top = level[1]
    allowed = level[2]
    secret = 1 + rng.below(top)
    print("I am thinking of a number from 1 to " + str(top) + ". You have " + str(allowed) + " guesses.")
    low = 1
    high = top
    used = 0
    while used < allowed:
        g = number(trim(input("guess " + str(used + 1) + "/" + str(allowed) + "> ")))
        if g < 1 or g > top:
            print("Guess a whole number from 1 to " + str(top) + ".")
        else:
            used = used + 1
            if g == secret:
                if used == 1:
                    print("Right! " + str(secret) + " in 1 guess.")
                else:
                    print("Right! " + str(secret) + " in " + str(used) + " guesses.")
                return used
            if g < secret:
                hint = "Too low."
                if g + 1 > low:
                    low = g + 1
            else:
                hint = "Too high."
                if g - 1 < high:
                    high = g - 1
            if used >= 3:
                hint = hint + " It is between " + str(low) + " and " + str(high) + "."
            print(hint)
    print("Out of guesses - it was " + str(secret) + ".")
    return 0
```

### rng.eml

```eml
# P008 number guessing - random numbers written in EML: a linear
# congruential generator with the constants of the C standard's example
# rand(). Python's random module is not used, so a seed gives the same games
# on every machine, and the interpreter can check a whole session byte for
# byte.

class Rng:
    def __init__(self, seed):
        seed % 2147483648 => self.state

    def step(self):
        (1103515245 * self.state + 12345) % 2147483648 => self.state
        return self.state

    def below(self, n):
        # A number from 0 to n - 1, taken from the high bits of the state: the
        # low bits of this generator repeat with short periods. Dividing by
        # 65536 is exact in a float for a state below 2^31.
        return int(self.step() / 65536) % n
```

Python projection of rng.eml:

```python
class Rng:
    def __init__(self, seed):
        self.state = seed % 2147483648
    def step(self):
        self.state = (1103515245 * self.state + 12345) % 2147483648
        return self.state
    def below(self, n):
        return int(self.step() / 65536) % n
```

## README

# P008 - Number guessing

Guess the number the program is thinking of, at three levels: easy (1 to 20,
5 guesses), normal (1 to 100, 7 guesses) and hard (1 to 1000, 10 guesses).
Every miss says higher or lower, and from the third miss on the program also
names the range the number must be in, narrowing it as you go. The fewest
guesses of each level is kept as the best score. A text menu; the scores live
while the program runs.

- `main.eml` - the menu, the best scores and the seed
- `game.eml` - one game: the levels, the guesses and their checks, the hints
  and the narrowing range
- `rng.eml` - random numbers written in EML: a linear congruential generator
  (the constants of the C standard's example `rand()`), taking a number from
  the high bits of its state, because the low bits of such a generator repeat
  with short periods

Python's `random` module is not used. A seed (2026 unless you set another)
always gives the same games on every machine, which is also what lets the
interpreter check a whole session against CPython byte for byte.

What is checked: a guess must be a whole number in the level's range - one
that is not does not use up a guess; a seed must be a whole number.

Sessions: `sessions/basic.in` wins a normal game in five guesses with the
range hint appearing, an easy game in three, loses a normal game, wins a hard
game by halving, and shows the best scores; `sessions/bad-input.in` types an
unknown choice, shows the empty best scores, types a seed that is a word,
sets seed 7, types guesses that are words, 0, 21, empty and 5.5 (none of them
counts), wins, and then wins in a single guess.

Built on the verified corpus case `mvp-number-guessing-game` (guesses
answered with higher or lower, and the number of attempts counted).
