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 3 — Bitmap Message

Bitmap Message asks for a short message, then draws a small picture out of it. The picture is really a grid of filled and blank cells; each filled cell shows one letter of the message, cycling back to its start whenever it runs out, while every blank cell stays blank. Read down the aligned columns and the message appears to flow through the shape rather than just sit underneath it.

This port draws an original small heart rather than the world map the original program uses, so that only the algorithm — never the original's picture — is reproduced here (see SOURCE.md).

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

Nothing here is random, so there is no seed. The one line of input.txt is the message to draw:

Hello!
ek9 bitmap-message.ek9 < input.txt

The typed message is never echoed back as a line of its own; it only appears woven through the bitmap below:

Bitmap Message, by Al Sweigart al@inventwithpython.com
Enter the message to display with the bitmap:
 el   He 
Hello!Hel
Hello!Hel
 ello!He 
  llo!H  
   lo!   
    o    

The program

#!ek9
defines module bigbook.bitmap

  defines constant

    Blank <- ' '

  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 message to draw; unset when the player types nothing or the input ends.
      nextMessage()
        <- rtn as String: String()
        stdout.println(say.askMessage())
        if entry <- input.next() then isMessage(entry)
          rtn: entry

      //The drawing is computed, not worded: each line is shown as the game made it.
      showDrawing()
        -> lines as List of String
        for line in lines
          stdout.println(line)

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

  defines function

    //THE GAME: the mask and the drawing, with no input or output.

    //The mask the message is drawn through: a filled cell shows a letter of the message, a
    //blank cell stays blank. It is data, not words - an original small heart, not the book's
    //world map.
    heartMask() as pure
      <- rtn as List of String: [
        " ##   ## ",
        "#########",
        "#########",
        " ####### ",
        "  #####  ",
        "   ###   ",
        "    #    "
      ]

    //The book exits on an empty message: there is nothing to draw with.
    isMessage() as pure
      -> entry as String
      <- rtn as Boolean: not entry.empty()

    //EK9 String has no subscript; the character at an index is the first of the tail from it.
    charAt() as pure
      ->
        text as String
        index as Integer
      <- rtn as Character: text.subString(index).first()

    //What one column of a mask line shows, so a stream can map the columns through it.
    Pixel() as pure abstract
      -> column as Integer
      <- rtn as Character?

    //The book picks the message character by COLUMN (message[i % len], restarting on every
    //line), so a message reads down the aligned columns; blank cells stay blank.
    overlaid() as pure
      ->
        line as String
        message as String
      <- rtn as String?

      pixel <- (line, message) is Pixel as pure function
        rtn: charAt(line, column) == Blank <- Blank : charAt(message, column mod length message)
      rtn: for column in 0 ... length line - 1 | map with pixel | collect as String

    //The message drawn through the whole mask, one String per line of it.
    drawing()
      -> message as String
      <- rtn as List of String: List() of String
      for line in heartMask()
        rtn += overlaid(line, message)

  defines function

    //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()
        "Bitmap Message, by Al Sweigart al@inventwithpython.com"

      askMessage()
        "Enter the message to display with the bitmap:"

  defines program

    //Bitmap Message (Big Book project 3). Draws the heart mask out of the letters of a typed
    //message. The game decides; the Console asks and tells, in the player's words.
    BitmapMessage()
      //A language with no words of its own is unset: ?: falls back to English.
      console <- Console(Say(language()) ?: Say("en"))

      console.showIntroduction()
      if message <- console.nextMessage()
        console.showDrawing(drawing(message))

How the port works

The original's main() asks, draws and prints in the same few lines. The port keeps the game, its input and output, and its words apart: