Sunday, 11 January 2026

A New CRTC for the Minstrel 4th? Part 1

This is the the start of a series made up from an amalgam of several Patreon posts over the last year or so, and various new and revised bits to try to make it flow.

I have written various versions of this introduction, some got into the Patreon posts, some got filed away into the "got a bit too ranty folder".

When I bought back the Minstrel 4th in May (almost, but not quite, entirely so I could do the May the 4th joke again), it was pretty much unchanged from the previous version.

As always, I couldn't resist tweaking a few things. Just minor things like updating the logo and adding an NMI button in case it comes in useful in future.

I only had limited parts left, so I only ordered a small batch of boards, to see how it went.

Now I need to order some more boards, it's time to consider a problem.

The Problem

Here in very simple terms in the problem:

The microcontroller I use for the CRTC was marked end of life in 2023 in the PDIP packages (plastic DIP through hole), along with the ones I use for the Mini PET.

I thought it would be useful to just talk through all of those, see if I can talk myself out of any of them, or if any of them suddenly seem the obvious option.

I have been playing with a new toy, a 9 input logic analyser, the IKA logic SP259i.

That has been very useful at looking at the signals on the video section of the Minstrel 4th.

I can do a single capture and get several complete frames of video with all the relevant signals captured.

I can then zoom into individual lines of video.

And even down to individual clock pulses, all on the same capture.

It also has a mode where the 9th input can be used as the clock for sampling, so now all the results are shown in terms of CPU clock cycles, so I can see there that line goes low for 3 clock cycles, then high for 5. Much easier than looking at time periods.

It avoids keep having to divide 460ns by 153.84ns to find that is 2.99 clock cycles. Less of an issue at 1MHz or 8MHz with 1ms or 125ns cycles, but with the 6.5MHz is really helps.

(N.B., that presents a somewhat idealistic view, with a sample on the falling edge of each clock cycle, there may be glitches or all sorts happening in between, but in this particular application, it is ideal for what I need to see)

Option 1 - The Simple Way

The simplest way is to change to a new microcontroller.

The part I used was the ATmega48PA.

The part numbering scheme here is ATmega <memory size> <package> <suffixes>

The memory size runs from 4K on the device I chose up to 32K on the ATmeag328 used on the Arduino etc.

