In Part 7 we reclaimed some space lost to improvements in time complexity by using bitwise
operations to compress data. We abstracted access
to our grid, representing the active area in Conway’s Game of
Life so that the rest of the code was
unaware that x and y coordinates were converted to and from different values, and bits shifted to
get or set data. The methods provided the abstraction but sat in the same main.c file, which was
now becoming noisy. We are already using the stdio.h library to update the screen as part of our
program, which is fairly wasteful (more on this later) but abstracts away how we set blocks
representing live cells at the correct coordinates. What we would like to do is move all the code
that performs the strange calculations and bit-shifting on the grid into a library of its own, so
that the rest of the code can just concern itself with the game logic. A separation of
concerns, if you will.
It turns out that when you start thinking about the grid and what it is responsible for that should
be hidden, and what should be exposed to the outside world, it’s a bit of a balancing act. Most
abstractions are to a certain extent leaky, and
you’ll never get away from having to know something about what’s going on under the hood, but it’s
still worth doing because it leads to code that’s easier to understand. In C, the language we are
using, you have .h and .c files. .h are the header files that you reference in the rest of
your code (like stdio.h) and you can look in the file to see what methods or variables you have
access to. The .c file is the implementation of those methods plus any other methods or variables
that aren’t exposed to the outside world. In our solution, we can extract a grid.h file, which
declares what the outside world has access to, as follows:
#define GRID_WIDTH 31
#define GRID_HEIGHT 22
unsigned char getGridValue(unsigned char x, unsigned char y);
void setGridValue(unsigned char x, unsigned char y, unsigned char value);
unsigned int getCellLocation(unsigned char x, unsigned char y);
unsigned char getCellXCoord(unsigned int cellLocation);
unsigned char getCellYCoord(unsigned int cellLocation);
The outside world needs to know the grid width and height, plus how to get or set data. There are
also methods for getting the numbered cell location from x and y coordinates, and methods to get
back to the x and y coordinates from the cell location. The last three methods you could argue about
whether it’s a concern for the grid or the rest of the program as it already knows the grid width
and height and can work them out for itself, and I’ve partly just included them to reduce the noise
in main.c. It’s my decision at the end of the day and I may well change my mind, and that’s all
there usually is to it. The implementation file grid.c is as follows:
#include "grid.h"
#define COMPRESSED_GRID_WIDTH 16 // half of GRID_WIDTH rounded up to next even number
unsigned char _grid[COMPRESSED_GRID_WIDTH][GRID_HEIGHT];
unsigned char getGridValue(unsigned char x, unsigned char y)
{
unsigned char gridValue = _grid[x / 2][y];
if (x % 2 != 0) {
gridValue = gridValue >> 4; // rotate the last 4 bits to the first 4
} else {
gridValue = gridValue & 15; // blank out the last 4 bits
}
return gridValue;
}
void setGridValue(unsigned char x, unsigned char y, unsigned char value)
{
unsigned char gridValue = _grid[x / 2][y];
if (x % 2 != 0) {
value = value << 4; // rotate the first 4 bits to the last 4
gridValue = gridValue & 15; // blank out last 4 bits of grid value
} else {
value = value & 15; // blank out the last 4 bits so we don't overwrite
gridValue = gridValue & 240; // blank out first 4 bits of grid value
}
gridValue = gridValue | value;
_grid[x / 2][y] = gridValue;
}
unsigned int getCellLocation(unsigned char x, unsigned char y)
{
unsigned int cellLocation = 0;
cellLocation = GRID_WIDTH * y;
cellLocation += x + 1;
return cellLocation;
}
unsigned char getCellXCoord(unsigned int cellLocation)
{
return (cellLocation % GRID_WIDTH) - 1;
}
unsigned char getCellYCoord(unsigned int cellLocation)
{
return cellLocation / GRID_WIDTH;
}
The grid global variable, and information about its compressed width, is an implementation concern
now. As are the bitwise operations. It’s all our code, and we’ll be in and out of it and it will all
change, but our main.c file is now less noisy.
Replacing stdio.h with our own screen.h library
I mentioned above that using stdio.h was a little wasteful. In fact, using it causes our output
.TAP file to double in size. It was very useful for us in the beginning, to be able to use one of
the standard C libraries to draw to the screen so we could focus on the rest of the code. Now that
we’re comfortable with libraries we can think about creating our own screen.h header and
screen.c implementations, as all we are doing is drawing blocks to the screen, or clearing them. I
have spent a minimal amount of time looking through the z88dk documentation
and forums to find out how we could implement our own and now have something working. Our screen.h
is as follows:
void printBlock(unsigned char x, unsigned char y);
void clearBlock(unsigned char x, unsigned char y);
void printStr(unsigned char x, unsigned char y, unsigned char *s);
void clearScreen();
There is more going on in screen.c than is given away, including a method that draws any kind of
graphic to the screen, but for now, we are only exposing what is needed by the rest of the program.
This code is very specific to how the ZX Spectrum graphics works with some hard-coded memory
addresses and quirks of the hardware. These quirks are all hidden from the rest of our program, and
in turn a library that we reference here arch/zx.h provides a method zx_cxy2saddr(x, y) that we
don’t have to worry about beyond the fact that it’s how we get the screen address for an x and y
coordinate.
#include <arch/zx.h>
// graphics
const unsigned char _block_udg[] = {
0b11111111,
0b11111111,
0b11111111,
0b11111111,
0b11111111,
0b11111111,
0b11111111,
0b11111111
};
const unsigned char _blank_udg[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
unsigned char *_font = (unsigned char *)(15360); // point font into rom character set
void printChr(unsigned char x, unsigned char y, unsigned char* c)
{
unsigned char *p;
unsigned char i;
p = zx_cxy2saddr(x, y);
for (i = 0; i < 8; ++i)
{
*p = *c++;
p += 256;
}
}
void printBlock(unsigned char x, unsigned char y)
{
printChr(x, y, _block_udg);
}
void clearBlock(unsigned char x, unsigned char y)
{
printChr(x, y, _blank_udg);
}
void printStr(unsigned char x, unsigned char y, unsigned char *s)
{
unsigned char c;
while (c = *s++)
{
printChr(x, y, _font + c*8);
if (++x == 32)
{
x = 0;
y++;
}
}
}
void clearScreen()
{
zx_cls(PAPER_WHITE);
}
Now that we have an abstraction for the screen we could develop it further. The bitwise operations
that we used to compress the data by rotating bits are how we could implement a kind of animation.
Character squares act as the anchor (so we use zx_cxy2saddr(x, y)) and then bits can be shifted
along so that sprites move in pixels rather than character squares. This is all probably beyond what
we need in Conway’s Game of Life but it’s nice to know that we have the basis for something more
interesting.
I feel like I’m getting closer to the end of this proof of concept, of a general approach to software development on the ZX Spectrum. I have spent quite a lot of time improving the code and would like to make the solution more interesting, and now that we have a good structure and separation of concerns, on top of the performance improvements we have been making, I can think of a few things I’d like to do. So I will continue on until I have something more interesting to show and perhaps something to say about how I think I can tackle what I really want to do, write a game for the ZX Spectrum. For now, the version of the code for this blog post is included in the project and the current version remains in the project repository.