Atic Atac draws a room once, when the player comes in, and from then on touches only what moves. A room is a flood of colour and a vector outline; its doors and furniture are wide pictures drawn once in any of eight orientations, with their colours repainted every pass; and the player, the creatures and the objects are sixteen-pixel sprites XORed onto the screen, each erased where it was and drawn where it is, a row at a time. This page follows a room being entered, stage by stage, through the game's own code in SkoolKit's simulator, then one step of the player, and ends with where the time goes.

The room followed

Room room $00, where every game starts: a red square hall (shape $00) with doors in three walls, the A.C.G. door in the fourth, and eight pieces of furniture. Nothing was staged. A game was started from the title screen with the knight, as the build starts one; the knight walked down through the south door into room room $07 (37 frames holding E), waited 20 frames there, and walked back up (R) until the door's handler, DOOR, found him in its box and called ENTER_ROOM. The screen at that moment, still showing the room he is leaving:

Room $07 as ENTER_ROOM is called: the knight in the doorway at the top.

Room $07 as ENTER_ROOM is called: the knight in the doorway at the top.

1. The room: a flood and an outline

ENTER_ROOM swaps to the door's far half, which gives the new room and where the player appears (see the doors), then falls into ARRIVE_IN_ROOM: mark the room seen (MARK_ROOM_VISITED), blank the play area (CLEAR_PLAY_AREA: the 24 left-hand columns, all 192 lines; the scroll is left alone), draw the room (DRAW_ROOM), colour the scroll (PAINT_PANEL), redraw the inventory (DRAW_INVENTORY) and start the new-room sound (PLAY_SOUND_NEW_ROOM).

DRAW_ROOM reads the room's two bytes in ROOM_TABLE, a colour and a shape. It fills all 576 attribute cells of the play area with the colour -- after which the screen still looks black, since there are no pixels in them yet -- and copies the shape's walk limits from ROOM_SHAPES to ROOM_HALF_WIDTH and ROOM_HALF_HEIGHT, which is all collision knows of the room's shape. Then DRAW_OUTLINE walks the shape's edge list and DRAW_LINE joins its corners a pixel at a time, ORing into the screen (every shape is drawn on the room types page). After the outline the scroll still wears the old room's colours, until PAINT_PANEL:

After DRAW_ROOM: the outline, in the room's red. The scroll is still coloured for the room just left.

After DRAW_ROOM: the outline, in the room's red. The scroll is still coloured for the room just left.

After PAINT_PANEL: the scroll recoloured to match.

After PAINT_PANEL: the scroll recoloured to match.

2. The doors and furniture, from the room's list

DRAW_ROOM also clears ROOM_DRAWN, and on a pass with it clear MAIN_LOOP skips the objects and monsters and goes straight to the room's list in ROOM_CONTENTS. Each entry names a sixteen-byte door or furniture record by its half in this room; DISPATCH_FROM_LIST dispatches it to its type's handler, and every handler ends in DRAW_DOOR, which draws the colours and, because ROOM_DRAWN is clear, the pixels too (DRAW_RECORD). Room $00's list has 12 entries; the screen after each, cut to what it changed (the two south and west doors are timed doors, open at the time):

1. timed door, open, mode 4 (south wall)

1. timed door, open, mode 4 (south wall)

2. timed door, open, mode 7 (west wall)

2. timed door, open, mode 7 (west wall)

3. cyan door, mode 0 (north wall)

3. cyan door, mode 0 (north wall)

4. A.C.G. door, mode 2

4. A.C.G. door, mode 2

5. suit of armour, mode 3

5. suit of armour, mode 3

6. suit of armour, mode 3

6. suit of armour, mode 3

7. pumpkin picture, mode 0

7. pumpkin picture, mode 0

8. ghost picture, mode 0

8. ghost picture, mode 0

9. A.C.G. shield, mode 4

9. A.C.G. shield, mode 4

10. wall antlers, mode 4

10. wall antlers, mode 4

11. wall shield, mode 7

11. wall shield, mode 7

12. wall trophy, mode 7

12. wall trophy, mode 7

