Sunday, 9 August 2026

Timebomb Part 2 - Bug Fixes and Code Optimisation

Following on from the previous post where I took a VIC20 BASIC game and rewrote it in 6502 assembler, I have a few issues to address.

  • Fix bugs that I introduced
  • A look at maze generation
  • Fix bugs in the original version
  • Tidy up the code
  • Optimise the code for speed and size
  • Improve readability and portability by using names constants
  • Test this by writing a PET version

Fix bugs that I introduced

First up, I need to fix some bugs which I introduced in the rewrite.

The first is an odd one, that is no immediately obvious reason how and why it happens.

Normally, the player starts in the middle of the screen, which is actually the bottom of the maze. There is a maze below, but they have to move upwards as there is a solid row of walls two lines below them.

Sometimes (a wonderfully vague and unhelpful term in troubleshooting), sometimes, that solid wall that demarcates the bottom of the maze is broken.

I have highlighted the line in black and you can see gaps.

If you move below that line, it confuses things and it all goes wrong.


I initially "fixed" this by fixing the symptom, that bottom line being broken, rather than the cause.

I added some code to check to see if the maze generation was trying to place a path on that line and blocked it and sent it scurrying back up into the safe bits of the maze.

Later on, I worked out what was happening, and why it was happening, but to understand that, I need to look at how the maze is generated. 

It is a random maze, freshly generated every time you start a new game, or the old one gets blown up because you didn't find the bomb in time.

A look at maze generation

To generate the maze, it first fills the maze with walls.

In the starts in the middle and creates paths.

(this is a modified debug build which draws the maze generation progress, see the github link at the end)

It picks a random direction, and sees what is two squares away in that direction. If it is a wall, it moves to that square and creates a path on the way.

It keeps moving in that direction until it hits something which isn't a wall, a previous path, or the edge or the maze.

At that point, it turns 90 degrees counter clockwise and tried to continue in that direction.

That repeats until it has turned a full 360 degrees, at which point, it backtracks to a previous square and tries again.

This continues until it gets back to the start.

That all works well, but I skipped over one detail - how does it know when it hits the edge of the maze?

It is 66 rows of 22 characters

It might look 2D.

It might even look 3D with the red and blue fringing.

But it is in fact 1D.

This is all dealt with linearly, a single run of 1452 squares (66 * 22). To move up or down, it jumps 22 squares in either direction, the width of the maze.

If it were generated using XY co-ordinates, it would be much easier to limit things, but I expect some of the other calculations would be more difficult. I don't know, maybe I will try to rewrite it like that, but not today. (please not today, this is already going to be a very long post)

To mark out the top of the maze, it adds a row of spaces. You can see this at the top where you also get to see what is in RAM before the start of the maze (I could fix that, but I have decided to leave it - for now).

You can see from that the right hand side also is a row of spaces.

This continues along the side of the whole maze.

I did think about trying to disguise that by shrinking the screen, or setting the spaces to be solid blue, but it didn't look right, so I left it alone.

In the generation algorithm, when it finds something which is not a wall, it stops and goes off in a different direction, creating a boundary.

That is the top and the side, what about the bottom?

Well, immediately following the maze in RAM is the top of video RAM, so on the original version, when it was generating the maze, the video RAM was full of spaces, other than the "MAKING MAZE" message on the second line.

A whole row of spaces, creating the 4th boundary of the maze.

How Did I Break It?

When I rewrote the maze generation in 6502 assembler, it was so lightening fast that I didn't actually need the "MAKING MAZE" screen, so I simply removed it.

The first time the game runs, the video RAM is probably going to be the BASIC screen.

That's fine, it is unlikely any of the characters will be walls, so that will work as the bottom boundary of the maze.

When you lose the game and it needs to generate a new maze. At that point, the screen contains the remains of the old maze, INCLUDING SOME WALLS.

Depending on where you finished, that might include places where there were runs of walls.

If it just happens that there is an old wall at the point the maze generator was poking it's nose into the video RAM, it will make a path there.

That won't happen very often, which is why this only happened "sometimes", and I now know never on the first run, which is why I didn't see it very often.

It is nice to get to the point where you understand why something happens, and more importantly, why it only happens sometimes.

To fix that, I moved the maze up in memory and added a row of spaces between the bottom of the maze and the top of video RAM.

A Random Bug

One random bug I caught was in the code which places the timebomb.

Part of that process needs a randomly generated horizontal location.

The maze is 22 characters wide. There are walls left and right, and is a space on the far right, making 19 possible locations. 

The original code used int(rnd(0)*20) which will generate a random number from 0-19. (I think that might be a an error in the original, it only needs an offset of 0-18 to make that?)

I got the random number for 0-19 wrong in the first version. My plan was to take a number from 0-3 (4 options) and another from 0-15 (16 options) to get a number from 0-19 (20 options), but it doesn't work like that. There were two ways to generate 0, and no way to get 19, the maximum was going to be 3+15 = 18.

The RAND16 function generates a 16 bit number. The MSB is returned in the accumulator (and also SEED_MSB), and the LSB is in SEED_LSB.

Here I take the MSB in the accumulator and AND with $03 to get a number from 0-3.

I next take the LSB and AND that with $0F to get a number from 0-15, and add those together.

jsr RAND16
and #$03
sta TEMP
lda SEED_LSB
and #$0F
clc
adc TEMP

The fix was fairly simple, rather than the CLC to clear the carry, I used ROR to rotate one bit of the LSB into the carry.

jsr RAND16
and #$03
sta TEMP
lda SEED_LSB
ror
and #$0F
adc TEMP

