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