Monthly Archives: February 2014

Arduino Pac-Man part 4 – maze

In the first entry of this series, I explained how I got started playing with Arduino video output. In the second update, I discussed getting the first TV output displayed. In part 3, I learned how to display bitmaps and animate a simple character. Today, I begin discussing how I built the maze for a Pac-Man style game.

Puck-Man was an arcade game released in Japan in 1980. When it was brought over to America, the name was changed to Pac-Man to avoid having a name that rhymed with a dirty word. To read more on the history of Pac-Man, check out the Wikipedia article:

http://en.wikipedia.org/wiki/Pac-Man

To create an Arduino Pac-Man, I would need to recreate the maze. I was originally going to try to draw out the maze in binary (see part 3). I looked for a screen shot of the actual Pac-Man arcade game so I could recreate the maze as closely as possible. I did a Google image search and found a screen shot like this one from the wikipedia entry:

Pac-man

The tiny image was 224×288 because that was the actual resolution of the arcade game. Many arcade games used standard monitors turned sideways so they were in portrait mode (taller). I would be using a standard landscape TV display and TVout video with a resolution of 120×96. If I was willing to turn a TV set sideways, I could almost make the maze work just by trimming everything in half. Instead, I decided I would get rid of the score at the top, and the lives left/level display at the bottom, and move them to the right side of the screen. This is a common approach taken by home versions of arcade games designed to play on sideways monitors.

I loaded up the image in a graphics program and cropped off the top and bottom, leaving me with something like this:

Pac-man-cropped

Since I was having problems trying to draw this out pixel by pixel in binary, I decided to scale the image down to the desired height (96) and then zoom in to it so I could get a closer look at where the pixels should go. This would produce an 87×96 pixel image, leaving 33 pixels to the size of the image to move the score and other items. (The size would have to be adjusted to 86×95 for some reasons I have already forgotten.)

My scaled image looked like this:

Pac-man-cropped-scaled

That was a bit too tiny to work on, so I zoomed in to 800% and started inspecting the layout.

Screenshot 2014-02-09 15.46.41

Now I was able to try to count dots and start recreating the level. I was too lazy to try to recreate this by hand in binary code, so I made a new layer to draw pixels on, and began drawing out the maze. It looked like this (you will notice I was actually using a different screen shot than the Wikipeida page example I showed above):

Screenshot 2014-02-09 15.52.15

Since the maze is a mirror image, I was only doing one side of the screen. When I got it done, I would just copy that half, flip it and paste it to the other side. The process to do this was a bit time consuming as I was figuring spacing out. Originally, I thought the sides and thing parts of the maze would be solid (two pixels side by side) but as I did the math, I realize I could make the thin sections of the maze have one pixel space, and the thicker portions have two pixel space, and it will all work out. I did quite a bit of trail and error as I adjusting things to make sure the spacing of every hallway was consistent.

I created a Pac-Man graphic object so I could move it around the maze to test clearance. In my TVout experiment, I was making an 8×7 Pac-Man. To fit in this maze, the character would have to be 5×5, as would the ghosts. Eventually, I had a maze created, including where the dots would go (even the correct number of them). My finished product looked like this:

Screenshot 2014-02-09 16.00.46

The grid lines were set to the spacing of an individual pixel, to help with drawing. You will see the test Pac-Man bitmaps as well as a ghost, which I would then move around the screen in the graphics editor to make sure all the tunnels and hallways were the same size. Eventually, I produced this final graphic:

pacman-arduino-mazedots

You may notice that there is some extra space on the right side. I kept the final output to be a multiple of 8-pixels (one per byte) because I thought it might make things easier for what comes next.

At this point, I could have used this graphic and recreated it in binary source code, but I decided to use a program that would convert a graphics file to C data structures. The TVout site suggests a program, but I would have had to compile it and run it from a command line. Instead, a quick Google led me to this site:

http://www.cemetech.net/sc

This web page allows you to upload a graphics file and have it translated in to a variety of formats. It seems to be for graphic calculators, but one of the options was “Prizm/Nspire C (1/2/4/8-Bit Palettized Color)” which did the trick. I uploaded my bitmap graphics file, selected that conversion option, and then it produced the following source code:

1-Bit Palettized Color: Each pixel takes 1 bits.

n

	const color_t sprite_palette[2] = {0x0000, 0xffff};

	const unsigned char sprite[1045] = {
		0x3f,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf0,
		0x40,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x08,
		0x9f,0xff,0xff,0xff,0xff,0x87,0xff,0xff,0xff,0xff,0xe4,
		0xa0,0x00,0x00,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x14,
		0xa0,0x00,0x00,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x14,
		0xa4,0x92,0x49,0x24,0x92,0x49,0x24,0x92,0x49,0x24,0x94,
		0xa0,0x00,0x00,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x14,
		0xa0,0x00,0x00,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x14,
		0xa4,0x7f,0x88,0xff,0xe2,0x49,0x1f,0xfc,0x47,0xf8,0x94,
		0xa0,0x80,0x41,0x00,0x10,0x48,0x20,0x02,0x08,0x04,0x14,
		0xa4,0x80,0x41,0x00,0x10,0x48,0x20,0x02,0x08,0x04,0x94,
		0xae,0x80,0x49,0x00,0x12,0x49,0x20,0x02,0x48,0x05,0xd4,
		0xa4,0x80,0x41,0x00,0x10,0x48,0x20,0x02,0x08,0x04,0x94,
		0xa0,0x80,0x41,0x00,0x10,0x48,0x20,0x02,0x08,0x04,0x14,
		0xa4,0x7f,0x88,0xff,0xe2,0x31,0x1f,0xfc,0x47,0xf8,0x94,
		0xa0,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x14,
		0xa0,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x14,
		0xa4,0x92,0x49,0x24,0x92,0x49,0x24,0x92,0x49,0x24,0x94,
		0xa0,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x14,
		0xa0,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x14,
		0xa4,0x7f,0x88,0xc4,0x7f,0xff,0xf8,0x8c,0x47,0xf8,0x94,
		0xa0,0x80,0x41,0x20,0x80,0x00,0x04,0x12,0x08,0x04,0x14,
		0xa0,0x80,0x41,0x20,0x80,0x00,0x04,0x12,0x08,0x04,0x14,
		0xa4,0x7f,0x89,0x24,0x7f,0xcf,0xf8,0x92,0x47,0xf8,0x94,
		0xa0,0x00,0x01,0x20,0x00,0x48,0x00,0x12,0x00,0x00,0x14,
		0xa0,0x00,0x01,0x20,0x00,0x48,0x00,0x12,0x00,0x00,0x14,
		0xa4,0x92,0x49,0x24,0x92,0x49,0x24,0x92,0x49,0x24,0x94,
		0xa0,0x00,0x01,0x20,0x00,0x48,0x00,0x12,0x00,0x00,0x14,
		0xa0,0x00,0x01,0x20,0x00,0x48,0x00,0x12,0x00,0x00,0x14,
		0xbf,0xff,0x89,0x1f,0xe0,0x48,0x1f,0xe2,0x47,0xff,0xf4,
		0x80,0x00,0x41,0x00,0x10,0x48,0x20,0x02,0x08,0x00,0x04,
		0x7f,0xfe,0x41,0x00,0x10,0x48,0x20,0x02,0x09,0xff,0xf8,
		0x00,0x01,0x49,0x1f,0xe0,0x30,0x1f,0xe2,0x4a,0x00,0x00,
		0x00,0x01,0x41,0x20,0x00,0x00,0x00,0x12,0x0a,0x00,0x00,
		0x00,0x01,0x41,0x20,0x00,0x00,0x00,0x12,0x0a,0x00,0x00,
		0x00,0x01,0x49,0x20,0x00,0x00,0x00,0x12,0x4a,0x00,0x00,
		0x00,0x01,0x41,0x20,0x00,0x00,0x00,0x12,0x0a,0x00,0x00,
		0x00,0x01,0x41,0x20,0x00,0x00,0x00,0x12,0x0a,0x00,0x00,
		0x00,0x01,0x49,0x20,0xff,0xff,0xfc,0x12,0x4a,0x00,0x00,
		0xff,0xfe,0x41,0x20,0xff,0x03,0xfc,0x12,0x09,0xff,0xfc,
		0x00,0x00,0x41,0x20,0xc0,0x00,0x0c,0x12,0x08,0x00,0x00,
		0xff,0xff,0x88,0xc0,0xc0,0x00,0x0c,0x0c,0x47,0xff,0xfc,
		0x00,0x00,0x00,0x00,0xc0,0x00,0x0c,0x00,0x00,0x00,0x00,
		0x00,0x00,0x00,0x00,0xc0,0x00,0x0c,0x00,0x00,0x00,0x00,
		0x00,0x00,0x08,0x00,0xc0,0x00,0x0c,0x00,0x40,0x00,0x00,
		0x00,0x00,0x00,0x00,0xc0,0x00,0x0c,0x00,0x00,0x00,0x00,
		0x00,0x00,0x00,0x00,0xc0,0x00,0x0c,0x00,0x00,0x00,0x00,
		0xff,0xff,0x88,0xc0,0xc0,0x00,0x0c,0x0c,0x47,0xff,0xfc,
		0x00,0x00,0x41,0x20,0xc0,0x00,0x0c,0x12,0x08,0x00,0x00,
		0xff,0xfe,0x41,0x20,0xff,0xff,0xfc,0x12,0x09,0xff,0xfc,
		0x00,0x01,0x49,0x20,0xff,0xff,0xfc,0x12,0x4a,0x00,0x00,
		0x00,0x01,0x41,0x20,0x00,0x00,0x00,0x12,0x0a,0x00,0x00,
		0x00,0x01,0x41,0x20,0x00,0x00,0x00,0x12,0x0a,0x00,0x00,
		0x00,0x01,0x49,0x20,0x00,0x00,0x00,0x12,0x4a,0x00,0x00,
		0x00,0x01,0x41,0x20,0x00,0x00,0x00,0x12,0x0a,0x00,0x00,
		0x00,0x01,0x41,0x20,0x00,0x00,0x00,0x12,0x0a,0x00,0x00,
		0x00,0x01,0x49,0x20,0x7f,0xff,0xf8,0x12,0x4a,0x00,0x00,
		0x7f,0xfe,0x41,0x20,0x80,0x00,0x04,0x12,0x09,0xff,0xf8,
		0x80,0x00,0x41,0x20,0x80,0x00,0x04,0x12,0x08,0x00,0x04,
		0xbf,0xff,0x88,0xc0,0x7f,0xcf,0xf8,0x0c,0x47,0xff,0xf4,
		0xa0,0x00,0x00,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x14,
		0xa0,0x00,0x00,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x14,
		0xa4,0x92,0x49,0x24,0x92,0x49,0x24,0x92,0x49,0x24,0x94,
		0xa0,0x00,0x00,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x14,
		0xa0,0x00,0x00,0x00,0x00,0x48,0x00,0x00,0x00,0x00,0x14,
		0xa4,0x7f,0x88,0xff,0xe2,0x49,0x1f,0xfc,0x47,0xf8,0x94,
		0xa0,0x80,0x41,0x00,0x10,0x48,0x20,0x02,0x08,0x04,0x14,
		0xa0,0x80,0x41,0x00,0x10,0x48,0x20,0x02,0x08,0x04,0x14,
		0xa4,0x7c,0x48,0xff,0xe2,0x31,0x1f,0xfc,0x48,0xf8,0x94,
		0xa0,0x02,0x40,0x00,0x00,0x00,0x00,0x00,0x09,0x00,0x14,
		0xa4,0x02,0x40,0x00,0x00,0x00,0x00,0x00,0x09,0x00,0x94,
		0xae,0x92,0x49,0x24,0x92,0x01,0x24,0x92,0x49,0x25,0xd4,
		0xa4,0x02,0x40,0x00,0x00,0x00,0x00,0x00,0x09,0x00,0x94,
		0xa0,0x02,0x40,0x00,0x00,0x00,0x00,0x00,0x09,0x00,0x14,
		0xbe,0x12,0x48,0xc4,0x7f,0xff,0xf8,0x8c,0x49,0x21,0xf4,
		0x81,0x02,0x41,0x20,0x80,0x00,0x04,0x12,0x09,0x02,0x04,
		0x81,0x02,0x41,0x20,0x80,0x00,0x04,0x12,0x09,0x02,0x04,
		0xbe,0x11,0x89,0x24,0x7f,0x8f,0xf8,0x92,0x46,0x21,0xf4,
		0xa0,0x00,0x01,0x20,0x00,0x48,0x00,0x12,0x00,0x00,0x14,
		0xa0,0x00,0x01,0x20,0x00,0x48,0x00,0x12,0x00,0x00,0x14,
		0xa4,0x92,0x49,0x24,0x92,0x49,0x24,0x92,0x49,0x24,0x94,
		0xa0,0x00,0x01,0x20,0x00,0x48,0x00,0x12,0x00,0x00,0x14,
		0xa0,0x00,0x01,0x20,0x00,0x48,0x00,0x12,0x00,0x00,0x14,
		0xa4,0x7f,0xff,0x3f,0xe2,0x49,0x1f,0xf3,0xff,0xf8,0x94,
		0xa0,0x80,0x00,0x00,0x10,0x48,0x20,0x00,0x00,0x04,0x14,
		0xa0,0x80,0x00,0x00,0x10,0x48,0x20,0x00,0x00,0x04,0x14,
		0xa4,0x7f,0xff,0xff,0xe2,0x31,0x1f,0xff,0xff,0xf8,0x94,
		0xa0,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x14,
		0xa0,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x14,
		0xa4,0x92,0x49,0x24,0x92,0x49,0x24,0x92,0x49,0x24,0x94,
		0xa0,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x14,
		0xa0,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x14,
		0x9f,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xe4,
		0x40,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x08,
		0x3f,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf0
	};

