Sprites: things that move without the screen moving

Everything drawn on this machine was in a cell. Something between two
cells meant rewriting both; something moving a pixel at a time meant
rewriting them sixty times a second, which is affordable for one thing
and not for twenty. A sprite is put at a pixel and the device draws it
over whatever is behind, so moving it costs two bytes.

MADE OF TILES, which is the decision the rest follows from: m by n taken
in reading order from one index, so there is no second pixel format, no
second kind of memory, and nothing a sprite can show that the map cannot.
A 16 by 16 character is four tiles and the background can name the same
four.

256 entries of 8 bytes at 0xC000 in the atlas - eight so the entry
address is a shift, the same no-multiply argument as the palette's four.
Position is signed and sixteen bits, because 640 by 400 does not fit in a
byte and a sprite has to be able to sit half off the left rather than
appearing whole at the edge.

A PIXEL OF ZERO IS NOT DRAWN, or every sprite is a rectangle. Tested
before the attribute is added, so a hole belongs to the art and not to
the colour scheme. The same rule the other way round is what "behind"
means: drawn only where the background pixel was zero, so a thing walks
behind a pillar and in front of the floor in one frame.

All of them draw, every frame, so they cannot flicker. Real machines
dropped them per scanline because they had a fixed number of shift
registers; this has a loop. The limit is the size of the table, which is
a constant rather than a property of what is on screen.

And the system takes them down at exit. The sprite table sits in the gap
the screen save walks around - to the end of the map, then the palette -
and that is right, because nothing the shell draws is a sprite: there is
nothing to give back, only something to take away. Otherwise a program
that put a ball up and left would leave it over the prompt, in front of
everything, with nothing able to type it away. Sprite.asm deliberately
leaves its own, because a program that faulted could not have cleared it.

