Project P037

Pixel paint

A 12 x 24 canvas of characters: paint single cells, Bresenham lines and boxes with the pen's color, fill an area with the paint bucket, switch pens (one of them erases), and undo as far back as you like. The canvas is a dictionary keyed by (row, column) tuples.

3 modules · 2 recorded sessionstext-menu UI in the terminalupdated 2026-10-10

Every screen below was recorded under CPython. When this page was built, the EML interpreter replayed each session from the same input and printed the same bytes.

About

A canvas of 12 rows and 24 columns, drawn in characters. Paint single cells, lines and boxes with the pen's color, fill an area with the paint bucket, change the pen - one of the pens paints the background back, like an eraser - and undo as far back as you like. The canvas is shown after every change.

  • main.eml - the menu, the questions and their checks, the pen, and the undo history
  • canvas.eml - the canvas, painting cells with a record of what changed, undoing such a record, and the canvas on screen
  • shapes.eml - which cells a line, a box or a fill covers

How each part works:

  • The canvas is a dictionary keyed by (row, column) tuples, as in the corpus case grid-coordinate-map: a cell never painted is not in it and shows the background. Painting returns the changes - each cell whose color really changed, with its old and new color - and undo puts the old colors back, last change first. A step that changes nothing is not recorded.
  • A line is Bresenham's: whole numbers only, an error term that tracks how far the drawn cells are from the true line, one step at a time along the longer direction and, when the error says so, along the shorter one too. Every cell it draws lies within half a cell of the true line, and both ends are drawn.
  • A fill spreads from the cell picked over every cell of the same color that can be reached by moving up, down, left and right - so a diagonal line is a wall it does not cross. The corpus case flood-fill recurses into the four neighbours; here the cells still to visit wait on a list, and a cell is marked when it joins the list, so none is taken twice.
  • A box is the outline of the rectangle with the two corners given; with both corners in one row or column it is a line, with one corner twice a single cell.

What is checked: a cell as a row from 1 to 12 and a column from 1 to 24, typed as 3 5 or 3,5; a pen of exactly one of # o + * ~ .. An empty answer cancels.

Sessions: sessions/basic.in draws a box and a diagonal line across it, fills the part above the line with o and the part below with + (33 cells each - the line keeps them apart), draws a long line and a short diagonal that crosses it with *, undoes both, erases a corner of the box with the . pen and fills the whole background with ~ (163 cells). sessions/bad-input.in gives menu choices 0 and x, an undo with nothing to undo, cells 0 5, 13 1, 1 25, a b and 3, empty answers at each question, pens x and ##, a cell painted twice, a box with one corner twice and a box in one row, a fill on cells already that color, an erasing fill, and undoes everything, one step past the start.

Built on the verified corpus cases flood-fill (the paint bucket: spread to the four neighbours while they hold the color being replaced) and grid-coordinate-map (a sparse grid in a dictionary keyed by row and column tuples).

Recorded sessions

What the screen shows while someone uses the program. Each typed line appears after its prompt, the way a terminal shows it.

bad-input

interpreter: byte-equal
== Pixel paint ==
A 12 x 24 canvas. Rows and columns count from 1; type a cell as 3 5.

1) dot  2) line  3) box  4) fill  5) pen  6) undo  7) show  8) quit
choice> 0
Pick a number from 1 to 8.

1) dot  2) line  3) box  4) fill  5) pen  6) undo  7) show  8) quit
choice> x
Pick a number from 1 to 8.

1) dot  2) line  3) box  4) fill  5) pen  6) undo  7) show  8) quit
choice> 6
Nothing to undo.

1) dot  2) line  3) box  4) fill  5) pen  6) undo  7) show  8) quit
choice> 1
cell> 0 5
Type a row from 1 to 12 and a column from 1 to 24, like 3 5.
cell> 13 1
Type a row from 1 to 12 and a column from 1 to 24, like 3 5.
cell> 1 25
Type a row from 1 to 12 and a column from 1 to 24, like 3 5.
cell> a b
Type a row from 1 to 12 and a column from 1 to 24, like 3 5.
cell> 3
Type a row from 1 to 12 and a column from 1 to 24, like 3 5.
cell> 
Cancelled.

