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:
- The game draws. The heart is a mask:
heartMask()is a list of lines in which a#cell is filled and a space is blank. It is data the message is overlaid on, not words — a different picture is a different list, and nothing else changes.drawing(message)overlays the message on every line of the mask and gives back the drawn lines, andisMessagesays whether a typed entry is something to draw with (the original stops on an empty one). - EK9's
Stringhas no subscript operator.charAtreads one character by taking the tail from a position and keeping only its first character:text.subString(index).first(). Pixelis declaredas pure abstract— a function type, not a function body — that says what one column of a line produces: aCharacter(or unset, for a blank cell).overlaidthen builds a value of that type as a dynamic function, capturing the currentlineandmessage:(line, message) is Pixel as pure function. The column logic is written once, as data, and handed to a stream.- Drawing one line is a single pipeline:
for column in 0 ... length line - 1 | map with pixel | collect as String. There is no index variable to increment and no accumulator string to build up by hand — the range is the column numbers, andmapapplies the pixel function to each. - The
Consoleasks and shows.nextMessage()prints the prompt, reads one line and asks the game,isMessage(entry), whether it will do. It is unset for an empty line and for input that ends before anything is typed, so the program's one guard,if message <- console.nextMessage(), covers both ways the original has nothing to draw.showDrawingprints the lines as the game made them: a drawing is computed, so it is not text. Sayholds the words — here only the title and the prompt. The program chooses them withSay(language()) ?: Say("en"), as every port in this book does, so a language with no words of its own falls back to English.- Because
overlaidandpixelare bothas pure, the picture-building code is provably free of side effects — nothing in it can reach out and touch the console, a file, or a shared variable. The only printing is the Console's.