RecordModeHandlerT-states
1timed door, open ($21)4TIMED_DOOR_OPEN12,020
2timed door, open ($21)7TIMED_DOOR_OPEN106,094
3cyan door ($0A)0DOOR_LOCKED_A9,550
4A.C.G. door ($24)2ACG_DOOR342,086
5suit of armour ($1E)3DRAW_DOOR69,997
6suit of armour ($1E)3DRAW_DOOR69,978
7pumpkin picture ($25)0DRAW_DOOR6,421
8ghost picture ($11)0DRAW_DOOR9,018
9A.C.G. shield ($1C)4DRAW_DOOR8,374
10wall antlers ($15)4DRAW_DOOR8,749
11wall shield ($1D)7DRAW_DOOR37,332
12wall trophy ($16)7DRAW_DOOR38,525

The dearest was the A.C.G. door in mode 2, at 342,086 T-states, where the cyan door in the north wall, mode 0, took 9,550. The A.C.G. door is the biggest graphic in the room and drawn in a turned mode, which costs most (below).

3. Everything else in the room

At the end of that first pass DRAW_ROOM_CONTENTS draws every object and monster whose room byte is this room, once, with DRAW_THING -- here only the knight himself, in the doorway -- and the loop sets ROOM_DRAWN. From the second pass on, the loop dispatches the objects and monsters again, each handler redrawing its own sprite when it moves, and the room list's handlers repaint only their colours. The room at the end of that first pass, and after the 16 frames in which the knight walks himself in (the arrival walk, on the doors page):

The first pass done: the room complete, the knight on the south door.

The first pass done: the room complete, the knight on the south door.

The arrival walk done.

The arrival walk done.

From the door to the second pass took 1,557,478 T-states -- 22.3 frames, 0.44 of a second -- in which nothing else moved:

StageT-statesShare
ENTER_ROOM and MARK_ROOM_VISITED: the far side of the door, the new position and heading, the room's bit in ROOMS_SEEN5900%
CLEAR_PLAY_AREA: 24 columns by 192 lines of zeros136,4729%
DRAW_ROOM: the room's colour into 24 × 24 attributes16,9271%
DRAW_ROOM: the outline (DRAW_OUTLINE, DRAW_LINE, a pixel at a time)620,69040%
PAINT_PANEL: the scroll's colours11,0111%
DRAW_INVENTORY: the three carried things26,1362%
The first pass: the 12 doors and pieces of furniture in the room's list718,14446%
The first pass: DRAW_ROOM_CONTENTS26,8572%
The rest: the new-room sound, the loop, the end of the pass6510%
From the door to the second pass1,557,478100%

The outline is the dearest part: every pixel of it is placed by a line stepper whose slope comes from a shift-and-subtract division (DIVIDE_SLOPE), and a square hall's outline is long.

Eight ways to lay a picture down

A door or piece of furniture is stored once, the right way up, and its +$05 byte says how to lay it down: the top three bits are a mode, 0-7, that picks one routine from PIXEL_DRAWERS for the pixels and one from COLOUR_DRAWERS for the colours; bits 1-0 pick how the pixels meet the screen, written into each drawer's loop by SPRITE_COMBINE_OPCODE (0 store, 1 OR, 2 or 3 XOR). Below, two graphics -- the suit of armour, which is symmetrical neither way, and a door -- each drawn in all eight modes by the game's own DRAW_SPRITE_PIXELS and DRAW_SPRITE_COLOURS on a blank screen, modes 0 to 7 left to right:

The suit of armour, modes 0-7.

The suit of armour, modes 0-7.

A door, modes 0-7.

A door, modes 0-7.

Each drawn picture was then compared, pixel for pixel, with the stored picture put through each of the eight symmetries of a rectangle (by the Python imaging library), which gives the fifth column; the last two count the door and furniture halves in the castle's template drawn in each mode:

Mode+$05PixelsColoursThe picture, as drawnDoor halvesFurniture halves
0$00BLIT_SPRITECOLOURS_AS_STOREDas stored11065
1$20BLIT_SPRITE_MIRROREDCOLOURS_MIRROREDmirrored left to right00
2$40DRAW_TURNED_RIGHTCOLOURS_TURNED_RIGHTturned a quarter clockwise212
3$60DRAW_FLIP_ANTIDIAGONALCOLOURS_FLIP_ANTIDIAGONALreflected in the other diagonal (bottom-left to top-right)9315
4$80DRAW_UPSIDE_DOWNCOLOURS_UPSIDE_DOWNupside down9730
5$A0BLIT_SPRITE_FLIPPEDCOLOURS_HALF_TURNturned a half turn30
6$C0DRAW_FLIP_DIAGONALCOLOURS_FLIP_DIAGONALreflected in the leading diagonal (top-left to bottom-right)00
7$E0DRAW_TURNED_LEFTCOLOURS_TURNED_LEFTturned a quarter anticlockwise9427