1) dot  2) line  3) box  4) fill  5) pen  6) undo  7) show  8) quit
choice> 2
from> 1 1
to> 
Cancelled.

1) dot  2) line  3) box  4) fill  5) pen  6) undo  7) show  8) quit
choice> 3
from> 
Cancelled.

1) dot  2) line  3) box  4) fill  5) pen  6) undo  7) show  8) quit
choice> 4
start> 
Cancelled.

1) dot  2) line  3) box  4) fill  5) pen  6) undo  7) show  8) quit
choice> 5
pen (# o + * ~, or . to erase)> x
Type one character: # o + * ~, or . to erase.
pen (# o + * ~, or . to erase)> ##
Type one character: # o + * ~, or . to erase.
pen (# o + * ~, or . to erase)> 
Cancelled.

1) dot  2) line  3) box  4) fill  5) pen  6) undo  7) show  8) quit
choice> 1
cell> 1 1
dot: 1 cell, 1 changed.
Pen #, 1 cell painted
    123456789012345678901234
  1 #.......................
  2 ........................
  3 ........................
  4 ........................
  5 ........................
  6 ........................
  7 ........................
  8 ........................
  9 ........................
 10 ........................
 11 ........................
 12 ........................

1) dot  2) line  3) box  4) fill  5) pen  6) undo  7) show  8) quit
choice> 1
cell> 1,1
Nothing changed: 1 cell, all already #.

1) dot  2) line  3) box  4) fill  5) pen  6) undo  7) show  8) quit
choice> 3
from> 5 5
to> 5 5
box: 1 cell, 1 changed.
Pen #, 2 cells painted
    123456789012345678901234
  1 #.......................
  2 ........................
  3 ........................
  4 ........................
  5 ....#...................
  6 ........................
  7 ........................
  8 ........................
  9 ........................
 10 ........................
 11 ........................
 12 ........................

1) dot  2) line  3) box  4) fill  5) pen  6) undo  7) show  8) quit
choice> 3
from> 7 9
to> 7 3
box: 7 cells, 7 changed.
Pen #, 9 cells painted
    123456789012345678901234
  1 #.......................
  2 ........................
  3 ........................
  4 ........................
  5 ....#...................
  6 ........................
  7 ..#######...............
  8 ........................
  9 ........................
 10 ........................
 11 ........................
 12 ........................

1) dot  2) line  3) box  4) fill  5) pen  6) undo  7) show  8) quit
choice> 4
start> 7 5
Nothing changed: 7 cells, all already #.

1) dot  2) line  3) box  4) fill  5) pen  6) undo  7) show  8) quit
choice> 5
pen (# o + * ~, or . to erase)> .
The pen is now . - it paints the background back, like an eraser.

1) dot  2) line  3) box  4) fill  5) pen  6) undo  7) show  8) quit
choice> 4
start> 1 1
fill: 1 cell, 1 changed.
Pen ., 8 cells painted
    123456789012345678901234
  1 ........................
  2 ........................
  3 ........................
  4 ........................
  5 ....#...................
  6 ........................
  7 ..#######...............
  8 ........................
  9 ........................
 10 ........................
 11 ........................
 12 ........................

1) dot  2) line  3) box  4) fill  5) pen  6) undo  7) show  8) quit
choice> 6
Undid the fill: 1 cell put back.
Pen ., 9 cells painted
    123456789012345678901234
  1 #.......................
  2 ........................
  3 ........................
  4 ........................
  5 ....#...................
  6 ........................
  7 ..#######...............
  8 ........................
  9 ........................
 10 ........................
 11 ........................
 12 ........................

1) dot  2) line  3) box  4) fill  5) pen  6) undo  7) show  8) quit
choice> 6
Undid the box: 7 cells put back.
Pen ., 2 cells painted
    123456789012345678901234
  1 #.......................
  2 ........................
  3 ........................
  4 ........................
  5 ....#...................
  6 ........................
  7 ........................
  8 ........................
  9 ........................
 10 ........................
 11 ........................
 12 ........................

