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 18 — Dice Roller

This is project 18 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:

3d6
1d20
2d6+3
d8
5d4-2
hello
QUIT

Run it with:

BIGBOOK_SEED=1 ek9 diceroller.ek9 < input.txt

It prints:

Dice Roller, by Al Sweigart
Enter what dice to roll (e.g. 3d6, 1d10+5, 2d4-1) or QUIT.
> 
Total: 6 (Each die: 1, 4, 1)
> 
Total: 11 (Each die: 11)
> 
Total: 12 (Each die: 3, 6, +3)
> 
Invalid input. Enter something like 3d6 or 1d10+2.
> 
Total: 8 (Each die: 3, 1, 1, 4, 1, -2)
> 
Invalid input. Enter something like 3d6 or 1d10+2.
> 

The program

#!ek9
defines module bigbook.diceroller

  defines constant

    //Dungeons & Dragons notation: count "d" sides, then an optional signed adjustment.
    //At least one die with at least one side, so every spec that parses can be rolled.
    DiceNotation <- /^(?<count>[1-9]\d*)d(?<sides>[1-9]\d*)(?<modifier>[+-]\d+)?$/

  defines type

    List of Integer

  defines record

    //What to roll: "2d6+3" is two six-sided dice plus 3. The modifier is signed.
    DiceSpec
      count as Integer: Integer()
      sides as Integer: Integer()
      modifier as Integer: Integer()

      DiceSpec() as pure
        ->
          count as Integer
          sides as Integer
          modifier as Integer

        this.count :=? count
        this.sides :=? sides
        this.modifier :=? modifier

      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 game reads the entry as dice; unset when the player QUITs or the input ends.
      nextDice()
        <- rtn as DiceSpec: DiceSpec()
        quitting <- false
        while not rtn? and not quitting and not input.ended()
          stdout.println(say.prompt())
          if line <- input.next()
            entry <- line.trim()
            if entry.upperCase() == say.quit()
              quitting: true
            else
              rtn: diceSpecOf(entry)
              if not rtn?
                stdout.println(say.notDice())

      //The game hands back the rolls; the report of them is worded here. The book lists a
      //non-zero adjustment after the dice, with its sign: "3, 6, +3".
      showRoll()
        ->
          spec as DiceSpec
          rolls as List of Integer

        shown <- cat rolls | map with asText | collect as List of String
        if spec.modifier <> 0
          shown += signed(spec.modifier)
        eachDie <- cat shown | join with withComma | collect as String
        stdout.println(say.rolled(totalOf(spec, rolls), eachDie))

      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.

    //A DiceSpec, or an unset one when the text is not dice notation. Spaces are ignored and
    //"D" reads as "d", as in the book.
    diceSpecOf() as pure
      -> entry as String
      <- rtn as DiceSpec: DiceSpec()
      notation <- entry.lowerCase().replace(/\s/, "")
      count <- Integer(notation.group(DiceNotation, "count"))
      sides <- Integer(notation.group(DiceNotation, "sides"))
      if count? and sides?
        modifier <- Integer(notation.group(DiceNotation, "modifier"))
        rtn: DiceSpec(count, sides, modifier? <- modifier : 0)

    roll()
      ->
        spec as DiceSpec
        rng as Random
      <- rtn as List of Integer: List() of Integer

      for die in 1 ... spec.count
        rtn += rng.next(1, spec.sides)

    //Collecting Integers into one Integer adds them up; the adjustment is signed.
    totalOf() as pure
      ->
        spec as DiceSpec
        rolls as List of Integer
      <- rtn as Integer: Integer()

      sum <- cat rolls | collect as Integer
      rtn: sum + spec.modifier

  defines function

    //The Console's number formatting: no words in it.

    asText() as pure
      -> number as Integer
      <- rtn as String: $number

    signed() as pure
      -> number as Integer
      <- rtn as String: number > 0 <- `+${number}` : $number

    withComma() 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 or types. The rolls are not here: the game rolls them
  //and the Console lists them.
  defines text for "en"

    Say

      introduction()
        `Dice Roller, by Al Sweigart
Enter what dice to roll (e.g. 3d6, 1d10+5, 2d4-1) or QUIT.`

      prompt()
        "> "

      quit()
        "QUIT"

      notDice()
        "Invalid input. Enter something like 3d6 or 1d10+2."

      rolled()
        ->
          total as Integer
          eachDie as String
        `Total: ${total} (Each die: ${eachDie})`

  defines program

    //Dice Roller (Big Book project 18). Rolls dice written in D&D notation until QUIT. The game
    //decides; the Console asks and tells, in the player's words. The golden replays a seeded game
    //(seed.txt).
    DiceRoller()
      //A language with no words of its own is unset: ?: falls back to English.
      console <- Console(Say(language()) ?: Say("en"))
      rng <- randomSource()

      console.showIntroduction()
      running <- true
      while running and not console.ended()
        if spec <- console.nextDice()
          console.showRoll(spec, roll(spec, rng))
        else
          running: false

About this port