The package was 4 for 40 pin and 8 for 2pin, (don't ask me why)

The suffix is where it got confusing. There was a plain version with no suffix, and ones with A, P, V, PA and PB on the end. These were lower power / die shrunk versions, although the exact differences are never clearly explained and cause much confusion, particularly as some parts have the same device ID and others do not.

I could use any of the 28 pin devices, other than the V which is low power and cannot run the higher clock frequencies. The PB is an updated version, but is only available in surface mount.

I could use the ATmega48, ATmega48A, ATmega48P and ATmega48PA. 

I could also use similar devices with more program memory (ATmega88PA, ATmega168PA, ATmega328PA etc.). These are pin compatible and largely code compatible, just requiring a rebuild due to changes such as additional timers in the devices with the larger memory space affecting the interrupt vector table.

At one point, it seemed all of those went out of stock, potentially as everyone stocked up on their last time buys.

(N.B. as of January 2026, the ATmega164PA and similar used in the MiniPET are no longer available in PDIP, but the ATmega48PA still appears to be)

Time to look at alternatives

(yes I know the new one is apparently 3 years old and the one that is meant to be discontinued was made earlier this year......)

The ones that appear to be in production and likely to continue are the AVRxDx and AVRxEx ranges, with a slightly (but only just slightly) less confusing naming scheme.

The naming convention here is AVR <memory size> <range> <pin count>. 

Memory size is 16, 32, 64 or 128, and pin count goes from 14 to 64, including through hole and surface mount variations.

The range is where the wheels come off a bit. There is DA, DB, DD, DE, DU, EA and probably several more. Nowhere have I found a useful guide to say what the differences are. Some have additional analogue components like OP Amps and Compactors. Some have port C able to run at a different IO voltage to the rest of the chip. The number and type of timers also change.

(there is now also a version with an S suffix that appears to be PDID support which allows you to disable the programmer and debugger features and completely lock the chips down)

The one I chose was the AVR16EA28. This is one of the simpler of the versions, with enough pins and memory for the task

The number of available IO pins worked out the same, but they are arranged differently, so I will need to rejig the code to cope with things like a counter being on PA2-PA6 rather than PD0-PD4. Handily, I can swap the single cycle increment instruction for a single cycle addition to a register which is preset to 4, so it will count up in 4s but I will still see 0-32 on the pins.

The downside is this will still need the dual port RAM chip.

My mind keeps idly wondering if I can maybe replace that as well, since that is also discontinued?

Option 2 - Cache and carry

This idea would still need the dual port RAM, but as I have been thinking about other options, one that has suggested itself to me is to try to cache the reads to the video RAM at the start of a row of characters (8 lines), a bit like the VIC-II chip in the C64 does. That would only needs reads to the font RAM to get the pixel bitmaps, but still half the reads during display generation.

But maybe I could cache the font RAM as well?

Or even, I could cache the whole frame in the microcontrollers 2K RAM (more is available in pin compatible chips).

Using that capture, it is easy to see that there are 50,000 cycles between the end of one frame of video and the next. Easily enough to read in 2000 bytes from the video RAM, whilst still generating the video framework. (and maybe skip the cache update if there have been no writes to the video RAM?)

That was a quick test of firing a series of interrupts at important points whilst a main thread could be filling up the cache / frame buffer in the background.

Option 3 - Be the video RAM

It then occurs to me, maybe I can cut out the middle man here and remove the dual port RAM altogether, making it easier in terms of parts in future.

The video RAM could be written by the Z80 at any time, but because of the number of cycles most of the write instructions take, that might only be relatively slow at the microcontroller. And I could always run the microcontroller at 13MHz to have twice as long to deal with things and just have an extra divider stage to get back to 6.5MHz externally. (note to self, try to find those 13MHz crystals you ordered last time you had a mad idea like this, or order some more before the RS cut off deadline)

The top line shows the Z80 writing to video RAM at the start of each frame in the "ZX81 BASIC for Minstrel 4th" ROM. The normal Ace ROM only writes to RAM when it needs to change, so this is worse case.

You can see about 4 writes during a line of video, and 64 instructions between them.

Of course, you can run the Z80 at double speed, 6.5MHz. (although loading from tape won't work, so I haven't mentioned that in the ZX81 BASIC posts, I might have a go at doubling the delays in the loading code and see if I could get that working for a 600% ZX81 speed option)

Here the Z80 frame buffer completes almost before the visible line drawing starts, but the writes are now twice as fast, 32 instructions separating them.

What is the fastest it could write to RAM? LD (DE), A is 7 cycles. Maybe if you set the stack pointer in video RAM a JSR would cause two writes close together as the return address is pushed onto the stack?

It's not impossible anyway. I think I would probably have some latches for the address and / or data, then it is just up to the microcontroller to read those in before the next write?

That would only work for writes. The character RAM is write only, so that is fine, but video RAM is read write.

(and I have since proved that I can't even make reads work with a 24MHz microcontroller and a 1MHz CPU - http://blog.tynemouthsoftware.co.uk/2025/12/faster-isnt-always-better-a-cautionary-tale.html )

That's fine, there is space in the main RAM chip that is not being used at those addresses. I could enable that in the video RAM range, and have writes going to both there and the microcontroller, and reads only coming from the main RAM.

It would need some trick logic in the addressing so that access to $2000-$23FF acts the same as the mirror at $2400-$27FF.

I could even bring in the Ace priority system, writes to $2000 upwards are Z80 priority, and should be actioned even at the cost of snow on the display.

Writes to $2400 upwards should favour the video display and could use the Wait line to halt the Z80 until the a quiet period in the display to process the write.

Hmm, lots of options there.

Option 4 - Use a Raspberry Pi

You will find the unsubscribe button in the usual place.

Revisiting Rabbit Holes

Using the internal RAM as a cache or the main video RAM opens the option of using the internal shift register rather than an external 74HC166, to save pins.

One of the great things about having 15 years worth of blog posts, is I can look back at ideas I was considering in the past and read through my thoughts at the time. (which is another reason for writing all of this now)

Reading some old posts may have saved me going down a rabbit hole again with this.

Thank you past me.

That was in the context of trying to run three shift registers for RGB, so maybe a single version would still be viable?

Which option then?

Lots of options to consider.

I think the best plan for now is to look at the new microcontroller option.

Tune in next week to see what fun I had with the "easy" option.


Adverts

Check out my Tindie store for all sort of kits, test gear and upgrades for the ZX80, ZX81, Jupiter ACE, Commodore PET.


Patreon

You can support me via Patreon, and get access to advance previews of development logs on new projects like the Mini PET II and Mini VIC (when I get back to them once I have finished tidying up) and other behind the scenes updates. This also includes access to my Patreon only Discord server for even more regular updates.

Sunday, 4 January 2026

Minstrel Expansion Bus Roundup

This is a quick overview of where things are with the Minstrel Expansion Bus.

There are now three modules and another couple in development, and two backplane options.

There are also various other boards which can be used with it, such as the Minstrel ZXpand:

And David Stephenson's ZX-IO:

Backplanes

I have recently updated the backplane options, so there are now two versions, powered and unpowered.

Previously the unpowered version was the same PCB as the powered version without the power supply bits fitted.

But now there is a dedicated unpowered version with just the bus connectors.

Available with a pin header for direct soldering to a Minstrel 2 or 3.

Or an edge connector to attached to a Minstrel 2, 3, ZX81 or TS1000.

This version is powered by the host, so that is fine for a few lower power cards.

If you plan to add more cards, the powered version is a better option.

This does two things. Firstly, it has a 7805 5V regulator which powers all the slots, plus the rear pass-through connector.

Secondly, it has a 9V DC input. You can either have this completely separate, and use it to power the slots and the 7805 and leave the host as it was. 

Alternatively, you can jumper it to connect to the hosts 9V rail. That allows you to supply power to either the 9V DC barrel jack on this board, or the power jack on the host.

This is most useful if you are using a ZX81 or TS1000, so you can avoid using the less than ideal 3.5mm DC power jack on there.

Having the bus connectors powered by their own regulator also avoids any additional load on the hosts 7805 regulator, which often gets quite toasty all on it's own if it's a ZX81 or TS1000.

The passthrough edge connector is also powered by the 7805 on the expansion bus, so you can use larger peripherals without overloading the host power supply.

Or even anther backplane if you need more slots:

Or a powered and unpowered combo:

The Modules

The number of modules is building up, with a few new ones in development, and ideas for several more.

Oops, pretend you didn't see that one yet.

I'll put a different one in, no one will notice.

Minstrel Joystick

The first module of this type was the Minstrel Joystick

This is a Kempston compatible joystick interface, for use in your own programs with a bit of simple machine code, or in games such as Paul Farrow's ZX81 Kong etc.

Minstrel Input Monitor

This started as a request to add a click when you press a key (like the ZX Spectrum), but grew to also include a monitor for the loading signal.

Minstrel ROM Cart

This is a new module to replace the internal ROM with multiple jumper selectable ROM images.

Minstrel ZXpand

Although not originally designed for it, the Minstrel ZXpand fits into this system, either as a vertical card, or on the rear expansion slot.

If you do attach it like that, you still need the wire to connect to the mainboard to select the ZXpand ROM.

Although you can replace that functionality with the new Minstrel ROM cart.

That means you can now use the Minstrel ZXpand on a ZX81 or TS1000 (internal RAM upgrade required)

Adverts

The various backplanes and modules are available from my Tindie store, as kits or assembled.


Patreon

You can support me via Patreon, and get access to advance previews of development logs on new projects like the Mini PET II and Mini VIC when I get back to them in the new year and other behind the scenes updates. This also includes access to my Patreon only Discord server for even more regular updates.

Sunday, 28 December 2025

Fitting Perilous Swamp into 8K on a PET 2001

In a previous post, I converted the ZX81 game "Perilous Swamp" into Commodore BASIC to run on the VIC20 and PET (and also zero effort C64 and TED ports of the PET version).

The PET version needs 16K, it won't run with only 8K installed.

The VIC20 version does run on a VIC20 with an 8K expansion, but there is the internal 5K to gives 13K in total and with the BASIC and OS overheads, that leaves 11775 bytes available.

It would be nice if the PET version could be made to run in 8K, so it would run on the PET 2001-8, the original model PET with the built in datasette and chiclet keyboard. (There was in theory a 4K model, but I think they had settled on 8K before production started)

The program file is about 8.5K, so that needs to be reduced in size, but it's not that far off.

The program is BASIC, so there are various things that can be done to reduce the file size.

10 PRINT "SAVING SPACE"

Let's have a look at much simpler programs, and see how they are stored under the hood and what can be done to reduce program size.

I'll start with the traditional example.

10 PRINT "HELLORLD"
20 GOTO 10

On the PET, the program memory starts at $0400 with a fixed $00. After that, each line is in the following format:

  • 1 word - the address of the start of the next line
  • 1 word - the line number
  • 1-73 bytes - the line itself (approximately two screen lines on the PET)
  • 1 byte - $00 to indicate the end of line

Looking at the memory with that program entered:

0400  00 12 04 0a  00 99 20 22  48 45 4c 4c  4f 52 4c 44   ...... "HELLORLD
0410  22 00 1b 04  14 00 89 20  31 30 00 00  00 24 24 24   "...... 10...$$$

Starting with the first line, that splits up as follows:

  • $0412 - address of the next line (the 6502 in the PET is little-endian)
  • $000A - line number 10
  • $99 - this is the BASIC token for PRINT
  • $20 - this is the space after PRINT
  • $22 - this is the first quotes "
  • $48 $45 $4C $4C $4f $52 $4C $44 - this is HELLORLD in PETSCII
  • $22 - this is the second quotes "
  • $00 - this is the end of the line end

There are a few points to make, but I will just go straight to the next line.

  • $041B - address of the next line (or the place where the next line would be if there were one)
  • $0014 - line number 20
  • $89 - this is the BASIC token for GOTO
  • $20 - this is the space after GOTO
  • $31 $30 - this is PETSCII for 10, the line number to go to
  • $00 - this is the end of the line end

Finally, there are two more zeroes, $0000, indicating no more lines.

What can we take from that?

Well, the BASIC keywords are represented by tokens, so you can type ? or PRINT, it doesn't actually matter, it will be stored as the token $99, so no saving in size there. (The same for the others like G shift O for GOTO etc.)

Secondly, the space between the line number and the first token is not stored. It is implicit. Whether you type it or not, it will not be stored, but it will be printed in the listing. So again, no savings there.

All of those are stored the same, and are listed the same and run the same.

0400  00 11 04 0a  00 99 22 48  45 4c 4c 4f  52 4c 44 22   ......"HELLORLD"
0410  00 21 04 14  00 99 22 48  45 4c 4c 4f  52 4c 44 22   .!...."HELLORLD"
0420  00 31 04 1e  00 99 22 48  45 4c 4c 4f  52 4c 44 22   .1...."HELLORLD"
0430  00 41 04 28  00 99 22 48  45 4c 4c 4f  52 4c 44 22   .A.(.."HELLORLD"
0440  00 00 00 24  24 24 24 24  24 24 24 24  24 24 24 24   ...$$$$$$$$$$$$$

I am using "HELLORD" for two reasons, one is it makes nice 16 bytes lines for examples like the above. The other would require you to watch Usagi Electirc's You Tube channel, which I recommend you do if you aren't already glued.

Unlike the space before the token, the spaces after the PRINT token are stored, so that does make a difference to the overall file size.

The extra spaces are stored, so take up extra memory, but they do not make any difference to how the program runs, so are only used to make the listing easier to read. (well, it makes the program slightly slower as it has to skip over the spaces)

If you are short of space, you can get rid of the spaces in the code. It will still run the same, but the file will be smaller, requiring less memory. The only downside is it becomes more difficult to follow.

0400  00 11 04 0a  00 99 22 48  45 4c 4c 4f  52 4c 44 22   ......"HELLORLD"
0410  00 22 04 14  00 99 20 22  48 45 4c 4c  4f 52 4c 44   .".... "HELLORLD
0420  22 00 3e 04  1e 00 99 20  20 20 20 20  20 20 20 20   ".>....         
0430  20 20 20 22  48 45 4c 4c  4f 52 4c 44  22 00 00 00      "HELLORLD"...
0440  24 24 24 24  24 24 24 24  24 24 24 24  24 24 24 24   $$$$$$$$$$$$$$$$

A line like

10 FOR N = 1 TO 10 STEP 2

is easier to read than

10FORN=1TO10STEP2

The compressed version saves seven bytes, and that can make a difference over a large program.

Here I am using PRINT FRE(0) to show the number of bytes free, the second figure being 7 bytes less than the first, at the cost of code readability

$000A PRINT "HELLO FROM LINE 10"

From the breakdown above, you can see the line number is stored in two bytes as a raw number, so line 10 was stored as $000A.

That means you will not make great savings by renumbering your program from 10,20,30 to 1,2,3.

They are all stored in the same amount of space, two bytes for each line number.

0400  00 11 04 01  00 99 22 48  45 4c 4c 4f  52 4c 44 22   ......"HELLORLD"
0410  00 21 04 0a  00 99 22 48  45 4c 4c 4f  52 4c 44 22   .!...."HELLORLD"
0420  00 31 04 64  00 99 22 48  45 4c 4c 4f  52 4c 44 22   .1.d.."HELLORLD"
0430  00 41 04 e8  03 99 22 48  45 4c 4c 4f  52 4c 44 22   .A...."HELLORLD"
0440  00 51 04 10  27 99 22 48  45 4c 4c 4f  52 4c 44 22   .Q..'."HELLORLD"
0450  00 00 00 24  24 24 24 24  24 24 24 24  24 24 24 24   ...$$$$$$$$$$$$$

The lines 1,10,100,1000 and 10000 are stored as $0001, $000A, $0064, $03E8 and $2710 respectively.

You might expect the largest line to be 65535, $FFFF, but apparently it is actually 63999, $F9FF.

There is a small benefit to be gained from using lower line numbers. Remember GOTO 10 was stored as GOTO and "1" and "0", so GOTO 10 is one byte more than GOTO 1.

That is stored as follows

0400  00 12 04 0a  00 99 20 22  48 45 4c 4c  4f 52 4c 44   ...... "HELLORLD
0410  22 00 1b 04  14 00 89 20  31 30 00 00  00 24 24 24   "...... 10...$$$

Where as

Is one byte shorter

0400  00 12 04 01  00 99 20 22  48 45 4c 4c  4f 52 4c 44   ...... "HELLORLD
0410  22 00 1a 04  02 00 89 20  31 00 00 00  24 24 24 24   "...... 1...$$$$

That is another one for size vs readability and maintainability, but you can save a byte or two for each GOTO or GOSUB in the program.

Revisiting the Swamp

The PET version of Perilous Swamp from back in November was 8207 bytes long.

The 8K PET has 7167 bytes free, so I need to save over 1000 bytes to get it to load (running is a different matter - more on that later).

Starting point: file 8207 bytes

1) Remove Spaces

I am using CBM PRG Studio and that has a reformat tool that can remove the spaces.

This reduces readability, but makes the program smaller and slightly faster. I wouldn't do this on the normal versions, but in this case, I think it is worthwhile.

That saves 646 bytes and takes us to 7561 bytes. Still too large.

2) Renumber

This makes a small difference, but reduces readability and maintainability.

That saves only 83 bytes, taking it down to 7478 bytes.

3) Combine Lines