The final ADC operation is now adding 0-3 to 0-15 and 0-1, giving a number from 0-19. (AND will never generate a carry, so does not touch the flag set in the previous instruction)

Testing that in practice, I did indeed see locations far left and far right. Sorted.

Fix bugs in the original version

There were a few issues I found that were present in the original.

One was the starting square was usually a wall, so when the player moved away, the wall the close up behind them.

Sort of a "reverse Homer".

That could lead to getting stuck in inescapable sections or blocking off a better route, so I hard wired it to mark that square as a path.

Another issue is also shown in those two pictures above.

The maze is composed of 66 lines of 22 characters.

This sits in memory directly below the screen RAM, 23 lines of 22 characters.

During the game, the screen is a view of 23 lines within the 66 lines maze.

The player is always on the middle row, and there are 11 lines of maze above, and 11 lines below.

That is fine when you are in the middle, but when you start, you are on the 64th line.

11 lines above that is line 53, so the screen will show lines 53-66, but what about the remaining 9 lines?

If you look at the starting screen.

This is composed of three parts, I have highlighted those below.

The maze is stored in RAM between the end of the program and the start of the screen RAM.

The red section at the top is a copy of last 14 lines of the maze.

The last line is highlighted in black. This is always a solid line across the maze to stop the player moving below (at least it is with the maze generation fixed).

Beneath that, highlighted in blue, is a copy of the next 9 x 22 characters in RAM following the maze.

This is actually the top of the screen RAM, so it copies that instead.

You get the maze lines 54-64, and then top of the screen, which is the maze lines 54-62

I have copied the blue squares up to the top so you can see.

That helps make the maze look larger, but the solid line stops the player moving into the "shadow maze".

When the player moves up, the maze moves down, and the pattern beneath the solid line changes to a copy of what is at the top of the screen now.

Previously what you saw was maze lines 54-64, followed by lines 54-62.

But now you get lines 53-64, followed by lines 53-61, so the pattern below the line changes.

That can be a little confusing or distracting if it catches your eye, and it continues until the solid line is off the bottom of the screen and the whole view is of valid maze.

I have highlighted the end of the maze in black, and as an example what is initially a 2 square section below that line.

When the maze moves up, the top of the screen changes, and so does the copy, so it looks like the layout is changing.

I fixed that when I added the row of spaces to fix the "hole in the wall" issue.

I rewrote that section so that rather than relying on the screen data being directly below the maze, there was now a lines space, so the original didn't work.

I changed it so that when it detected it had reached the end of the maze, it reset to a specific location, always line 54, matching what would have been shown on the start screen.

That means the maze under the line, will now remain consistent. It is a repeat of part of the maze you can already see, so it is not giving anything away as it would if I had duplicated a different portion of the maze. I did consider using the top of the maze, but that might give you hints if you knew it was that.

I picked an example with a similar two square wall on the left.

You can now see it stays intact when the maze moves.

The final one in this area was another of those "sometimes" issues.

I would occasionally see one of the squares around the middle row disappear at the start, and then reappear when the maze scrolled.

I thought I had got some screenshots, but I can't find them (they have also disappeared). So these are cheated (by modifying video RAM).

It was usually one on the left, just above the middle.

Did you see it (or rather not see it?)

This was another one that didn't make any sense until I worked it out.

When the player moves, it clears the old square and then redraws the player in the new square. (the process was slower in BASIC, so you can see the player character flickering between clear and redraw - that is something else I optimised out, don't redraw the maze if the player hasn't moved)

When the game first starts, the old square is initialised to $0000, so it writes the space character to that address (good job this is not a Commodore 64).

When you fail to find the bomb in time, it explodes, destroying the maze, so a new one has to be built.

The original, used RUN to restart the program (and then spent a minute creating a new maze). As part of that, the old square would have been initialised to $0000 again.

What I (finally) realised, is when you complete a round successfully, and restart the game, it does not reset the old position, so when you first move, it overwrites the old position and draws the new position.

But the old position was from the end of the last round, and if it turns out to be a wall now the maze has scrolled back to the start, it clears the wall until the maze scrolls, when it reappears.

Again, it makes sense once you work it out, but until then I just had a random square that sometimes flickered when a new round started.

That was an easy fix, I just set the old position to $0000, why not, that is just to STY commands. (Y is always 0 in my world, see the previous post)

Tidy up the code

Whilst I have been going through fixing things like that, to make things clearer, I have been expanding the comments where appropriate, and looking around for unnecessary code etc.

I found a few more variables that were set to a constant value, and then not changed, so I just changed those to be load the constant value when required. (those are a hangover from the BASIC version)

I found quite a few bits of similar or repeated code that I could combine, and things that could be rearranged to make more sense.

This was particularly true in the joystick section, where I was able to change a large block of code which set direction and player character if a direction was detected, part of which is shown on the left.

The new version (on the right above) load an index in the X register and later in a single block of code, uses an existing array, and a new 4 byte array to set the direction and player character once.

There were several places where multiple things were being set to the same thing, so shuffling code around could save the duplication.

There were also a few times variables were used for temporary storage that I was able to optimise out by using the X register as a temporary store.

Optimise the code for speed and size

Time for some more top tips?

Top Tip - Optimise Loops # 1

There is often quite a few things that can be done to tighten code loops. This can reduce size and increase speed, both quite useful things.

This loop writes wall characters to an area of memory. It keep going until the MSB reaches an address after the end.

    lda #CHAR_WALL
-
    sta (DST),y 
    inc DST_LSB
    bne -
    inc DST_MSB
    lda DST_MSB
    cmp #>MAZE_END+1    
    bne -

This increments the MSB, then loads it, then compares it, and tests the result.

