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 59 — Rock Paper Scissors

This is project 59 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=5. The player's answers are piped in:

r
p
x
s
q

Run it with:

BIGBOOK_SEED=5 ek9 rockpaperscissors.ek9 < input.txt

It prints:

Rock, Paper, Scissors, by Al Sweigart al@inventwithpython.com
- Rock beats scissors.
- Paper beats rocks.
- Scissors beats paper.

0 Wins, 0 Losses, 0 Ties
Enter your move: (R)ock (P)aper (S)cissors or (Q)uit
ROCK versus...
1...
2...
3...
SCISSORS
You win!
1 Wins, 0 Losses, 0 Ties
Enter your move: (R)ock (P)aper (S)cissors or (Q)uit
PAPER versus...
1...
2...
3...
SCISSORS
You lose!
1 Wins, 1 Losses, 0 Ties
Enter your move: (R)ock (P)aper (S)cissors or (Q)uit
Type one of R, P, S, or Q.
1 Wins, 1 Losses, 0 Ties
Enter your move: (R)ock (P)aper (S)cissors or (Q)uit
SCISSORS versus...
1...
2...
3...
SCISSORS
It's a tie!
1 Wins, 1 Losses, 1 Ties
Enter your move: (R)ock (P)aper (S)cissors or (Q)uit
Thanks for playing!

The program

#!ek9
defines module bigbook.rockpaperscissors

  defines constant

    //The book's pauses for suspense: one before the count, one between counts, one after the reveal.
    RevealPause <- 500ms
    CountPause <- 250ms
    CountTo <- 3

  defines type

    //A closed set of moves is an enumeration: iterable in this order, and `$` gives the name.
    Move
      Rock
      Paper
      Scissors

    Outcome
      Win
      Lose
      Tie

  defines class

    //THE GAME's state: the score so far, how many games the player has won, lost and tied.
    Tally
      wins as Integer: 0
      losses as Integer: 0
      ties as Integer: 0

      record()
        -> outcome as Outcome
        if outcome == Outcome.Win
          wins += 1
        else if outcome == Outcome.Lose
          losses += 1
        else if outcome == Outcome.Tie
          ties += 1

      //Copies, because `+= 1` changes a count in place.
      winCount() as pure
        <- rtn as Integer: Integer(wins)

      lossCount() as pure
        <- rtn as Integer: Integer(losses)

      tieCount() as pure
        <- rtn as Integer: Integer(ties)

      default operator ?

    //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.
    //It also keeps the book's pauses: they are how the game is shown, not what it decides.
    Console
      stdout as Stdout: Stdout()
      input as LineInput: LineInput()
      os as OS: OS()
      say as Say: Say("en")

      Console()
        -> words as Say
        say: words

      showIntroduction()
        stdout.println(say.introduction())

      //The player's next move. Unset when they quit or the input ends: either way there is no move.
      //The score is shown before every prompt, so a rejected letter shows it again, as in the book.
      nextMove()
        -> tally as Tally
        <- rtn as Move: Move()
        quitting <- false
        while not rtn? and not quitting and not input.ended()
          stdout.println(say.score(tally.winCount(), tally.lossCount(), tally.tieCount()))
          stdout.println(say.askMove())
          if entry <- input.next()
            letter <- entry.lowerCase()
            if letter == say.quit()
              quitting: true
            else
              rtn: moveOf(letter)
              if not rtn?
                stdout.println(say.notAMove())

      //Count to three with dramatic pauses. os.delay prints nothing, so the golden is the same
      //with or without them.
      showCountdown()
        -> move as Move
        stdout.println(say.versus(moveWord(move)))
        os.delay(RevealPause)
        for count in 1 ... CountTo
          stdout.println(say.counted(count))
          os.delay(CountPause)

      showComputerMove()
        -> move as Move
        stdout.println(moveWord(move))
        os.delay(RevealPause)

      showOutcome()
        -> outcome as Outcome
        stdout.println(outcome == Outcome.Win <- say.won() : outcome == Outcome.Lose <- say.lost() : say.tied())

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

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

      private moveWord()
        -> move as Move
        <- rtn as String: move == Move.Rock <- say.rock() : move == Move.Paper <- say.paper() : say.scissors()

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

  defines function

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

    //A letter the player typed, as a Move; anything else is an UNSET Move, which the caller
    //tests with `?` instead of comparing strings.
    moveOf() as pure
      -> letter as String
      <- rtn as Move: letter == "r" <- Move.Rock : letter == "p" <- Move.Paper : letter == "s" <- Move.Scissors : Move()

    //The computer draws from the moves the type declares, so the choices cannot drift from it.
    computerMove()
      -> rng as Random
      <- rtn as Move: Move()
      moves <- cat Move | collect as List of Move
      rtn: moves.getOrDefault(rng.next(0, length moves - 1), Move())

    //The rules of the game, in one place: which move each move beats.
    beaten() as pure
      -> move as Move
      <- rtn as Move?
      switch move
        case Move.Rock
          rtn: Move.Scissors
        case Move.Paper
          rtn: Move.Rock
        case Move.Scissors
          rtn: Move.Paper
        default
          rtn: Move()

    outcomeOf() as pure
      ->
        player as Move
        computer as Move
      <- rtn as Outcome: player == computer <- Outcome.Tie : beaten(player) == computer <- Outcome.Win : Outcome.Lose

  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()
        `Rock, Paper, Scissors, by Al Sweigart al@inventwithpython.com
- Rock beats scissors.
- Paper beats rocks.
- Scissors beats paper.
`

      score()
        ->
          wins as Integer
          losses as Integer
          ties as Integer
        `${wins} Wins, ${losses} Losses, ${ties} Ties`

      askMove()
        "Enter your move: (R)ock (P)aper (S)cissors or (Q)uit"

      quit()
        "q"

      notAMove()
        "Type one of R, P, S, or Q."

      //The book prints moves in capitals.
      rock()
        "ROCK"

      paper()
        "PAPER"

      scissors()
        "SCISSORS"

      versus()
        -> moveName as String
        `${moveName} versus...`

      counted()
        -> count as Integer
        `${count}...`

      won()
        "You win!"

      lost()
        "You lose!"

      tied()
        "It's a tie!"

      goodbye()
        "Thanks for playing!"

  defines program

    //Rock Paper Scissors (Big Book project 59). Play rock, paper, scissors against the computer,
    //which chooses at random. The game decides; the Console asks and tells, in the player's
    //words. The golden replays a seeded game (seed.txt).
    RockPaperScissors()
      //A language with no words of its own is unset: ?: falls back to English.
      console <- Console(Say(language()) ?: Say("en"))
      rng <- randomSource()

      console.showIntroduction()
      tally <- Tally()
      running <- true
      while running and not console.ended()
        if move <- console.nextMove(tally)
          console.showCountdown(move)
          computer <- computerMove(rng)
          console.showComputerMove(computer)
          outcome <- outcomeOf(move, computer)
          console.showOutcome(outcome)
          tally.record(outcome)
        else
          running: false
      console.showGoodbye()

About this port