January 2018
This project demonstrates how to use lists to run a multiple Snake simulation on a Canvas, without using any Sprites or Balls. Instead, it draws Circles on grid squares (cells) for snake eggs, and hatches a few of them into colored snakes that move randomly and grow in length as they eat free standing snake eggs. When a snake bites another snake, the tail breaks off and reverts to individual snake eggs.
This is an advanced project, and employs doubly linked lists.
The black dots are snake eggs. The colored dots are snakes, fairly tightly coiled because of the random motion automatic movement routine. The brown snake has eaten the most eggs so far, and the light blue snake has eaten no eggs yet. The colored bubbles are artifacts of the drawing and erasing of the colored dots that make up the snakes, as the snakes move. (I rather like them, so I have left them as is.)
The Designer is as simple as can be, with just the Canvas and a Clock.
The Clock is disabled, to allow clean startup, and hard wired for 100 milliseconds per tick.
Feel free to run the clock faster to experiment.
This function returns the number of pixels in the side of each grid cell.
R is the radius of the circles used to draw the snake bits. Anything other than half the cell size leaves debris on the canvas or makes the snakes look too loose.
Cells are identified by row and column number, 1 based. To combine both the row and column number into a single value, we assume no more than 999 rows or columns, and express a (row, column) as a single number, rrrccc, using multiplication by 1000. To go back and forth between cell identifiers and rows and columns, we do modulo math.
We don’t actually keep a 1,000 by 1,000 array. We just keep the locations of the small fixed set of eggs that we start with, that serve as links in the chains that are our snakes.
Given a cell ID, return the row number of that cell.
Return how many columns will fit onto the Canvas based on its Width and the number of pixels per cell.
Return how many rows will fit onto the Canvas based on its Height and the number of pixels per cell.
Extract the column number from a cell ID.
Build a cell ID from a row number and column number.
Return the x value (in pixels) for the center of a circle at a given column number.
Return the y value (in pixels) for the center of a circle at a given row number.
Erase the circle at a given rrrccc cell. This technique leaves a bubble on the Canvas. I could have changed this to draw a fat line to fill the entire rectangular cell, to eliminate the bubble, but I like the bubble trail left by the snake.
Several layers of conversion take the row and column numbers from the cell ID, then extract the centerX and centerY graphic coordinates for the center of that cell. Then we draw a circle of radius R and the given color at that location.
Snakes are chains of links in a doubly linked list of constant size, once the board has been seeded with eggs. The constant qty_links controls how many links (rrrccc items in list link_cells) will be produced, and that number will remain unchanged after the seeding operation. Because that number remains unchanged, we can make parallel lists of predecessor and follower link numbers (indices into link_cells). An egg at position 23014 (row 23, column 14) whose link is at index 9 in list link_cells will start out with 0 at index 9 of lists link_follower and link_predecessor, since it is a chain of length 1.
When we hatch an egg, we add its starting link number to list snakes. We limit the number of snakes for ecological reasons, based on constant max_snakes. List snakes has a matching list snake_colors, holding the color of snake i in list snakes. As snakes are born and die, we have to keep those lists in parallel.
A third list snake_directions might be used to keep persistent snake directions, to allow faster movement than the current Brownian Motion random model.
At app startup, we seed the lists of links and their structures, and paint them as eggs. We then start the Clock Timer to get some snakes hatched and moving.
To seed the board, we generate the required number of links, pointing to random row and column locations, and draw an egg at each location. Being eggs, they have no followers and predecessors for the chains they would be part of if they were parts of snakes.
(Bug alert: I did not check for random duplicates here.)
To concisely navigate chains of links, we provide next and prev functions accepting a link (index into link_cells).
The Clock ticks rapidly, and does just a little bit at each tick, hatching one snake if there aren’t enough snakes, and regardless, moving one snake.
The first step in hatching a random egg is to go through the lists of links, and collect the links that have no followers or predecessors, i.e. eggs. It’s possible that there are none left, because they are parts of snakes now. In that case, we do nothing. If there is at least one egg on our list of eggs, we pick a random egg from that list. Since the returned egg is from a list of link indices in list link_cells, we need to look up the cell (rrrccc) for that link to draw it with a random color.
We record the head link of the new snake in global list snakes, and its color in the matching list snake_colors.
Eggs have no previous or next links.
To move a random snake, we pick a random snake from list snakes and move it.
To move a snake, we first need to look up the head link of that snake by its snake index, using our snake function. We then extract the rrrccc location of that link, and generate a list of its neighbors’ rrrccc values. For the simplest possible implementation, we settle for picking a random neighbor rrrccc to try, using routine try_target.
This is a simple lookup in global list snakes.
Links are indices into the list of rrrccc cells, global list link_cells.
The cell ID is broken down into its row and column numbers, and its 8 surrounding cells are calculated, wrapped around the edges of the board, and reassembled into rrrccc cell IDs and returned.
We deal with the edge of the board by using doubly cylindrical geometry.
There are three possibilities when we want to move a snake onto a target cell:
To find the owner (snake, if any) of an rrrccc cell, we first look up the link (if any) pointing to that cell. If a link is found (non-zero), then we look up the first link in whatever chain that link might be part of, then try to look up that head link in our list of snake heads (global snakes). Failure at any stage returns 0.
This function accepts a cell (row, column) and returns the link (index into links list) of the link on that cell, or 0 if the cell is empty.
This function accepts a link number, and follows the previous links to the very head of the chain (snake) holding this link.
This procedure is for the case where a snake encounters an egg, a solitary link, and absorbs it into itself (eats it.) The target parameter is the cell (location) holding the target link to be eaten.
The mouth of the snake is in its head, the link we can look up in the snake table.
To show that the link has been eaten, we change its color to that of the snake eating it.
To absorb the link into the snake,
This is the case where a snake bites another snake at a target cell location. First we need to identify the victim, the owner of the target cell. We also identify which link is at the target cell.
Because we are merciful, we will allow the target snake to escape and heal if it has been bitten anywhere behind its head, so we will need to identify the link that will serve as its new tail link. That link is the previous link to the bitten link.
If we have a new tail for the target snake, we seal it off by setting its follower link to 0.
Otherwise, we have eaten the head of another snake, and must kill that snake entirely.
Regardless, the bitten target link and its followers in that snake are now loose food, and must revert to eggs.
This function returns the color of a snake, based on the lookup table snake_colors.
This procedure accepts the head link of a chain of links, that need to be de-chained and reverted back to eggs. It is a two phase operation:
This procedure kills a snake, by removing it from the snakes list, and cleaning up any matching lists. It does no link manipulation; that’s left to the calling attack routine.
This routine follows the follower chain of a link, returning the last link in the chain.
This is complex, simulating the movement of a caterpillar. First the drawing is handled at the new head of the snake, and at its old tail. Then we start a wave at the tail, lifting each link and dropping it onto the location of its predecessor, pushing the wave forward towards the head of the snake, until we reach the head of the snake (prev = 0). All this movement happens in the list link_cells, which maps the link numbers into their cell locations.
http://ai2.appinventor.mit.edu/?galleryId=5592523749457920
https://docs.google.com/document/d/1acg2M5KdunKjJgM3Rxpy_Rf6vT6OozxdIWglgbmzroA/edit?usp=sharing