From the previous descriptions, you will see that each line has an overhead of five bytes with a two byte pointer, a two byte line number and a 1 byte terminator.

If I take something like this:

2620 a=x
2630 b=y
2640 x=int(rnd(1)*7)+2
2650 y=int(rnd(1)*7)+2

and combine in to:

2620 a=x:b=y:x=int(rnd(1)*7)+2:y=int(rnd(1)*7)+2

That should save 15 bytes minus the three colons, so 12 bytes in total.

You can only do so much of this, as you need to keep below the line length limit.

This is another of those sacrifices of space over readability

Also some lines are jumped to directly, so can't be split up. I couldn't use the above example as there is a GOTO 2640.

{down}, {down}, Deeper and {down}

There are a few example like that, but far more common are multiple PRINT statements, left over from what were ZX81 SCROLL instructions.

10 print "{clear}"
20 print
30 print
40 print "            =============="
50 print "            perilous swamp"
60 print "            =============="
70 print
80 print
90 print
100 print "         a new adventure game"

One way to reduce that is to combine as above, something like this:

10 print "{clear}":print:print:print "            =============="
50 print "            perilous swamp"
60 print "            ==============":print:print:print
100 print "         a new adventure game"

However, you can take it a step further and replace the blank PRINT lines with {down} characters at the start of the next line.