1) dot  2) line  3) box  4) fill  5) pen  6) undo  7) show  8) quit
choice> 6
Undid the box: 1 cell put back.
Pen ., 1 cell painted
    123456789012345678901234
  1 #.......................
  2 ........................
  3 ........................
  4 ........................
  5 ........................
  6 ........................
  7 ........................
  8 ........................
  9 ........................
 10 ........................
 11 ........................
 12 ........................

1) dot  2) line  3) box  4) fill  5) pen  6) undo  7) show  8) quit
choice> 6
Undid the dot: 1 cell put back.
Pen ., 0 cells painted
    123456789012345678901234
  1 ........................
  2 ........................
  3 ........................
  4 ........................
  5 ........................
  6 ........................
  7 ........................
  8 ........................
  9 ........................
 10 ........................
 11 ........................
 12 ........................

1) dot  2) line  3) box  4) fill  5) pen  6) undo  7) show  8) quit
choice> 6
Nothing to undo.

1) dot  2) line  3) box  4) fill  5) pen  6) undo  7) show  8) quit
choice> 7
Pen ., 0 cells painted
    123456789012345678901234
  1 ........................
  2 ........................
  3 ........................
  4 ........................
  5 ........................
  6 ........................
  7 ........................
  8 ........................
  9 ........................
 10 ........................
 11 ........................
 12 ........................

1) dot  2) line  3) box  4) fill  5) pen  6) undo  7) show  8) quit
choice> 8
Bye.
What was typed (44 lines)
0
x
6
1
0 5
13 1
1 25
a b
3

2
1 1

3

4

5
x
##

1
1 1
1
1,1
3
5 5
5 5
3
7 9
7 3
4
7 5
5
.
4
1 1
6
6
6
6
6
7
8

basic

interpreter: byte-equal
== Pixel paint ==
A 12 x 24 canvas. Rows and columns count from 1; type a cell as 3 5.

1) dot  2) line  3) box  4) fill  5) pen  6) undo  7) show  8) quit
choice> 3
from> 2 3
to> 8 20
box: 46 cells, 46 changed.
Pen #, 46 cells painted
    123456789012345678901234
  1 ........................
  2 ..##################....
  3 ..#................#....
  4 ..#................#....
  5 ..#................#....
  6 ..#................#....
  7 ..#................#....
  8 ..##################....
  9 ........................
 10 ........................
 11 ........................
 12 ........................

1) dot  2) line  3) box  4) fill  5) pen  6) undo  7) show  8) quit
choice> 2
from> 3 5
to> 7 18
line: 14 cells, 14 changed.
Pen #, 60 cells painted
    123456789012345678901234
  1 ........................
  2 ..##################....
  3 ..#.##.............#....
  4 ..#...###..........#....
  5 ..#......####......#....
  6 ..#..........###...#....
  7 ..#.............##.#....
  8 ..##################....
  9 ........................
 10 ........................
 11 ........................
 12 ........................

1) dot  2) line  3) box  4) fill  5) pen  6) undo  7) show  8) quit
choice> 5
pen (# o + * ~, or . to erase)> o
The pen is now o.

1) dot  2) line  3) box  4) fill  5) pen  6) undo  7) show  8) quit
choice> 4
start> 4 10
fill: 33 cells, 33 changed.
Pen o, 93 cells painted
    123456789012345678901234
  1 ........................
  2 ..##################....
  3 ..#.##ooooooooooooo#....
  4 ..#...###oooooooooo#....
  5 ..#......####oooooo#....
  6 ..#..........###ooo#....
  7 ..#.............##o#....
  8 ..##################....
  9 ........................
 10 ........................
 11 ........................
 12 ........................

1) dot  2) line  3) box  4) fill  5) pen  6) undo  7) show  8) quit
choice> 5
pen (# o + * ~, or . to erase)> +
The pen is now +.

1) dot  2) line  3) box  4) fill  5) pen  6) undo  7) show  8) quit
choice> 4
start> 3 4
fill: 33 cells, 33 changed.
Pen +, 126 cells painted
    123456789012345678901234
  1 ........................
  2 ..##################....
  3 ..#+##ooooooooooooo#....
  4 ..#+++###oooooooooo#....
  5 ..#++++++####oooooo#....
  6 ..#++++++++++###ooo#....
  7 ..#+++++++++++++##o#....
  8 ..##################....
  9 ........................
 10 ........................
 11 ........................
 12 ........................