Mirroring costs the copy loop a DEC DE for an INC DE and a trip through REVERSE_BITS for each byte; turning upside down, one call to START_AT_LAST_ROW first; the four modes that turn or reflect in a diagonal build each screen byte from one bit of eight rows, which is why they are slower. A door's mode is its wall -- the picture of a door in the north wall, turned, serves the other three -- and bit 6, set for the four diagonal modes, is also what PLAYER_AT_DOOR reads as “in a side wall”. The labels of modes 3 and 6 follow the drawers' loops, not the geometry: DRAW_FLIP_ANTIDIAGONAL reflects in the diagonal from bottom-left to top-right, and DRAW_FLIP_DIAGONAL in the one a mathematician would call the transpose. Modes 1 and 6 are in no record of the template.

The colour tables, one per graphic from FURNITURE_COLOURS, are a width and height in cells and a byte per cell, walked in the same order as the pixels. A byte of $00 leaves the cell as it is, $FF writes the room's colour, and anything else is written as it is. Across the 39 tables there are 16 cells of $00, 97 of $FF and 338 of fixed colours; $FF is used by A.C.G. door, cave door, cyan cave door, ghost picture, green cave door, red cave door, skeleton, timed cave door, open, timed cave door, shut, wall shield, yellow cave door. So those take the colour of whatever room they stand in, and the four coloured doors are one graphic with four colour tables. Since the colours are repainted on every pass, a creature that crosses a door leaves it coloured correctly again a moment later.

One step of a moving thing

The player, creatures and objects are drawn another way: sixteen pixels wide, any height, placed at any pixel and XORed on, so that drawing the same thing twice rubs it out. A handler starts with ACTOR_TO_WORKSPACE, which copies the sprite and position into the workspace before the record is moved, and ends by redrawing: REDRAW_MOVED sets up the new picture in one register bank (SETUP_SPRITE_DRAW) and the old in the other (SETUP_ERASE); ALIGN_ROWS deals with the rows the two do not share, and ERASE_DRAW_ROWS does the rest two at a time, swapping banks with EXX: erase a row of the old, draw a row of the new, bottom up.

Followed in the simulator, one instruction at a time: in room $00, the knight walking up and to the right, from ($60, $8A) to ($62, $88), sprite $07 to $07. The 18 rows erased and 18 drawn went: the old picture's rows on lines 138, 137 erased on their own; then 16 pairs, each erasing a row of the old picture and then drawing the new picture's row on the same line, from line 136 up to line 121; then the new picture's rows on lines 120, 119 drawn on their own. So each line of the screen changes once, from old to new, and there is no moment at which the knight is missing. The pixels before, after the rows only the old picture had, halfway, and at the end, four times size: white where a pixel was set before and still is, red where it has been rubbed out, green where it has been drawn:

One step, in four moments.

One step, in four moments.

The shift that places the sprite at its pixel costs no loop. Each row's two bytes go into HL and A is cleared; SHIFT_CHAIN is seven copies of ADD HL,HL / ADC A,A, each shifting the three bytes A, H, L one pixel left, and SETUP_SPRITE_DRAW writes the displacement of the JR in front of it so that the jump lands part-way down the chain. Run for each x AND 7 on a copy of the game, it writes:

x AND 7JR displacementLands atShiftColumns touched
0$E8DRAW_UNSHIFTEDnone2
1$00SHIFT_CHAIN + 07 shifts left3
2$02SHIFT_CHAIN + 26 shifts left3
3$04SHIFT_CHAIN + 45 shifts left3
4$06SHIFT_CHAIN + 64 shifts left3
5$08SHIFT_CHAIN + 83 shifts left3
6$0ASHIFT_CHAIN + 102 shifts left3
7$0CSHIFT_CHAIN + 121 shift left3

Seven shifts left of three bytes leave the picture one pixel into the first byte, and no shift leaves it on the byte boundary, so the sprite's left edge is at x itself; with no shift only two columns are touched. The erase side has its own chain and its own patched JR (ERASE_SHIFT_CHAIN).

Then the colours. DRAW_FROM_RECORD compares the old and new position to choose one of the fillers in COLOUR_FILLERS; each paints the thing's block of cells -- two columns, three when it is not on a cell boundary, as tall as the sprite -- in its own colour, and the column or row it has just left in the room's (two of the eleven entries, for combinations that cannot happen, point at INERT_SPRITE):

