HWYDT: Snake Game
Tags: miniscript, minimicro, howto, programming
Published: 2024-05-03 15:07:17

One of the most classic video games is generically known as "[Snake](https://en.wikipedia.org/wiki/Snake_(video_game_genre))".  In this game, you play a long snake that winds is way around a level, making only 90-degree turns, and growing longer every time you eat something (usually an apple).  The game is over when you bite your own tail, and as you get longer, it gets harder and harder to avoid doing that.

Early versions of the game were done on low-resolution screens, or simply used plain squares for every segment of the snake.  This made it very easy to draw the moving snake: you simply draw a new square for the new position of the head, and erase the last square to update the tail.

![Single-player classic Snake game](https://upload.wikimedia.org/wikipedia/commons/5/55/Snake_can_be_completed.gif)

However, modern players expect a little more panache.  We'd like to see a head with features (such as eyes) that make it clear which way it's facing, and likewise for a tail.  And ideally, it should have a smooth body in between, like a real snake.

How would you do that?

## Level One: Round Segmented Body

We're going to tackle this job in two stages.  For the initial, simpler version, we will draw a segmented snake, with round body parts that fit together in any orientation.  This looks more like _Centipede_ than Snake, but it's a good start.

![_Centipede_ arcade game screen shot](https://upload.wikimedia.org/wikipedia/en/8/80/Centipede_arcade.png)

### Creating the Images

Fire up [Mini Micro](https://miniscript.org/MiniMicro/) (on your local machine — you can't do this project in the web runner, since you need to be able to create and save images in addition to the code).  Click on the top disk slot, and mount some folder where you want to store your project.  Now enter

```
run "/sys/demo/fatbits"
```

This runs the **fatbits** program, which is a demo but also a useful little image editor.  Create a New Image called "body.png", 32x32 pixels in size; and then use the circle and fill tools to make a round body segment, with a 4-pixel empty margin all the way around, like this.

![Screen shot of body segment in fatbits editor](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7prwefrwag0za5nwve1p.png)

{% details Why the 4-pixel margin? %}
I often like to make my tiles a little bit bigger than they "need" to be, and then set up my TileDisplay with a corresponding amount of overlap.  In this case, I'll set `td.overlap = 8` (where `td` is my TileDisplay), but still with `td.cellSize = 32`.  That gives me an effective cell spacing of 24 pixels, _but_ it allows cell images to extend beyond this 24-pixel grid a bit, overlapping if necessary.  You'll see this used on both the head and tail images, below. This trick breaks up the obvious square grid of the TileDisplay and makes the game look a little fancier than it really is.
{% enddetails %}

Now click the little "+" tab at the top, and repeat the process to create "head.png", and then "tail.png".

![Close-up of head image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e625tvemf1zuunjwy5sg.png)

![Close-up of tail image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hxecuuxn2tsfbsb259zf.png)

Press Q to quit when you're done.  (Note that I made my images in white and gray so that they can be tinted at runtime, any color we like.)

### What Kind of Display?

Now before we really get going with any code, we have to decide which of Mini Micro's several different types of display we're going to use.  There are three reasonable options:

1. PixelDisplay: we could use drawImage to draw each part of the snake to the screen, and fillRect to clear it.

2. SpriteDisplay: we could create a sprite for each segment of the snake.  This has the advantage that sprites can be easily rotated.

3. TileDisplay: we could prepare a tile set with all the snake parts in all rotations, and then draw and erase by simply setting cells in the display to the appropriate tile index.

Any of these would work.  If all we needed to do was draw a moving snake, the SpriteDisplay might be easiest.  But to make a full game, we also have to think about collision detection.  We need to know when our snake has hit its own tail, or an apple, or any other game hazards.  This implies that somewhere, we'll need a big matrix keeping track of what's on the map.  A TileDisplay already has that: for every cell of the display, we can get the current tile index just as easily as we can set it.  So that leads me to option 3.

### Making a Tile Set at Runtime

A TileDisplay makes use of a _tile set_: a big image that is divided into even rectangular sub-images called _tiles_.  Each cell of the display draws any one of these tiles at any given time, selected by its index within the tile set.

Normally we think of the tile set as something prepared ahead of time in an art program, and just loaded.  But it doesn't have to be; you can assemble it at runtime, and the "tileUtil" module (in /sys/lib) provides some utilities to make this easier.

We're going to do that here, because we drew our snake parts in just one orientation — facing the right.  But we need them in all four orientations.  So, start my making a little helper function that loads an image, and also rotates it by some (multiple of 90) degrees.

```
loadRotated = function(path, angle=0)
	img = file.loadImage(path)
	if img isa Image then
		img.rotate(angle)
		return img
	end if
	print "No such image: " + path
	exit
end function
```

With that helper, we can write another function to actually make the tileset.

```
makeTileSet = function
	globals.empty = Image.create(32, 32, color.clear)
	globals.images = [empty]*13
	images[1] = loadRotated("head.png", 0)
	images[2] = loadRotated("head.png", 90)
	images[3] = loadRotated("head.png", 180)
	images[4] = loadRotated("head.png", 270)
	images[5] = loadRotated("body.png", 0)
	images[6] = loadRotated("body.png", 90)
	images[7] = loadRotated("body.png", 180)
	images[8] = loadRotated("body.png", 270)
	images[9] = loadRotated("tail.png", 0)
	images[10] = loadRotated("tail.png", 90)
	images[11] = loadRotated("tail.png", 180)
	images[12] = loadRotated("tail.png", 270)	
	return tileUtil.toTileSet(images)
end function
```

Let's take a moment to understand what this does.  First we create an empty image, 32x32 pixels, initialized to `color.clear`.  Then we prepare a list called `images`, which initially just refers to that empty image 13 times.  (Why 13?  Because we need one slot for empty areas where there is no snake, and then 4 orientations each of our 3 body parts.)  The rest of the function just stuffs the correct image into each slot, and then uses `tileUtil.toTileSet` to turn these into a single big image, suitable for use with a TileDisplay.

(I assigned to `globals.empty` and `globals.images` here just to aid with debugging; I found it useful to peek at these with the `view` command to make sure it was working.  You could remove `globals.` to keep the global namespace clean if you prefer.)

Note that when you set up your tile set this way, it's easy to see which index to use on the TileDisplay.  Looking for the tail image rotated 180°?  From the code above, we can see that's index 11.

### Moving and Growing

Once we have our TileDisplay set up with the tile set create above, the other key bit of the program is `move`, which moves the snake and optionally makes it grow.  (To "grow" the snake simply means to advance the head while leaving the tail alone, thus making the snake 1 unit longer.)

```
move = function(nextDir, grow=false)
	// convert the old head into a body
	x = snakePos[0][0]; y = snakePos[0][1]
	td.setCell x, y, td.cell(x,y) + 4
	
	// place the new head	
	if nextDir == RIGHT then x += 1
	if nextDir == UP then y += 1
	if nextDir == LEFT then x -= 1
	if nextDir == DOWN then y -= 1
	td.setCell x, y, 1 + nextDir
	snakePos.insert 0, [x,y]
	
	// remember our new current direction
	globals.curDir = nextDir
	
	// if we want to grow the snake, then we're done
	if grow then return
	
	// remove the old tail
	td.setCell snakePos[-1][0], snakePos[-1][1], 0
	snakePos.pop
	
	// convert the new last segment into a tail
	x = snakePos[-1][0]; y = snakePos[-1][1]
	td.setCell x, y, td.cell(x,y) + 4	
end function
```
This code takes advantage of the way we organized our snake parts in the tile set.  For each of the four heads (i.e. one for each direction), we have four bodies — so we can convert a head to a body by simply adding 4 to the tile index.  Similarly, if we add 4 again, we convert a body into a tail.  Moreover, when it comes to adding a new head, we can simply use `1 + nextDir`, where `nextDir` is a value from 0-3 indicating directions in the same order we used in `makeTileSet`.

The full program is a bit over 100 lines — twist it open below if you want to see it.

{% details Full "Level 1" Snake Game %}
```
// Snake! (Non-bendy-body version)
import "tileUtil"

// define our directions
RIGHT = 0
UP = 1
LEFT = 2
DOWN = 3

loadRotated = function(path, angle=0)
	img = file.loadImage(path)
	if img isa Image then
		img.rotate(angle)
		return img
	end if
	print "No such image: " + path
	exit
end function

makeTileSet = function
	globals.empty = Image.create(32, 32, color.clear)
	globals.images = [empty]*13
	images[1] = loadRotated("head.png", 0)
	images[2] = loadRotated("head.png", 90)
	images[3] = loadRotated("head.png", 180)
	images[4] = loadRotated("head.png", 270)
	images[5] = loadRotated("body.png", 0)
	images[6] = loadRotated("body.png", 90)
	images[7] = loadRotated("body.png", 180)
	images[8] = loadRotated("body.png", 270)
	images[9] = loadRotated("tail.png", 0)
	images[10] = loadRotated("tail.png", 90)
	images[11] = loadRotated("tail.png", 180)
	images[12] = loadRotated("tail.png", 270)	
	return tileUtil.toTileSet(images)
end function

// display set-up
clear
display(4).mode = displayMode.tile
td = display(4)
td.cellSize = 32
td.overlap = 8
td.tileSet = makeTileSet
td.tileSetTileSize = 32
td.extent = [floor(960/24), floor(640/24)]
td.clear 0

// function to move the snake in any direction
move = function(nextDir, grow=false)
	// convert the old head into a body
	x = snakePos[0][0]; y = snakePos[0][1]
	td.setCell x, y, td.cell(x,y) + 4
	
	// place the new head	
	if nextDir == RIGHT then x += 1
	if nextDir == UP then y += 1
	if nextDir == LEFT then x -= 1
	if nextDir == DOWN then y -= 1
	td.setCell x, y, 1 + nextDir
	snakePos.insert 0, [x,y]
	
	// remember our new current direction
	globals.curDir = nextDir
	
	// if we want to grow the snake, then we're done
	if grow then return
	
	// remove the old tail
	td.setCell snakePos[-1][0], snakePos[-1][1], 0
	snakePos.pop
	
	// convert the new last segment into a tail
	x = snakePos[-1][0]; y = snakePos[-1][1]
	td.setCell x, y, td.cell(x,y) + 4	
end function

// initialize the snake
snakePos = []  // list of x,y positions with head at 0
snakePos.push [12,10]  // head
for i in range(10)
	move RIGHT, true  // move to the right, and grow
end for
move RIGHT  // move one more to the right, without growing

// main loop
secPerMove = 0.25
nextDir = curDir
nextMoveTime = time + secPerMove
while true
	// handle input
	dx = key.axis("Horizontal")
	dy = key.axis("Vertical")
	if abs(dx) or abs(dy) then
		if abs(dx) > abs(dy) then
			if dx > 0 then newDir = RIGHT else newDir = LEFT
		else
			if dy > 0 then newDir = UP else newDir = DOWN
		end if
		if abs(newDir - curDir) != 2 then nextDir = newDir
	end if
	
	// move snake when it's time
	if time > nextMoveTime then
		move nextDir
		nextMoveTime = time + secPerMove
	end if
	yield
end while
```
{% enddetails %}

![Animated GIF of Phase 1 snake](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e7o0rp5t4tweo0qdq5u0.gif)

## Level 2: Smooth, Bendy Snake Body

The round body segments work, but they don't look much like a snake.  To really do this right, we need to take it further.

The big idea here is to have two versions of our body segments: one that's straight, and one that bends.  By rotating these images as needed, we can make any part of a windy bendy snake body.

Run /sys/demo/fatbits again, and first make a straight snake body like so:

![bodyStraight.png](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f1u6uhqphs440jhw3k2o.png)

Note that while we've still left the 4-pixel margin on the sides, the top and bottom are actually **6** pixels away from the edge, because we want to leave a little gap in that direction when the snake has done a U-turn and slithered back right next to itself.

Then, make another image for the body that bends.  This would be the one used when the snake has turned from going DOWN to going RIGHT, or conversely, turned from LEFT to UP.

![bodyBend.png](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5wjiwiet2t5mjwboxnn4.png)

Then adjust the head and tail images to mesh smoothly with the new body shape as well.

![head.png and tail.png](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qp7rbql1tc14fpt1xry1.png)

Now we're ready to adjust the code.  Loading the head and tail is exactly the same as before; there's still only one image needed in 4 different orientations.  Loading the body is a bit trickier.  To do that, we're going to do a bit of math involving the snake's previous direction, and its new direction.  Keep in mind that directions are represented as numbers 0-3 (for RIGHT, UP, LEFT, and DOWN).  So for example, to store the tile to use when the snake turns from LEFT to DOWN, we'll calculate LEFT * 4 + DOWN (and add 5, since the body images begin at index 5).

```
makeTileSet = function
	globals.empty = Image.create(32, 32, color.clear)
	globals.images = [empty]*25
	images[1] = loadRotated("head.png", 0)
	images[2] = loadRotated("head.png", 90)
	images[3] = loadRotated("head.png", 180)
	images[4] = loadRotated("head.png", 270)
	
	// for the body, define image for each valid combination
	// of prevDir, nextDir as 5 + prevDir*4 + newDir
	images[5+RIGHT*4+RIGHT] = loadRotated("bodyStraight.png", 00)
	images[5+UP*4+UP] = loadRotated("bodyStraight.png", 90)
	images[5+LEFT*4+LEFT] = loadRotated("bodyStraight.png", 180)
	images[5+DOWN*4+DOWN] = loadRotated("bodyStraight.png", 270)
	
	images[5+LEFT*4+UP] = loadRotated("bodyBend.png", 0)
	images[5+DOWN*4+LEFT] = loadRotated("bodyBend.png", 90)
	images[5+RIGHT*4+DOWN] = loadRotated("bodyBend.png", 180)
	images[5+UP*4+RIGHT] = loadRotated("bodyBend.png", 270)
	images[5+DOWN*4+RIGHT] = loadRotated("bodyBend.png", 0)
	images[5+RIGHT*4+UP] = loadRotated("bodyBend.png", 90)
	images[5+UP*4+LEFT]= loadRotated("bodyBend.png", 180)
	images[5+LEFT*4+DOWN] = loadRotated("bodyBend.png", 270)
	
	images[21] = loadRotated("tail.png", 0)
	images[22] = loadRotated("tail.png", 90)
	images[23] = loadRotated("tail.png", 180)
	images[24] = loadRotated("tail.png", 270)	
	return tileUtil.toTileSet(images)
end function
```

Having done that, our `move` function is almost the same as before.  There are only two differences; first is where we convert the old head into a new body segment.  Instead of

```
	td.setCell x, y, td.cell(x,y) + 4
```

we now do

```
	td.setCell x, y, 5 + curDir * 4 + nextDir
```
(where `curDir` is the snake's previous direction, and `nextDir` is its next direction).  The math in this line mirrors the math we used to load the tile set.  

The second difference is at the other end, where we convert the last body segment into a tail.  This one now looks like:
```
	td.setCell x, y, 21 + (td.cell(x,y) - 5) % 4	
```
This is using the modulo operator (`%`) to find the remainder after dividing the previous index (minus 5, which is where the body segments begin) by 4, leaving us with just right number to represent the direction of the tail.  Neat, huh?

{% details Here's the full program listing. %}
```
// Snake! (Bendy-body version)

import "tileUtil"

// define our directions
RIGHT = 0
UP = 1
LEFT = 2
DOWN = 3

loadRotated = function(path, angle=0)
	img = file.loadImage(path)
	if img isa Image then
		img.rotate(angle)
		return img
	end if
	print "No such image: " + path
	exit
end function

makeTileSet = function
	globals.empty = Image.create(32, 32, color.clear)
	globals.images = [empty]*25
	images[1] = loadRotated("head.png", 0)
	images[2] = loadRotated("head.png", 90)
	images[3] = loadRotated("head.png", 180)
	images[4] = loadRotated("head.png", 270)
	
	// for the body, define image for each valid combination
	// of prevDir, nextDir as 5 + prevDir*4 + newDir
	images[5+RIGHT*4+RIGHT] = loadRotated("bodyStraight.png", 00)
	images[5+UP*4+UP] = loadRotated("bodyStraight.png", 90)
	images[5+LEFT*4+LEFT] = loadRotated("bodyStraight.png", 180)
	images[5+DOWN*4+DOWN] = loadRotated("bodyStraight.png", 270)
	
	images[5+LEFT*4+UP] = loadRotated("bodyBend.png", 0)
	images[5+DOWN*4+LEFT] = loadRotated("bodyBend.png", 90)
	images[5+RIGHT*4+DOWN] = loadRotated("bodyBend.png", 180)
	images[5+UP*4+RIGHT] = loadRotated("bodyBend.png", 270)
	images[5+DOWN*4+RIGHT] = loadRotated("bodyBend.png", 0)
	images[5+RIGHT*4+UP] = loadRotated("bodyBend.png", 90)
	images[5+UP*4+LEFT]= loadRotated("bodyBend.png", 180)
	images[5+LEFT*4+DOWN] = loadRotated("bodyBend.png", 270)
	
	images[21] = loadRotated("tail.png", 0)
	images[22] = loadRotated("tail.png", 90)
	images[23] = loadRotated("tail.png", 180)
	images[24] = loadRotated("tail.png", 270)	
	return tileUtil.toTileSet(images)
end function

// display set-up
clear
display(4).mode = displayMode.tile
td = display(4)
td.cellSize = 32
td.overlap = 8
td.tileSet = makeTileSet
td.tileSetTileSize = 32
td.extent = [floor(960/24), floor(640/24)]
td.clear 0

// function to move the snake in any direction
move = function(nextDir, grow=false)
	// convert the old head into a body
	x = snakePos[0][0]; y = snakePos[0][1]
	td.setCell x, y, 5 + curDir * 4 + nextDir
	
	// place the new head	
	if nextDir == RIGHT then x += 1
	if nextDir == UP then y += 1
	if nextDir == LEFT then x -= 1
	if nextDir == DOWN then y -= 1
	td.setCell x, y, 1 + nextDir
	snakePos.insert 0, [x,y]
	
	// remember our new current direction
	globals.curDir = nextDir
	
	// if we want to grow the snake, then we're done
	if grow then return
	
	// remove the old tail
	td.setCell snakePos[-1][0], snakePos[-1][1], 0
	snakePos.pop
	
	// convert the new last segment into a tail
	x = snakePos[-1][0]; y = snakePos[-1][1]
	td.setCell x, y, 21 + (td.cell(x,y) - 5) % 4	
end function

// initialize the snake
curDir = RIGHT
snakePos = []  // list of x,y positions with head at 0
snakePos.push [12,10]  // head
for i in range(10)
	move curDir, true  // move to the right, and grow
end for
move curDir  // move one more to the right, without growing

// main loop
secPerMove = 0.25
nextDir = curDir
nextMoveTime = time + secPerMove
while true
	// handle input
	dx = key.axis("Horizontal")
	dy = key.axis("Vertical")
	if abs(dx) or abs(dy) then
		if abs(dx) > abs(dy) then
			if dx > 0 then newDir = RIGHT else newDir = LEFT
		else
			if dy > 0 then newDir = UP else newDir = DOWN
		end if
		if abs(newDir - curDir) != 2 then nextDir = newDir
	end if
	
	// move snake when it's time
	if time > nextMoveTime then
		move nextDir
		nextMoveTime = time + secPerMove
	end if
	yield
end while
```
{% enddetails %}

![Animated GIF of smooth, bendy snake](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x130lrf8t9ti1g9ef2s3.gif)

## Conclusion

While _Snake_ is a simple game, and easy enough to draw with only blocky rectangles, making it look good — with nice smooth snaky curves — requires a bit of thought.  But now you know how it's done!

As far as I'm aware, nobody has yet made a complete, polished Snake implementation for the Mini Micro platform.  Will you be the first?

If you enjoyed this "How Would You Do That" post, be sure to check out [HWYDT: Donkey Kong](https://dev.to/joestrout/hwydt-donkey-kong-29me) and [HWYDT: Ladders](https://dev.to/joestrout/hwydt-ladders-b30)!  And as always, if you have any questions or thoughts to share, please use the comment form below.  Happy coding!