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 10 — Cho-Han

Cho-Han is a traditional Japanese dice game. The player stakes part of their purse on whether two dice, rolled hidden under a cup, will total an even number (cho) or an odd one (han). A correct call wins back the bet less a ten per cent house fee; a wrong one forfeits it. The game ends when the player quits or the purse runs dry.

Adapted from project 10 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

Run with BIGBOOK_SEED=1 ek9 chohan.ek9 < input.txt, betting 1000 on han, then 500 on han, then quitting:

1000
han
500
han
QUIT
Cho-Han, by Al Sweigart al@inventwithpython.com

In this traditional Japanese dice game, two dice are rolled in a bamboo
cup by the dealer sitting on the floor. The player must guess if the
dice total to an even (cho) or odd (han) number.

You have 5000 mon. How much do you bet? (or QUIT)
> 
The dealer swirls the cup and you hear the rattle of dice.
The dealer slams the cup on the floor, still covering the
dice and asks for your bet.

     CHO (even) or HAN (odd)?
> 
The dealer lifts the cup to reveal:
  ICHI - SHI
      1 - 4
You won! You take 1000 mon.
The house collects a 100 mon fee.
You have 5900 mon. How much do you bet? (or QUIT)
> 
The dealer swirls the cup and you hear the rattle of dice.
The dealer slams the cup on the floor, still covering the
dice and asks for your bet.

     CHO (even) or HAN (odd)?
> 
The dealer lifts the cup to reveal:
  ICHI - ICHI
      1 - 1
You lost!
You have 5400 mon. How much do you bet? (or QUIT)
> 
Thanks for playing!

The first bet wins (the dice show 1 and 4, totalling the odd number 5 — han), so 1000 mon comes back less a 100 mon fee; the second bet loses (1 and 1, an even total — cho) and the 500 mon stake is forfeit. The player then quits with 5400 mon left.

The program