Look familiar? We do not need the “const color_t sprite_palette” structure, and only need to make a minor change to the other one (adding PROGMEM so it is stored in Arduino flash rather than RAM, and adding the width and height values as the start):

n

PROGMEM const unsigned char playfield[] = {
  88,95,
  0x3f,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xf0,
  0x40,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x08,
  ...

This gave me a bitmap I could display on startup with “TV.bitmap(0, 0, play field)”. I created the new Pac-Man 5×5 bitmaps by hand, as well as the ghosts. For Pac-man, he can face four directions, so I decided to add another dimension to the array: direction. Then, each direction had four frames. And each frame had the array of actual bitmap data. Fun! It looked like this:

n

#define PLAYERW    5
#define PLAYERH    5
#define PDIRS      2
#define PFRAMES    4

PROGMEM const unsigned char player[PDIRS][PFRAMES][PLAYERH+2] = {
  { // Right
    {
      PLAYERW,PLAYERH,  // width, height
      //12345---
      0b01110000,
      0b11111000,
      0b11111000,
      0b11111000,
      0b01110000
    }
    ,
    {
      PLAYERW,PLAYERH,  // width, height
      //12345---
      0b01110000,
      0b11111000,
      0b11000000,
      0b11111000,
      0b01110000
    }
    ,
    {
      PLAYERW,PLAYERH,  // width, height
      //12345---
      0b01110000,
      0b11100000,
      0b11000000,
      0b11100000,
      0b01110000
    }
    ,
    {
      PLAYERW,PLAYERH,  // width, height
      //12345---
      0b01110000,
      0b11111000,
      0b11000000,
      0b11111000,
      0b01110000
    }
  }
  ,
  { // Left
    {
      PLAYERW,PLAYERH,  // width, height
      //12345---
      0b01110000,
      0b11111000,
      0b11111000,
      0b11111000,
      0b01110000
    }
    ,
    {
      PLAYERW,PLAYERH,  // width, height
      0b01110000,
      0b11111000,
      0b00111000,
      0b11111000,
      0b01110000
    }
    ,
    {
      PLAYERW,PLAYERH,  // width, height
      //12345---
      0b01110000,
      0b00111000,
      0b00011000,
      0b00111000,
      0b01110000
    }
    ,
    {
      PLAYERW,PLAYERH,  // width, height
      //12345---
      0b01110000,
      0b11111000,
      0b00111000,
      0b11111000,
      0b01110000
    }
  }
  ,
};

Since I was just making things up as I went (I started working on this around 10pm or so, and spent a few hours on it), I probably will clean things up in the code I present in these articles. In the above example, the bitmaps are 0=right, 1=left, 2=up and 3=down. I think I may reorder them to follow the “NEWS” format (0=north/up, 1=wast/right, 2=west/left, and 3=south/down) just so there is a pattern. Not that it really matters, since a good program would be using #defines for the directions anyway, so the magic numbers won’t be seen.

For the ghosts, since there are no colors, and the resolution is so small you can’t really even do tricks with checkerboard patterns, I decided all the ghosts needed were two frames (to animate the bottoms), and four bitmaps to represent the ghost looking up, down, left or right. I am still not sure I like the bottom of the ghosts, but here is the data for them:

n

#define GHOSTS     1
#define GHOSTW     5
#define GHOSTH     5
#define GDIRS      4
#define GFRAMES    2

PROGMEM const unsigned char ghost[GDIRS][GFRAMES][GHOSTH+2] = {
  { // Right
    {
      GHOSTW,GHOSTH,  // width, height
      //12345---
      0b01110000,
      0b11111000,
      0b11010000,
      0b11111000,
      0b10101000
    }
    ,
    {
      GHOSTW,GHOSTH,  // width, height
      //12345---
      0b01110000,
      0b11111000,
      0b11010000,
      0b11111000,
      0b01010000
    }
  }
  ,
  { // Left
    {
      GHOSTW,GHOSTH,  // width, height
      //12345---
      0b01110000,
      0b11111000,
      0b01011000,
      0b11111000,
      0b10101000
    }
    ,
    {
      GHOSTW,GHOSTH,  // width, height
      //12345---
      0b01110000,
      0b11111000,
      0b01011000,
      0b11111000,
      0b01010000
    }
  }
  ,
  { // Up
    {
      GHOSTW,GHOSTH,  // width, height
      //12345---
      0b01110000,
      0b10101000,
      0b11111000,
      0b11111000,
      0b10101000
    }
    ,
    {
      GHOSTW,GHOSTH,  // width, height
      //12345---
      0b01110000,
      0b10101000,
      0b11111000,
      0b11111000,
      0b01010000
    }
  }
  ,
  { // Down
    {
      GHOSTW,GHOSTH,  // width, height
      //12345---
      0b01110000,
      0b11111000,
      0b10101000,
      0b11111000,
      0b10101000
    }
    ,
    {
      GHOSTW,GHOSTH,  // width, height
      //12345---
      0b01110000,
      0b11111000,
      0b10101000,
      0b11111000,
      0b01010000
    }
  }
};

I can now animate the ghosts the same way I did the Pac-Man in the earlier example. My next task would be to put Pac-Man on the screen and let him move around.

To be continued… (After the next part, I will have covered all the work I did that night, and I will begin turning this test in to an actual game. Assuming I have time to work on it.)

Arduino Pac-Man part 3 – animating bitmaps

In the first entry of this series, I discussed what led me to experimenting with video output on an Arduino UNO. In the second entry, I describe getting my first simple sketch showing on the TV, including bouncing some balls around the screen and moving a player. Today I continue sharing my progress as I started playing with bitmap graphics.

Another feature of the TVout library is the support of bitmapped graphics. Since the display is black and white, the graphics are 1-bit images, where each bit of a byte represents 8 pixels on the screen. “00000000” would be all pixels off, and “11111111” is all pixels on. The TVout library has a simple structure for these bitmaps, which is just an array of bytes. The first two bytes are the width (in pixels) and height (in bytes) of the bitmap. A simple 8×8 square might look like this:

n

PROGMEM const unsigned char square[] = {
  8, 8,
  0xFF,
  0xFF,
  0xFF,
  0xFF,
  0xFF,
  0xFF,
  0xFF
}

Digression: Hexidecimal, binary and other weird numbers.

If you are already familiar with hexadecimal, binary and such, skip this section.

0xFF in hexidecimal is 255, which is 11111111 in binary (all bits on). If you have never used hex before, here’s a quick explanation. Our “normal” numbers are base ten. We count 0 to 9 (ten) and then increment the digit to the left. We can count 0 to 9, and then we add a 1 to the beginning and start over going 10 to 19, then we add 1 to the first digit and go 20 to 29. When the first digit reaches 9 (at 99), we continue with 100 and so on.

Hexideximal is base 16. We do exactly the same thing, but counting to 16. Since our digits only cover 0-9 (ten), hex continues after the 9 with the letters A through F. So, with hex, you count 0-9, followed by A-F, then you increment the first digit and it becomes 10 through 1F, and then 20 through 2F and so on.

I wish someone would have explained it to me like this back in 1982. I found it far more confusing then.

Anyway, in a hex number (represented in C by 0x at the start), each digit represents four pixels. 0xF0 would be “11110000” and 0x0F would be “00001111”. In binary (base 2), the numbering counts to 2 and then adds the one, so you get 0, 1, 10, 11, 100, 101, 110, 111, and so on. At this point, it gets more confusing.

My point is, drawing a square in hex may be easy (all pixels on is F), but trying to create anything else quickly gets confusing. You may know binary enough to know that you can figure out the bit values (0001=1, 0010=2, 0100=4, 1000=8), but trying to draw something using 3C EF AE 31 is not the way I want to spend my time.  Instead, we cheat.

End of digression.

Creating the bitmap in hex is cumbersome, but there is a non-standard way to represent binary in the compiler that the Arduino IDE uses. This is non-standard and generally you should not write code using things that are non-standard because that code may not work in other places. But, since we are cheating, and will only be running the code on an Arduino, here is how it works:

In C, the prefix “0x” makes a number hexadecimal. In non-standard Arduino C, you can use “0b” to make the number binary. It looks like this:

n

x = 255;        // 255 (11111111) in decimal
x = 0xFF;       // 255 (11111111) in hexidecimal
x = 0b11111111; // 255 (11111111) in binary

This would make creating 1-bit graphics much easier in source code:

n

PROGMEM const unsigned char square[] = {
  8, 8,
  0b11111111,
  0b11111111,
  0b11111111,
  0b11111111,
  0b11111111,
  0b11111111,
  0b11111111,
  0b11111111
}

Don’t those 1s look more like a solid square now? How about a non-solid square with an “X” across it?

n

PROGMEM const unsigned char square[] = {
  8, 8,
  0b11111111,
  0b11000011,
  0b10100101,
  0b10011001,
  0b10011001,
  0b10100101,
  0b11000011,
  0b11111111
}

If you squint just right, you can see it. And this is how I decided to change my filled circle to a Pac-Man shape. Here are three variations of Pac-Man facing the right (mouth closed, mouth partially open, and mouth fully open). To get things to work out evenly, the character size was 8×7.

n

PROGMEM const unsigned char pacmanClosed[] = {
  PLAYERW,PLAYERH,  // width, height
  0b00111100,
  0b01111110,
  0b11111111,
  0b11111111,
  0b11111111,
  0b01111110,
  0b00111100
};

PROGMEM const unsigned char pacmanOpenSmall[] = {
  PLAYERW,PLAYERH,  // width, height
  0b00111100,
  0b01111110,
  0b11111100,
  0b11110000,
  0b11111100,
  0b01111110,
  0b00111100
};

PROGMEM const unsigned char pacmanOpenBig[] = {
  PLAYERW,PLAYERH,  // width, height
  0b00111100,
  0b01111100,
  0b11111000,
  0b11110000,
  0b11111000,
  0b01111100,
  0b00111100
};

Any one of these bitmaps could be displayed by doing “TV.bitmap(x, y, pacmanClosed);” Initially I just drew an open-mouthed Pac-Man and moved him around instead of the filled circle. Here is the complete code, with some extra stuff added, like a border around the screen (and adjusting the X/Y edge detection to know about that).

n

#include <TVout.h>

TVout TV;

#define BALLS      10 // Number of balls to bounce.
#define BALLSIZE   4  // Size of balls.
#define PLAYERSIZE 6  // Size of player.

#define ANALOGXPIN 0  // Pin 0 is X on iTead joystick
#define ANALOGYPIN 1  // Pin 1 is Y on iTead joystick

#define PLAYERW    8
#define PLAYERH    7

#define BORDERSIZE 2  // 1 pixel border around the screen

PROGMEM const unsigned char pacmanOpenBig[] = {
  PLAYERW, PLAYERH,  // width, height
  0b00111100,
  0b01111100,
  0b11111000,
  0b11110000,
  0b11111000,
  0b01111100,
  0b00111100
};

void setup()
{
  uint8_t i;

  TV.begin(NTSC, 120, 96);
  Serial.begin(9600);

  TV.clear_screen();

  for (i=0; i<BORDERSIZE; i++)
  {
    TV.draw_rect(i, i, TV.hres()-i*2-1, TV.vres()-i*2-1, WHITE);
  }
}

void loop()
{
  uint8_t  x[BALLS], y[BALLS];    // X and Y position of ball
  int8_t   xm[BALLS], ym[BALLS];  // X and Y movement of ball
  uint8_t  i;       // counter

  uint8_t  px, py;                // X and Y position of player

  // Initialize balls.
  for (i=0; i<BALLS; i++)
  {
    // Random position
    x[i] = random(BALLSIZE+BORDERSIZE, TV.hres()-BALLSIZE-BORDERSIZE-1);
    y[i] = random(BALLSIZE+BORDERSIZE, TV.vres()-BALLSIZE-BORDERSIZE-1);

    // Random direction
    xm[i] = random(2)*2 - 1;
    ym[i] = random(2)*2 - 1;
  }

  // Initialize player.
  px = TV.hres()/2;
  py = TV.vres()/2;

  // We will do our own control loop here.
  while(1)
  {
    // Wait for end of screen to be drawn.
    TV.delay_frame(1);

    for (i=0; i<BALLS; i++)
    {
      // Erase balls.
      TV.draw_circle(x[i], y[i], BALLSIZE, BLACK);

      x[i] = x[i] + xm[i];
      if (x[i]<=BALLSIZE+BORDERSIZE || x[i]>=TV.hres()-BALLSIZE-BORDERSIZE-1)
      {
        xm[i] = -xm[i];
        x[i] = x[i] + xm[i];
      }
      y[i] = y[i] + ym[i];
      if (y[i]<=BALLSIZE+BORDERSIZE || y[i]>=TV.vres()-BALLSIZE-BORDERSIZE-1)
      {
        ym[i] = -ym[i];
        y[i] = y[i] + ym[i];
      }

      TV.draw_circle(x[i], y[i], BALLSIZE, WHITE);
    }

    // Erase player
    TV.draw_rect(px, py, PLAYERW, PLAYERH, BLACK, BLACK);

    // Read joystick (0-1023) and convert to screen resolution.
    px = analogRead(ANALOGXPIN)/(1024/TV.hres());
    if (px<BORDERSIZE)
    {
      px = BORDERSIZE;
    } else if (px>=TV.hres()-PLAYERW-BORDERSIZE-1)
    {
      px = TV.hres()-PLAYERW-BORDERSIZE-1;
    }

    py = analogRead(ANALOGYPIN)/(1024/TV.vres());
    if (py<BORDERSIZE)
    {
      py = BORDERSIZE;
    } else if (py>=TV.vres()-PLAYERH-BORDERSIZE-1)
    {
      py = TV.vres()-PLAYERH-BORDERSIZE-1;
    }

    // Draw player.
    TV.bitmap(px, py, pacmanOpenBig);
  }
}

In the above code, the right and bottom edge detection is still off by one pixel, but I was just rushing from experiment to experiment and wasn’t taking the time to fix things.

So now we have a Pac-Man that can be moved around the screen… But Pac-Man animates. I had already created the different frames of Pac-Man, but rather than use code to choose which one to display, I decided I would put them all in an array, and let the program cycle through them. Instead of just having one array of bitmap bytes, I would use a multidimensional array so I could hold each array of bitmap frames in it. I duplicated one of the animation frames so I could just cycle through them (1 closed, 2 slightly open, 3 fully open, 4 slightly open, 1 closed, 2 slightly open, 3 fully , 4 closed) instead of having to control the sequence myself.

I created the array like this:

n

PROGMEM const unsigned char pacman[PFRAMES][PLAYERH+2] = {
  { // Closed
    PLAYERW,PLAYERH,  // width, height
    0b00111100,
    0b01111110,
    0b11111111,
    0b11111111,
    0b11111111,
    0b01111110,
    0b00111100
  }
  ,
  { // Open Small
    PLAYERW,PLAYERH,  // width, height
    0b00111100,
    0b01111110,
    0b11111100,
    0b11110000,
    0b11111100,
    0b01111110,
    0b00111100
  }
  ,
  { // Open Big
    PLAYERW,PLAYERH,  // width, height
    0b00111100,
    0b01111100,
    0b11111000,
    0b11110000,
    0b11111000,
    0b01111100,
    0b00111100
  }
  ,
  { // Open Small
    PLAYERW,PLAYERH,  // width, height
    0b00111100,
    0b01111110,
    0b11111100,
    0b11110000,
    0b11111100,
    0b01111110,
    0b00111100
  }
};

I needed a new #define for the number of frames (four frames, an array of four), and then had to specify the size of each array. If the bitmap is 7 bytes tall, the array is 7 plus the extra two bytes at the start that tell the width and height.

Now to display a frame, I would use “TV.bitmap(px, py, pacman[frame]);” where frame is 0-3 (four frames). I added a new frame counter that would cycle from 0 to 3 over and over, but it was way too fast. I added a frame delay value so it would only increment the frame every type that count was reached. The code that does that looks like this:

n

#include <TVout.h>

TVout TV;

#define BALLS      10 // Number of balls to bounce.
#define BALLSIZE   4  // Size of balls.
#define PLAYERSIZE 6  // Size of player.

#define ANALOGXPIN 0  // Pin 0 is X on iTead joystick
#define ANALOGYPIN 1  // Pin 1 is Y on iTead joystick

#define PLAYERW    8  // Width of player.
#define PLAYERH    7  // Height of player.
#define PFRAMES    4  // Frames for the player.
#define PFRATE     5  // Count to 5, display next frame.

#define BORDERSIZE 2  // 1 pixel border around the screen

PROGMEM const unsigned char pacman[PFRAMES][PLAYERH+2] = {
  { // Closed
    PLAYERW,PLAYERH,  // width, height
    0b00111100,
    0b01111110,
    0b11111111,
    0b11111111,
    0b11111111,
    0b01111110,
    0b00111100
  }
  ,
  { // Open Small
    PLAYERW,PLAYERH,  // width, height
    0b00111100,
    0b01111110,
    0b11111100,
    0b11110000,
    0b11111100,
    0b01111110,
    0b00111100
  }
  ,
  { // Open Big
    PLAYERW,PLAYERH,  // width, height
    0b00111100,
    0b01111100,
    0b11111000,
    0b11110000,
    0b11111000,
    0b01111100,
    0b00111100
  }
  ,
  { // Open Small
    PLAYERW,PLAYERH,  // width, height
    0b00111100,
    0b01111110,
    0b11111100,
    0b11110000,
    0b11111100,
    0b01111110,
    0b00111100
  }
};

void setup()
{
  uint8_t i;

  TV.begin(NTSC, 120, 96);
  Serial.begin(9600);

  TV.clear_screen();

  for (i=0; i<BORDERSIZE; i++)
  {
    TV.draw_rect(i, i, TV.hres()-i*2-1, TV.vres()-i*2-1, WHITE);
  }
}

void loop()
{
  uint8_t  x[BALLS], y[BALLS];    // X and Y position of ball
  int8_t   xm[BALLS], ym[BALLS];  // X and Y movement of ball
  uint8_t  i;       // counter

  uint8_t  px, py;                // X and Y position of player
  uint8_t  playerFrame;           // Player frame to display
  uint8_t  playerRate;            // Frame speed counter

  // Initialize balls.
  for (i=0; i<BALLS; i++)
  {
    // Random position
    x[i] = random(BALLSIZE+BORDERSIZE, TV.hres()-BALLSIZE-BORDERSIZE-1);
    y[i] = random(BALLSIZE+BORDERSIZE, TV.vres()-BALLSIZE-BORDERSIZE-1);

    // Random direction
    xm[i] = random(2)*2 - 1;
    ym[i] = random(2)*2 - 1;
  }

  // Initialize player.
  px = TV.hres()/2;
  py = TV.vres()/2;

  playerFrame = 0;
  playerRate = 0;

  // We will do our own control loop here.
  while(1)
  {
    // Wait for end of screen to be drawn.
    TV.delay_frame(1);

    for (i=0; i<BALLS; i++)
    {
      // Erase balls.
      TV.draw_circle(x[i], y[i], BALLSIZE, BLACK);

      x[i] = x[i] + xm[i];
      if (x[i]<=BALLSIZE+BORDERSIZE || x[i]>=TV.hres()-BALLSIZE-BORDERSIZE-1)
      {
        xm[i] = -xm[i];
        x[i] = x[i] + xm[i];
      }
      y[i] = y[i] + ym[i];
      if (y[i]<=BALLSIZE+BORDERSIZE || y[i]>=TV.vres()-BALLSIZE-BORDERSIZE-1)
      {
        ym[i] = -ym[i];
        y[i] = y[i] + ym[i];
      }

      TV.draw_circle(x[i], y[i], BALLSIZE, WHITE);
    }

    // Erase player
    TV.draw_rect(px, py, PLAYERW, PLAYERH, BLACK, BLACK);

    // Read joystick (0-1023) and convert to screen resolution.
    px = analogRead(ANALOGXPIN)/(1024/TV.hres());
    if (px<BORDERSIZE)
    {
      px = BORDERSIZE;
    }
    else if (px>=TV.hres()-PLAYERW-BORDERSIZE-1)
    {
      px = TV.hres()-PLAYERW-BORDERSIZE-1;
    }

    py = analogRead(ANALOGYPIN)/(1024/TV.vres());
    if (py<BORDERSIZE)
    {
      py = BORDERSIZE;
    }
    else if (py>=TV.vres()-PLAYERH-BORDERSIZE-1)
    {
      py = TV.vres()-PLAYERH-BORDERSIZE-1;
    }

    // Draw player.
    TV.bitmap(px, py, pacman[playerFrame]);

    playerRate++;
    if (playerRate>=PFRAMES)
    {
      playerRate = 0;
      playerFrame++;
      if (playerFrame>=PFRAMES)
      {
        playerFrame = 0;
      }
    }
  }
}

And now I had an animated Pac-Man I could move around the screen… At this point, I think I had decided I would try to draw a Pac-Man maze and see if I could move the Pac-Man around in it.

In the next update, we will look at how I recreated the Pac-Man maze.

Arduino Pac-Man part 2 – It lives!

In the first entry of this series, I discussed what led me to experimenting with video output on an Arduino UNO. Today I will begin retracing my steps of what I did on the night of February 9th, 2014, which led to be creating the basic structure of an unplanned Pac-Man style video game.

To hook video up to an Arduino, all you need is two resistors and an RCA connector. I used an old audio/video cable for a digital camera, and had to visit my local Radio Shack to pick up one of the resistors I did not already have. On the TVout project page, you will see you need 1K ohm resistor, and a 470 ohm resistor. Each resistor connects to specific pins of the Ardunio, then they are wired together on the other side and connected to the center (video) wire of the RCA cable/jack. The shield wire of the RCA cable/jack goes to the Arduino ground. That’s all there is too it. Here is a cluttered photo of how I wired things up, using an iTead Studios screw shield to make things a bit easier, and a random terminal block screw thing just to hold some wires together:

20140210-122859.jpg

To install the TVout library, visit the Google-hostred project page and download the zip file. I extracted the zip file, and then from the Arduino IDE I selected “Sketch -> Import Library -> Add Library…”

Screenshot 2014-02-09 13.09.27

After this, I was able to create a new project based on one of the included TVout examples:

Screenshot 2014-02-09 13.10.51

I built and uploaded this example project, and watched it run on the only thing I had handy with composite video input: some old video goggles I bought from a liquidator clearance years ago for dirt cheap. These low resolution displays were useless for most things, but would have more resolution than what the TVout could provide: 120×96 default.

The demo looks like this (video from YouTube user Cottees):

It lives! I must have connected the two wires and two resistors properly!

Now, with video output, the first thing I wanted to do was create my own sketch that drew something. I gutted the demo leaving just the bare lines of code:

n

#include <TVout.h>

TVout TV;

void setup()
{
  TV.begin(NTSC, 120, 96);

  TV.clear_screen();

  TV.draw_circle(TV.hres()/2,TV.vres()/2,TV.vres()/3,WHITE);
}

void loop()
{
}

This simple sketch draw a circle in the middle of the display. Once I had this working, my next thought was to test animation by making the circle bounce around the screen. I added variables to track X and Y position, and movement for X and Y (either +1 or -1, or 0 if not moving). I also added edge detection so the ball wouldn’t fly off the screen. (The TVout library appears to be coded for speed and does little if any error checking – drawing things outside of displayed screen crashed my UNO).

The circle blazed across the screen leaving a trail of pixels.

n

#include <TVout.h>

TVout TV;

void setup()
{
  TV.begin(NTSC, 120, 96);

  TV.clear_screen();
}

void loop()
{
  uint8_t  x, y;    // X and Y position of ball
  int8_t   xm, ym;  // X and Y movement of ball

  // Center of screen
  x = TV.hres()/2;
  y = TV.vres()/2;

  // Start moving to the right, and down.
  xm = 1;
  ym = 1;

  // We will do our own control loop here.
  while(1)
  {
    TV.draw_circle(x, y, 4, WHITE);

    x = x + xm;
    if (x<4 || x>TV.hres()-4) xm = -xm;
    y = y + ym;
    if (y<4 || y>TV.vres()-4) ym = -ym;
  }
}

To resolve this, I added some code to erase the circle before drawing it at a new position. This created much flickering, so I used the “wait for frame” function so I could slow things down and do the drawing/erasing at the end of a screen being updated.

n

#include <TVout.h>

TVout TV;

void setup()
{
  TV.begin(NTSC, 120, 96);

  TV.clear_screen();
}

void loop()
{
  uint8_t  x, y;    // X and Y position of ball
  int8_t   xm, ym;  // X and Y movement of ball

  // Center of screen
  x = TV.hres()/2;
  y = TV.vres()/2;

  // Start moving to the right, and down.
  xm = 1;
  ym = 1;

  // We will do our own control loop here.
  while(1)
  {
    // Wait for end of screen to be drawn.
    TV.delay_frame(1);

    // Erase circle
    TV.draw_circle(x, y, 4, BLACK);

    x = x + xm;
    if (x<4 || x>=TV.hres()-4) xm = -xm;
    y = y + ym;
    if (y<4 || y>=TV.vres()-4) ym = -ym;

    TV.draw_circle(x, y, 4, WHITE);
  }
}

Next, I wondered how many more balls I could bounce before it started to slow down. I changed my X and Y variables to arrays, and used a #define to set the number of balls. I also made a #define for the ball size, and could use this for checking the edge of the screen. If a circle size is 4, the center will be at the specified X and Y coordinate, and the circle will be drawn 4 pixels away from that. So, if you tried to “draw_circle(0, 0, 4)”, the left and top of the circle would be drawn off the screen. To adjust, X and Y should never go lower than the circle size, and should never go larger than screen width/height minus circle size. (Note the use of “BALLSIZE-1” in the example below. TVout will report the horizontal size as 120, and vertical size as 96. The pixels are actually 0-119 and 0-95, so you subtract one to get the end pixel.)

n

#include <TVout.h>

TVout TV;

#define BALLS    10 // Number of balls to bounce.
#define BALLSIZE 4  // Size of balls.

void setup()
{
  TV.begin(NTSC, 120, 96);

  TV.clear_screen();
}

void loop()
{
  uint8_t  x[BALLS], y[BALLS];    // X and Y position of ball
  int8_t   xm[BALLS], ym[BALLS];  // X and Y movement of ball
  uint8_t  i;       // counter

  // Initialize balls.
  for (i=0; i<BALLS; i++)
  {
    // Random position
    x[i] = random(BALLSIZE, TV.hres()-BALLSIZE-1);
    y[i] = random(BALLSIZE, TV.vres()-BALLSIZE-1);

    // Start moving to the right, and down.
    xm[i] = 1;
    ym[i] = 1;
  }

  // We will do our own control loop here.
  while(1)
  {
    // Wait for end of screen to be drawn.
    TV.delay_frame(1);

    for (i=0; i<BALLS; i++)
    {
      // Erase balls.
      TV.draw_circle(x[i], y[i], BALLSIZE, BLACK);

      x[i] = x[i] + xm[i];
      if (x[i]<=BALLSIZE || x[i]>TV.hres()-BALLSIZE-1) xm[i] = -xm[i];

      y[i] = y[i] + ym[i];
      if (y[i]<=BALLSIZE || y[i]>=TV.vres()-BALLSIZE-1) ym[i] = -ym[i];

      TV.draw_circle(x[i], y[i], BALLSIZE, WHITE);
    }
  }
}

Ten circles was still fast. Twenty showed some slowdown. But it still looked cool. To make it cooler, you could randomize the ball directions:

n

    // Random direction
    xm[i] = random(2)*2 - 1;
    ym[i] = random(2)*2 - 1;

The random(2) function will return 0 or 1, so multiplying that by 2 produces 0 or 2, and subtracting 1 from that produces -1 or 1. (It took me a bit to figure this out. Math is hard.)

My next thought was to put a player character on the screen and allow it to be moved by the joystick. (This could also be done by the serial console and keyboard presses.) I initially drew a square to make it look different from the circles, but then I had to do different math for screen edge detection since X and Y of a square is the top left corner. I decided to use a filled circle. I would move it around using the analog joystick.

n

#include <TVout.h>

TVout TV;

#define BALLS      10 // Number of balls to bounce.
#define BALLSIZE   4  // Size of balls.
#define PLAYERSIZE 6  // Size of player. 

#define ANALOGXPIN 0  // Pin 0 is X on iTead joystick
#define ANALOGYPIN 1  // Pin 1 is Y on iTead joystick

void setup()
{
  TV.begin(NTSC, 120, 96);
  Serial.begin(9600);

  TV.clear_screen();
}

void loop()
{
  uint8_t  x[BALLS], y[BALLS];    // X and Y position of ball
  int8_t   xm[BALLS], ym[BALLS];  // X and Y movement of ball
  uint8_t  i;       // counter

  uint8_t  px, py;                // X and Y position of player

  // Initialize balls.
  for (i=0; i<BALLS; i++)
  {
    // Random position
    x[i] = random(BALLSIZE, TV.hres()-BALLSIZE-1);
    y[i] = random(BALLSIZE, TV.vres()-BALLSIZE-1);

    // Random direction
    xm[i] = random(2)*2 - 1;
    ym[i] = random(2)*2 - 1;
  }

  // Initialize player.
  px = TV.hres()/2;
  py = TV.vres()/2;

  // We will do our own control loop here.
  while(1)
  {
    // Wait for end of screen to be drawn.
    TV.delay_frame(1);

    for (i=0; i<BALLS; i++)
    {
      // Erase balls.
      TV.draw_circle(x[i], y[i], BALLSIZE, BLACK);

      x[i] = x[i] + xm[i];
      if (x[i]<BALLSIZE || x[i]>TV.hres()-BALLSIZE-1) xm[i] = -xm[i];

      y[i] = y[i] + ym[i];
      if (y[i]<=BALLSIZE || y[i]>=TV.vres()-BALLSIZE-1) ym[i] = -ym[i];

      TV.draw_circle(x[i], y[i], BALLSIZE, WHITE);
    }

    // Erase player
    TV.draw_circle(px, py, PLAYERSIZE, BLACK, BLACK);

    // Read joystick (0-1023) and convert to screen resolution.
    px = analogRead(ANALOGXPIN)/(1024/TV.hres());
    if (px<PLAYERSIZE)
    {
      px = PLAYERSIZE;
    } else if (px>TV.hres()-PLAYERSIZE-1)
    {
      px = TV.hres()-PLAYERSIZE-1;
    }

    py = analogRead(ANALOGYPIN)/(1024/TV.vres());
    if (py<PLAYERSIZE)
    {
      py = PLAYERSIZE;
    } else if (py>TV.vres()-PLAYERSIZE-1)
    {
      py = TV.vres()-PLAYERSIZE-1;
    }

    // Draw player.
    TV.draw_circle(px, py, PLAYERSIZE, WHITE, WHITE);
  }
}

At this point, I was starting to think a simple game might be “dodge the circles” and the player would see how long they could survive moving their larger filled circle around while trying to avoid all the bouncing, smaller circles.

But I wasn’t done experimenting with TVout yet. There was still more it could do, with bitmaps, to draw things that weren’t just lines or circles.

Next time, I will discuss how I turned my filled circle in to a simple Pac-Man type character, which made the rest of the night turn in to an attempt to recreate that game.

Arduino Pac-Man project

See also: part 1, part 2, part 3, part 4, part 5, part 6, part 7, part 8, part 9 and part 10.

Here is a quick demo of something I wrote last night for an Arduino UNO. I have an iTead Studios Screw Shield attached to it ($3.50, to simplify hooking up wires), and an iTead Joystick Shield ($4.50, for input). Then, using only two resistors and an RCA cable, plus a clever library, I was able to start programming a Pac-Man style game on the Arduino. This is the first of a multi-part series of articles explaining the steps in this project.

When I first began playing with an Arduinio Duemilanove at work in 2012,  I learned how to program it by reading through the reference material at the main Arduino website. I had heard of Arduino, but had never learned anything about it, so I was quite impressed with all the various libraries that were available to handle everything from serial communication to I2C protocol. One of the more surprising discoveries was that you could do  video output by wiring up two resistors to an RCA phono jack. Clever programming allowed the Arduino to create the scan line signals that would produce a low resolution black and white composite video screen. Here is the information page on it, showing a simple diagram of how you wire things up:

http://playground.arduino.cc/Main/TVout

At the time, I thought we might be able to use low-cost Arduinos to output some information displays at a local haunted house event (wait times, “now serving” queue management systems, etc.) but I never pursued it.

Later, I found out about two projects that were based on this TVout concept to produce retro Arduino video games: Hackvision, and the Video Game Shield.

The Hackvision was a custom Arduino device (based around the UNO) with directional and fire buttons right on the circuit board, as well as RCA jacks for audio and video output. It is currently available in a kit for $33.95, or fully assembled for $43.95.

The Video Game Shield is an add-on shield for an Arduino UNO that provided RCA jacks for audio and video output, as well as connectors for two Nintendo Wii nunchuck controllers. It is available in a kit for $22.50.

A third, similar project, called the Gamby, is a shield that includes a low resolution LCD display as well as bottoms, and turns an Arduinio in to a portable Gameboy-style gaming device. It is available in a kit for $25.

If you visit the project websites, you will find some example videos of games written for these add-ons. Due to the different ways that input is handled, games written for one platform do not play on the others (and I think the Gamby has a different video system). It does appear that games written for the generic TVout library are easily ported between systems.

Some of the games that have been written include clones of Space Invaders, Pong, Tetris, and Asteroids. The Gamby site has quite a few other titles (like a Joust clone) that I do not think have been ported to the other platforms.

I have recently become re-interested in retro video games. Two  retro video arcades have recently opened here in Des Moines, Iowa. (UP-DOWN opened in October 2013, followed by  Barcadium opened in January 2014.) Being able to step back in time and play games like Space Invaders (1978) and Pac-Man (1980) makes me feel both young again, and very old, as I realize most of the visitors to these arcades were not even born when I was first inserting quarters in these machines when they were brand new.

This led me to dusting off the old MAME emulator and once again exploring various classic arcade games. I was particularly intrigued by some of the late 1970s games that came out before the arcade scene got popular. These games used low resolution, black and white graphics and simple sound effects. I couldn’t help but think “I could have written this.” But back then, “no one” had a computer – especially not some seven year old kid growing up in Houston, Texas (like I was). So no, I could not have written any of those games, back then, two decades later, I did create and sell my own Space Invaders clone for the Radio Shack Color Computer running under OS-9. If I could have done that in 1978, maybe I would be a millionaire right now ;-)

Up next: It lives!