With something like that, one trick I like to use is to preset X to the compare value.

You can then skip loading the value and compare X to the store value and save some instructions.

    lda #CHAR_WALL
    ldx #>MAZE_END+1
-
    sta (DST),y 
    inc DST_LSB
    bne -
    inc DST_MSB
    cpx DST_MSB
    bne -

Top Tip - Optimise Loops # 2

Another thing when you have a more complicated compare to do it to simply avoid doing it.

There is one bit of code that writes spaces all down the right hand side.

But after the adding, it needs to check if it has reached the end each time

-   lda #CHAR_PATH
    sta (DST),y
    lda DST_LSB
    clc
    adc #22
    sta DST_LSB
    bcc +
    inc DST_MSB
+
    lda DST_MSB
    cmp #$1E
    bne -

That is a simple example, sometimes it would be a 16-bit comparison.

Here, the start point and end point are fixed, so it is always going to run the same number of cycles, the 66 lines between them. So why not just turn it into a 66 cycle loop?

    ldx #MAZE_HEIGHT
-   lda #CHAR_PATH
    sta (DST),y
    lda DST_LSB
    clc
    adc #22
    sta DST_LSB
    bcc +
    inc DST_MSB
+
    dex
    bne -

Top Tip - Optimise Loops # 3

You can do similar things with other loops, avoid the comparison by counting down to zero.

    ldy #0
-   lda (MAP),y
    sta (SCREEN),y
    iny
    cpy #22
    bne -

Can become

    #MAZE_WIDTH-1
-   lda (MAP),y
    sta (SCREEN),y
    dey
    bpl -

In this case, Y need to got from 21 to 0, so the comparison is BPL which will stop when it hits -1

(as long as it does not matter that it is copying the last character first. The end result is the same, nothing is reversed, only the order that things happen in.)

Top Tip - Optimise Loops # 4

One rule to consider with loops is to move anything you can out side of the loop to speed it up.

And example is toggling the clock pin on an IO port 8 times as fast as possible

One version could look like:

    lda #0
    sta COUNTER
-   lda DATA
    ora #CLOCK_PIN
    sta VIA_PORTA
    lda DATA
    sta VIA_PORTA
    inc COUNTER
    lda COUNTER
    cmp #8
    bne -

A more optimised version could be

    ldy #8
    lda DATA
    tax
    ora #CLOCK_PIN

-   sta VIA_PORTA
    stx VIA_PORTA
    dey
    bne -

Here the value to be written (the contents of the DATA byte with and without the clock bit set) are pre-loaded into A and X and can then be written out to the port without reloading or recalculating.

Top Tip - Optimise Loops # 5

Continuing with that example, if you really want to go as fast as possible, but are less concerned about space, you can unroll the loop.

    lda DATA
    tax
    ora #CLOCK_PIN

    sta VIA_PORTA
    stx VIA_PORTA
    sta VIA_PORTA
    stx VIA_PORTA
    sta VIA_PORTA
    stx VIA_PORTA
    sta VIA_PORTA
    stx VIA_PORTA
    sta VIA_PORTA
    stx VIA_PORTA
    sta VIA_PORTA
    stx VIA_PORTA
    sta VIA_PORTA
    stx VIA_PORTA
    sta VIA_PORTA
    stx VIA_PORTA

That will toggle the clock pin 8 times without any delays between instructions.

The only way you could speed that up further is to arrange for the IO port to be mapped into zero page (which I have done before).

Fitting It All In Under 1K

Although I mentioned in the previous post that the code was a little over 1K in size and it would be nice to get it below 1K, for no real reason, I wasn't actively trying to achieve that.

However, as I was optimising things to make it better or faster or clearer, I did end up shedding quite a few bytes, so I had another pass through the code to see if there was anywhere I could make savings without affecting performance or readability of the code.

I had made sure that Y was always going to be left as zero, so I removed a couple of places where it was reset to 0.

I changed a few JMP label instructions to BNE label, in cases where label was within 127 bytes, and the zero flag was going to be clear, often the previous instruction was lda #constant and constant was not zero.

There were a few places I could reorder things to the code dropped through to the right place and didn't need any jumps or branches.

There were also a few hangover variables stored in the $03xx region. I moved those into zero page, which saved 1 byte per access.

Almost there. There was one variable which indicated if the bomb has been found. It only ever contained 0 or 1. This is where it was set to 1.

lda #1
sta f

I changed that to

inc FOUND

That saved the final two bytes.

After all that, the current version is now down to 977 bytes.

I am happy with that, I would take the rest of the day off, but it's past midnight already.

Improve readability and portability by using named constants

I have used code examples from various version, in the more recent ones, I have tried to get rid of all the literals like 22 and $1E00 etc. and replace those with named constants.

That should make it easier to follow, and often it makes the comments redundant.

; start in the middle
lda #<MAZE_MIDDLE
sta CURRENT_SQUARE_LSB
lda #>MAZE_MIDDLE
sta CURRENT_SQUARE_MSB

I have also gone for calculating all the positions and offsets in the constants. Avoids the need for code which loads the address of the start of the maze and then adds and offset.

If both of those values are constants, why not calculate them in advance and then just load the pre-calculated value in.

MAZE_WIDTH = 22
MAZE_HEIGHT = 66
MAZE_TOP = MAZE_END -(MAZE_HEIGHT * MAZE_WIDTH) + 1
MAZE_END_OF_TOP_LINE = MAZE_TOP + MAZE_WIDTH - 1

Test this by writing a PET version

One advantage of not having any screen size or address information in the code, and having it all in one location in the defines file means it is easier to change.