IndexMovementFiller
0stillCOLOUR_STILL
1moved rightCOLOUR_MOVED_RIGHT
2moved leftCOLOUR_MOVED_LEFT
4moved downCOLOUR_MOVED_DOWN
5moved down and rightCOLOUR_MOVED_DOWN_RIGHT
6moved down and leftCOLOUR_MOVED_DOWN_LEFT
8moved upCOLOUR_MOVED_UP
9moved up and right (this step)COLOUR_MOVED_UP_RIGHT
10moved up and leftCOLOUR_MOVED_UP_LEFT

The cells round the knight before and after the same step, lines on the cell boundaries: the white block goes with him and the room's red comes back behind.

The step's colours, before and after.

The step's colours, before and after.

One line of the knight is left red, at the top of his helmet. DRAW_FROM_RECORD works the block's height out from the sprite's (its rows over eight, by an RRCA sequence, plus one): the knight's 18 rows make 3 cells, counted up from the cell his bottom line is in. At y 136 his rows run from line 119 to 136, across 4 cells, so the top line is outside the block; the simulator's screen shows 2 of his pixels standing in a cell not painted his colour.

So a creature carries its colour with it and leaves the room's behind; where two things overlap, whichever is painted last colours the shared cells, which is the attribute clash the game lives with. Doors and furniture get their colours back on the next pass.

The scroll

The status panel is drawn once per game by DRAW_SCROLL: a character set of its own laid out 8 by 24 cells from x 192 (see the graphics). During a game only its contents change -- the clock (TICK_CLOCK), the score (ADD_SCORE), the roast (DRAW_FOOD), the lives (DRAW_LIVES) and the inventory (DRAW_INVENTORY), all on the quest page -- and its colour, which PAINT_PANEL sets on every room change: the room's ink complemented (bright green where that would be black or blue), with fixed bands of magenta, white, cyan and yellow behind the clock and score. Room $07 is cyan, room $00 red:

The scroll in room $07 and in room $00.

The scroll in room $07 and in room $00.

Where the time goes

Once the room is drawn, a pass of MAIN_LOOP handles every object and creature in the room and every big monster wherever it is, and between records calls FRAME_TICK whenever FRAMES has moved, so the player moves once a frame whatever the pass costs. The game was run on from the arrival above for ten seconds of emulated time, the knight walking right, left, left, right, up and down a second each and creatures arriving as they do. Every instruction's T-states were charged to the record being dispatched at the time (IX, read as the dispatcher jumps) and to the labelled routine it is in. 213 passes ran in 502 frames: 164,746 T-states a pass on average (104,550 – 379,675), 2.36 frames, about 21 passes a second. At the end the three creature slots held sprites $5A, $99, $4F.

ForT-states a passShare
The player's handler: MOVE_PLAYER and the collision tests, the spawner, the drain, the redraw56,85935%
The three creature slots: movers and redraws, or INERT_SPRITE's delay for an empty one34,49621%
Doors and furniture from the room's list: their colours30,14818%
MAIN_LOOP itself: walking 119 object records and 8 monster slots, checking FRAMES between each28,75717%
The big five, wherever they are5,0343%
The sound slot: effects beeped a frame at a time4,1713%
The ROM's frame interrupt2,6362%

The labelled routines that took the most:

RoutineT-states a passShare
TEST_ROOM_BOXES17,27610.5%
MAIN_LOOP11,5157.0%
NEXT_OBJECT10,8276.6%
SHIFT_CHAIN8,5715.2%
ERASE_DRAW_ROWS8,3785.1%
XOR_BYTE8,2035.0%
ERASE_SHIFT_CHAIN7,7094.7%
BEEP_BC6,1423.7%
SCREEN_ROW_UP5,8443.5%
COLOURS_TURNED_RIGHT4,2742.6%
SHIFT_AND_PLOT4,0982.5%
FETCH_ROW4,0892.5%

The player's share is mostly the walk test: TEST_ROOM_BOXES walks the room's list twice a frame, once for each axis, testing the step against every door and table (see movement). The sprite routines -- the shift chains, the XOR plots, the row loop -- come next; then the doors' and furniture's colours, repainted every pass. These are T-states of the simulator, which has no memory contention: a real Spectrum loses more wherever the code reads or writes the screen.

What is confirmed and what is inferred

Inferred, or not tried: