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
- Behaviour: reads dice in Dungeons & Dragons notation (
3d6,1d10+2,2d38-1) until QUIT and prints the total, each die, and any adjustment with its sign:Total: 12 (Each die: 3, 6, +3). Spaces are ignored andDreads asd. Anything else, including a spec with no dice count such asd8, is "Invalid input". - Idioms:
- The book finds the
d,+and-by index and slices between them. Here one regular expression with named groups (count,sides,modifier) parses the whole spec into aDiceSpecrecord, anddiceSpecOfreturns an unsetDiceSpecwhen the text is not dice notation, so the loop guards on it (else if spec <- diceSpecOf(entry)). - The modifier is one signed
Integer(absent means 0), so the total istotal + spec.modifierwith no sign flag. - Streams sum the rolls (
collect as Integer) and join them (join with withComma). - Every input loop also stops at the end of input (
LineInput).
- The book finds the
- Divergences: the long intro is shortened; the book's second line naming the reason for
invalid input is not printed; a count or side count of 0 is rejected by the notation itself
(the book accepts
0d6and fails on1d0).