To test this, I wanted to write a PET version. This is similar to the VIC20 version, much of the code remains the same, only reading the joystick, making the sounds and the size and location of the screen change.

I did look at making the screen wider, but that made the maze too complicated. Maybe I could offer the user a choice of difficulty at the start, but the one on the VIC just seems about right, and so I have a 22 character maze in the middle of the screen.

I still need to write a new sound routine to make the ticks and explosion, but other than that it is working and looks rather good in green and black.

The updated VIC20 code is on github, I will upload the PET version once I have added the missing bits.

I now need to read my own blog post on PET Sounds.


All items are still available, and I can ship worldwide.

International shipping is a bit complicated at the moment, so if you want to order from outside the UK, please see this page for more information:

Sorry to have to do this, and I know this is going to affect sales, but please understand it is the only option I have right now.

Patreon

If you enjoy posts like this, you can support me via Patreon, and get access to advance previews of blog posts, and exclusive posts.

You also get progress updates on new projects and other behind the scenes updates, as well as access to my Patreon only Discord server for even more regular updates, and to discuss your own projects.

I currently have 50% off your fist month if you want to try it out.

Sunday, 2 August 2026

Timebomb Part 1 - Speeding It Up

"Listen Willard, there's a kind of a little hootymajig thing in there... a wheel of some kind. Why don't you try turning the wheel, Willard?...

Oh, I don't know... turn it to the left and see what happens...

Yeh!...I can hear it, Willard, it's ticking a lot faster, isn't it?...

You'd better turn it back, Willard!"

Bob Newhart - Defusing a Bomb

This is a post I started about two years ago, and never got around to finishing it.

Now I have finally finished it.

It started after I went through a list of someones top 22 VIC20 games. There were a lot of odd choices on there, but one I quite liked was Timebomb.

I like maze games, and this was quite good, scrolling up and down a maze looking for a bomb.

If you don't find the bomb in time (about 80 seconds), it explodes, destroying the maze, so it has to build a new one so you can try again.

If you find the bomb in time, you restart the same maze, but each round you have 5 seconds less.

As the maze is the same, you should get increasingly fast at it, once you discount all the dead ends.

It decrements each round but always lets you have at least 15 seconds.

However, there were a few things I didn't like.

The main one was there was a full minute to wait whilst it was "Making (the) Maze".

It also takes another full minute to create a new maze if you didn't find the bomb in time.

At the top of the maze, it looked like you could see code on the screen

There was no on screen countdown to shown when the bomb was going to go off

There were no instructions to tell you what to do.

Although this was considered to be added to the Penultimate Cartridge, it didn't make the cut, mainly due to the glacial speed of the maze generation.

Later on, some space became free in the cartridge, before the next release, so I wondered if I could have a go at speeding up the maze generation, and maybe some of the other things and getting it into the cartridge.

I did some initial work and made good progress, but somewhere along the line I messed something up, and it was drawing the maze, but the movement was all wrong.

Ironically, the clock was ticking, and I ran out of time, and the bomb exploded.

This was put to one side and I moved onto finishing the cartridge and testing, testing and more testing of all the games within.


I went back once or twice to see if I could find what was wrong, but didn't see anything, so again it got put aside.


Back to today, and it's time for another refresh of the cartridge. I have managed to free up quite a bit of space, so let's finally have a proper look at Timebomb, and get it working.


The Original Version

I had a look around and found the original was actually a type-in game by Doug Smoak from Compute! magazine in July 1983.

It is a very short game, so any deficiencies are understandable.

There are no unnecessary spaces, it is very compressed.

To make it easier to follow, I added all the spaces in.

This was just to make the program more readable. It didn't actually work with all the spaces added to the program, the file size increased from 1843 bytes to 2574 bytes, which did not leave enough space in the unexpanded VIC20 to draw the maze.

Top Tip - PETCAT

PETCAT is a very handy program that is part of the VICE suite. It can be used to list PRG files as text, or turn text into PRG files.

petcat timebomb.prg > timebomb.bas

Will generate a text file with a listing of the program.

petcat -w2 -l 1001 -o timebomb.prg -- timebomb.bas

Will do the reverse and generate a PRG from the listing

-w2 = BASIC V2,
-l 1001 = Load address $1001 (unexpanded VIC)

I did considered rewriting it in C, but looking through the code it is mostly poke and loops, and the data statements contain some assembler routines.

So, I decided to write it all in assembler.

What could possibly go wrong.....

Actually, it wasn't that bad.

I worked through it line by line, starting with the spaced out line in comments and added assembler below to recreate the functionality.

Top Tip - Line Numbers

This program is a good example of speeding things up (a little) with careful use of line numbers.

The code is numbered from 2-42 (line 1 was probably a comment that got removed).

The line numbers themselves are stored as a two-byte value, so the actual number used does not, in itself, make a difference.

1 PRINT "HELLO"
10 PRINT "HELLO"
1000 PRINT "HELLO"

All take up the same space, as all the line numbers are two bytes.

However, when you have a GOTO or a GOSUB, the destination line number is stored as text characters, so the longer the line number, the more bytes it takes up in the file, and the longer it takes to parse.

2 GOTO 1
20 GOTO 10
2000 GOTO 1000

The GOTO 1 part is three bytes (the token for GOTO 1, the space and the character 1).

GOTO 10 is four bytes (GOTO, space, 1 and 0).

GOTO 1000 would be six bytes.

The space after the line number you get for free, that is not stored, it is always displayed in listings. The spaces within the line are stored as part of the program, so add bytes to the file and slow the parsing.

GOTO1 is even better (if less readable) at only two bytes.

This program also moves the least used functions (the initialisation) to line 19 onwards, so the main loop is at line 5, saving a few bytes in various GOTOs.