#!ek9
defines module bigbook.chohan

  defines constant

    StartPurse <- 5000
    HouseCutDivisor <- 10
    DieSides <- 6

  defines type

    //Even and odd, the two calls a player can make.
    Call
      Cho
      Han

  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 bet in mon; unset when the player quits or the input ends first.
      nextPot()
        -> purse as Integer
        <- rtn as Integer: Integer()
        stdout.println(say.askPot(purse))
        quitting <- false
        while not rtn? and not quitting and not input.ended()
          stdout.println(say.prompt())
          reply <- input.next().trim()
          amount <- Integer(reply)
          if reply.upperCase() == say.quit()
            quitting: true
          else if not isAmount(amount)
            stdout.println(say.notANumber())
          else if not canCover(amount, purse)
            stdout.println(say.notEnough())
          else
            rtn: amount

      showCupSlammed()
        stdout.println(say.cupSlammed())

      //Asks until the reply is CHO or HAN; unset only if the input ends first.
      nextCall()
        <- rtn as Call: Call()
        while not rtn? and not input.ended()
          stdout.println(say.prompt())
          rtn: callOf(input.next())
          if not rtn?
            stdout.println(say.notChoOrHan())

      showDice()
        ->
          first as Integer
          second as Integer

        stdout.println(say.liftsCup())
        stdout.println(say.dieNames(japaneseNumber(first), japaneseNumber(second)))
        stdout.println(say.dieNumbers(first, second))

      showWon()
        ->
          pot as Integer
          fee as Integer

        stdout.println(say.won(pot, fee))

      showLost()
        stdout.println(say.lost())

      showOutOfMoney()
        stdout.println(say.outOfMoney())

      showGoodbye()
        stdout.println(say.goodbye())

      ended() as pure
        <- rtn as Boolean: input.ended()

      override operator ? as pure
        <- rtn as Boolean: true

  defines function

    //THE GAME: its rules, with no input or output.

    rollDie()
      -> rng as Random
      <- rtn as Integer: rng.next(1, DieSides)

    //A bet is a whole number of mon, zero or more, as the book's isdecimal() check makes it.
    isAmount() as pure
      -> amount as Integer
      <- rtn as Boolean: false
      if amount?
        rtn: amount >= 0

    canCover() as pure
      ->
        amount as Integer
        purse as Integer
      <- rtn as Boolean: amount <= purse

    //Enumerations parse from their own names and are unset for anything else, so "cho",
    //"CHO" and "Cho" all give Call.Cho and "even" gives an unset Call.
    callOf() as pure
      -> reply as String
      <- rtn as Call: Call(reply.trim().lowerCase().title())

    winningCall() as pure
      -> total as Integer
      <- rtn as Call: total mod 2 == 0 <- Call.Cho : Call.Han

    houseFee() as pure
      -> pot as Integer
      <- rtn as Integer: pot / HouseCutDivisor

    //The dealer names each die in Japanese, as in the book, whatever language the words are in.
    japaneseNumber() as pure
      -> die as Integer
      <- rtn as String: String()
      names <- {1: "ICHI", 2: "NI", 3: "SAN", 4: "SHI", 5: "GO", 6: "ROKU"}
      rtn: names.getOrDefault(die, "")

  defines function

    //THE FLOW, one round of the book's main loop: the dealer rolls, the player calls, and the
    //bet is won or lost. The purse after the round; unchanged if the input ends before the call.
    takeTurn()
      ->
        purse as Integer
        pot as Integer
        rng as Random
        console as Console
      <- rtn as Integer: Integer(purse)

      first <- rollDie(rng)
      second <- rollDie(rng)
      console.showCupSlammed()
      if call <- console.nextCall()
        console.showDice(first, second)
        if call == winningCall(first + second)
          fee <- houseFee(pot)
          console.showWon(pot, fee)
          rtn: purse + pot - fee
        else
          console.showLost()
          rtn: purse - pot

  defines function

    //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.
  defines text for "en"

    Say

      introduction()
        `Cho-Han, by Al Sweigart al@inventwithpython.com

In this traditional Japanese dice game, two dice are rolled in a bamboo
cup by the dealer sitting on the floor. The player must guess if the
dice total to an even (cho) or odd (han) number.
`

      askPot()
        -> purse as Integer
        `You have ${purse} mon. How much do you bet? (or QUIT)`

      prompt()
        "> "

      quit()
        "QUIT"

      notANumber()
        "Please enter a number."

      notEnough()
        "You do not have enough to make that bet."

      cupSlammed()
        `The dealer swirls the cup and you hear the rattle of dice.
The dealer slams the cup on the floor, still covering the
dice and asks for your bet.

     CHO (even) or HAN (odd)?`

      notChoOrHan()
        `Please enter either "CHO" or "HAN".`

      liftsCup()
        "The dealer lifts the cup to reveal:"

      dieNames()
        ->
          first as String
          second as String
        `  ${first} - ${second}`

      dieNumbers()
        ->
          first as Integer
          second as Integer
        `      ${first} - ${second}`

      won()
        ->
          pot as Integer
          fee as Integer
        `You won! You take ${pot} mon.
The house collects a ${fee} mon fee.`

      lost()
        "You lost!"

      outOfMoney()
        "You have run out of money!"

      goodbye()
        "Thanks for playing!"

  defines program

    //Cho-Han (Big Book project 10). Bet whether two dice total even (cho) or odd (han); a win pays
    //the bet less a 10% house fee. The game decides; the Console asks and tells, in the player's
    //words. The golden replays a seeded game (seed.txt).
    ChoHan()
      //A language with no words of its own is unset: ?: falls back to English.
      console <- Console(Say(language()) ?: Say("en"))
      rng <- randomSource()

      console.showIntroduction()
      purse <- StartPurse
      running <- true
      while running and not console.ended()
        if pot <- console.nextPot(purse)
          purse: takeTurn(purse: purse, pot: pot, rng: rng, console: console)
          if purse == 0
            console.showOutOfMoney()
            running: false
        else
          running: false
      //The player quit, or ran out of money: the game says goodbye. At the end of the input it does not.
      console.showGoodbye()

How the port works

The original's main() rolls, asks, pays and words everything in the same lines. The port keeps those apart. The game is a handful of small functions that take values and give back values: rollDie, isAmount and canCover (is the bet a number, and does the purse cover it?), callOf, winningCall and houseFee. Call is an enumeration of Cho and Han, and callOf parses the player's typed reply straight through the enumeration's constructor — Call(reply.trim().lowerCase().title()) — which is unset for anything that isn't one of those two names. That one line replaces a hand-written string-comparison validation loop, and the call is then compared with winningCall's result using == rather than against a string. A Dict literal, {1: "ICHI", 2: "NI", ...}, gives the dealer's Japanese die names; they are data, not words, because the dealer says ICHI whatever language the rest is in.

The Console asks and tells. nextPot returns an unset Integer when the player types QUIT or the input runs out, and the main loop reads it with a guard, if pot <- console.nextPot(purse) — a function hands back "no answer" as a value rather than a separate exit signal. nextCall works the same way for the CHO/HAN prompt. Each asks the game whether the answer will do, and words the reason when it will not. Say holds every word: the banner with its credit line, and the dealer's lines about the cup, are each one multi-line String; a win's two lines are one method, won(pot, fee); even QUIT is a word, so a translation can use its own.

The program is the flow, laid out as the original's main(): bet, roll the two dice, describe the cup, take the call, reveal and pay. One round - from the roll to the payout - is a flow function, takeTurn(purse, pot, rng, console), which hands back the purse after the round, so the purse changes in one place and the program's loop stays three levels deep. The outer loop is driven by a flag, while running and not console.ended(), EK9's replacement for a while True with several different break points: running out of money and quitting both simply set running: false, and after the loop the program says goodbye unless the input has run out. It chooses the words with Say(language()) ?: Say("en"); there is no break, continue or return statement anywhere in the port.