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 1 — Bagels
Bagels is a code-breaking game. The computer secretly picks a three-digit number with no digit repeated, and the player has ten guesses to find it. After every guess the computer hands back a set of clues: "Fermi" means a digit is correct and in the right place, "Pico" means a digit is correct but in the wrong place, and "Bagels" means nothing in the guess was right at all. The clues are printed in a fixed order, so they never give away which digit earned which — only how many of each. Once the secret is found, or the guesses run out, the game offers another round.
Adapted from project 1 of Al Sweigart's The Big Book of Small Python Projects (No Starch Press, 2021), under CC BY-NC-SA 4.0.
A sample run
The secret number is random, so the golden run fixes it by setting BIGBOOK_SEED=1
(from seed.txt) before piping in the player's guesses from input.txt:
123
456
638
no
BIGBOOK_SEED=1 ek9 bagels.ek9 < input.txt
Because the guesses are piped in rather than typed at a terminal, they are never echoed back — only the clues the program itself prints appear below. With seed 1 the secret turns out to be 638, found on the third guess:
Bagels, a deductive logic game.
I am thinking of a 3-digit number with no repeated digits.
Try to guess what it is. Here are some clues:
When I say: That means:
Pico One digit is correct but in the wrong position.
Fermi One digit is correct and in the right position.
Bagels No digit is correct.
I have thought up a number.
You have 10 guesses to get it.
Guess #1:
Pico
Guess #2:
Pico
Guess #3:
You got it!
Do you want to play again? (yes or no)
Thanks for playing!
The same game in French and Chinese
The game's words are kept apart from the game, so playing it in another language is a matter of
choosing which words to use. BIGBOOK_LANG=fr chooses the French ones; the player's guesses are the
same, and only their last answer changes, to "non":
BIGBOOK_SEED=1 BIGBOOK_LANG=fr ek9 bagels.ek9 < input.fr.txt
Bagels, un jeu de logique déductive.
Je pense à un nombre de 3 chiffres, sans chiffre répété.
Essayez de le deviner. Voici quelques indices :
Quand je dis : Cela signifie :
Pico Un chiffre est correct mais mal placé.
Fermi Un chiffre est correct et bien placé.
Bagels Aucun chiffre n'est correct.
J'ai choisi un nombre.
Vous avez 10 essais pour le trouver.
Essai n° 1 :
Pico
Essai n° 2 :
Pico
Essai n° 3 :
Vous l'avez trouvé !
Voulez-vous rejouer ? (oui ou non)
Merci d'avoir joué !
And BIGBOOK_LANG=zh, answering "否":
百吉饼(Bagels),一个推理游戏。
我想好了一个没有重复数字的 3 位数。
请猜猜它是多少。以下是提示:
我说: 意思是:
Pico 有一个数字正确,但位置不对。
Fermi 有一个数字正确,位置也对。
Bagels 没有一个数字正确。
我已经想好了一个数。
你有 10 次机会猜中它。
第 1 次猜测:
Pico
第 2 次猜测:
Pico
第 3 次猜测:
猜对了!
还想再玩一次吗?(是 或 否)
谢谢游玩!
A language the game has no words for (BIGBOOK_LANG=de) plays in English.
The program
#!ek9 defines module bigbook.bagels defines constant NumDigits <- 3 MaxGuesses <- 10 defines type //The two clues a digit can earn, in the order they are shown. Clue Fermi Pico List of Clue 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()) showNewNumber() stdout.println(say.newNumber()) //Asks until the answer is a guess; unset only if the input ends first. nextGuess() -> guessNumber as Integer <- rtn as String: String() while not rtn? and not input.ended() stdout.println(say.askGuess(guessNumber)) if entry <- input.next() if isGuess(entry) rtn: entry showClues() -> solved as Boolean clues as List of Clue stdout.println(solved <- say.gotIt() : clueLine(clues)) showRanOut() -> secret as String stdout.println(say.ranOut(secret)) wantsAnotherGame() <- rtn as Boolean: false stdout.println(say.playAgain()) if answer <- input.next() rtn: answer.lowerCase().startsWith(say.yes()) showGoodbye() stdout.println(say.goodbye()) ended() as pure <- rtn as Boolean: input.ended() private clueLine() -> clues as List of Clue <- rtn as String: say.bagels() if not clues.empty() words <- List() of String for clue in clues words += clue == Clue.Fermi <- say.fermi() : say.pico() rtn: cat words | join with spaced | collect as String override operator ? as pure <- rtn as Boolean: true defines function //THE GAME: its rules, with no input or output. //Three different random digits: a digit already drawn is drawn again. secretNumber() -> rng as Random <- rtn as String: "" while length rtn < NumDigits digit <- $rng.next(0, 9) if rtn not contains digit rtn += digit isGuess() as pure -> entry as String <- rtn as Boolean: length entry == NumDigits and entry matches /^[0-9]+$/ //Unset when the digit at this position earns no clue at all. clueAt() as pure -> guess as String secret as String position as Integer <- rtn as Clue: Clue() digit <- guess.subString(position, position + 1) if digit == secret.subString(position, position + 1) rtn: Clue.Fermi else if secret contains digit rtn: Clue.Pico //Sorted, so the clues never say which digit earned which. cluesFor() -> guess as String secret as String <- rtn as List of Clue: List() found <- List() of Clue for position in 0 ... NumDigits - 1 if clue <- clueAt(guess, secret, position) found += clue rtn: cat found | sort | collect as List of Clue defines function 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, one block per language. defines text for "en" Say introduction() `Bagels, a deductive logic game. I am thinking of a ${NumDigits}-digit number with no repeated digits. Try to guess what it is. Here are some clues: When I say: That means: Pico One digit is correct but in the wrong position. Fermi One digit is correct and in the right position. Bagels No digit is correct.` newNumber() `I have thought up a number. You have ${MaxGuesses} guesses to get it.` askGuess() -> guessNumber as Integer `Guess #${guessNumber}:` fermi() "Fermi" pico() "Pico" bagels() "Bagels" gotIt() "You got it!" ranOut() -> secret as String `You ran out of guesses. The answer was ${secret}.` playAgain() "Do you want to play again? (yes or no)" yes() "y" goodbye() "Thanks for playing!" defines text for "fr" Say introduction() `Bagels, un jeu de logique déductive. Je pense à un nombre de ${NumDigits} chiffres, sans chiffre répété. Essayez de le deviner. Voici quelques indices : Quand je dis : Cela signifie : Pico Un chiffre est correct mais mal placé. Fermi Un chiffre est correct et bien placé. Bagels Aucun chiffre n'est correct.` newNumber() `J'ai choisi un nombre. Vous avez ${MaxGuesses} essais pour le trouver.` askGuess() -> guessNumber as Integer `Essai n° ${guessNumber} :` fermi() "Fermi" pico() "Pico" bagels() "Bagels" gotIt() "Vous l'avez trouvé !" ranOut() -> secret as String `Vous n'avez plus d'essais. La réponse était ${secret}.` playAgain() "Voulez-vous rejouer ? (oui ou non)" yes() "o" goodbye() "Merci d'avoir joué !" defines text for "zh" Say introduction() `百吉饼(Bagels),一个推理游戏。 我想好了一个没有重复数字的 ${NumDigits} 位数。 请猜猜它是多少。以下是提示: 我说: 意思是: Pico 有一个数字正确,但位置不对。 Fermi 有一个数字正确,位置也对。 Bagels 没有一个数字正确。` newNumber() `我已经想好了一个数。 你有 ${MaxGuesses} 次机会猜中它。` askGuess() -> guessNumber as Integer `第 ${guessNumber} 次猜测:` fermi() "Fermi" pico() "Pico" bagels() "Bagels" gotIt() "猜对了!" ranOut() -> secret as String `你的机会用完了。 答案是 ${secret}。` playAgain() "还想再玩一次吗?(是 或 否)" yes() "是" goodbye() "谢谢游玩!" defines program //Bagels (Big Book project 1). Guess a 3-digit number with no repeated digits; the clues say //how close each guess is. The game decides; the Console asks and tells, in the player's words. //The golden replays a seeded game (seed.txt). Bagels() //A language with no words of its own is unset: ?: falls back to English. console <- Console(Say(language()) ?: Say("en")) rng <- randomSource() console.showIntroduction() running <- true while running and not console.ended() secret <- secretNumber(rng) console.showNewNumber() guessNumber <- 1 solved <- false while guessNumber <= MaxGuesses and not solved and not console.ended() if guess <- console.nextGuess(guessNumber) solved: guess == secret console.showClues(solved, cluesFor(guess, secret)) guessNumber += 1 if not solved and guessNumber > MaxGuesses console.showRanOut(secret) running: console.wantsAnotherGame() console.showGoodbye()
How the port works
The original is one main() that decides, asks, tells and words everything in the same lines. The
port keeps those apart, in four parts you can read and change on their own:
- The game decides.
secretNumber,isGuess,clueAtandcluesFortake values and give back values; none of them reads input, prints anything or holds a word the player sees.cluesForanswers with aList of Clue, sorted - theClueenumeration declaresFermibeforePico, so sorting puts every Fermi first and the clues never say which digit earned which.clueAtreturns an unsetCluewhen a digit earns nothing, so the clues are collected with a guard,if clue <- clueAt(...), rather than by testing for an empty string. The secret draws a random digit and keeps it only if it is not already in the secret, until it has three. - The
Consoleasks and tells. Everything the player sees or types passes through this one class: it holds the output, the input and the words. It turns the game's values into words (Clue.Fermibecomessay.fermi()), and when it reads a guess it asks the game,isGuess(entry), whether the entry will do, asking again until it does. Sayholds the words - every one, in onedefines textblock per language. The rules banner is one method,introduction(), holding a multi-line String just as the original holds it in one triple-quotedprint; its lines after the first start at the left margin because a String's content is literal, leading spaces included. Even "yes" is a word: the French player answers "oui" and the Chinese player "是", sowantsAnotherGameaskssay.yes()what a yes starts with.- The program is the flow between them, laid out as the original's
main(): a game loop while the player wants to play, a round loop on the guess count and asolvedflag (EK9 has nowhile Trueorbreak, so each loop has a real condition). It chooses the words withSay(language()) ?: Say("en"): a text made for a language it has no block for is unset, and?:falls back to English. WritingSay("de")in the source, with no German block, would not compile at all. - Every loop that waits for input also carries
not console.ended(), which is what stops the game looping for ever once piped input runs out.LineInput,randomSource()andlanguage()are copied unchanged into every port in this book: one stops at the end of input, one gives a different secret every real run but the same one under a seed (which is how the sample runs above replay exactly), and one chooses the language.
This is a little more code than the Python, and it is the point of the port: each part has one job, so a new language is a new block of words, a new rule touches only the game, and the game can be tested without a player.
In the EK9 reference: Text Properties in EK9