<!-- canonical: https://efficientnewlanguage.org/eml-p/projects/P051-hangman/ | updated: 2026-10-10 -->

# P051 Hangman

Guess a secret word from a list of over a hundred, a letter at a time or the whole word at once, before six wrong guesses finish the drawing. A hint counts, among the words that still fit what you know, how many hold each letter you have not tried. Words come from a seed, none twice until the list is used up.

EML-P project `projects/hangman` in the EML language repo: 3 module(s), entry `main.eml`, terminal UI. There, `eml project run projects/hangman` runs it and `eml project verify projects/hangman` 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: char-frequency-table (https://efficientnewlanguage.org/cases/157-char-frequency-table/).

## Sessions

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

Input:

```text
0
x
2
abc
-1
1234567890

2
7
1
1
a b
ab!
E
e
zebra
bottle
?

3
4
```

Screen:

```text
== Hangman ==
Guess the word a letter at a time; six wrong guesses and you are hanged.

1) new word  2) seed  3) score  4) quit
choice> 0
Pick a number from 1 to 4.

1) new word  2) seed  3) score  4) quit
choice> x
Pick a number from 1 to 4.

1) new word  2) seed  3) score  4) quit
choice> 2
seed (0 to 999999999)> abc
Type a number from 0 to 999999999.
seed (0 to 999999999)> -1
Type a number from 0 to 999999999.
seed (0 to 999999999)> 1234567890
Type a number from 0 to 999999999.
seed (0 to 999999999)> 
Cancelled.

1) new word  2) seed  3) score  4) quit
choice> 2
seed (0 to 999999999)> 7
Words now come from seed 7.

1) new word  2) seed  3) score  4) quit
choice> 1
A word of 6 letters.
  +---+
  |   |
      |
      |
      |
      |
=========
Word: _ _ _ _ _ _    missed: none    wrong guesses left: 6
letter, the whole word, ? for a hint, or nothing to give up> 1
Type a letter from a to z, the whole word, or ?.
  +---+
  |   |
      |
      |
      |
      |
=========
Word: _ _ _ _ _ _    missed: none    wrong guesses left: 6
letter, the whole word, ? for a hint, or nothing to give up> a b
Type a letter from a to z, the whole word, or ?.
  +---+
  |   |
      |
      |
      |
      |
=========
Word: _ _ _ _ _ _    missed: none    wrong guesses left: 6
letter, the whole word, ? for a hint, or nothing to give up> ab!
Type a letter from a to z, the whole word, or ?.
  +---+
  |   |
      |
      |
      |
      |
=========
Word: _ _ _ _ _ _    missed: none    wrong guesses left: 6
letter, the whole word, ? for a hint, or nothing to give up> E
Yes - e is in it 1 time.
  +---+
  |   |
      |
      |
      |
      |
=========
Word: _ _ _ _ _ e    missed: none    wrong guesses left: 6
letter, the whole word, ? for a hint, or nothing to give up> e
You have tried e already.
  +---+
  |   |
      |
      |
      |
      |
=========
Word: _ _ _ _ _ e    missed: none    wrong guesses left: 6
letter, the whole word, ? for a hint, or nothing to give up> zebra
The word has 6 letters - that guess costs nothing.
  +---+
  |   |
      |
      |
      |
      |
=========
Word: _ _ _ _ _ e    missed: none    wrong guesses left: 6
letter, the whole word, ? for a hint, or nothing to give up> bottle
Not bottle.
  +---+
  |   |
  O   |
      |
      |
      |
=========
Word: _ _ _ _ _ e    missed: none    wrong guesses left: 5
letter, the whole word, ? for a hint, or nothing to give up> ?
14 words in the list still fit. The letters in the most of them: l (11), a (7), d (5).
  +---+
  |   |
  O   |
      |
      |
      |
=========
Word: _ _ _ _ _ e    missed: none    wrong guesses left: 5
letter, the whole word, ? for a hint, or nothing to give up> 
You give up - the word was fiddle.

1) new word  2) seed  3) score  4) quit
choice> 3
Won 0, lost 1.

1) new word  2) seed  3) score  4) quit
choice> 4
Bye.
```

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

Input:

```text
1
e
?
s
o
spoon
1
?
e
?
a
?
m
c
?
TRUMPET
1
a
i
o
u
y
s
3
4
```

Screen:

```text
== Hangman ==
Guess the word a letter at a time; six wrong guesses and you are hanged.

1) new word  2) seed  3) score  4) quit
choice> 1
A word of 5 letters.
  +---+
  |   |
      |
      |
      |
      |
=========
Word: _ _ _ _ _    missed: none    wrong guesses left: 6
letter, the whole word, ? for a hint, or nothing to give up> e
No e.
  +---+
  |   |
  O   |
      |
      |
      |
=========
Word: _ _ _ _ _    missed: e    wrong guesses left: 5
letter, the whole word, ? for a hint, or nothing to give up> ?
2 words in the list still fit. The letters in the most of them: s (2), a (1), g (1).
  +---+
  |   |
  O   |
      |
      |
      |
=========
Word: _ _ _ _ _    missed: e    wrong guesses left: 5
letter, the whole word, ? for a hint, or nothing to give up> s
Yes - s is in it 1 time.
  +---+
  |   |
  O   |
      |
      |
      |
=========
Word: s _ _ _ _    missed: e    wrong guesses left: 5
letter, the whole word, ? for a hint, or nothing to give up> o
Yes - o is in it 2 times.
  +---+
  |   |
  O   |
      |
      |
      |
=========
Word: s _ o o _    missed: e    wrong guesses left: 5
letter, the whole word, ? for a hint, or nothing to give up> spoon
You got it: spoon, with 1 wrong guess.

1) new word  2) seed  3) score  4) quit
choice> 1
A word of 7 letters.
  +---+
  |   |
      |
      |
      |
      |
=========
Word: _ _ _ _ _ _ _    missed: none    wrong guesses left: 6
letter, the whole word, ? for a hint, or nothing to give up> ?
10 words in the list still fit. The letters in the most of them: e (8), a (6), i (5).
  +---+
  |   |
      |
      |
      |
      |
=========
Word: _ _ _ _ _ _ _    missed: none    wrong guesses left: 6
letter, the whole word, ? for a hint, or nothing to give up> e
Yes - e is in it 1 time.
  +---+
  |   |
      |
      |
      |
      |
=========
Word: _ _ _ _ _ e _    missed: none    wrong guesses left: 6
letter, the whole word, ? for a hint, or nothing to give up> ?
4 words in the list still fit. The letters in the most of them: a (2), c (2), i (2).
  +---+
  |   |
      |
      |
      |
      |
=========
Word: _ _ _ _ _ e _    missed: none    wrong guesses left: 6
letter, the whole word, ? for a hint, or nothing to give up> a
No a.
  +---+
  |   |
  O   |
      |
      |
      |
=========
Word: _ _ _ _ _ e _    missed: a    wrong guesses left: 5
letter, the whole word, ? for a hint, or nothing to give up> ?
2 words in the list still fit. The letters in the most of them: m (2), c (1), h (1).
  +---+
  |   |
  O   |
      |
      |
      |
=========
Word: _ _ _ _ _ e _    missed: a    wrong guesses left: 5
letter, the whole word, ? for a hint, or nothing to give up> m
Yes - m is in it 1 time.
  +---+
  |   |
  O   |
      |
      |
      |
=========
Word: _ _ _ m _ e _    missed: a    wrong guesses left: 5
letter, the whole word, ? for a hint, or nothing to give up> c
No c.
  +---+
  |   |
  O   |
  |   |
      |
      |
=========
Word: _ _ _ m _ e _    missed: a, c    wrong guesses left: 4
letter, the whole word, ? for a hint, or nothing to give up> ?
Only one word in the list still fits.
  +---+
  |   |
  O   |
  |   |
      |
      |
=========
Word: _ _ _ m _ e _    missed: a, c    wrong guesses left: 4
letter, the whole word, ? for a hint, or nothing to give up> TRUMPET
You got it: trumpet, with 2 wrong guesses.

1) new word  2) seed  3) score  4) quit
choice> 1
A word of 6 letters.
  +---+
  |   |
      |
      |
      |
      |
=========
Word: _ _ _ _ _ _    missed: none    wrong guesses left: 6
letter, the whole word, ? for a hint, or nothing to give up> a
No a.
  +---+
  |   |
  O   |
      |
      |
      |
=========
Word: _ _ _ _ _ _    missed: a    wrong guesses left: 5
letter, the whole word, ? for a hint, or nothing to give up> i
No i.
  +---+
  |   |
  O   |
  |   |
      |
      |
=========
Word: _ _ _ _ _ _    missed: a, i    wrong guesses left: 4
letter, the whole word, ? for a hint, or nothing to give up> o
No o.
  +---+
  |   |
  O   |
 /|   |
      |
      |
=========
Word: _ _ _ _ _ _    missed: a, i, o    wrong guesses left: 3
letter, the whole word, ? for a hint, or nothing to give up> u
No u.
  +---+
  |   |
  O   |
 /|\  |
      |
      |
=========
Word: _ _ _ _ _ _    missed: a, i, o, u    wrong guesses left: 2
letter, the whole word, ? for a hint, or nothing to give up> y
No y.
  +---+
  |   |
  O   |
 /|\  |
 /    |
      |
=========
Word: _ _ _ _ _ _    missed: a, i, o, u, y    wrong guesses left: 1
letter, the whole word, ? for a hint, or nothing to give up> s
No s.
  +---+
  |   |
  O   |
 /|\  |
 / \  |
      |
=========
Hanged - the word was pebble.

1) new word  2) seed  3) score  4) quit
choice> 3
Won 2, lost 1.

1) new word  2) seed  3) score  4) quit
choice> 4
Bye.
```

## Modules

### main.eml

```eml
# P051 hangman: guess the secret word a letter at a time before the drawing
# is finished - six wrong guesses hang you. A hint counts, among the words
# that still fit, how many hold each letter you have not tried.
import words
import rng

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 lowered(s):
    # Capital letters to small ones; anything else unchanged.
    "ABCDEFGHIJKLMNOPQRSTUVWXYZ" => big
    "abcdefghijklmnopqrstuvwxyz" => small
    "" => out
    for ch in s:
        ch => c
        for k in [0:25]:
            if ch == big[k]:
                small[k] => c
        out + c => out
    return out

def letters_only(s):
    if s == "":
        return False
    for ch in s:
        if not (ch >= "a" and ch <= "z"):
            return False
    return True

def has(s, ch):
    for x in s:
        if x == ch:
            return True
    return False

def counted(n, one, many):
    if n == 1:
        return "1 " + one
    return str(n) + " " + many

def gallows(wrong):
    # The drawing after `wrong` wrong guesses: head, body, two arms, two legs.
    " " => head
    "   " => body
    "   " => legs
    if wrong >= 1:
        "O" => head
    if wrong == 2:
        " | " => body
    elif wrong == 3:
        "/| " => body
    elif wrong >= 4:
        "/|\\" => body
    if wrong == 5:
        "/  " => legs
    elif wrong >= 6:
        "/ \\" => legs
    return ["  +---+", "  |   |", "  " + head + "   |", " " + body + "  |", " " + legs + "  |", "      |", "========="]

def pattern_of(secret, guessed):
    "" => p
    for ch in secret:
        if has(guessed, ch):
            p + ch => p
        else:
            p + "_" => p
    return p

def spaced(p):
    "" => out
    for ch in p:
        if out != "":
            out + " " => out
        out + ch => out
    return out

def listed(s):
    "" => out
    for ch in s:
        if out != "":
            out + ", " => out
        out + ch => out
    if out == "":
        return "none"
    return out

def pick(game):
    # A word not played since the list was last used up.
    words.word_list() => ws
    if len(game[3]) == len(ws):
        [] => game[3]
    while True:
        ws[game[0].below(len(ws))] => w
        False => played
        for p in game[3]:
            if p == w:
                True => played
        if not played:
            game[3] + [w] => game[3]
            return w

def hint(pattern, misses, guessed, ruled_out):
    words.candidates(pattern, misses, ruled_out) => fit
    if len(fit) == 1:
        "Only one word in the list still fits." ^0
        return
    words.best_letters(words.letter_table(fit, guessed), 3) => best
    "" => s
    for b in best:
        if s != "":
            s + ", " => s
        s + b[0] + " (" + str(b[1]) + ")" => s
    (str(len(fit)) + " words in the list still fit. The letters in the most of them: " + s + ".") ^0

def play(game):
    pick(game) => secret
    ("A word of " + str(len(secret)) + " letters.") ^0
    "" => guessed
    "" => misses
    [] => ruled_out
    0 => wrong
    while True:
        pattern_of(secret, guessed) => pattern
        for line in gallows(wrong):
            line ^0
        ("Word: " + spaced(pattern) + "    missed: " + listed(misses) + "    wrong guesses left: " + str(6 - wrong)) ^0
        lowered(trim(input("letter, the whole word, ? for a hint, or nothing to give up> "))) => answer
        if answer == "":
            ("You give up - the word was " + secret + ".") ^0
            return False
        if answer == "?":
            hint(pattern, misses, guessed, ruled_out)
        elif not letters_only(answer):
            "Type a letter from a to z, the whole word, or ?." ^0
        elif len(answer) == 1:
            if has(guessed, answer):
                ("You have tried " + answer + " already.") ^0
            else:
                guessed + answer => guessed
                0 => n
                for ch in secret:
                    if ch == answer:
                        n + 1 => n
                if n > 0:
                    ("Yes - " + answer + " is in it " + counted(n, "time", "times") + ".") ^0
                else:
                    misses + answer => misses
                    wrong + 1 => wrong
                    ("No " + answer + ".") ^0
        elif len(answer) != len(secret):
            ("The word has " + str(len(secret)) + " letters - that guess costs nothing.") ^0
        elif answer == secret:
            secret => guessed
        else:
            ruled_out + [answer] => ruled_out
            wrong + 1 => wrong
            ("Not " + answer + ".") ^0
        if pattern_of(secret, guessed) == secret:
            ("You got it: " + secret + ", with " + counted(wrong, "wrong guess", "wrong guesses") + ".") ^0
            return True
        if wrong == 6:
            for line in gallows(wrong):
                line ^0
            ("Hanged - the word was " + secret + ".") ^0
            return False

def ask_seed():
    while True:
        trim(input("seed (0 to 999999999)> ")) => answer
        if answer == "":
            return -1
        True => ok
        if len(answer) > 9:
            False => ok
        for ch in answer:
            if not (ch >= "0" and ch <= "9"):
                False => ok
        if ok:
            return int(answer)
        "Type a number from 0 to 999999999." ^0

"== Hangman ==" ^0
"Guess the word a letter at a time; six wrong guesses and you are hanged." ^0
# [the generator, words won, words lost, words played since the list was used up]
[rng.Rng(2026), 0, 0, []] => game
True => running
while running:
    "" ^0
    "1) new word  2) seed  3) score  4) quit" ^0
    trim(input("choice> ")) => choice
    if choice == "1":
        if play(game):
            game[1] + 1 => game[1]
        else:
            game[2] + 1 => game[2]
    elif choice == "2":
        ask_seed() => seed
        if seed == -1:
            "Cancelled." ^0
        else:
            rng.Rng(seed) => game[0]
            [] => game[3]
            ("Words now come from seed " + str(seed) + ".") ^0
    elif choice == "3":
        ("Won " + str(game[1]) + ", lost " + str(game[2]) + ".") ^0
    elif choice == "4":
        False => running
    else:
        "Pick a number from 1 to 4." ^0
"Bye." ^0
```

Python projection of main.eml:

```python
import words
import rng

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 lowered(s):
    big = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    small = "abcdefghijklmnopqrstuvwxyz"
    out = ""
    for ch in s:
        c = ch
        for k in range(0, 26):
            if ch == big[k]:
                c = small[k]
        out = out + c
    return out

def letters_only(s):
    if s == "":
        return False
    for ch in s:
        if not (ch >= "a" and ch <= "z"):
            return False
    return True

def has(s, ch):
    for x in s:
        if x == ch:
            return True
    return False

def counted(n, one, many):
    if n == 1:
        return "1 " + one
    return str(n) + " " + many

def gallows(wrong):
    head = " "
    body = "   "
    legs = "   "
    if wrong >= 1:
        head = "O"
    if wrong == 2:
        body = " | "
    elif wrong == 3:
        body = "/| "
    elif wrong >= 4:
        body = "/|\\"
    if wrong == 5:
        legs = "/  "
    elif wrong >= 6:
        legs = "/ \\"
    return ["  +---+", "  |   |", "  " + head + "   |", " " + body + "  |", " " + legs + "  |", "      |", "========="]

def pattern_of(secret, guessed):
    p = ""
    for ch in secret:
        if has(guessed, ch):
            p = p + ch
        else:
            p = p + "_"
    return p

def spaced(p):
    out = ""
    for ch in p:
        if out != "":
            out = out + " "
        out = out + ch
    return out

def listed(s):
    out = ""
    for ch in s:
        if out != "":
            out = out + ", "
        out = out + ch
    if out == "":
        return "none"
    return out

def pick(game):
    ws = words.word_list()
    if len(game[3]) == len(ws):
        game[3] = []
    while True:
        w = ws[game[0].below(len(ws))]
        played = False
        for p in game[3]:
            if p == w:
                played = True
        if not played:
            game[3] = game[3] + [w]
            return w

def hint(pattern, misses, guessed, ruled_out):
    fit = words.candidates(pattern, misses, ruled_out)
    if len(fit) == 1:
        print("Only one word in the list still fits.")
        return
    best = words.best_letters(words.letter_table(fit, guessed), 3)
    s = ""
    for b in best:
        if s != "":
            s = s + ", "
        s = s + b[0] + " (" + str(b[1]) + ")"
    print(str(len(fit)) + " words in the list still fit. The letters in the most of them: " + s + ".")

def play(game):
    secret = pick(game)
    print("A word of " + str(len(secret)) + " letters.")
    guessed = ""
    misses = ""
    ruled_out = []
    wrong = 0
    while True:
        pattern = pattern_of(secret, guessed)
        for line in gallows(wrong):
            print(line)
        print("Word: " + spaced(pattern) + "    missed: " + listed(misses) + "    wrong guesses left: " + str(6 - wrong))
        answer = lowered(trim(input("letter, the whole word, ? for a hint, or nothing to give up> ")))
        if answer == "":
            print("You give up - the word was " + secret + ".")
            return False
        if answer == "?":
            hint(pattern, misses, guessed, ruled_out)
        elif not letters_only(answer):
            print("Type a letter from a to z, the whole word, or ?.")
        elif len(answer) == 1:
            if has(guessed, answer):
                print("You have tried " + answer + " already.")
            else:
                guessed = guessed + answer
                n = 0
                for ch in secret:
                    if ch == answer:
                        n = n + 1
                if n > 0:
                    print("Yes - " + answer + " is in it " + counted(n, "time", "times") + ".")
                else:
                    misses = misses + answer
                    wrong = wrong + 1
                    print("No " + answer + ".")
        elif len(answer) != len(secret):
            print("The word has " + str(len(secret)) + " letters - that guess costs nothing.")
        elif answer == secret:
            guessed = secret
        else:
            ruled_out = ruled_out + [answer]
            wrong = wrong + 1
            print("Not " + answer + ".")
        if pattern_of(secret, guessed) == secret:
            print("You got it: " + secret + ", with " + counted(wrong, "wrong guess", "wrong guesses") + ".")
            return True
        if wrong == 6:
            for line in gallows(wrong):
                print(line)
            print("Hanged - the word was " + secret + ".")
            return False

def ask_seed():
    while True:
        answer = trim(input("seed (0 to 999999999)> "))
        if answer == "":
            return -1
        ok = True
        if len(answer) > 9:
            ok = False
        for ch in answer:
            if not (ch >= "0" and ch <= "9"):
                ok = False
        if ok:
            return int(answer)
        print("Type a number from 0 to 999999999.")

print("== Hangman ==")
print("Guess the word a letter at a time; six wrong guesses and you are hanged.")
game = [rng.Rng(2026), 0, 0, []]
running = True
while running:
    print("")
    print("1) new word  2) seed  3) score  4) quit")
    choice = trim(input("choice> "))
    if choice == "1":
        if play(game):
            game[1] = game[1] + 1
        else:
            game[2] = game[2] + 1
    elif choice == "2":
        seed = ask_seed()
        if seed == -1:
            print("Cancelled.")
        else:
            game[0] = rng.Rng(seed)
            game[3] = []
            print("Words now come from seed " + str(seed) + ".")
    elif choice == "3":
        print("Won " + str(game[1]) + ", lost " + str(game[2]) + ".")
    elif choice == "4":
        running = False
    else:
        print("Pick a number from 1 to 4.")
print("Bye.")
```

### words.eml

```eml
# P051 hangman - the word list, and what the letters of the words that still
# fit say about the next guess.

def word_list():
    return ["anchor", "apple", "badger", "basket", "beacon", "bicycle", "blanket", "bottle", "bridge", "bucket",
            "butter", "cactus", "camera", "candle", "canyon", "carpet", "castle", "cherry", "chimney", "circus",
            "cobweb", "comet", "compass", "cookie", "cotton", "crayon", "dolphin", "donkey", "dragon", "drawer",
            "eagle", "engine", "falcon", "feather", "fiddle", "forest", "fossil", "garden", "giraffe", "glacier",
            "goblet", "guitar", "hammer", "harbor", "helmet", "hockey", "island", "jacket", "jigsaw", "jungle",
            "kettle", "kitten", "ladder", "lantern", "lemon", "lizard", "magnet", "marble", "meadow", "mitten",
            "monkey", "muffin", "needle", "nugget", "oyster", "paddle", "parrot", "pebble", "pepper", "pickle",
            "pillow", "pirate", "planet", "pocket", "puzzle", "rabbit", "ribbon", "rocket", "saddle", "salmon",
            "sandal", "shadow", "silver", "socket", "spider", "spoon", "squirrel", "stable", "string", "sugar",
            "teapot", "ticket", "tomato", "trumpet", "tunnel", "turtle", "umbrella", "valley", "velvet", "violin",
            "walnut", "window", "wizard", "yogurt", "zipper"]

def fits(word, pattern, misses):
    # Could word be the secret? pattern has a letter where one is known and
    # "_" elsewhere; misses are letters known not to be in it. A known
    # letter also cannot hide behind a "_": the game shows every place a
    # guessed letter stands.
    if len(word) != len(pattern):
        return False
    for i in [0:len(word) - 1]:
        word[i] => ch
        if pattern[i] != "_":
            if ch != pattern[i]:
                return False
        else:
            for m in misses:
                if ch == m:
                    return False
            for p in pattern:
                if ch == p:
                    return False
    return True

def candidates(pattern, misses, ruled_out):
    # The words that fit, leaving out whole words already guessed wrong.
    [] => out
    for w in word_list():
        if fits(w, pattern, misses):
            False => wrong
            for r in ruled_out:
                if r == w:
                    True => wrong
            if not wrong:
                out + [w] => out
    return out

def letter_table(words, guessed):
    # For every letter not guessed yet, in how many of the words it stands -
    # counted once a word, kept in the order letters first appear (the
    # check-then-insert tally of the corpus case char-frequency-table).
    {} => counts
    for w in words:
        "" => seen
        for ch in w:
            0 => known
            for g in guessed + seen:
                if g == ch:
                    1 => known
            if known == 0:
                seen + ch => seen
                if ch in counts:
                    counts[ch] + 1 => counts[ch]
                else:
                    1 => counts[ch]
    return counts

def best_letters(counts, k):
    # The k letters in the most words, most first; a tie goes to the letter
    # that comes first in the alphabet.
    [] => out
    "" => taken
    while len(out) < k and len(out) < len(counts):
        "" => pick
        0 => most
        for ch in counts:
            0 => used
            for t in taken:
                if t == ch:
                    1 => used
            if used == 0:
                if counts[ch] > most or (counts[ch] == most and ch < pick):
                    ch => pick
                    counts[ch] => most
        taken + pick => taken
        out + [[pick, most]] => out
    return out
```

Python projection of words.eml:

```python
def word_list():
    return ["anchor", "apple", "badger", "basket", "beacon", "bicycle", "blanket", "bottle", "bridge", "bucket", "butter", "cactus", "camera", "candle", "canyon", "carpet", "castle", "cherry", "chimney", "circus", "cobweb", "comet", "compass", "cookie", "cotton", "crayon", "dolphin", "donkey", "dragon", "drawer", "eagle", "engine", "falcon", "feather", "fiddle", "forest", "fossil", "garden", "giraffe", "glacier", "goblet", "guitar", "hammer", "harbor", "helmet", "hockey", "island", "jacket", "jigsaw", "jungle", "kettle", "kitten", "ladder", "lantern", "lemon", "lizard", "magnet", "marble", "meadow", "mitten", "monkey", "muffin", "needle", "nugget", "oyster", "paddle", "parrot", "pebble", "pepper", "pickle", "pillow", "pirate", "planet", "pocket", "puzzle", "rabbit", "ribbon", "rocket", "saddle", "salmon", "sandal", "shadow", "silver", "socket", "spider", "spoon", "squirrel", "stable", "string", "sugar", "teapot", "ticket", "tomato", "trumpet", "tunnel", "turtle", "umbrella", "valley", "velvet", "violin", "walnut", "window", "wizard", "yogurt", "zipper"]

def fits(word, pattern, misses):
    if len(word) != len(pattern):
        return False
    for i in range(0, len(word)):
        ch = word[i]
        if pattern[i] != "_":
            if ch != pattern[i]:
                return False
        else:
            for m in misses:
                if ch == m:
                    return False
            for p in pattern:
                if ch == p:
                    return False
    return True

def candidates(pattern, misses, ruled_out):
    out = []
    for w in word_list():
        if fits(w, pattern, misses):
            wrong = False
            for r in ruled_out:
                if r == w:
                    wrong = True
            if not wrong:
                out = out + [w]
    return out

def letter_table(words, guessed):
    counts = {}
    for w in words:
        seen = ""
        for ch in w:
            known = 0
            for g in guessed + seen:
                if g == ch:
                    known = 1
            if known == 0:
                seen = seen + ch
                if ch in counts:
                    counts[ch] = counts[ch] + 1
                else:
                    counts[ch] = 1
    return counts

def best_letters(counts, k):
    out = []
    taken = ""
    while len(out) < k and len(out) < len(counts):
        pick = ""
        most = 0
        for ch in counts:
            used = 0
            for t in taken:
                if t == ch:
                    used = 1
            if used == 0:
                if counts[ch] > most or counts[ch] == most and ch < pick:
                    pick = ch
                    most = counts[ch]
        taken = taken + pick
        out = out + [[pick, most]]
    return out
```

### rng.eml

```eml
# P051 hangman - random numbers written in EML: the linear congruential
# generator of P008 (number guessing), with the constants of the C
# standard's example rand(). Python's random module is not used, so a seed
# gives the same words 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

# P051 - Hangman

Guess the secret word before the drawing is finished. Guess one letter at a
time, or the whole word at once; each wrong guess adds a part to the
drawing - head, body, two arms, two legs - and the sixth one hangs you. The
words come from a list of 105, picked from a seed, none twice until the
whole list has been played.

- `main.eml` - the menu, one word from the first guess to the last, the
  drawing and the score
- `words.eml` - the word list, which words still fit what you know, and
  the letter counts for a hint
- `rng.eml` - random numbers written in EML, the generator of P008

How each part works:

- A guessed letter shows in every place it stands in the word. Upper case
  is read as lower case, and a letter you have tried already costs
  nothing.
- A whole word of the wrong length costs nothing, only a reminder of the
  length. A wrong word of the right length counts as a wrong guess.
- A hint looks at the words in the list that still fit what you know:
  the right length, the shown letters in their places, none of the missed
  letters, no shown letter hiding behind a blank - the game would have
  shown it there too - and none of the whole words you guessed wrong. For
  each letter you have not tried, it counts in how many of those words the
  letter stands, once a word, and names the three highest, a tie going to
  the letter first in the alphabet. The count is the corpus case
  `char-frequency-table`'s tally: check whether the letter is already in
  the table, then add one or start at one. A hint costs nothing.

What is checked: menu choices 1 to 4; a guess of letters only, a to z,
after spaces at the ends are dropped; a seed from 0 to 999999999. An empty
guess gives the word up, and an empty seed cancels.

Sessions: `sessions/basic.in` plays three words from the default seed
2026.
- spoon: one miss, then a hint, where 2 words still fit and s stands in
  both. Then s and o, and the whole word.
- trumpet: four hints along the way, from 10 words that fit down to one,
  and the word typed in capitals.
- pebble: six misses in a row, which show the drawing grow, and the word
  is lost.
Then the score: 2 won, 1 lost.

`sessions/bad-input.in` gives:
- menu choices 0 and x;
- seeds abc, -1 and 1234567890, and an empty seed that cancels;
- seed 7, then the guesses 1, "a b" and ab!;
- E in capitals, and then e again;
- zebra, which has the wrong length, and bottle, which has the right
  length but is wrong. After that the hint counts 14 words, with bottle
  left out;
- an empty guess, which gives the word up.

Built on the verified corpus case `char-frequency-table` (counting each
character in a dict, in the order the characters first appear).
