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 55 — Powerball Lottery
This is project 55 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:
5 17 23 42 50
7
3
Run it with:
BIGBOOK_SEED=1 ek9 powerball-lottery.ek9 < input.txt
It prints:
Powerball Lottery, by Al Sweigart al@inventwithpython.com
Each ticket costs $2. The odds are 1 in 292,201,338, so you won't win.
Enter 5 different numbers from 1 to 69, with spaces between
each number. (For example: 5 17 23 42 50)
Enter the powerball number from 1 to 26.
How many times do you want to play? (Max: 1000000)
It costs $6 to play 3 times, but don't worry.
The winning numbers are: 22 2 54 64 29 and 3
You lost.
The winning numbers are: 22 15 44 47 8 and 2
You lost.
The winning numbers are: 35 2 31 27 3 and 10
You lost.
You have wasted $6
Thanks for playing!
The program
#!ek9 defines module bigbook.powerball defines constant LowestBall <- 1 HighestBall <- 69 HighestPower <- 26 BallsPerDraw <- 5 MostPlays <- 1000000 TicketPrice <- 2 defines type List of Integer List of String Set of Integer shuffled of Integer //What can be wrong with the numbers a player types, in the order the original checks them. TicketProblem WrongCount NotNumbers OutOfRange Repeated defines record //A ticket is what the player chose, a draw is what came out of the machine, and the two //win when they are equal. The numbers are a Set because the order they were picked or //drawn in does not decide a win. Ticket numbers as Set of Integer: Set() of Integer powerball as Integer: Integer() Ticket() -> numbers as Set of Integer powerball as Integer this.numbers: numbers this.powerball: powerball default operator == default operator ? 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()) //Asks until the player types five valid numbers. A Set is always set (empty is not unset), //so here it is empty only if the input ends first. nextPicks() <- rtn as Set of Integer: Set() of Integer while rtn.empty() and not input.ended() stdout.println(say.askPicks()) if entry <- input.next() if problem <- picksProblem(entry) showProblem(problem) else rtn: picksOf(entry) nextPowerball() <- rtn as Integer: nextNumber(say.askPowerball(), say.notAPowerball(), HighestPower) nextPlayCount() <- rtn as Integer: nextNumber(say.askPlays(), say.notAPlayCount(), MostPlays) showCost() -> cost as Integer plays as Integer stdout.println(say.costs(cost, plays)) showDraw() -> balls as List of Integer powerball as Integer numbers <- cat balls | map with asText | join with spaced | collect as String stdout.println(say.winningNumbers(numbers, powerball)) showOutcome() -> won as Boolean stdout.println(won <- say.won() : say.lost()) showWasted() -> cost as Integer stdout.println(say.wasted(cost)) showGoodbye() stdout.println(say.goodbye()) //Asks until the game accepts the answer as a number up to highest; unset only if the input //ends first. private nextNumber() -> prompt as String complaint as String highest as Integer <- rtn as Integer: Integer() while not rtn? and not input.ended() stdout.println(prompt) if entry <- input.next() if number <- numberUpTo(entry, highest) rtn: number else stdout.println(complaint) private showProblem() -> problem as TicketProblem switch problem case TicketProblem.WrongCount stdout.println(say.wrongCount()) case TicketProblem.NotNumbers stdout.println(say.notNumbers()) case TicketProblem.OutOfRange stdout.println(say.outOfRange()) case TicketProblem.Repeated stdout.println(say.repeated()) default //picksProblem returns a set problem or none; the compiler requires this branch (E07320). stdout.println(say.wrongCount()) override operator ? as pure <- rtn as Boolean: true defines function //THE GAME: the lottery's rules, with no input or output. //shuffled: a new List in random order (see lib/Shuffle.ek9). Inlined because each //port is a single compilation unit. Uses the Random it is given. shuffled() of type T as open -> source as List of T rng as Random <- rtn as List of T: List() of T work as List of T: List() of T work += source for pass in source size as Integer: length work pick as Integer: rng.next(0, size - 1) remaining as List of T: List() of T cursor as Integer: 0 for element in work if cursor == pick rtn += element else remaining += element cursor += 1 work: remaining asNumber() as pure -> text as String <- rtn as Integer: Integer(text) isNumber() as pure -> text as String <- rtn as Boolean: Integer(text)? isBallNumber() as pure -> number as Integer <- rtn as Boolean: number >= LowestBall and number <= HighestBall //The numbers the player typed, separated by spaces. partsOf() as pure -> entry as String <- rtn as List of String: entry.trim().split(RegEx("[ ]+")) //Why the typed numbers cannot be a ticket, checked in the original's order; unset when they can. picksProblem() as pure -> entry as String <- rtn as TicketProblem: TicketProblem() parts <- partsOf(entry) numbers <- cat parts | filter with isNumber | map with asNumber | collect as List of Integer balls <- cat numbers | filter with isBallNumber | collect as List of Integer distinct <- cat balls | collect as Set of Integer if length parts <> BallsPerDraw rtn: TicketProblem.WrongCount else if length numbers <> BallsPerDraw rtn: TicketProblem.NotNumbers else if length balls <> BallsPerDraw rtn: TicketProblem.OutOfRange else if length distinct <> BallsPerDraw rtn: TicketProblem.Repeated picksOf() as pure -> entry as String <- rtn as Set of Integer: cat partsOf(entry) | map with asNumber | collect as Set of Integer //A whole number from LowestBall to highest; unset for anything else. numberUpTo() as pure -> entry as String highest as Integer <- rtn as Integer: Integer() if number <- Integer(entry.trim()) then number >= LowestBall and number <= highest rtn: number costOf() as pure -> plays as Integer <- rtn as Integer: TicketPrice * plays ballPool() <- rtn as List of Integer: List() of Integer for ball in LowestBall ... HighestBall rtn += ball drawBalls() -> pool as List of Integer rng as Random <- rtn as List of Integer: List() of Integer shuffle <- () is shuffled of Integer as function rtn: cat shuffle(pool, rng) | head BallsPerDraw | collect as List of Integer drawPowerball() -> rng as Random <- rtn as Integer: rng.next(LowestBall, HighestPower) //The draw wins when it is the same ticket as the player's. wins() -> ticket as Ticket balls as List of Integer powerball as Integer <- rtn as Boolean: false drawn <- cat balls | collect as Set of Integer rtn: Ticket(drawn, powerball) == ticket defines function asText() as pure -> number as Integer <- rtn as String: $number 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. In a backtick String `\$` is a dollar sign. defines text for "en" Say introduction() `Powerball Lottery, by Al Sweigart al@inventwithpython.com Each ticket costs \$${TicketPrice}. The odds are 1 in 292,201,338, so you won't win.` askPicks() `Enter ${BallsPerDraw} different numbers from ${LowestBall} to ${HighestBall}, with spaces between each number. (For example: 5 17 23 42 50)` wrongCount() `Please enter ${BallsPerDraw} numbers, separated by spaces.` notNumbers() "Please enter numbers, like 27, 35, or 62." outOfRange() `The numbers must all be between ${LowestBall} and ${HighestBall}.` repeated() `You must enter ${BallsPerDraw} different numbers.` askPowerball() `Enter the powerball number from ${LowestBall} to ${HighestPower}.` notAPowerball() `The powerball number must be between ${LowestBall} and ${HighestPower}.` askPlays() `How many times do you want to play? (Max: ${MostPlays})` notAPlayCount() `You can play between ${LowestBall} and ${MostPlays} times.` costs() -> cost as Integer plays as Integer `It costs \$${cost} to play ${plays} times, but don't worry.` winningNumbers() -> numbers as String powerball as Integer `The winning numbers are: ${numbers} and ${powerball}` won() "You have won the Powerball Lottery! Congratulations!" lost() "You lost." wasted() -> cost as Integer `You have wasted \$${cost}` goodbye() "Thanks for playing!" defines program //Powerball Lottery (Big Book project 55). Pick five numbers and a powerball, choose how many //draws to play, and watch yourself lose. The game decides; the Console asks and tells, in the //player's words. The golden replays a seeded game (seed.txt). PowerballLottery() //A language with no words of its own is unset: ?: falls back to English. console <- Console(Say(language()) ?: Say("en")) rng <- randomSource() console.showIntroduction() picks <- console.nextPicks() powerball <- console.nextPowerball() plays <- console.nextPlayCount() //No play count means the input ended first: there is nothing to play. if plays? cost <- costOf(plays) console.showCost(cost, plays) ticket <- Ticket(picks, powerball) pool <- ballPool() won <- false played <- 0 //Draws until the ticket wins or the plays run out. while played < plays and not won balls <- drawBalls(pool, rng) drawn <- drawPowerball(rng) console.showDraw(balls, drawn) won: wins(ticket, balls, drawn) console.showOutcome(won) played += 1 console.showWasted(cost) console.showGoodbye()
About this port
- Faithful to the book: pick 5 unique numbers 1-69 and a powerball 1-26; choose how many plays (1..1,000,000); each draw shuffles 1-69, takes 5 + a 1-26 powerball, prints them, and stops at a (near-impossible) match; reports cost/waste; the original's four complaints about a bad ticket (count, not numbers, range, repeats), in its order.
- Divergence: the "Press Enter" pauses are omitted.
- Idioms:
- A
Ticketrecord holds aSet of Integerand the powerball. The original comparesset(numbers) == set(winningNumbers); here the player's ticket and the draw are both Tickets, so a win is simplyTicket(drawn, powerball) == ticket(default operator ==, which compares the Sets by value). - The
Setalso does the "five different numbers" check:lengthof the distinct numbers againstBallsPerDraw, instead of comparing every pair. picksProblemisas pureand built from three small stream filters (numbers, then in range, then distinct); the input loop asks one question,if problem <- picksProblem(entry).- The draw is
cat shuffled-pool | head BallsPerDraw, and the 1-69 pool is a range loop. The random draws are the same, in the same order, as before the rework. - The enumeration is
TicketProblem, notPicksProblem: a type and a function that differ only by case is E01089.
- A