Development Environment

I had a question about this, so here is some more information on what I use for projects like this.

I use some ancient command line assemblers. These are the ones I have been using for years, they just work and do what I need, and I know how to work around any little issues they may have.

  • For 6502 I use ACME.
  • For Z80 I use TASM.

I run those on linux, ACME.EXE and TASM.EXE are DOS programs, so I use Wine to run those.

I am not a fan of Makefiles, they allways seem to make things overly complicated.

I use simple command line builds, sometimes in shell scrips.

All it usually takes is something like

wine TASM.EXE -t80 -b Lambda8300.asm Lambda8300.bin

or

wine ACME.exe timebomb.asm

or if you want a listing file

wine ACME.exe -r timebomb.lst timebomb.asm

To edit, I use various text editors.

I have been using Visual Studio recently, it is a nice editor which many useful features. But also some absolute howlers that I can't believe a dedicated code editor is so bad at.

I like the split views, being able to see multiple files at the same time, even safely edit the same file in multiple windows. That is very useful to have function in one window and the caller in another etc.

There is a powerful regular expression based search and replace that I used a lot for things like reformatting the Lambda 8300 ROM code.

e.g. sub_([0-9a-f]{4})h  => SUB_\U$1 

Would change all the labels like sub_da42h to they style of SUB_DA42

However, there are a couple of major issues I have.

Firstly, the menu keeps locking up so I can't open it. That is the only place there is a "save all" button. Luckily (and probably because they forgot to disable it) ctrl+S still works, but I have to do that regularly until the menu decides to start working again.

Autocomplete is very overzealous.

"Hey, I see you are typing a number. You have typed a number before, it was a different number, but I assume that must be the one you like. Let me change it to the other one for you."

No.

I want to type several different number, that's not unusual is it?

Also, I can't use things like LDA #255 for decimal numeric constants, which is annoying, I have to convert everything to hex LDA #$FF.

If I use # followed by any 3 digit number, even in an asm file, it thinks

"ah, that must be an HTML colour code. Let me add a little square in that colour to annoy you"

"oh, and if you click on it to try to get rid of it, I will change it to 225555"

What?

You just broke my code.

"Oh, and if you try to find the setting to disable it, I will fill you nice neat folders with more folders and XML files. And still not give you the option to disable it."

I ask you, what kind of person would let features like that through?

Who? Only a yoghurt.

Code Breakdown

The code is broken down as follows:

1 - missing, presumably a comment
2 - initialisation then gosub 29

29 - copy code to cassette buffer then return
30-36 data statements

3 - initialise some constants then goto 19

19 - initialise maze creation
20 - sys 861 then clear screen and print "MAKING MAZE"
21 - add spaces down the right (why?) and a line of spaces at the top
22 - 26 - create maze
27 - randomly place the timebomb
28 - sys 830 then set the start point then sys 923 and finally goto 4

4 - play the starting sounds

5 - main loop, if bomb found goto 40
6 - increment time, if timed out, goto 37
7 - ominous ticking sounds
8-14 - read joystick and move
15 - if you tried to move into a wall, go back where you were
16 - if you found the bomb, go back to 5 (which will goto 40)
17-18 - redraw the maze then go back to 5

37-39 - the bomb explodes, gosub 42 then run (full restart)

40-41 - bomb defused, reset for next round, gosub 42 then got 27

42 - print "ROUND r" and "PRESS F7" then wait for F7 and return

The values in the data statements in lines 30-36 are POKEd into RAM in the cassette buffer region, always a good place to store your assembler routines.

The functions within are called from the code, and are as follows:

SYS 830 - setup VIC and fill the colour memory
SYS 861 - fill maze with walls
SYS 887 - scroll screen up
SYS 905 - scroll screen down
SYS 923 - redraw screen

Oh, so all the data is code? There are no data statements for the graphics?

They are interesting, the red and blue gives a sort of "I should be wearing 3D glasses" type effect.

But it's normal PETSCII graphics and a clever trick I haven't seen used like that before.

The wall characters are all the inverse circle, character $B1, the inverse of character $51, as seen here in the font ROM.

; Character $51
.BYTE %00000000 ; ........
.BYTE %00111100 ; ..OOOO..
.BYTE %01111110 ; .OOOOOO.
.BYTE %01111110 ; .OOOOOO.
.BYTE %01111110 ; .OOOOOO.
.BYTE %01111110 ; .OOOOOO.
.BYTE %00111100 ; ..OOOO..
.BYTE %00000000 ; ........

If I set the colours of the first 8 characters to colours 0-7, you can see the circles.

What is happening here is colour 10 is being used. Any colour greater than 7 triggers the multicolour character mode. Here the 8 pixels of the character are used to generate 4 pixels, each of which can be one of four colours.

00 - uses the background colour
01 - uses the character colour
10 - uses the border colour
11 - uses the auxilliary colour (the top 4 bits of $900E)

So the middle lines of the font are split from

.BYTE %01111110 ; .OOOOOO.

to

.BYTE %01 11 11 10

The first pixel is 01, so uses the character colour, in this case set to red.

The second and third pixels are 11, so use the aux colour (light blue)

The final pixel is 10, so uses the border colour, blue

You can see that better if I add spaces and set the alternate squares to colours 8-15.

The time bomb looks more threatening in this mode, but it is just as asterisk.

The player characters are a little bit of a let down, V, <, > and ^, but adding UDGs would be tricky with the way the memory is arranged.

Top Tip - ON -(x) GOTO n

One little trick that is used a few times in the code is ON -(x) GOTO n, the function of which is not immediately obvious if you are not familiar with it.