1) dot  2) line  3) box  4) fill  5) pen  6) undo  7) show  8) quit
choice> 5
pen (# o + * ~, or . to erase)> *
The pen is now *.

1) dot  2) line  3) box  4) fill  5) pen  6) undo  7) show  8) quit
choice> 2
from> 10 2
to> 10 23
line: 22 cells, 22 changed.
Pen *, 148 cells painted
    123456789012345678901234
  1 ........................
  2 ..##################....
  3 ..#+##ooooooooooooo#....
  4 ..#+++###oooooooooo#....
  5 ..#++++++####oooooo#....
  6 ..#++++++++++###ooo#....
  7 ..#+++++++++++++##o#....
  8 ..##################....
  9 ........................
 10 .**********************.
 11 ........................
 12 ........................

1) dot  2) line  3) box  4) fill  5) pen  6) undo  7) show  8) quit
choice> 2
from> 9 24
to> 12 21
line: 4 cells, 3 changed.
Pen *, 151 cells painted
    123456789012345678901234
  1 ........................
  2 ..##################....
  3 ..#+##ooooooooooooo#....
  4 ..#+++###oooooooooo#....
  5 ..#++++++####oooooo#....
  6 ..#++++++++++###ooo#....
  7 ..#+++++++++++++##o#....
  8 ..##################....
  9 .......................*
 10 .**********************.
 11 .....................*..
 12 ....................*...

1) dot  2) line  3) box  4) fill  5) pen  6) undo  7) show  8) quit
choice> 6
Undid the line: 3 cells put back.
Pen *, 148 cells painted
    123456789012345678901234
  1 ........................
  2 ..##################....
  3 ..#+##ooooooooooooo#....
  4 ..#+++###oooooooooo#....
  5 ..#++++++####oooooo#....
  6 ..#++++++++++###ooo#....
  7 ..#+++++++++++++##o#....
  8 ..##################....
  9 ........................
 10 .**********************.
 11 ........................
 12 ........................

1) dot  2) line  3) box  4) fill  5) pen  6) undo  7) show  8) quit
choice> 6
Undid the line: 22 cells put back.
Pen *, 126 cells painted
    123456789012345678901234
  1 ........................
  2 ..##################....
  3 ..#+##ooooooooooooo#....
  4 ..#+++###oooooooooo#....
  5 ..#++++++####oooooo#....
  6 ..#++++++++++###ooo#....
  7 ..#+++++++++++++##o#....
  8 ..##################....
  9 ........................
 10 ........................
 11 ........................
 12 ........................

1) dot  2) line  3) box  4) fill  5) pen  6) undo  7) show  8) quit
choice> 5
pen (# o + * ~, or . to erase)> .
The pen is now . - it paints the background back, like an eraser.

1) dot  2) line  3) box  4) fill  5) pen  6) undo  7) show  8) quit
choice> 1
cell> 2 3
dot: 1 cell, 1 changed.
Pen ., 125 cells painted
    123456789012345678901234
  1 ........................
  2 ...#################....
  3 ..#+##ooooooooooooo#....
  4 ..#+++###oooooooooo#....
  5 ..#++++++####oooooo#....
  6 ..#++++++++++###ooo#....
  7 ..#+++++++++++++##o#....
  8 ..##################....
  9 ........................
 10 ........................
 11 ........................
 12 ........................

1) dot  2) line  3) box  4) fill  5) pen  6) undo  7) show  8) quit
choice> 5
pen (# o + * ~, or . to erase)> ~
The pen is now ~.

1) dot  2) line  3) box  4) fill  5) pen  6) undo  7) show  8) quit
choice> 4
start> 1 1
fill: 163 cells, 163 changed.
Pen ~, 288 cells painted
    123456789012345678901234
  1 ~~~~~~~~~~~~~~~~~~~~~~~~
  2 ~~~#################~~~~
  3 ~~#+##ooooooooooooo#~~~~
  4 ~~#+++###oooooooooo#~~~~
  5 ~~#++++++####oooooo#~~~~
  6 ~~#++++++++++###ooo#~~~~
  7 ~~#+++++++++++++##o#~~~~
  8 ~~##################~~~~
  9 ~~~~~~~~~~~~~~~~~~~~~~~~
 10 ~~~~~~~~~~~~~~~~~~~~~~~~
 11 ~~~~~~~~~~~~~~~~~~~~~~~~
 12 ~~~~~~~~~~~~~~~~~~~~~~~~