Every check here was re-broken and failed: transparency, reading order,
draw order, priority, and size. Size needed breaking twice - the first
attempt did not compile, and a silent build failure had left the old
binary passing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
This commit is contained in:
Anachronaut
2026-09-02 11:42:11 -04:00
co-authored by Claude Opus 5
parent eee95ef0ce
commit a916103a7f
21 changed files with 801 additions and 14 deletions
+2 -1
View File
@@ -23,6 +23,7 @@ Grid.sbx 571
Press.sbx 872
Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Crash.sbx 632
vars.script 50
blocks.script 343
@@ -39,7 +40,7 @@ outer.script 376
inner.script 44
loop.script 35
crossed.txt 560
26 files, 1 directory
27 files, 1 directory
> exit
halted
Execution halted.
+2 -1
View File
@@ -13,6 +13,7 @@ Grid.sbx 571
Press.sbx 872
Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Crash.sbx 632
vars.script 50
blocks.script 343
@@ -28,7 +29,7 @@ nonl.script 38
outer.script 376
inner.script 44
loop.script 35
25 files, 1 directory
26 files, 1 directory
> drive 1
> dir
other.txt 28
+2 -1
View File
@@ -30,6 +30,7 @@ Grid.sbx 571
Press.sbx 872
Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Crash.sbx 632
vars.script 50
blocks.script 343
@@ -45,7 +46,7 @@ nonl.script 38
outer.script 376
inner.script 44
loop.script 35
25 files, 1 directory
26 files, 1 directory
> exit
halted
Execution halted.
+2 -1
View File
@@ -20,6 +20,7 @@ Grid.sbx 571
Press.sbx 872
Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Crash.sbx 632
vars.script 50
blocks.script 343
@@ -35,7 +36,7 @@ nonl.script 38
outer.script 376
inner.script 44
loop.script 35
25 files, 1 directory
26 files, 1 directory
> exit
halted
Execution halted.
+2 -1
View File
@@ -20,6 +20,7 @@ Grid.sbx 571
Press.sbx 872
Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Crash.sbx 632
vars.script 50
blocks.script 343
@@ -35,7 +36,7 @@ nonl.script 38
outer.script 376
inner.script 44
loop.script 35
25 files, 1 directory
26 files, 1 directory
> exit
halted
Execution halted.
+2 -1
View File
@@ -106,6 +106,7 @@ Grid.sbx 571
Press.sbx 872
Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Crash.sbx 632
vars.script 50
blocks.script 343
@@ -121,7 +122,7 @@ nonl.script 38
outer.script 376
inner.script 44
loop.script 35
25 files, 1 directory
26 files, 1 directory
> exit
halted
Execution halted.
+2 -1
View File
@@ -25,6 +25,7 @@ Grid.sbx 571
Press.sbx 872
Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Crash.sbx 632
vars.script 50
blocks.script 343
@@ -40,7 +41,7 @@ nonl.script 38
outer.script 376
inner.script 44
loop.script 35
25 files, 1 directory
26 files, 1 directory
> exit
halted
Execution halted.
+2 -1
View File
@@ -13,6 +13,7 @@ Grid.sbx 571
Press.sbx 872
Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Crash.sbx 632
vars.script 50
blocks.script 343
@@ -28,7 +29,7 @@ nonl.script 38
outer.script 376
inner.script 44
loop.script 35
25 files, 1 directory
26 files, 1 directory
> load
load what?
> load nosuch.sbx
+2 -1
View File
@@ -11,6 +11,7 @@ Grid.sbx 571
Press.sbx 872
Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Crash.sbx 632
vars.script 50
blocks.script 343
@@ -26,7 +27,7 @@ nonl.script 38
outer.script 376
inner.script 44
loop.script 35
25 files, 1 directory
26 files, 1 directory
> load Say.sbx
loaded, starting at 5000
> run the disk took its time
+43
View File
@@ -0,0 +1,43 @@
CosmOS
> Say before Sprite
it says: before Sprite
finished
> Sprite
A ball, over the shell's own words. Nothing underneath is written to. Press a key.
finished
> Say after Sprite
it says: after Sprite
finished
> dir
greet.sbx 211
hello.sbx 53
Life.sbx 1396
Snake.sbx 2164
Keys.sbx 664
Say.sbx 156
Break.sbx 149
Grid.sbx 571
Press.sbx 872
Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Crash.sbx 632
vars.script 50
blocks.script 343
loops.script 272
tune.sbx 306
notes.txt 21
Apps <dir>
hi.script 121
bad.script 45
plain.script 24
cross.script 280
nonl.script 38
outer.script 376
inner.script 44
loop.script 35
26 files, 1 directory
> exit
halted
Execution halted.
[exit 0]
Binary file not shown.
+5
View File
@@ -130,6 +130,11 @@ for i in 1 2 3 4 5 6 7 8; do "$TOOL" put "$DISKS/sbfs.img" "filler$i.txt" >/dev/
"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \
"$ROOT/Programs/CosmOS/Apps/Flip.asm" -o "$WORK/Flip.sbx" >/dev/null
"$TOOL" put "$DISKS/cosmos.img" "$WORK/Flip.sbx" >/dev/null
# Sprite.sbx moves a ball across the shell's own text without writing a byte of the map, and
# leaves the sprite in the table on the way out - so it is what says the system clears them.
"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \
"$ROOT/Programs/CosmOS/Apps/Sprite.asm" -o "$WORK/Sprite.sbx" >/dev/null
"$TOOL" put "$DISKS/cosmos.img" "$WORK/Sprite.sbx" >/dev/null
# Crash.sbx breaks in each of the four ways the system now catches, which is the only way to
# reach a fault screen from a recorded test.
"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \
+13
View File
@@ -977,6 +977,19 @@ cosmosGrid | CosmOS/Source/cosmos.asm | run | cosmosGri
# osTakeScreen, which restores the screen as it was BEFORE the line was printed and so wiped
# out the one thing it was pointing at. A program that damages nothing should not ask.
cosmosFlip | CosmOS/Source/cosmos.asm | run | cosmosFlip.in | 60000000 | disks/cosmos.img
# ---- A thing that moves without the screen moving ----
#
# Sprite draws a ball over the shell's own text and writes not one byte of the map to do it,
# which is what a sprite is for. The picture is video.sh's business; what is checked here is
# that the words either side of it are untouched - the line printed before it ran and the one
# printed after are both still readable, because nothing underneath the ball was ever written.
#
# A KEYBOARD FIXTURE and not standard input, which is the difference between a test that
# runs this program and one that only looks like it does. A null on standard input is a
# character like any other, so the first of them satisfied the "has a key arrived" test and
# the ball never moved at all - the whole 150 frames of it went past in the shell, reading a
# line made of nulls. Through a keyboard a null is silence, which is what makes it a wait.
cosmosSprite | CosmOS/Source/cosmos.asm | run | - | 60000000 | disks/cosmos.img | cosmosSprite.keys
# Which disk the registers mean. Several disks are one controller with a drive register
# rather than several devices, because a port is an immediate byte inside the instruction
# that names it and a program cannot compute one. Run with a single disk, so drive 1 is a
+224 -1
View File
@@ -100,13 +100,26 @@ print(counts.most_common(1)[0][0].hex())
" "$1" 2>/dev/null || echo none
}
# How many pixels of one exact colour a picture has. What a sprite is counted by: it is a
# shape rather than a screenful, so the commonest colour says nothing about it.
countColour() {
# countColour <ppm> <rrggbb>
python3 -c "
import sys
d = open(sys.argv[1], 'rb').read()
px = d[d.index(b'255\n') + 4:]
want = bytes.fromhex(sys.argv[2])
print(sum(1 for o in range(0, len(px), 3) if px[o:o + 3] == want))
" "$1" "$2" 2>/dev/null || echo -1
}
pokeTo() {
# pokeTo <bank> <address> <byte>
printf ' INIA 0d%d\n OUTA 0xE3\n INIA 0x%02X\n OUTA 0xE4\n INIA 0x%02X\n OUTA 0xE5\n INIA 0x%02X\n OUTA 0xE9\n' \
"$1" $(( ($2 >> 8) & 0xFF )) $(( $2 & 0xFF )) $(( $3 & 0xFF ))
}
pokeAtlas() { pokeTo 3 "$1" "$2"; } # Tiles and the palette.
pokeAtlas() { pokeTo 3 "$1" "$2"; } # Tiles, the palette and the sprite table.
pokeScreen() { pokeTo 4 "$1" "$2"; } # The map, or a bitmap.
pokeBack() { pokeTo 5 "$1" "$2"; } # The same, in the screen not being shown.
@@ -121,6 +134,48 @@ pokeAtlasRun() {
for (( i = 0; i < $3; i++ )); do printf ' OUTA 0xE9\n'; done
}
# One entry of the sprite table, which is eight bytes at 0xC000 plus eight times its number.
# X and Y are signed and go in low byte first, so a negative one is written as its two's
# complement here rather than being worked out at every call.
spriteAt() {
# spriteAt <n> <tile> <attribute> <x> <y> <size> <flags>
local base=$(( 0xC000 + $1 * 8 ))
local x=$(( $4 & 0xFFFF ))
local y=$(( $5 & 0xFFFF ))
pokeAtlas "$base" "$2"
pokeAtlas "$(( base + 1 ))" "$3"
pokeAtlas "$(( base + 2 ))" "$(( x & 0xFF ))"
pokeAtlas "$(( base + 3 ))" "$(( (x >> 8) & 0xFF ))"
pokeAtlas "$(( base + 4 ))" "$(( y & 0xFF ))"
pokeAtlas "$(( base + 5 ))" "$(( (y >> 8) & 0xFF ))"
pokeAtlas "$(( base + 6 ))" "$6"
pokeAtlas "$(( base + 7 ))" "$7"
}
# Every pixel of one tile the same index. Tile n begins at n times 64.
solidTile() {
# solidTile <tile> <index>
pokeAtlasRun "$(( $1 * 64 ))" "$2" 64
}
# The top half one index and the bottom half another, which is how a tile gets a hole in it:
# index nought is what a sprite does not draw.
halfTile() {
# halfTile <tile> <top> <bottom>
pokeAtlasRun "$(( $1 * 64 ))" "$2" 32
pokeAtlasRun "$(( $1 * 64 + 32 ))" "$3" 32
}
# Four colours to tell tiles apart by, and the pair a background cell in scheme one uses.
spriteColours() {
pokeAtlas 0xFC04 0xFF; pokeAtlas 0xFC05 0x00; pokeAtlas 0xFC06 0x00 # 1 red
pokeAtlas 0xFC08 0x00; pokeAtlas 0xFC09 0xFF; pokeAtlas 0xFC0A 0x00 # 2 green
pokeAtlas 0xFC0C 0x00; pokeAtlas 0xFC0D 0x00; pokeAtlas 0xFC0E 0xFF # 3 blue
pokeAtlas 0xFC10 0xFF; pokeAtlas 0xFC11 0xFF; pokeAtlas 0xFC12 0x00 # 4 yellow
pokeAtlas 0xFC40 0x00; pokeAtlas 0xFC41 0x00; pokeAtlas 0xFC42 0x00 # 16 black paper
pokeAtlas 0xFC44 0x00; pokeAtlas 0xFC45 0xFF; pokeAtlas 0xFC46 0xFF # 17 cyan ink
}
port() {
# port <port> <byte>
printf ' INIA 0x%02X\n OUTA 0x%02X\n' $(( $2 & 0xFF )) $(( $1 & 0xFF ))
@@ -344,6 +399,145 @@ echo "Checking what the video device draws."
&& result ok "the map is a ring" "row 0 follows row 127" \
|| result no "the map is a ring" "got $(pixel ring 0 8)"
# ---- Sprites ----
#
# A thing put at a PIXEL rather than in a cell. Tile 1 is solid index one, which the palette
# above makes red, and the sprite sits exactly over the cell at row 3 column 2 - so where it
# is can be checked against where it is not, which is the way a tile engine is usually wrong.
{ prologue
spriteColours
solidTile 1 0x01
spriteAt 0 1 0x00 16 24 0x11 0x00
epilogue
} | run sprite || exit 1
[ "$(pixel sprite 16 24)" = "255,0,0" ] && [ "$(pixel sprite 23 31)" = "255,0,0" ] \
&& result ok "a sprite lands where it is put" "and fills its whole eight by eight" \
|| result no "a sprite lands where it is put" "corner $(pixel sprite 16 24), far $(pixel sprite 23 31)"
[ "$(pixel sprite 15 24)" = "0,0,0" ] && [ "$(pixel sprite 24 24)" = "0,0,0" ] \
&& result ok "and stops at its own edge" "a pixel either side is background" \
|| result no "and stops at its own edge" "left $(pixel sprite 15 24), right $(pixel sprite 24 24)"
# ---- What it does not cover ----
#
# Index nought is a hole and not a colour. The tile is solid on top and empty underneath, and
# the cell behind it is cyan, so the bottom half of the sprite must show the cell.
{ prologue
spriteColours
halfTile 3 0x01 0x00
pokeScreen 0x4304 0x01; pokeScreen 0x4305 0x01 # Row 3, column 2: tile 1 in scheme 1.
solidTile 1 0x01
spriteAt 0 3 0x00 16 24 0x11 0x00
epilogue
} | run spritehole || exit 1
[ "$(pixel spritehole 16 24)" = "255,0,0" ] && [ "$(pixel spritehole 16 28)" = "0,255,255" ] \
&& result ok "a pixel of nought is not drawn" "the cell behind shows through the hole" \
|| result no "a pixel of nought is not drawn" "top $(pixel spritehole 16 24), bottom $(pixel spritehole 16 28)"
# ---- Bigger than a tile ----
#
# Two by two, so four tiles in reading order from the one named: 4 and 5 across the top, 6 and
# 7 underneath. Each is its own colour, which is the only way to catch a sprite that draws all
# four in the right places in the wrong order.
{ prologue
spriteColours
solidTile 4 0x01; solidTile 5 0x02; solidTile 6 0x03; solidTile 7 0x04
spriteAt 0 4 0x00 16 24 0x22 0x00
epilogue
} | run spritebig || exit 1
[ "$(pixel spritebig 16 24)" = "255,0,0" ] && [ "$(pixel spritebig 24 24)" = "0,255,0" ] \
&& [ "$(pixel spritebig 16 32)" = "0,0,255" ] && [ "$(pixel spritebig 24 32)" = "255,255,0" ] \
&& result ok "a sprite is m by n tiles" "four of them, in reading order" \
|| result no "a sprite is m by n tiles" "$(pixel spritebig 16 24) $(pixel spritebig 24 24) $(pixel spritebig 16 32) $(pixel spritebig 24 32)"
# Mirrored, which has to move the TILES and not only the pixels inside them - a two tile wide
# thing whose halves stayed put would turn inside out rather than round.
{ prologue
spriteColours
solidTile 4 0x01; solidTile 5 0x02; solidTile 6 0x03; solidTile 7 0x04
spriteAt 0 4 0x00 16 24 0x22 0x01
epilogue
} | run spriteflip || exit 1
[ "$(pixel spriteflip 16 24)" = "0,255,0" ] && [ "$(pixel spriteflip 24 24)" = "255,0,0" ] \
&& result ok "mirroring moves the tiles too" "the right hand tile came out on the left" \
|| result no "mirroring moves the tiles too" "$(pixel spriteflip 16 24) $(pixel spriteflip 24 24)"
# And upside down, the same argument on the other axis.
{ prologue
spriteColours
solidTile 4 0x01; solidTile 5 0x02; solidTile 6 0x03; solidTile 7 0x04
spriteAt 0 4 0x00 16 24 0x22 0x02
epilogue
} | run spriteover || exit 1
[ "$(pixel spriteover 16 24)" = "0,0,255" ] && [ "$(pixel spriteover 16 32)" = "255,0,0" ] \
&& result ok "and turning it over does as well" "the bottom tile came out on top" \
|| result no "and turning it over does as well" "$(pixel spriteover 16 24) $(pixel spriteover 16 32)"
# ---- Off the edge ----
#
# The reason the position is signed. Four pixels off the left is half a tile showing; a whole
# tile off is nothing at all, and must be nothing rather than a wrapped one at the far side.
{ prologue
spriteColours
solidTile 1 0x01
spriteAt 0 1 0x00 -4 24 0x11 0x00
spriteAt 1 1 0x00 -8 40 0x11 0x00
epilogue
} | run spriteedge || exit 1
[ "$(pixel spriteedge 0 24)" = "255,0,0" ] && [ "$(pixel spriteedge 4 24)" = "0,0,0" ] \
&& result ok "a sprite can sit off the edge" "half of it showing, and half not" \
|| result no "a sprite can sit off the edge" "at 0 $(pixel spriteedge 0 24), at 4 $(pixel spriteedge 4 24)"
[ "$(pixel spriteedge 0 40)" = "0,0,0" ] && [ "$(pixel spriteedge 312 40)" = "0,0,0" ] \
&& result ok "and right off it is gone" "not wrapped round to the other side" \
|| result no "and right off it is gone" "left $(pixel spriteedge 0 40), right $(pixel spriteedge 312 40)"
# ---- In front, and behind ----
#
# Behind means drawn only where the background had NOTHING - the same rule that makes a
# sprite's own nought a hole, read the other way round. The cell is solid on top and empty
# underneath, so a sprite behind it shows through the bottom half only.
{ prologue
spriteColours
halfTile 2 0x01 0x00
pokeScreen 0x4304 0x02; pokeScreen 0x4305 0x01 # Row 3, column 2: tile 2 in scheme 1.
solidTile 1 0x01
spriteAt 0 1 0x00 16 24 0x11 0x04
epilogue
} | run spritebehind || exit 1
[ "$(pixel spritebehind 16 24)" = "0,255,255" ] && [ "$(pixel spritebehind 16 28)" = "255,0,0" ] \
&& result ok "a sprite can go behind the map" "hidden where the cell had something" \
|| result no "a sprite can go behind the map" "top $(pixel spritebehind 16 24), bottom $(pixel spritebehind 16 28)"
# Where two overlap, the lower number is in front. Both solid, both at the same place, and
# the one that wins says which way round the table is read.
{ prologue
spriteColours
solidTile 1 0x01; solidTile 2 0x02
spriteAt 0 1 0x00 16 24 0x11 0x00
spriteAt 1 2 0x00 16 24 0x11 0x00
epilogue
} | run spriteorder || exit 1
[ "$(pixel spriteorder 16 24)" = "255,0,0" ] \
&& result ok "the lower number is in front" "sprite nought covered sprite one" \
|| result no "the lower number is in front" "got $(pixel spriteorder 16 24)"
# ---- Nothing, which is what the table wakes up as ----
#
# A size of nought either way draws nothing, and that is the off switch. Everything above
# would pass on a device that drew every entry regardless, because every other entry in those
# tables happens to be zeroed - this is the one that says zero MEANS something.
{ prologue
spriteColours
solidTile 1 0x01
spriteAt 0 1 0x00 16 24 0x01 0x00
spriteAt 1 1 0x00 40 24 0x10 0x00
spriteAt 2 1 0x00 64 24 0x11 0x00
epilogue
} | run spritenone || exit 1
[ "$(pixel spritenone 16 24)" = "0,0,0" ] && [ "$(pixel spritenone 40 24)" = "0,0,0" ] \
&& [ "$(pixel spritenone 64 24)" = "255,0,0" ] \
&& result ok "no width or no height draws nothing" "and the one beside them still does" \
|| result no "no width or no height draws nothing" "$(pixel spritenone 16 24) $(pixel spritenone 40 24) $(pixel spritenone 64 24)"
# ---- Two screens, and the flip between them ----
#
# One red cell in each screen, in different rows: row one of the screen being shown, and the
@@ -1054,6 +1248,35 @@ timeout 30 "$EMU" --fast --cycles 200000000 --keyboard "$BUILD/flipafter.keys" \
&& result ok "and the system puts the screen back" "black again, and not the filled screen" \
|| result no "and the system puts the screen back" "commonest colour $(commonest "$BUILD/flipafter.ppm")"
# ---- A sprite, from inside the system ----
#
# Sprite moves a ball across the shell's own text and writes NOT ONE BYTE of the map to do
# it. The ball is 52 pixels of scheme one's ink, drawn from a tile whose corners are index
# nought - so counting that exact colour finds the ball and nothing else, the shell printing
# in grey.
python3 -c "open('$BUILD/ball.keys','wb').write(b'Sprite\n' + b'\x00'*40000)"
timeout 30 "$EMU" --fast --cycles 60000000 --keyboard "$BUILD/ball.keys" \
--screen "$BUILD/ball.ppm" --disk "$ROOT/Tests/build/disks/cosmos.img" \
--ram-disk 2048 "$BUILD/cosmos.bin" > "$BUILD/ball.out" 2>&1 || true
[ "$(countColour "$BUILD/ball.ppm" d04038)" = "52" ] \
&& result ok "a program can put a sprite up" "52 pixels of ball, and a round one" \
|| result no "a program can put a sprite up" "$(countColour "$BUILD/ball.ppm" d04038) pixels, not 52"
# ---- And the system takes it down ----
#
# The sprite table is in the atlas at 0xC000, and the screen save walks the pages either side
# of it: to the end of the map, then the palette. So a sprite is not something the system can
# GIVE BACK, and Sprite deliberately does not clear its own - a program that faulted could
# not have either. What must not happen is a ball left sitting over the prompt, in front of
# everything, with nothing able to type it away.
python3 -c "open('$BUILD/ballgone.keys','wb').write(b'Sprite\n' + b'\x00'*600 + b' ' + b'\x00'*600)"
timeout 30 "$EMU" --fast --cycles 60000000 --keyboard "$BUILD/ballgone.keys" \
--screen "$BUILD/ballgone.ppm" --disk "$ROOT/Tests/build/disks/cosmos.img" \
--ram-disk 2048 "$BUILD/cosmos.bin" > "$BUILD/ballgone.out" 2>&1 || true
[ "$(countColour "$BUILD/ballgone.ppm" d04038)" = "0" ] \
&& result ok "and the system takes the sprite down" "not one pixel of it left over the shell" \
|| result no "and the system takes the sprite down" "$(countColour "$BUILD/ballgone.ppm" d04038) pixels still there"
# ---- Clearing puts the cursor back at the top ----
#
# A screen with nothing on it and a cursor half way down it is not a cleared screen. This