Fun with DATA — Building a Blocky World Map on the ZX81


I adapted a blocky world map program to the ZX81, working around its missing DATA command. The result is a fun, compressed map that runs.

I’d seen a few different BASIC programs that print out a map of the world. Generally blocky, they still looked decent, but much of the data was too big for the ZX81—until I ran across a C64 version based on a Spectrum program. Examples in hand, I built a version for the ZX81.

# Beginnings.
When I first saw the Commodore 64 version of the block world map, I got the thought that it might fit on the ZX81 screen. I realized why when I dug into the listing. It was based on a Sinclair Spectrum program by Luboš Janku's which shares the same screen size as the ZX81. I was intrigued as other versions tended to be for larger resolutions.

On the positive side, the program was relatively short. Most of the length was in the DATA routines toward the end. I originally thought the program would be a series of data points that would be plotted. While converting, I realized it was instead a compressed screen image. Although not quite what I was looking for, it offered a challenge: how would I deal with the ZX81 BASIC not having DATA and READ commands?

World, Map Expanding, ZX81  Screenshot, 2025 by Steven ReidWorld, Map Expanding, ZX81 Screenshot, 2025 by Steven Reid

# Approaching the lack of data—strings.
The lack of DATA is a long standing problem for the ZX81, even back in the eighties. Many of my books offered tips for converting other programs. The easiest, and partially the way I dealt with it, was to use strings.

Strings are a good choice overall. They offer flexibility since there are different ways to read the data out of them. The challenge is often understanding how the program will read and use the data. With a string, you can either read it in parts using slicing or as a whole. If you’re feeling particularly ambitious, you can even create string arrays. Although, that might waste a good amount of memory since all the string arrays have to be the same size.

The problem with strings is you basically store the data twice. Once in the program itself, and a second time when the data is read and loaded into the variable. This isn’t a huge problem on machines with more memory, but with the ZX81 it can be a challenge.

# Inputing into your variables.
To get around that, another way to handle data is to read it directly into variables. A text adventure interpreter I used to play with from a book used this trick. Basically, it had a loader program where it read the data through a series of INPUT commands. The data was stored directly in the variables for the program.

In addition to saving memory, it also meant you couldn’t list the program and see what the data was. A good choice if you are obfuscating your program. Just don’t forget where you placed your notes—it makes it hard to share anything other than the binary.

One other word of caution: you have to remember never to RUN or CLEAR your program. Doing so erases the variables. On the positive side, using SAVE on the ZX81 always stores the variables as well. Thus, this was often a good choice for large games that needed to save on memory.

# Peeking around the program.
Another way you can approach this problem is to store the code in a REM statement and use PEEK to read the data. I used this approach in an early version of Gem Quest where each REM held the tile graphics. It’s a bit trickier since you need to calculate the memory location. However, it has the advantage of saving memory and retaining the ability to list the program.

The disadvantage to this method is that you have to get each entry one byte at a time. This isn’t particularly fast. I was using some compiled BASIC with MCODER to get around the problem. You could also use a small machine language routine to speed things up. But if you’re going to do that, you might as well just write the whole thing in assembly. Kind of defeats the purpose.

# Reading strings.
The original program had data strings that were variable in length. It would read a line of data each time into a string array. Since the program was relatively small, I decided to just concatenate each of the data lines together into D$. Example below without the inverted characters:

1000 REM **DATA**
1010 LET D$="EEEDDNPPLCIBJBCIH."
1020 LET D$=D$+"DGEADABPPKBBCJBP
EMAEE."

The data itself is a compressed screen image. The structure is straightforward: a number displays a series of spaces, a letter displays a graphic block, and an inverted letter displays a number of inverted spaces. I’ve used similar compression routines myself, and this isn’t a bad approach. For the most part, I didn’t want to change the data. However, I did add a period to the end of each data string so the read would work.

To mimic the read, I wrote a small routine that basically copies each character into a new string, A$. I used a string index in variable L to know what to read next. That string would then be decompressed and displayed on screen. It isn’t super fast, but it works well enough:

 500 REM **READ**
 510 LET A$=""
 520 IF D$(L)="." THEN GOTO 560
 530 LET A$=A$+D$(L)
 540 LET L=L+1
 550 GOTO 520
 560 LET L=L+1
 570 RETURN

# Running the program.
After tweaking the print routine, the final program generated a decent world map. I think it looks pretty good on the ZX81, even if a bit slow. The program will pause for a bit before clearing the screen and starting over.

World, Map Done, ZX81  Screenshot, 2025 by Steven ReidWorld, Map Done, ZX81 Screenshot, 2025 by Steven Reid

# Other thoughts.
There are a multitude of ways to approach the DATA command. Specific to this one, a better method would have been to place each line of the program as a string pointing to A$. Instead of tracking the string position, I would have tracked which line it was on. In this case, F from the FOR loop could be used to calculate which line to read from. Thus, the program would have looked like this:

500 REM **ALT READ**
510 GOTO 10*F+990
1000 REM **ALT DATA**
1010 LET A$="EEEDDNPPLCIBJBCIH"
1015 RETURN
1020 LET A$="DGEADABPPKBBCJBPEMA
EE"
1025 RETURN

This is much tighter, faster, and more memory efficient. Sadly, I thought of this after I understood how the program worked. Same goes for the compression algorithm. Now that I understand how it works, I’d probably tighten up the code a bit. In fact, I could probably remove the outer FOR loop and skip the READ altogether.

Overall, this was a fun program to convert and dissect. There is no right or wrong in programming as there are always trade-offs and compromises. I look forward to seeing the different ways people solve the same problems. Until next time!

---

Want to try it out? You can run the program, or view the code if you’d like to see how it works.



Comments on this article:

No comments so far.

Write a comment:

Type The Letters You See.
[captcha image][captcha image][captcha image][captcha image][captcha image][captcha image]
not case sensitive