ON x GOTO 10,20,30

Will GOTO 10 if x=1, 20 if x=2, 30 if x=3 or continue for any other values.

It will even continue along the line, so you can have code for the "else" case.

ON x GOTO 10,20,30 : PRINT "none of the above"

In the Timebomb code there are things like line 17

17 on -(me>7921) goto 18 : sys 887 : me = me+22 : goto 5

This is saying if "-(me>7921)" = 1 then goto 18 else do the rest of the line.

The conditional (me>7921) returns -1 if true, 0 if false, so the - sign outside the brackets converts that to +1.

This makes more sense to me if I rewrite it as an IF THEN ELSE statement

17 IF (me>7921) THEN goto 18 ELSE sys 887 : me = me+22 : goto 5

Note the -1 is no longer required as IF is only testing for non-zero.

Quite handy if you need to save space, or want to obfuscate your code, or both.

Numbers

There were a few interesting challenges with numbers.

The standard numbers in Commodore BASIC are floating point, so can happily go over 255.

Floating point numbers are a difficult to work with in assembler. Several of the variables will never go over 255, so I can replace those with single byte variables.

One thing I missed is that BASIC variable as initialised to zero, so I needed to add that to the start.

Top Tip - Always Leave Y=0

I have found it useful over the years to get into the habit of leaving the Y register in the 6502 set as 0.

That means I can initialise variables at the start

L1:

    ldy #0                  ; (leave Y as 0)
    sty OM_LSB              ; om = 0
    sty OM_MSB
    sty F                   ; f = 0
    sty C                   ; c = 0
    sty R_UNITS             ; r = 0
    sty R_TENS
    sty K_UNITS             ; k = 0
    sty K_TENS

And later on, anything that needs to be set to 0, I just use STY

; 41 poke v-1, 0 : f = 0 : k = x : ...
L41:
    sty VIC_NOISE_FREQ      ; v-1 = 0
    sty F                   ; f = 0
    lda X_TENS              ; k = x, start K counter at X
    sta K_TENS
    sty K_UNITS             ; X_UNITS is always 0

A handy 6502 feature is being able to use two sequential bytes in zero page as a pointer, however the only addressing mode which supports it needs X or Y.

lda (ZP_PTR),y

That loads the accumulator with the value read from address make up from the values stored at those two bytes and Y, (ZP_PTR) + 256*(ZP_PTR+1) + Y.

Quite often I just want the value at that address, so I need Y to be 0 for those accesses, another reason to leave Y set to 0.

Some of the number are addresses of things like the VIC chip and the VIA, which make the BASIC program smaller, but in assembler, it is easier to just hard code the values (since they do not change).

3 d = 37154 : p1 = d-3 : p2 = d-2
8 poke d, 127 : p = peek(p2)

Can be replaced with the hard coded values

lda #$7F
sta $9122
lda $9120

Or, it is better to used named constants for things like that.

VIA1_PORTA = $911F ; p1
VIA2_PORTB = $9120 ; p2
VIA2_DDRB = $9122 ; d

lda #$7F ; set joystick 3 as input
sta VIA2_DDRB
lda VIA2_PORTB ; read the port

That makes it easier to follow.

There are some other numeric variables, which are used as counters which count to numbers in the hundreds, so cannot be stored in one byte.

Normally you would store these as a 16 bit value in two bytes, but any arithmetic involving 16-bit numbers is complicated as there are no 16-bit arithmetic instructions in the 6502. Any time you need to do any, it requires separate chunks of code to work on the LSB, and if required, the MSB.

Even comparison between two 16-bit numbers is complicated, and I think this is where I messed up on the first version of this code.

Top Tip - Comparing 16 bit numbers

I will start with comparing two 8 bit values.

    LDA val1
    CMP val2

The zero flag is set if val1 = val2

The carry flag is set if val1 >= val2

So you can test with

    BEQ numbers_are_equal
    BCS val1_larger_or_equal
    BCC val2_larger

With a 16 bit number, you compare the MSB first. If they are the same, then compare the LSB instead.

    LDA val1_msb
    CMD val2_msb
    BNE go_compare    ; MSBs are different, so compare those
    ; MSBs are the same, so compare LSB
    LDA val1_lsb
    CMD val2_lsb
go_compare:
    BEQ numbers_are_equal
    BCS val1_larger_or_equal
    BCC val2_larger