1) dot  2) line  3) box  4) fill  5) pen  6) undo  7) show  8) quit
choice> 8
Bye.
What was typed (33 lines)
3
2 3
8 20
2
3 5
7 18
5
o
4
4 10
5
+
4
3 4
5
*
2
10 2
10 23
2
9 24
12 21
6
6
5
.
1
2 3
5
~
4
1 1
8

Modules

The program as written, entry module first. Each module transpiles to its own Python file, which is what eml project run executes.

main.eml(entry)

eml
# P037 pixel paint: a 12 x 24 canvas of characters. Paint single cells,
# lines and boxes with the pen's color, fill an area with the paint bucket,
# change the pen, and undo as far back as you like.
import canvas
import shapes

def trim(s):
    0 => i
    len(s) => j
    while i < j and s[i] == " ":
        i + 1 => i
    while j > i and s[j - 1] == " ":
        j - 1 => j
    return s[i:j]

def whole_number(s):
    # The value of 1 to 3 digits, otherwise -1.
    if s == "" or len(s) > 3:
        return -1
    0 => n
    for c in s:
        if not (c in "0123456789"):
            return -1
        n * 10 + int(c) => n
    return n

def ask_cell(prompt):
    # A cell typed as "row column" or "row,column", counting from 1, as a
    # (row, column) tuple counting from 0; [] when the answer is empty.
    while True:
        trim(input(prompt)) => answer
        if answer == "":
            return []
        [] => parts
        "" => word
        for c in answer + " ":
            if c == " " or c == ",":
                if word != "":
                    parts + [word] => parts
                "" => word
            else:
                word + c => word
        if len(parts) == 2:
            whole_number(parts[0]) => r
            whole_number(parts[1]) => c
            if r >= 1 and r <= canvas.rows and c >= 1 and c <= canvas.cols:
                return (r - 1, c - 1)
        ("Type a row from 1 to " + str(canvas.rows) + " and a column from 1 to " + str(canvas.cols) + ", like 3 5.") ^0

def cells_text(n):
    if n == 1:
        return "1 cell"
    return str(n) + " cells"

def done(cv, pen, history, name, cells, changes):
    # Records a drawing step for undo and shows the result. Returns history.
    if len(changes) == 0:
        ("Nothing changed: " + cells_text(len(cells)) + ", all already " + pen + ".") ^0
        return history
    (name + ": " + cells_text(len(cells)) + ", " + str(len(changes)) + " changed.") ^0
    canvas.show(cv, pen)
    return history + [[name, changes]]

def draw(cv, pen, history, kind):
    if kind == "dot":
        ask_cell("cell> ") => a
        if len(a) == 0:
            "Cancelled." ^0
            return history
        [a] => cells
    elif kind == "fill":
        ask_cell("start> ") => a
        if len(a) == 0:
            "Cancelled." ^0
            return history
        shapes.area(cv, a) => cells
    else:
        ask_cell("from> ") => a
        if len(a) == 0:
            "Cancelled." ^0
            return history
        ask_cell("to> ") => b
        if len(b) == 0:
            "Cancelled." ^0
            return history
        if kind == "line":
            shapes.line(a[0], a[1], b[0], b[1]) => cells
        else:
            shapes.box(a[0], a[1], b[0], b[1]) => cells
    canvas.paint(cv, cells, pen) => changes
    return done(cv, pen, history, kind, cells, changes)

def choose_pen(pen):
    while True:
        trim(input("pen (# o + * ~, or . to erase)> ")) => answer
        if answer == "":
            "Cancelled." ^0
            return pen
        if len(answer) == 1 and answer in canvas.colors:
            if answer == canvas.background:
                "The pen is now . - it paints the background back, like an eraser." ^0
            else:
                ("The pen is now " + answer + ".") ^0
            return answer
        "Type one character: # o + * ~, or . to erase." ^0