10 print "{clear}{down}{down}{down}            =============="
50 print "            perilous swamp"
60 print "            =============="
100 print "{down}{down}{down}         a new adventure game"

It is not a clear in terms of alignment of the various lines. In the original version it is easier to align the === lines with the text and check centering, but again, it is readability vs file size.

The {down} characters are just used in the development environment, the code itself contains a single character, the inverse Q (the inverse heart is the clear/home as previously) .

Each of the {down} codes converts to a single byte ($11) in memory.

0400  00 28 04 0a  00 99 20 22  93 11 11 11  20 20 20 20   .(.... "....    
0410  20 20 20 20  20 20 20 20  3d 3d 3d 3d  3d 3d 3d 3d           ========
0420  3d 3d 3d 3d  3d 3d 22 00  4b 04 32 00  99 20 22 20   ======".K.2.. " 
0430  20 20 20 20  20 20 20 20  20 20 20 50  45 52 49 4c              PERIL
0440  4f 55 53 20  53 57 41 4d  50 22 00 6e  04 3c 00 99   OUS SWAMP".n.<..
0450  20 22 20 20  20 20 20 20  20 20 20 20  20 20 3d 3d    "            ==
0460  3d 3d 3d 3d  3d 3d 3d 3d  3d 3d 3d 3d  22 00 97 04   ============"...
0470  64 00 99 20  22 11 11 11  20 20 20 20  20 20 20 20   d.. "...        
0480  20 41 20 4e  45 57 20 41  44 56 45 4e  54 55 52 45    A NEW ADVENTURE

The single character replacing a six byte line means each line replaced with a {down} character saves five bytes. This is 126 bytes just on the title and instruction page.

OK, lots of editing to do.....

I went back to the original PET version from November, 8207 bytes. After some judicious merging of lines and removing PRINT statements in favour of {down} codes, I got that down to 7836 bytes.

With the spaces removed, that went down to 7192 bytes.

That's so close.

Time for a few more combined lines, this time, being less precious about making it readable.

Trying it out

OK, that's got it down to 7128 bytes.

That should fit, right?

Yay! it fits, 40 whole glorious bytes free.

Let's give it a try.

So far, so good.

Oh dear, I guess 40 bytes wasn't enough to run the program in.

Line 430 is where all the arrays are DIMensioned, space is reserved in the heap at the end of RAM, and there is not enough space available there as it grows down and hits the end of the program.

It probably failed on the first statement, DIM A(11,11). Commodore BASIC used zero-based arrays, so that is actually a 12x12 array, 144 elements.

It only needs to be 11x11, the map is 9x9 with a border, but ZX81 BASIC uses one-based arrays, so the code accesses elements from 1 through 11, rather than 0 through 10.

I could rewrite the code to use a(x-1,y-1) type accesses, but all those "-1"s will use up space, make it more difficult to follow and potentially error prone if I miss one, so easier to waste the 23 elements for the unused 0th elements.

I had been thinking in terms of bytes rather than elements, but of course this is a floating point BASIC, so each number is actually 5 bytes.

Wow, I didn't expect that, I was willing to blame the string arrays.

That has thrown me slightly. I did have a plan for the string arrays, which I might still need, but I hadn't counted on losing 720 bytes to the map array.

Each elements only needs to hold one of 5 values (border, empty square, swamp, princess, player), so a single byte would be more than enough. 

Ideally, I would use a 1 byte per element array, but I can't get that. I could use a block of RAM and PEEK and POKE the values in there, or maybe a 144 character string and some access function to pick an element (possibly already converted from number into display form). Or maybe a 12 element array with one row of the map in each element?

All of those require quite a bit of change. I think I can save 432 bytes by making a small change.

If I change

DIM A(12,12)

to

DIM A%(12,12)

That will convert it into an integer array. That is not single byte, it is a 16 bit integer, so two bytes per element, but still better than 5 bytes per element.

I just need to change all 23 references to the A array to be A%(X,Y) etc.

Oh, that was easy.

Those 23 changes added 23 bytes to the file, and it is just small enough to load, but even less space available to run.

What about changing the rest of the variables?

Well, changing to an integer array made sense since it saved 3 bytes per entry in a 144 entry array (432; bytes total) and only added 23 bytes to the file, so a net saving of 409 bytes.

If I change the Q variable to Q%, that would only save 3 bytes, but it is references 18 times, so would add 18 bytes to the file changing all the Q to Q%, a net increase of 15 bytes.

No one reads them anyway

One quick thing I can do to save space is to lose the title screen and instructions.

Not ideal, I would like to keep those. Maybe I can put them back later if there is space.

That takes it down to 6378 bytes,

Before running, there are 790 whole bytes available! That should be enough.

You could fly to the moon on 790 bytes. (incidentally, it is frightening to think that there is only about 10 years between those Apollo missions and the ZX81 and the PET)

Yay!

If I quit out immediately, there are still 156 bytes available.

Success.

Now, I wonder what I would need to do to add the titles back?

How much space do I need?

The file size dropped from 7151 with title screen and instructions to 6378 without, so I need to save 773 further bytes to be able to add the title and instructions back.

Currently it is using 634 bytes (790 free after loading, 156 bytes free after running). I know 288 of those are from the map which has to stay (unless I can think of something else for that), but there could be savings to be made from the rest?

Some of that is from the string arrays.

The ZX81 version stored the list of treasures, monster names and some suitable adjectives for the monsters.

Each time it accessed those, it used string manipulation to get to the appropriate string.

When I concerted the program to Commodore BASIC, the string access was different, I couldn't define the string on a single line, and would have to use MID$ to snip out the section (which would require two calculations as it is string, start, end, rather then string, start, length).

I change those to string arrays and DATA statements into arrays.

That means all of these strings at the end will appear twice in memory when the program is running.

Firstly the strings appear as lines of code, the DATA statements. When the program runs, it will DIMension some arrays, and READ the values from the DATA statements into the arrays.

That eats away at the extra RAM required to run the game.

The V and V$ arrays define the treasure you can find, it's value and description

430 ... : dimv(8) : dimv$(8) : ...
440 for n = 1 to 8 : read v(n), v$(n) : next n

(I have put back in the spaces for clarity, and redacted some irrelevant sections)

The data statements appear near the end of the file (this is the original version, the 8K version currently has them packed onto fewer lines to save space):

3980 data 10, "10 silver spoons"
3990 data 30, "a jewelled sword"
4000 data 50, "a jar of rubies"

etc.

The v and v$ arrays are only used once, here:

1670 print "is guarding "; v$(i)
1680 ...
1690 p=v(i)

If I rewrite that as:

1670 if i=1 then p=10 : print "10 silver spoons"
1671 if i=2 then p=30 : print "a jewelled sword"
1672 if i=3 then p=50 : print "a jar of rubies"

etc.

That will mean those strings only appear once in the file. There is extra overhead with the additional IF statements, but overall it only adds 5 bytes to the program file, and crucially, it will save 86 bytes from the heap.

If I run that now, it still works (which is nice). But ouch, that's an unfortunate map, quite a journey to get to the princess. You can get there, you can move diagonally, there are actually two routes.

So far, I have only seen a few maps that were not at first glance possible, but could still work if something happened to move you around the map...... (spoilers)

Quitting out after the arrays have been setup, shows 237 bytes free, previously it was 156, so that is an 81 byte overall saving.

Not a bad start, I think it is worth doing the other arrays.

Both n$ and p$ and setup in the same way, but both are referenced twice:

1620 print "a "; p$(a1); ", "; p$(a2); " "; n$(i)
...
3550 print "{down}the wizard has his pet "; n$(j)

"A FOUL, SMELLY OGRE" and "THE WIZARD HAS HIS PET 2001" sorry "THE WIZARD HAS HIS PET DRAGON 32" sorry "THE WIZARD HAS HIS PET ORIC". Oh I give in.

Time passes.

Code is edited.

Because they were used twice, I went for subroutines which print the strings then return.

1620 print "a "; : gosub 4000+a1 : print ", "; : gosub 4000+a2 : print " "; : gosub 4010+i

The functions are very simple, again with spaces for clarity, later removed.

4010 print "nothing" : return
4011 print "werewolf" : return
4012 print "bunyip" : return
4013 pat "phoenix" : return

etc.

The file has grown again, but hopefully it will be worth it.

Oh, apparently not.

That's now only showing 227 bytes free, that extra work has made it 10 bytes worse.

Undo Undo Undo Undo.....

So, back to where I was before. The first array changes saved a bit of space so it worth keeping, but there is no benefit in keeping the changes to the other two.

Other ideas?

I could make it a multi-part loader. Display the title and instructions, then load the game itself?

I'd rather not. Let's keep going.

I had several more passes through the code, combining lines and adding {down} and {return} to PRINT lines to allow them to be combined and replaced. (I don't like using {return} as it looks odd in the listing, but it has saved quite a few bytes, so I will accept it here).

Other tweaks like NEXT N doesn't actually need the N, it's just there from force of habit. NEXT will loop the most recent FOR loop. Actually, the N is there because I wrote it, and when I write FOR loops I use N so that NEXT N is two presses of the N key on the ZX81. I've been doing that since about 1982, so I do it automatically, even though I am writing this on a PC to run on a PET.

Here N is used in some of the loops I added, but I could use a different variable, one already defined, and possibly save 5 bytes from the heap (depending how good the garbage collection is).

All the NEXT N => NEXTN => NEXT has saved maybe a dozen bytes, but it all adds up.

And after all of that, I got the file down to 5782 bytes.

Still working, and an even more fiendish (but still possible) path to the princess.

That has 840 bytes free after setting up the map etc. and 791 free after completing the game.

That should be enough to put the titles and instructions back.

6540 with titles, 60 free at first out.

That's a bit tight

OK, I hadn't been doing this to make it easier to check when I made changes, but lets renumber the file from line 1 at step 1.

The renumbered file went down to 6428 bytes, and there were 172 bytes free at the first out.

And a far less swampy route.

There were 130 bytes free after completing the game.

I think I am happy with that, I have succeeded in getting the whole game, titles, instructions and all to run on a base 8K PET.

The 130 bytes free should be a decent safety margin.

The code has changed quite a bit.

340 print
350 print "if you could release her..."
360 print
370 print
380 print "should you have to leave early,"
390 print
400 print "type out to leave - permanently"
410 print
420 clr : restore
430 dim a(11,11) : dim v(8) : dim v$(8) : dim n$(10) : dim p$(8) : dim q$(5)
440 for n = 1 to 8 : read v(n), v$(n) : next n
450 for n = 0 to 10 : read n$(n) : next n
460 for n = 0 to 8 : read p$(n) : next n
470 for n = 0 to 5 : read q$(n) : next n

Became

14print"{down}if you could release her..."
15print"{down}{down}should you have to leave early,"
16print"{down}{down}type out to leave - permanently"
17clr:restore:dima%(11,11):dimn$(10):dimp$(8):dimq$(5):fori=0to10:readn$(i):next
18fori=0to8:readp$(i):next:fori=0to5:readq$(i):next

Not the easiest code to follow, but needs must to fit in 8K. The full version is still there if you want to follow the code.

In summary:

Well, that was a lot of work, but it was an interesting challenge, and I like setting myself little challenges like this. Sometimes it's good to get a win with something like this when other things I am meant to be working on are stuck for whatever reason.

I learned a few things, especially the space used by the map array, really wasn't expecting that little 12x12 map to use almost 3/4K of RAM.

The reduced size version also runs faster, as BASIC has less code to parse, so takes less time.

  • Combining Lines: My original PET version had 493 lines. The 8K version has 193 lines. That's 300 lines removed, saving between 4 and 5 bytes per line, 1200-1500 bytes.
  • Removing Space: That saved about 650 bytes.
  • Changing from floating point to integer array: That saved about 400 bytes.
  • Renumbering starting at 1, step 1: Saved 114 bytes.
  • Converting V and V$ Array to IF statements: 86 bytes.
  • Removing variable from NEXTs: 12 bytes.

Some of the later ones may seem like they weren't worth it, but really every byte counts. There are only 130 bytes free after running the game.

Final Comparisons

Version
File Size
RAM Used
Total
PET 16K/32K
8207
1127
9334
PET 8K
6428
610
7038
Savings
1779
517
2296

Both PET versions, along with the VIC20, C64 and TED versions are available on my github:


Update:

After posting this to Patreon, I had some suggestions of other things I could do to save space, these from Andy Hewco:

Remove quotes in data statements

I can change

191data"nothing","werewolf","bunyip","phoenix","troll","goblin","giant"

to

191datanothing,werewolf,bunyip,phoenix,troll,goblin,giant

That seems to work, and saved 40 bytes.

I had to leave the quotes around the map characters as that seemed to break things.

Remove quotes at the end of a line

If the line ends with a PRINT statement that ends with closing quotes, those can be omitted.

10 PRINT "HELLO WORLD"

Can be changed to:

10 PRINT "HELLO WORLD

And that seems to work.

Changing that saved 46 bytes.

Semicolons are not required for string concatenation

10 A$=" WO"
20 PRINT "HELLO";A$;"RLD"
30 PRINT "HELLO"A$"RLD"

That also seems to work, and saved 20 bytes.

I still need ones at the end of a line where I want to continue printing e.g.

10 PRINT "HELLO ";
20 PRINT "WORLD"

Overall, that saved 107 bytes. Nice.

Thanks Andy.

Thandy.


Update 2 - Bug Fix

Whilst looking at other things, I noticed one bug (shock! horror!)

There is a random event that you get given a ring, which can be marked with three things.

I had noticed I was only ever seeing the first.

It would appear that GOTO line+random number does not work.

181 print"he had a magic ring marked ";:goto182+int(rnd(1)*3)

Actually, GOTO line + number doesn't work either.

this is still showing the result of goto 182, not goto 184.

I changed that to ON random number GOTO line, other line

181 print"he had a magic ring marked ";:onrnd(1)*3goto183,184

I have tested it, and it appears I don't need the INT( ) around the number as it is implicit in the ON instruction.

I only needed 2 cases here as INT(RND(1)*3) returns 0, 1 or 2. The ON statement goes to 1, 2, 3 etc. if the number is 0 or greater than the last number in the list, it drops to the next line, which is the 0th case.

10 on rnd(1)*3 goto 30, 40
20 print "option 0" : return
30 print "option 1" : return
40 print "option 2" : return

Testing that, it looks good

And testing it in the game, I finally saw all three options dished out to the bold traveller.

Jac Goudsmit had suggested the ON x GOTO ... as an option. It didn't work out in the place suggested as the PETSCII line numbers and extra RETURN statements took up more space than the IF statements they relaced, but is ideal here.

The bug fix cost 1 byte, so overall the updates have saved 106 bytes, taking the total saved with the 8K version to 2402 bytes.


I have updated the versions in the github:

I have also uploaded all the versions to my itch.io page.

That shows a mock up tape cover, but maybe if enough of you persuade TFW8b that you would buy one.....


Adverts

My Tindie store is starting to pick up orders again, thank you to those of you who are spending your Christmas money wisely.


Patreon

You can support me via Patreon, and get access to advance previews of development logs on new projects like the Mini PET II and Mini VIC when I get back to them in the new year and other behind the scenes updates. This also includes access to my Patreon only Discord server for even more regular updates.