To avoid that, I like to use a shortcut which saves some unnecessary multiplication and division (which is tricky enough with 8-bit numbers if it's not a power of 2 that you can relace with a shift operation).

Top Tip - Tens and Units

With several of these variables, e.g. K, the clocK, it counts from 0-600 at which point the bomb goes off.

If this was stored as a 16-bit value, to check if it has reached 600, I would need to load and check both bytes to see if they equaled $02 and $58.

Instead, I store the values as two bytes, X_TENS and X_UNITS. I increment X_UNITS and check to see if it has gone past 9. If it has, I set it to 0 (Y = 0) and increment X_TENS.

inc K_UNITS
lda K_UNITS
cmp #10
bcc +
sty K_UNITS
inc K_TENS
+

When it comes to test for > 600, I only need to test the X_TENS value in a single byte comparison, is it over 60.

lda K_TENS
cmp #60
bcc L7

Another issue I hit a few times was adding an 8 bit signed number to a 16 bit number. There were a lot of "add 22 to the position to move down 1 row" or "subtract 22 to the position to move up 1 row".

Top Tip - Adding a signed 8 bit value to a 16 bit number

Something like me = me+c looks simple, but when me is a 16-bit location and c could be positive or negative, it took a bit of thought.

Positive numbers are relatively easy, you add the number to the LSB, and check if there is a carry. If there is, you increment the MSB.

lda C
clc
adc ME_LSB
sta ME_LSB
bcc +
inc ME_MSB
+

Note you need to clear the carry before you start as the 6502's only addition instruction is adc (add with carry).

(and unlike other 8-bit processors I could name, the 6502 has two nice simple instructions to set and clear the carry flag. None of this "one instruction sets the carry flag, the other one inverts the carry flag")

Negative numbers are stored in two's compliment, so -1 is actually $100-$01 = $FF.

The range goes from -128 to + 127. In this case, C could be -22, which is stored as $EA.

Rather than subtracting $16 (which is 22 in hex), you add $EA (-22).

So $40 - $16 is implemented as $40 + $EA. The result is $012A, but if this is an 8-bit number, the carry is ignored, so you get the result $2A.

When looking at 16-bit values, you end up with an answer which is $0100 too high.

If $2040 - $16 is implemented as $2040 + $EA, the result is $212A, which is wrong.

You need to do $2040 + $EA - $0100, and you get $202A.

In practice, you decrememnt the MSB before you add the two's compliment of the number you want to subtract.

Yes, my head hurts too.

lda C
bpl +
dec ME_MSB
+
clc
adc ME_LSB
sta ME_LSB
bcc +
inc ME_MSB
+

Here the bpl + skips over the decrement of the MSB if the value is positive. The remainder is an addition, which is the same for positive or negative numbers.

There was also an array of directions in the BASIC program.

If the array is very small, there is a trick you can use to simplify things.

Top Tip - Simple Arrays

A small array was setup as part of line 19

; 19 dim a(3) : a(0) = 2 : a(1) = -44 : a(2) = -2 : a(3) = 44

A simple way to store that is to define some space in zero page for the 4 elements

; a = 2, -44, -2, 44
lda #2
sta ARRAY
lda #-44
sta ARRAY+1
lda #-2
sta ARRAY+2
lda #44
sta ARRAY+3

Then when it is used, you can use zero paged X-indexed addressing to read the value from the array.

; 23 b = a9 + a(j)
ldx J
lda ARRAY,x

However, another place it was used then divided by 2.

; poke a9 + a(j)/2, hl

My brain was already melted, and I think I could have handled the division by a rotate right, but it would need to decide to set carry and then doing a rotate right for a negative number, or a logical shift right if it was positive. It seemed like that would be a lot of extra steps for each access.

I decided to take a different approach.

Top Tip - Avoid Doing the Maths

Instead of doing the divide by 2 each time, I created a second array.

This contained pre-calculated values that were half of those in the normal array.

lda #1
sta ARRAY_HALF
lda #-22
sta ARRAY_HALF+1
lda #-1
sta ARRAY_HALF+2
lda #22
sta ARRAY_HALF+3

I could then access those immediately using

lda ARRAY_HALF,x

The same thing happened in a few FOR loops, where the itterator was always divided or multiplied.

For example, part of line 4

for tt = 0 to 30 : poke v, tt/2 : next

I instead implemented that as

for tt= 0 to 15 : poke v, tt : next

It does the same thing, but faster, so it needs a longer delay.

Top Tip - Simple Delays

"you've got to speed it up, and then you've got to slow it down"

As assembler is a lot faster than BASIC, with FOR loops like this used to create sounds, I needed to add quite a lot of delay loops to match the speed of the BASIC.

My main delay loop of choice was a simple 256 cycle delay using the Y register.

-   dey
    bne -

Y is always left as 0 (see above), so the first DEY takes it to $FF, that is not zero, so it loop. $FE is not zero, loop again, down to $00. That is zero, so it exits, and leaves Y as zero as it should be.

Where I needed more of those, I involved the X register as well.

    ldx #$04
-   dey
    bne -
    dex
    bne -

This loops around the 256 cycle loop 4 times.

What more can I tell you?

Well, in a few places a needed a random number.

Top Tip - Random Numbers

When I first looked at converting this program, George Beckett had recently been discussing Random Number Generators in out Patreon discord (hint, join the Patreon if you want to join the discord, a great place to find out about project updates and behind the scenes stuff, and discuss your own projects).

He found a routine on a page that has since disappeared, but was thanks to the wonderful archive.org "wayback machine", it is still available.

(Incidentally, that is an incredibly useful resource to have when you follow a link to a webpage that isn't there anymore. They need donations to keep going, so if you find it useful, and you are able to do so, please give them something.)

From that page:

"Xorshift is a fast pseudorandom generator algorithm originally developed by George Marsaglia. John Metcalf found a 16-bit version of the algorithm that is fast on 8-bit platforms with only single bit shifts available. It has a period of 65535 and passes reasonable tests for randomness. His pseudocode is reprinted here:

/* 16-bit xorshift PRNG */
unsigned x = 1;
unsigned xorshift( )
{
    x ^= x << 7;
    x ^= x >> 9;
    x ^= x << 8;
    return x;
}

The page then presented a version for the Commodore 64.

George used it on an assembler version of a 10 PRINT maze for the Commodore PET.

I have used that same code here.

RAND16:
    lda SEED_MSB
    lsr
    lda SEED_LSB
    ror
    eor SEED_MSB
    sta SEED_MSB
    ror
    eor SEED_LSB
    sta SEED_LSB
    eor SEED_MSB
    sta SEED_MSB
    rts

It is seeded at the start of the program from the VIA1 timer, which is set counting at boot.

start:
    lda VIA1_TIMER2_LSB
    sta SEED_LSB
    lda VIA1_TIMER2_MSB
    sta SEED_MSB

When I started this process, I followed the path of the code, starting with the maze generation, and then movement and redraw (which is where I got the 16-bit comparison wrong).

Once I fixed the movement, I could progress to the "you won" and "you lost" stages.

There isn't much displayed on the screen, both cases display the same:

The number of the current round needs to be displayed, as part of that.

Top Tip - Displaying Simple Numbers

If you have a number from 0-9 to display, simply add $30 to it and store it in screen RAM.

    lda R
    clc
    adc #$30     ; A = R+$30  (0->"0", 1->"1" etc.)
    sta ROUND_DIGIT_1

That works fine until you hit 10.

Here I again split into R_TENS and R_UNITS, to make it easier to display. If R_TENS was 0, then just display R_UNITS, if not, display R_TENS and then R_UNITS.

    lda R_TENS
    beq L42_1
    ; double digit
    clc
    adc #$30
    sta ROUND_DIGIT_1
    lda R_UNITS
    clc
    adc #$30
    sta ROUND_DIGIT_2
    bne L42_2              ; branch always
L42_1:
    ; single digit
    lda R_UNITS
    clc
    adc #$30
    sta ROUND_DIGIT_1

That could probably be optimised a bit, but it only runs one at the end of each round, so it's not that critical.

The next step is to wait for F7 to be pressed. I was considering changing this to "press fire", as it is easier to check the joystick port, but when the keyboard is only being used for this, you can take a shortcut and use a very simple technique.

Top Tip - Simple "Press a Key"

There is a simple way to just check for a single key press, or a specific key.

$C5 in zero page holds the last key pressed. This is updated by the interrupt routine as it scans the keyboard.

If no key is pressed, it will return $64.

-
    lda LAST_KEY        ; check last key pressed
    cmp #NO_KEY         ; is a key pressed?
    beq -               ; no, keep checking

Here I want to check for F7, which is $63.

-
    lda LAST_KEY        ; check last key pressed
    cmp #KEY_F7         ; was it F7?
    bne -               ; no, keep checking

You might want to write your own keyboard routines if you are using the keyboard for directional movement, but it's fine for "press the any key".

With all that done, there was a lot of tweaking of delays to make the sounds right, and the loop timing.

The game has a clock, K, which counts up to 600. I am not sure if that is meant to represent 60 seconds, but in practice on a PAL VIC20, the original game gave you about 80 seconds.

I think I got the sounds and explosion effect reasonably close, and the ominous ticking sound.

The only real difference to the timing is the maze generation.

On the BASIC version, that takes about 55 seconds.

On the assembler version, it is pretty much instantaneous, so much so that I didn't even bother putting up the "making maze" screen as you would barely see it.

I found the same thing when I converted 3D Monster Maze, rather than a minute watching "the mists of time", I had to add a dissolve effect to make it look like something had happened.

Once all that was sorted out, I had a final pass through and was able to optimise out X_UNITS, as this was always zero (X was incremented by 50 each time).

I also rewrote the original assembler routines, with I think shorter and faster ones (he said with the arrogance of a programmer with a long grey beard who has been doing this for a long time).

e.g. part of SYS 830

This fills the colour RAM from $9600 to $97FF with $0A (the colour light red)

I don't think it is beneficial to use zero page here when the destination is always going to be fixed.

    lda #$00      ; destination 9600
    sta $FB
    lda #$96
    sta $FC

    ldy #$00
L034D:
    lda #$0A      ; fill with 0A
L034F:
    sta ($FB),y
    iny
    bne L034F

    inc $FC       ; keep going until 9800
    lda $FC

    cmp #$98

    bne L034D

I went with a simpler approach

    lda #COLOUR_LIGHTRED     ; all text colour to light red

-   sta COLOUR_RAM_PAGE0,y
    sta COLOUR_RAM_PAGE1,y
    iny

    bne -

I also inline that function into line 28 to save a JSR and RTS.

Since line numbers no longer affect speed, I also shuffled things around so that the maze generation was at the start, where it logically comes in the sequence.

Other Things to Consider

The bottom of the maze is neatly hidden, the maze if stored in RAM right below the screen RAM, so when it hits the bottom of the maze, it continues copying the top of the screen.

You can see the top half is repeated below the solid line.

The top of the maze is not handled this way, instead you get a view of the BASIC heap I think.

I did consider tidying that up, but I haven't.*

I also considered a visible timer, but I think the ominous ticking of the clock is fine.

I did think about adding a title screen, even instructions, but I don't think it needs it.

I will call it done for now that.

I have also uploaded the source to github, with the original BASIC, spaced out BASIC and assembler versions.

There is also a PRG file if you want to play it on a real VIC or an emulator - remember to set remove or disable any memory expansions, or set the memory on the emulator to none (not 0, that is very different).

xvic -memory none timebomb.prg

The original was BASIC version was 1843 bytes.

The final tally of the assembler rewrite is 1067 bytes. It seems like I should try to get it down to below 1024 bytes, but I won't.*

* Part 2

OK, I did tidy things up a bit and fixed a few bugs (which were present in the original) and tried to get it under 1K.

I also wrote a version for the PET. That is all to follow in Part 2 next week.


All items are still available, and I can ship worldwide.

International shipping is a bit complicated at the moment, so if you want to order from outside the UK, please see this page for more information:

Sorry to have to do this, and I know this is going to affect sales, but please understand it is the only option I have right now.

Patreon

If you enjoy posts like this, you can support me via Patreon, and get access to advance previews of blog posts, and exclusive posts.

You also get progress updates on new projects and other behind the scenes updates, as well as access to my Patreon only Discord server for even more regular updates, and to discuss your own projects.

I currently have 50% off your fist month if you want to try it out.