"== Pixel paint ==" ^0
"A 12 x 24 canvas. Rows and columns count from 1; type a cell as 3 5." ^0
{} => cv
"#" => pen
[] => history
True => running
while running:
    "" ^0
    "1) dot  2) line  3) box  4) fill  5) pen  6) undo  7) show  8) quit" ^0
    trim(input("choice> ")) => choice
    if choice == "1":
        draw(cv, pen, history, "dot") => history
    elif choice == "2":
        draw(cv, pen, history, "line") => history
    elif choice == "3":
        draw(cv, pen, history, "box") => history
    elif choice == "4":
        draw(cv, pen, history, "fill") => history
    elif choice == "5":
        choose_pen(pen) => pen
    elif choice == "6":
        if len(history) == 0:
            "Nothing to undo." ^0
        else:
            history[len(history) - 1] => last
            canvas.undo(cv, last[1])
            history[0:len(history) - 1] => history
            ("Undid the " + last[0] + ": " + cells_text(len(last[1])) + " put back.") ^0
            canvas.show(cv, pen)
    elif choice == "7":
        canvas.show(cv, pen)
    elif choice == "8":
        False => running
    else:
        "Pick a number from 1 to 8." ^0
"Bye." ^0
Python projection (main.py)
import canvas
import shapes

def trim(s):
    i = 0
    j = len(s)
    while i < j and s[i] == " ":
        i = i + 1
    while j > i and s[j - 1] == " ":
        j = j - 1
    return s[i:j]

def whole_number(s):
    if s == "" or len(s) > 3:
        return -1
    n = 0
    for c in s:
        if not c in "0123456789":
            return -1
        n = n * 10 + int(c)
    return n

def ask_cell(prompt):
    while True:
        answer = trim(input(prompt))
        if answer == "":
            return []
        parts = []
        word = ""
        for c in answer + " ":
            if c == " " or c == ",":
                if word != "":
                    parts = parts + [word]
                word = ""
            else:
                word = word + c
        if len(parts) == 2:
            r = whole_number(parts[0])
            c = whole_number(parts[1])
            if r >= 1 and r <= canvas.rows and c >= 1 and c <= canvas.cols:
                return (r - 1, c - 1)
        print("Type a row from 1 to " + str(canvas.rows) + " and a column from 1 to " + str(canvas.cols) + ", like 3 5.")

def cells_text(n):
    if n == 1:
        return "1 cell"
    return str(n) + " cells"

def done(cv, pen, history, name, cells, changes):
    if len(changes) == 0:
        print("Nothing changed: " + cells_text(len(cells)) + ", all already " + pen + ".")
        return history
    print(name + ": " + cells_text(len(cells)) + ", " + str(len(changes)) + " changed.")
    canvas.show(cv, pen)
    return history + [[name, changes]]

def draw(cv, pen, history, kind):
    if kind == "dot":
        a = ask_cell("cell> ")
        if len(a) == 0:
            print("Cancelled.")
            return history
        cells = [a]
    elif kind == "fill":
        a = ask_cell("start> ")
        if len(a) == 0:
            print("Cancelled.")
            return history
        cells = shapes.area(cv, a)
    else:
        a = ask_cell("from> ")
        if len(a) == 0:
            print("Cancelled.")
            return history
        b = ask_cell("to> ")
        if len(b) == 0:
            print("Cancelled.")
            return history
        if kind == "line":
            cells = shapes.line(a[0], a[1], b[0], b[1])
        else:
            cells = shapes.box(a[0], a[1], b[0], b[1])
    changes = canvas.paint(cv, cells, pen)
    return done(cv, pen, history, kind, cells, changes)

def choose_pen(pen):
    while True:
        answer = trim(input("pen (# o + * ~, or . to erase)> "))
        if answer == "":
            print("Cancelled.")
            return pen
        if len(answer) == 1 and answer in canvas.colors:
            if answer == canvas.background:
                print("The pen is now . - it paints the background back, like an eraser.")
            else:
                print("The pen is now " + answer + ".")
            return answer
        print("Type one character: # o + * ~, or . to erase.")

