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 75 — Three-Card Monte

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

MIDDLE

Run it with:

BIGBOOK_SEED=4 ek9 threecardmonte.ek9 < input.txt

It prints:

Three-Card Monte, by Al Sweigart al@inventwithpython.com

Find the red lady (the Queen of Hearts)! Keep an eye on how
the cards move.

Here are the cards:
 ___   ___   ___  
|Q  | |10 | |7  | 
| ♥ | | ♦ | | ♣ | 
|__Q| |_10| |__7| 

swapping right and left...
swapping middle and left...
swapping middle and right...
swapping left and right...
swapping middle and left...
swapping middle and left...
swapping right and left...
swapping right and middle...
swapping right and middle...
swapping right and middle...
swapping middle and right...
swapping right and middle...
swapping middle and right...
swapping left and right...
swapping left and right...
swapping middle and right...

Which card has the Queen of Hearts? (LEFT MIDDLE RIGHT)
 ___   ___   ___  
|10 | |7  | |Q  | 
| ♦ | | ♣ | | ♥ | 
|_10| |__7| |__Q| 

You lost!
Thanks for playing, sucker!

The program

#!ek9
defines module bigbook.threecardmonte

  defines constant

    NumberOfSwaps <- 16

  defines type

    //Rank and suit are closed sets, as in Blackjack: a card is a record of the two, so there
    //is no "rank|suit" string to split at every use and no "Q|♥" string to compare against.
    Suit
      Hearts
      Diamonds
      Spades
      Clubs

    Rank
      Two
      Three
      Four
      Five
      Six
      Seven
      Eight
      Nine
      Ten
      Jack
      Queen
      King
      Ace

    //The three places on the table. The player's answer parses into one of these names; the
    //Console words each one for the swap messages.
    Position
      Left
      Middle
      Right

    List of Rank
    List of Suit
    List of Position
    List of Swap
    Dict of (Rank, String)
    Dict of (Position, Card)
    DictEntry of (Position, Card)

  defines record

    //Fields default UNSET: Card() is "no card yet", which is what getRandomCard loops on.
    Card
      rank as Rank: Rank()
      suit as Suit: Suit()

      default Card() as pure

      Card() as pure
        ->
          cardRank as Rank
          cardSuit as Suit

        rank :=? cardRank
        suit :=? cardSuit

      default operator ?
      default operator ==

    //One move of the dealer's hands: the cards at two positions change places.
    Swap
      first as Position: Position()
      second as Position: Position()

      default Swap() as pure

      Swap() as pure
        ->
          firstPosition as Position
          secondPosition as Position

        first :=? firstPosition
        second :=? secondPosition

      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())

      showDealt()
        -> table as Dict of (Position, Card)
        stdout.println(say.hereAreTheCards())
        showCards(table)

      showSwap()
        -> swap as Swap
        stdout.println(say.swapping(positionWord(swap.first), positionWord(swap.second)))

      //Asks until the answer is one of the three positions; unset only if the input ends first.
      nextGuess()
        <- rtn as Position: Position()
        stdout.println("")
        while not rtn? and not input.ended()
          stdout.println(say.askGuess())
          rtn: positionOf(input.next())

      //The cards are drawn, not worded: rows built side by side from the table, so the cards
      //print next to each other. The bottom row pads with '_' on the left, as the book's
      //rjust(2, '_') does. The book keeps FIVE rows and fills four, so its cards end with an
      //empty fifth row: a blank line, printed here too.
      showCards()
        -> table as Dict of (Position, Card)
        labels <- rankLabels()
        top <- ""
        rankRow <- ""
        suitRow <- ""
        bottom <- ""
        for position in Position
          card <- table.getOrDefault(position, Card())
          label <- labels.getOrDefault(card.rank, "")
          top += " ___  "
          rankRow += `|${label.rightPadded(2)} | `
          suitRow += `| ${suitGlyph(card.suit)} | `
          bottom += `|_${label.leftPadded(2, '_')}| `
        stdout.println(top)
        stdout.println(rankRow)
        stdout.println(suitRow)
        stdout.println(bottom)
        stdout.println("")

      showOutcome()
        -> won as Boolean
        stdout.println(won <- say.won() : say.lost())

      private positionWord()
        -> position as Position
        <- rtn as String: position ==
        Position.Left <- say.left() : position == Position.Middle <- say.middle() : say.right()

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

  defines function

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

    queenOfHearts() as pure
      <- rtn as Card: Card(Rank.Queen, Suit.Hearts)

    //The marks a card shows are the game's data, not words: they are drawn, and do not translate.
    rankLabels() as pure
      <- rtn as Dict of (Rank, String): {
        Rank.Two: "2", Rank.Three: "3", Rank.Four: "4", Rank.Five: "5",
        Rank.Six: "6", Rank.Seven: "7", Rank.Eight: "8", Rank.Nine: "9",
        Rank.Ten: "10", Rank.Jack: "J", Rank.Queen: "Q", Rank.King: "K",
        Rank.Ace: "A"
      }

    suitGlyph() as pure
      -> suit as Suit
      <- rtn as String: ""
      switch suit
        case Suit.Hearts
          rtn: "♥"
        case Suit.Diamonds
          rtn: "♦"
        case Suit.Spades
          rtn: "♠"
        case Suit.Clubs
          rtn: "♣"
        default
          rtn: ""

    //The book's six swaps, in the book's order: the pick is an index into this list, so the
    //order is part of what a seeded game replays.
    allSwaps() as pure
      <- rtn as List of Swap: [
        Swap(Position.Left, Position.Middle),
        Swap(Position.Middle, Position.Right),
        Swap(Position.Left, Position.Right),
        Swap(Position.Middle, Position.Left),
        Swap(Position.Right, Position.Middle),
        Swap(Position.Right, Position.Left)
      ]

    randomSwap()
      -> rng as Random
      <- rtn as Swap: Swap()
      swaps <- allSwaps()
      rtn: swaps.getOrDefault(rng.next(0, length swaps - 1), Swap())

    //A new table with the two cards changed over; the table passed in is left as it was.
    swapped() as pure
      ->
        table as Dict of (Position, Card)
        swap as Swap
      <- rtn as Dict of (Position, Card): Dict() of (Position, Card)

      atFirst <- table.getOrDefault(swap.first, Card())
      atSecond <- table.getOrDefault(swap.second, Card())
      rtn: table + DictEntry(swap.first, atSecond) + DictEntry(swap.second, atFirst)

    //Enumeration names parse exactly ("Middle"), so the reply is put in that case first.
    //Anything else - including the end of the input - parses to an UNSET Position.
    positionOf() as pure
      -> reply as String
      <- rtn as Position: Position(reply.trim().lowerCase().title())

    isQueenAt() as pure
      ->
        table as Dict of (Position, Card)
        guess as Position
      <- rtn as Boolean: table.getOrDefault(guess, Card()) == queenOfHearts()

    //Faithful to the book's code rather than to its comment: the comment says "not the
    //Queen of Hearts", but the test rejects every Queen and every heart. Kept, so the cards
    //dealt are the ones the book's program deals.
    getRandomCard()
      -> rng as Random
      <- rtn as Card: Card()
      ranks <- cat Rank | collect as List of Rank
      suits <- cat Suit | collect as List of Suit
      while not rtn?
        rank <- ranks.getOrDefault(rng.next(0, length ranks - 1), Rank())
        suit <- suits.getOrDefault(rng.next(0, length suits - 1), Suit())
        if rank <> Rank.Queen and suit <> Suit.Hearts
          rtn: Card(rank, suit)

    //The Queen of Hearts goes to a random position and the two other cards fill the rest, left
    //to right. An Iterator's next() is only allowed once hasNext() has been checked (E08030).
    dealTable()
      -> rng as Random
      <- rtn as Dict of (Position, Card): Dict() of (Position, Card)
      otherCards <- [getRandomCard(rng), getRandomCard(rng)]
      others <- otherCards.iterator()
      positions <- cat Position | collect as List of Position
      queenAt <- positions.getOrDefault(rng.next(0, length positions - 1), Position())
      for position in Position
        if position == queenAt
          rtn += DictEntry(position, queenOfHearts())
        else if others.hasNext()
          rtn += DictEntry(position, others.next())

  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. The cards' ranks and suits are not here: they are the
  //game's marks, and the Console draws them.
  defines text for "en"

    Say

      introduction()
        `Three-Card Monte, by Al Sweigart al@inventwithpython.com

Find the red lady (the Queen of Hearts)! Keep an eye on how
the cards move.
`

      hereAreTheCards()
        "Here are the cards:"

      left()
        "left"

      middle()
        "middle"

      right()
        "right"

      swapping()
        ->
          fromPosition as String
          toPosition as String
        `swapping ${fromPosition} and ${toPosition}...`

      askGuess()
        "Which card has the Queen of Hearts? (LEFT MIDDLE RIGHT)"

      won()
        `You won!
Thanks for playing!`

      lost()
        `You lost!
Thanks for playing, sucker!`

  defines program

    //Three-Card Monte (Big Book project 75). Follow the Queen of Hearts through the swaps, then
    //guess where she is. The game decides; the Console asks and tells, in the player's words. The
    //golden replays a seeded game (seed.txt).
    ThreeCardMonte()
      //A language with no words of its own is unset: ?: falls back to English.
      console <- Console(Say(language()) ?: Say("en"))
      rng <- randomSource()

      console.showIntroduction()
      table <- dealTable(rng)
      console.showDealt(table)

      for swapNumber in 1 ... NumberOfSwaps
        swap <- randomSwap(rng)
        console.showSwap(swap)
        table: swapped(table, swap)

      if guess <- console.nextGuess()
        console.showCards(table)
        console.showOutcome(isQueenAt(table, guess))

About this port