The Big Book of Small EK9 Programs
Adapted from Al Sweigart's The Big Book of Small Python Projects: 81 Easy Practice Programs (No Starch Press, 2021; copyright © 2021 Al Sweigart), which he makes freely available under a Creative Commons Attribution-NonCommercial-ShareAlike licence at inventwithpython.com/bigbookpython. Changes were made: each program was ported from Python to EK9 and remodelled in EK9's idioms.
Project 34 — Hangman and Guillotine
This is project 34 of the book, ported to EK9. Its chapter is still being written; until then, this page shows the port itself: a run of it, the whole program, and how it keeps to the original.
A sample run
The program is random, so the run fixes it by setting BIGBOOK_SEED=1. The player's answers are piped in:
E
A
N
T
Run it with:
BIGBOOK_SEED=1 ek9 hangman-and-guillotine.ek9 < input.txt
It prints:
Hangman, by Al Sweigart al@inventwithpython.com
+--+
| |
|
|
|
=====
The category is: Animals
Missed letters: none yet
_ _ _
Guess a letter.
+--+
| |
O |
|
|
=====
The category is: Animals
Missed letters: E
_ _ _
Guess a letter.
+--+
| |
O |
|
|
=====
The category is: Animals
Missed letters: E
A _ _
Guess a letter.
+--+
| |
O |
|
|
=====
The category is: Animals
Missed letters: E
A N _
Guess a letter.
Yes! The secret word is: ANT
You have won!
The program
#!ek9 defines module bigbook.hangman defines constant //The game's data: the category its words are drawn from. Category <- "Animals" defines type List of String List of Character Set of Character //Why a guess is not accepted, in the order the book checks. GuessProblem NotOneCharacter AlreadyGuessed NotALetter defines class //THE input idiom of this book, copied unchanged from lib/LineInput.ek9 (the reasons are //there): read with next(), and every input loop also stops when ended() is true. LineInput stdin as Stdin: Stdin() inputEnded as Boolean: false next() <- rtn as String: stdin.next() if not rtn? inputEnded: true ended() as pure <- rtn as Boolean: Boolean(inputEnded) override operator ? as pure <- rtn as Boolean: true //THE INPUT AND OUTPUT: everything the player sees or types goes through here, worded by Say. Console stdout as Stdout: Stdout() input as LineInput: LineInput() say as Say: Say("en") Console() -> words as Say say: words showIntroduction() stdout.println(say.introduction()) //The gallows for the misses so far, then what the player knows: the category, the missed //letters in the order they were guessed, and the word with a blank for each letter not found. showGallows() -> secret as String missed as List of Character correct as Set of Character cat gallowsFor(length missed) > stdout stdout.println(say.category(Category)) if missed.empty() stdout.println(say.noneMissed()) else letters <- cat missed | map with followedBySpace | collect as String stdout.println(say.missed(letters)) stdout.println(blanksFor(secret, correct)) //Asks until the game accepts a letter; unset only if the input ends first. nextGuess() -> missed as List of Character correct as Set of Character <- rtn as Character: Character() while not rtn? and not input.ended() stdout.println(say.askLetter()) if entry <- input.next() guess <- entry.upperCase().trim() if problem <- problemWith(guess, missed, correct) showProblem(problem) else rtn: guess.first() showWon() -> secret as String stdout.println(say.won(secret)) showLost() -> secret as String stdout.println(say.lost(secret)) ended() as pure <- rtn as Boolean: input.ended() private showProblem() -> problem as GuessProblem switch problem case GuessProblem.NotOneCharacter stdout.println(say.notOneLetter()) case GuessProblem.AlreadyGuessed stdout.println(say.alreadyGuessed()) case GuessProblem.NotALetter stdout.println(say.notALetter()) default //problemWith returns a set problem or none; the compiler requires this branch (E07320). stdout.println(say.notALetter()) //The word as far as it is known: each letter not yet guessed is a blank. private blanksFor() -> secret as String correct as Set of Character <- rtn as String: String() shown <- List() of String for letter in secret shown += correct.contains(letter) <- $letter : "_" rtn: cat shown | join with spaced | collect as String override operator ? as pure <- rtn as Boolean: true defines function //THE GAME: its rules, its words and its picture, with no input or output. //The picture IS data, as the book's HANGMAN_PICS is: one picture per number of misses, so the //number of pictures sets how many misses a player has, and a different picture needs no change //to the logic. It does not translate, so it is not text. gallowsStages() as pure <- rtn as List of List of String: [ [" +--+", " | |", " |", " |", " |", "====="], [" +--+", " | |", " O |", " |", " |", "====="], [" +--+", " | |", " O |", " | |", " |", "====="], [" +--+", " | |", " O |", "/| |", " |", "====="], [" +--+", " | |", " O |", "/|\\ |", " |", "====="], [" +--+", " | |", " O |", "/|\\ |", "/ |", "====="], [" +--+", " | |", " O |", "/|\\ |", "/ \\ |", "====="] ] gallowsFor() as pure -> misses as Integer <- rtn as List of String: gallowsStages().getOrDefault(misses, List() of String) mostMisses() as pure <- rtn as Integer: length gallowsStages() - 1 //The game's data: the words the secret is drawn from, which the player's letters are compared with. animals() as pure <- rtn as List of String: ["ANT", "BEAR", "CAT", "DOG", "FROG", "GOAT", "HAWK", "LION", "MOLE", "MOOSE", "OWL", "PANDA", "RAT", "SEAL", "SWAN", "TIGER", "WOLF", "ZEBRA"] secretWord() -> rng as Random <- rtn as String: String() words <- animals() rtn: words.getOrDefault(rng.next(0, length words - 1), String()) //Unset when the guess is a letter not tried before; otherwise the first thing wrong with it. problemWith() as pure -> guess as String missed as List of Character correct as Set of Character <- rtn as GuessProblem: GuessProblem() letter <- guess.first() if length guess <> 1 rtn: GuessProblem.NotOneCharacter else if missed.contains(letter) or correct.contains(letter) rtn: GuessProblem.AlreadyGuessed else if not letter.isAlpha() rtn: GuessProblem.NotALetter isSolved() as pure -> secret as String correct as Set of Character <- rtn as Boolean: true for letter in secret if not correct.contains(letter) rtn: false defines function followedBySpace() as pure -> letter as Character <- rtn as String: `${letter} ` spaced() as pure -> left as String right as String <- rtn as String: `${left} ${right}` //THE source of randomness, copied unchanged from lib/RandomSource.ek9: SystemRandom(), or //SeededRandom(seed) when BIGBOOK_SEED holds a number (run-and-diff.sh sets it from seed.txt). randomSource() <- rtn as Random: SeededRandom(Integer(EnvVars().get("BIGBOOK_SEED"))) ?: SystemRandom() //THE language of the words, copied unchanged from lib/Language.ek9: BIGBOOK_LANG when it //is set (fr, zh, ...), otherwise en. language() <- rtn as String: EnvVars().get("BIGBOOK_LANG") ?: "en" //THE WORDS: every word the player sees. The animals and the gallows are not here: they are the //game's data. defines text for "en" Say introduction() "Hangman, by Al Sweigart al@inventwithpython.com" category() -> category as String `The category is: ${category}` noneMissed() "Missed letters: none yet" //The letters come each followed by a space, as the book prints them. missed() -> letters as String `Missed letters: ${letters}` askLetter() "Guess a letter." notOneLetter() "Please enter a single letter." alreadyGuessed() "You have already guessed that letter. Choose again." notALetter() "Please enter a LETTER." won() -> secret as String `Yes! The secret word is: ${secret} You have won!` lost() -> secret as String `You have run out of guesses! The word was "${secret}"` defines program //Hangman (Big Book project 34). Guess the letters of a secret animal before the gallows is //complete. The game decides; the Console asks and tells, in the player's words. The golden //replays a seeded game (seed.txt). Hangman() //A language with no words of its own is unset: ?: falls back to English. console <- Console(Say(language()) ?: Say("en")) rng <- randomSource() secret <- secretWord(rng) console.showIntroduction() //Missed letters are shown in the order they were guessed, so they are a List; the correct //ones are only ever asked "is this letter in?", so they are a Set. missed <- List() of Character correct <- Set() of Character running <- true while running and not console.ended() console.showGallows(secret, missed, correct) if guess <- console.nextGuess(missed, correct) if secret contains guess correct += guess if isSolved(secret, correct) console.showWon(secret) running: false else missed += guess if length missed == mostMisses() console.showGallows(secret, missed, correct) console.showLost(secret) running: false
About this port
- Faithful to the book: guess single letters of a secret animal; the gallows gains a part per wrong guess (6 misses = loss); shows category, missed letters, and the word's blanks; win reveals the word; validation rejects multi-character input, repeat guesses and non-letters, in the original's order.
- Divergences: own 7-stage gallows art and a compact animal word list (both original; the game logic is the port).