print("== Pixel paint ==")
print("A 12 x 24 canvas. Rows and columns count from 1; type a cell as 3 5.")
cv = {}
pen = "#"
history = []
running = True
while running:
    print("")
    print("1) dot  2) line  3) box  4) fill  5) pen  6) undo  7) show  8) quit")
    choice = trim(input("choice> "))
    if choice == "1":
        history = draw(cv, pen, history, "dot")
    elif choice == "2":
        history = draw(cv, pen, history, "line")
    elif choice == "3":
        history = draw(cv, pen, history, "box")
    elif choice == "4":
        history = draw(cv, pen, history, "fill")
    elif choice == "5":
        pen = choose_pen(pen)
    elif choice == "6":
        if len(history) == 0:
            print("Nothing to undo.")
        else:
            last = history[len(history) - 1]
            canvas.undo(cv, last[1])
            history = history[0:len(history) - 1]
            print("Undid the " + last[0] + ": " + cells_text(len(last[1])) + " put back.")
            canvas.show(cv, pen)
    elif choice == "7":
        canvas.show(cv, pen)
    elif choice == "8":
        running = False
    else:
        print("Pick a number from 1 to 8.")
print("Bye.")

canvas.eml

eml
# P037 pixel paint - the canvas. 12 rows and 24 columns, counted from 0
# inside the program. The canvas is a dictionary keyed by (row, column)
# tuples, as in the corpus case grid-coordinate-map: a cell that was never
# painted is simply not in it and shows the background ".".

12 => rows
24 => cols
"." => background
"#o+*~." => colors

def color_at(canvas, cell):
    if cell in canvas:
        return canvas[cell]
    return background

def inside(r, c):
    return r >= 0 and r < rows and c >= 0 and c < cols

def paint(canvas, cells, color):
    # Paints the cells and returns the changes [cell, old, new] - only the
    # cells whose color really changed - so that they can be undone.
    [] => changes
    for cell in cells:
        color_at(canvas, cell) => old
        if old != color:
            changes + [[cell, old, color]] => changes
            color => canvas[cell]
    return changes

def undo(canvas, changes):
    # Puts back the old colors, last change first.
    len(changes) - 1 => i
    while i >= 0:
        changes[i][1] => canvas[changes[i][0]]
        i - 1 => i

def painted(canvas):
    # How many cells hold something other than the background.
    0 => n
    for cell in canvas:
        if canvas[cell] != background:
            n + 1 => n
    return n

def show(canvas, pen):
    painted(canvas) => n
    "cells" => word
    if n == 1:
        "cell" => word
    ("Pen " + pen + ", " + str(n) + " " + word + " painted") ^0
    "    " => header
    for c in [1:cols]:
        header + str(c % 10) => header
    header ^0
    for r in [0:rows - 1]:
        str(r + 1) => label
        while len(label) < 3:
            " " + label => label
        label + " " => line
        for c in [0:cols - 1]:
            line + color_at(canvas, (r, c)) => line
        line ^0
Python projection (canvas.py)
rows = 12
cols = 24
background = "."
colors = "#o+*~."

def color_at(canvas, cell):
    if cell in canvas:
        return canvas[cell]
    return background

def inside(r, c):
    return r >= 0 and r < rows and c >= 0 and c < cols

def paint(canvas, cells, color):
    changes = []
    for cell in cells:
        old = color_at(canvas, cell)
        if old != color:
            changes = changes + [[cell, old, color]]
            canvas[cell] = color
    return changes

def undo(canvas, changes):
    i = len(changes) - 1
    while i >= 0:
        canvas[changes[i][0]] = changes[i][1]
        i = i - 1

def painted(canvas):
    n = 0
    for cell in canvas:
        if canvas[cell] != background:
            n = n + 1
    return n

def show(canvas, pen):
    n = painted(canvas)
    word = "cells"
    if n == 1:
        word = "cell"
    print("Pen " + pen + ", " + str(n) + " " + word + " painted")
    header = "    "
    for c in range(1, cols+1):
        header = header + str(c % 10)
    print(header)
    for r in range(0, rows):
        label = str(r + 1)
        while len(label) < 3:
            label = " " + label
        line = label + " "
        for c in range(0, cols):
            line = line + color_at(canvas, (r, c))
        print(line)

shapes.eml

eml
# P037 pixel paint - which cells a shape covers: a line, a box, and the area
# a fill spreads over. Cells are (row, column) tuples.
import canvas

