How do you write a snake game? Very carefully. ;-)
Open source at GitHub
For me, it started as an idea to make a featured game for my handheld Commodore emulator platform, most importantly the ones I can easily transport on my person. I've written a simple but not complete Commodore emulator that behaves well on ESP32 and other available microcontrollers. One example hardware platform is the M5Stack Fire that I can stick in my pocket and has physical buttons for inputs.
I was away on vacation with the family when I came up with the idea of replicating the snake game. I was yearning for a good coding puzzle, and this was it!
So I started brainstorming on how one would accomplish it.
First, I decided to just use BASIC. It comes with the Commodores, and is relatively easy to work with. I only have 40+ years experience so I should be able to get by. So no machine language this time.
What system should I develop for? Vic-20 is good for games as the text screen is small (22x23) and the characters are big, easy to see.
The key to snake is that each time it eats food it gets longer, so the snake is bigger and bigger as the game progresses, becoming an obstacle to your own movements. As the snake moves, it inches along, the head advancing in one direction, and the tail catching up to where its next to last segment was. So constantly the head and tail are both moving, and we have to keep track of where they are. The answer is a circular buffer! Segment locations will be stored in a circular buffer.
What data structures do we have with BASIC? Just arrays. Can we implement a circular buffer with an array? Certainly, the array must be pre-allocated (DIM) with a fixed size, and keep track of the head and tail of the snake for the extents of the segments. This is how you would do it in Pascal or C with an array, so BASIC is not much different.
How big should the array be? Let's go big and say the snake can be as large as the screen. But we want a title and score on the screen too, so let's say the bottom line is reserved for those. That leaves 22x22 locations for the segments = 484. Let's plan on keeping an offset into screen memory in each array element.
How should game inputs work? Since my target platform has three buttons pre-configured for Up/Enter/Down, I want to use those, specifically Up/Down since I don't have access to four direction buttons. I intend to use those buttons to rotate the direction left or right relative to the snake's current direction.
How shall we display the snake? I bring up a PETSCII chart and look at the graphics characters. I pick a circle outline as the head, a solid circle as body segments, and a graphic X as the death character. Later in the process I chose the diamond as the food character.
So first step I implement the screen layout, place a single segment in the center of the screen, and implement moving around every second. With some fine-tuning I get it working well, including implementing dying if hit the edge of the screen. I am tracking horizontal position in X (0 to 21) and vertical position in Y (0 to 21) with DX and DY being -1, 0, or 1 each. The D stands for difference or direction. Moving is as easy as adding DX to X, and DY to Y, then making sure it is still within bounds, otherwise dead.
Quickly I found that allocating floating point values for the snake segments caused the original Vic-20 to run out of memory, so the array was changed to DIM S%(SZ) to use 2-byte integers instead of 5-byte floats.
The basics are in place, but the snake is not growing yet. Next the circular buffer is implemented, and I implement computing the length of the snake for the score. It took a few tries to get right, and once finished, I realized I could just track the score/length separately without all that fancy work, but it's done, so I let it be.
If the next position is another character or out of bounds, the snake dies. If the next position is not food, the snake's tail is shortened so the snake appears to move. But if the next position is food, then the snake appears to grow because the tail remains in place. The head is drawn at the next position.
Between moves there is a delay of a fraction of seconds. And as the snake gets longer, the delay gets smaller with a minimum delay to speed up the game, but keep it reasonable.
It took many incremental tries to get everything just right, took the opportunity of beta testing and feedback from my son, and addressed stuff including an opening information screen, the title/copyright/score bar, timing, and fixing bugs. The bugs included running into your own tail that was about to move -- that is allowed, testable by spinning in a tight loop when 4 segments long.
For inputs on a standard Vic-20, I remembered that cursor keys use shift to go the opposite direction. That would be unnatural to require on a real Commodore, so I added Z and / (slash) as alternate inputs for rotating the head. These keys are at opposite sides of the keyboard, so easy to indicate turn left or turn right. (If someone doesn't like the key layout, it's in BASIC, easy enough to change.)
During the process of refinement I added random colors to the food, adjusted the randomizer to pick different colors than the head/tail of the snake so it appears to change, and the snake picks up the color of the food for an interesting effect. Also the background color is avoided so the snake doesn't become invisible.
Next added Vic-20 joystick support. Joystick support is implemented to direct the snake the literal direction of the joystick instead of rotating. And avoids the direction opposite that the snake is already going so it impossible to reverse onto itself (otherwise the snake would die more often). Also because of how the joystick and the keyboard share some input lines, it is necessary to disable some keyboard lines to read the joystick successfully. Pressing STOP will break the program, but not reset the keyboard. This renders some keys unusable until STOP+RESTORE resets the keyboard.
Once the Vic-20 version was stable, source was pushed to GitHub.
Then the game was ported to Commodore 64 with 40 column screen.
And after that, Commodore Plus/4 and 16 port. initially with a machine language routine for reading the joystick until realizing that support is already in BASIC.
Next came a Commodore PET port designed to support most if not various models all running Commodore BASIC, including 40/80 column models, and actively switching to the uppercase/graphics character set in case a model defaults to lowercase. Since the PET doesn't normally have a visible border, inverse spaces are placed around the edges to contain the snake a bit more. Some screen real estate is lost but playability is gained.
I fell in love with the green on black look of the PET game, mostly for nostalgia, but to me it just looks perfect! And that's when I adjusted the PET version to also run successfully on Vic-20 and C64. It happens to work on C128 and Plus/4, 16 as well. The PET version doesn't support joystick though, is all keyboard. Detection of C64 turns colors to green on black, and Vic-20 does white on black.
Then I created Snake targeting my cbmish-script for the web...
And I'm still playing this game often daily...