def step_toward(a, b):
    if a < b:
        return 1
    if a > b:
        return -1
    return 0

def line(r0, c0, r1, c1):
    # Bresenham's line from (r0, c0) to (r1, c1), with whole numbers only:
    # err tracks how far the drawn cells are from the true line, and each
    # step moves along the longer direction and, when err says so, along the
    # shorter one too. Both ends are included.
    abs(c1 - c0) => dc
    abs(r1 - r0) => dr
    step_toward(c0, c1) => sc
    step_toward(r0, r1) => sr
    dc - dr => err
    [] => cells
    True => going
    while going:
        cells + [(r0, c0)] => cells
        if r0 == r1 and c0 == c1:
            False => going
        else:
            2 * err => e2
            if e2 > 0 - dr:
                err - dr => err
                c0 + sc => c0
            if e2 < dc:
                err + dc => err
                r0 + sr => r0
    return cells

def box(r0, c0, r1, c1):
    # The outline of the rectangle with these two corners.
    min(r0, r1) => top
    max(r0, r1) => bottom
    min(c0, c1) => left
    max(c0, c1) => right
    [] => cells
    for c in [left:right]:
        cells + [(top, c)] => cells
        if bottom != top:
            cells + [(bottom, c)] => cells
    for r in [top + 1:bottom - 1]:
        cells + [(r, left)] => cells
        if right != left:
            cells + [(r, right)] => cells
    return cells

def area(cv, start):
    # The cells a fill from start reaches: every cell of start's color that
    # can be reached through up, down, left and right moves over that color.
    # The corpus case flood-fill recurses into the four neighbours; this
    # keeps the cells still to visit on a list instead, and marks a cell when
    # it is put on the list, so none is taken twice.
    canvas.color_at(cv, start) => target
    {} => seen
    True => seen[start]
    [start] => todo
    [] => cells
    while len(todo) > 0:
        todo[len(todo) - 1] => cell
        todo[0:len(todo) - 1] => todo
        cells + [cell] => cells
        cell[0] => r
        cell[1] => c
        for d in [(r - 1, c), (r + 1, c), (r, c - 1), (r, c + 1)]:
            if canvas.inside(d[0], d[1]) and not (d in seen) and canvas.color_at(cv, d) == target:
                True => seen[d]
                todo + [d] => todo
    return cells
Python projection (shapes.py)
import canvas

def step_toward(a, b):
    if a < b:
        return 1
    if a > b:
        return -1
    return 0

def line(r0, c0, r1, c1):
    dc = abs(c1 - c0)
    dr = abs(r1 - r0)
    sc = step_toward(c0, c1)
    sr = step_toward(r0, r1)
    err = dc - dr
    cells = []
    going = True
    while going:
        cells = cells + [(r0, c0)]
        if r0 == r1 and c0 == c1:
            going = False
        else:
            e2 = 2 * err
            if e2 > 0 - dr:
                err = err - dr
                c0 = c0 + sc
            if e2 < dc:
                err = err + dc
                r0 = r0 + sr
    return cells

def box(r0, c0, r1, c1):
    top = min(r0, r1)
    bottom = max(r0, r1)
    left = min(c0, c1)
    right = max(c0, c1)
    cells = []
    for c in range(left, right+1):
        cells = cells + [(top, c)]
        if bottom != top:
            cells = cells + [(bottom, c)]
    for r in range(top + 1, bottom):
        cells = cells + [(r, left)]
        if right != left:
            cells = cells + [(r, right)]
    return cells

def area(cv, start):
    target = canvas.color_at(cv, start)
    seen = {}
    seen[start] = True
    todo = [start]
    cells = []
    while len(todo) > 0:
        cell = todo[len(todo) - 1]
        todo = todo[0:len(todo) - 1]
        cells = cells + [cell]
        r = cell[0]
        c = cell[1]
        for d in [(r - 1, c), (r + 1, c), (r, c - 1), (r, c + 1)]:
            if canvas.inside(d[0], d[1]) and not d in seen and canvas.color_at(cv, d) == target:
                seen[d] = True
                todo = todo + [d]
    return cells

Built on these corpus cases