# The snake game

[Courses](https://asm-editor.specy.app/learn/courses.md) › [M68K assembly](https://asm-editor.specy.app/learn/courses/m68k.md) › Examples

Web page: https://asm-editor.specy.app/learn/courses/m68k/examples/snake

> Play Snake while tracing packed board cells, a body shifted by a loop, arrow key steering, food placement, and a score drawn each frame.

The same program in: [MIPS](https://asm-editor.specy.app/learn/courses/mips/examples/snake.md), [RISC-V](https://asm-editor.specy.app/learn/courses/risc-v/examples/snake.md), [Z80](https://asm-editor.specy.app/learn/courses/z80/examples/snake.md)

Build and play Snake on a board of 32 columns by 24 rows. Choose **Build**, then **Run**. Click
inside the **Screen** so it has keyboard focus, then use the arrow keys to steer. The snake keeps
moving after you release a key. Reaching food adds one point and grows the snake; hitting a wall or
its own body ends the game. **Game over** appears on the Screen and the final score appears in the
console. Choose **Run** again to restart, or **Stop** when you are done.

The listing is long because it includes input, movement, drawing and a score. Read it first as one
pass through `frame`:

| Part     | What happens each frame                                                      |
| -------- | ---------------------------------------------------------------------------- |
| Input    | Poll the arrows; keep the last accepted direction.                           |
| Movement | Shift the body, move the head one cell, then check for a wall, body or food. |
| Picture  | Clear the hidden image, draw food, snake and score, show it, then wait.      |

Positions are **board cells** until `draw_cell` converts them to pixels. Each cell is 20 pixels
wide, so the 32 by 24 board fills the 640 by 480 Screen.

```m68k
COLS    equ 32              ; the board in cells
ROWS    equ 24
CELL    equ 20              ; and one cell in pixels
MAXLEN  equ 64
SNAKE   equ $0040D040
FOOD    equ $000040FF
WHITE   equ $00FFFFFF

    move.b #92, d0
    move.b #17, d1
    trap #15                ; task 92 mode 17: draw off screen

frame:
* --- the arrows, one poll for all four ---------------------------------------
    move.b #19, d0
    move.l #$25262728, d1   ; left $25, up $26, right $27, down $28
    trap #15
    btst #24, d1
    beq not_left
    move.w #-1, d2
    clr.w d3
    bsr try_direction
not_left:
    btst #16, d1
    beq not_up
    clr.w d2
    move.w #-1, d3
    bsr try_direction
not_up:
    btst #8, d1
    beq not_right
    move.w #1, d2
    clr.w d3
    bsr try_direction
not_right:
    btst #0, d1
    beq not_down
    clr.w d2
    move.w #1, d3
    bsr try_direction
not_down:

* --- every segment takes the place of the one in front of it -----------------
    lea body, a0
    move.w length, d1
    move.w d1, d2
    add.w d2, d2
    add.w d2, a0            ; a0 = one word past the last segment
    subq.w #2, d1           ; length - 1 copies, and dbra counts one less
    blt moved
shift:
    move.w -4(a0), -2(a0)   ; body[i] = body[i - 1]
    subq.l #2, a0
    dbra d1, shift
moved:

* --- the new head, one cell on from the old one ------------------------------
    move.w body, d4         ; the head, x in the high byte and y in the low
    move.w d4, d3
    andi.w #$FF, d3         ; y
    lsr.w #8, d4            ; x
    add.w dx, d4
    add.w dy, d3
    tst.w d4
    blt game_over           ; off the left
    cmp.w #COLS-1, d4
    bgt game_over           ; off the right
    tst.w d3
    blt game_over
    cmp.w #ROWS-1, d3
    bgt game_over
    move.w d4, d5
    lsl.w #8, d5
    or.w d3, d5             ; the new head, packed again
    move.w d5, body

* --- did it run into itself --------------------------------------------------
    lea body+2, a0
    move.w length, d1
    subq.w #2, d1
    blt no_bite
bite:
    cmp.w (a0)+, d5
    beq game_over
    dbra d1, bite
no_bite:

* --- did it reach the food ---------------------------------------------------
    cmp.w food, d5
    bne no_meal
    addq.w #1, score
    move.w length, d1
    cmp.w #MAXLEN, d1
    bge no_room
    move.w d1, d2
    add.w d2, d2
    lea body, a0
    add.w d2, a0
    move.w -2(a0), (a0)     ; the new tail starts on top of the old one
    addq.w #1, length
no_room:
    bsr place_food
no_meal:

* --- draw the whole frame off screen and show it in one go -------------------
    move.b #11, d0
    move.w #$FF00, d1
    trap #15                ; clear the off screen image

    move.l #FOOD, d1
    bsr both_colours
    move.w food, d5
    bsr draw_cell

    move.l #SNAKE, d1
    bsr both_colours
    lea body, a0
    move.w length, d6
    subq.w #1, d6
draw_body:
    move.w (a0)+, d5
    bsr draw_cell
    dbra d6, draw_body

    bsr draw_score

    move.b #94, d0
    trap #15                ; the frame becomes visible here, all at once
    move.b #23, d0
    move.l #12, d1
    trap #15                ; wait twelve hundredths of a second per frame
    bra frame

game_over:
    move.l #WHITE, d1
    move.b #80, d0
    trap #15
    lea over, a1
    move.l #250, d1
    move.l #230, d2
    move.b #95, d0
    trap #15                ; over the last frame, which is still there
    move.b #94, d0
    trap #15
    lea final, a1
    move.w score, d1
    andi.l #$FFFF, d1
    move.b #17, d0          ; task 17: the transcript gets the final score
    trap #15
    move.b #9, d0
    trap #15

* try_direction(nx, ny): take the new direction unless it turns the snake back
* on itself, which would be an instant bite
try_direction:
    move.w dx, d4
    add.w d2, d4
    move.w dy, d5
    add.w d3, d5
    or.w d4, d5             ; both zero means the new way is the opposite one
    beq no_turn
    move.w d2, dx
    move.w d3, dy
no_turn:
    rts

* draw_cell(c): the packed cell in d5, drawn as a square in the current colours
draw_cell:
    move.w d5, d1
    lsr.w #8, d1
    mulu #CELL, d1          ; x in pixels
    move.w d5, d2
    andi.w #$FF, d2
    mulu #CELL, d2          ; y in pixels
    move.l d1, d3
    add.l #CELL-1, d3       ; one pixel short, so the cells have a gap
    move.l d2, d4
    add.l #CELL-1, d4
    move.b #87, d0
    trap #15
    rts

* both_colours(c): the fill and the pen both become the colour in d1
both_colours:
    move.b #81, d0
    trap #15
    move.b #80, d0
    trap #15
    rts

* draw_score(): the label and the number, at the top left corner
draw_score:
    lea score_end, a1
    clr.b -(a1)             ; the digits are built backwards from the end
    move.w score, d2
    andi.l #$FFFF, d2
score_digit:
    divu #10, d2
    move.l d2, d3
    swap d3
    andi.l #$FFFF, d3       ; the digit
    andi.l #$FFFF, d2       ; what is left of the number
    add.b #'0', d3
    move.b d3, -(a1)
    tst.l d2
    bne score_digit
    move.l a1, a2           ; keep it, the label is drawn first
    move.l #WHITE, d1
    move.b #80, d0
    trap #15
    lea label, a1
    move.l #8, d1
    move.l #8, d2
    move.b #95, d0
    trap #15
    move.l a2, a1
    move.l #64, d1
    move.l #8, d2
    move.b #95, d0
    trap #15
    rts

* place_food(): select a board cell from a sixteen bit generator
place_food:
    bsr next_random
    andi.w #COLS-1, d0      ; a column, 0 to 31
    lsl.w #8, d0
    move.w d0, d7
    bsr next_random
    andi.l #$FFFF, d0
    divu #ROWS, d0
    swap d0
    andi.w #$FF, d0         ; a row, 0 to 23
    or.w d0, d7
    move.w d7, food
    rts

* next_random(): the next number of an xorshift, in d0
next_random:
    move.w seed, d0
    move.w d0, d1
    lsl.w #7, d1
    eor.w d1, d0            ; x = x ^ (x << 7)
    move.w d0, d1
    move.w #9, d2
    lsr.w d2, d1
    eor.w d1, d0            ; x = x ^ (x >> 9)
    move.w d0, d1
    lsl.w #8, d1
    eor.w d1, d0            ; x = x ^ (x << 8)
    move.w d0, seed
    rts

    org $3000
dx:     dc.w 1              ; the direction, in cells
dy:     dc.w 0
length: dc.w 3
score:  dc.w 0
seed:   dc.w $ACE1
food:   dc.w $140C          ; column 20, row 12
body:   dc.w $050C, $040C, $030C
        ds.w MAXLEN-3
label:  dc.b 'Score:', 0
over:   dc.b 'Game over', 0
final:  dc.b 'Game over. Score: ', 0
score_buffer: ds.b 8
score_end:
```

*Testcase for the program above (JSON):*

```json
{ "runFor": 20000 }
```

## One word per cell

`body` holds one word per segment. The first word, `body[0]`, is the head; `length` says how many
words are in use. In each word the high byte is the column and the low byte is the row:

| Word    |    Column |        Row |
| ------- | --------: | ---------: |
| `$050C` | `$05` = 5 | `$0C` = 12 |
| `$040C` | `$04` = 4 | `$0C` = 12 |

The starting head is therefore at column 5, row 12, with two segments behind it. Packing a cell
this way lets `cmp.w` check whether the head and a body segment occupy the same place.

## Follow one move

The initial `length` is 3, `dx` is 1 and `dy` is 0. Before the first move, the body words are
`[$050C, $040C, $030C]`. The shift loop starts at the tail: it copies `body[1]` to `body[2]`,
then `body[0]` to `body[1]`. `dbra` makes those two copies. Finally the code adds the direction
to the old head and writes `$060C` into `body[0]`. The new body is
`[$060C, $050C, $040C]`. Copying backward preserves each old cell until it is needed; the old
tail at `$030C` disappears.

The code unpacks the old head into X and Y before adding `dx` and `dy`. It tests the new coordinates
against columns 0–31 and rows 0–23 before packing them again. A negative coordinate must be caught
here: squeezing -1 into an eight-bit coordinate would lose the fact that it crossed an edge.
After writing the new head, the collision loop starts at `body+2`, which is `body[1]`. It compares
the head with each body segment and skips `body[0]` because that is the head itself.

If the new head reaches food, `score` increases. When there is room below `MAXLEN`, the code copies
the current last segment into one new word and increases `length`. The last two segments overlap
for this frame; the next backward shift separates them as the snake moves.

## Arrow keys and food

Task 19 receives the packed key codes `$25262728`: left, up, right and down, one byte each. Its
answer uses the same byte order, with `$FF` for a held key and `$00` otherwise. The four `btst`
instructions inspect bits 24, 16, 8 and 0 of that answer. For instance, a held left arrow sets bit 24. A direction remains in `dx` and `dy` until an accepted arrow changes it.

`try_direction` rejects a turn straight back. While the snake moves right, its direction is
`(dx, dy) = (1, 0)`. A left press proposes `(-1, 0)`. Both sums, `1 + (-1)` and `0 + 0`, are
zero, so the routine leaves the direction alone. An up press proposes `(0, -1)`, whose sums are
not both zero, and turns the snake upward.

`place_food` uses the saved `seed` to generate two new values. It masks one value to get a column
from 0 to 31. It divides the other by 24: after `divu`, the remainder is in the high word, so
`swap` brings it down to obtain a row from 0 to 23. The routine does **not** check the body; a
new food cell can land on a segment. The fixed starting seed makes the sequence repeat on each run.

## Draw the frame

The board position becomes pixels only in `draw_cell`: column 5 becomes X = `5 × 20 = 100`, and
row 12 becomes Y = `12 × 20 = 240`. Task 87 fills a square at those coordinates. Each frame clears
the hidden image, draws the food and all `length` body cells, then draws the score. Task 94 shows
that complete image at once. Task 23 waits at least 12 hundredths of a second (0.12 seconds) before
the next frame; drawing and instructions also take time. `draw_score` builds the decimal digits
backward in `score_buffer`. At game over, task 17 writes the final score to the console.

## Try a different food colour

Screen colours use `$00BBGGRR`: blue is the highest colour byte, and red is the lowest. Change
`FOOD` from `$000040FF` to `$00FF0000` (blue `$FF`, green `$00`, red `$00`). Before choosing
**Build** and **Run** again, predict what will change on the Screen and what will happen to the
score when the head reaches food. Click the Screen to steer, then choose **Stop** when you are done.
Restore the original value afterward.

<details>
<summary>Show what to expect</summary>

The food squares become blue. The snake stays green, and reaching food still increases the
score by one: `FOOD` is passed to the drawing tasks as a colour, while the food's board cell is
stored separately in `food`.

</details>

## Instructions in this Lecture

- M68K: [`move`](https://asm-editor.specy.app/documentation/m68k/instruction/move.md), [`trap`](https://asm-editor.specy.app/documentation/m68k/instruction/trap.md), [`btst`](https://asm-editor.specy.app/documentation/m68k/instruction/btst.md), [`beq`](https://asm-editor.specy.app/documentation/m68k/instruction/beq.md), [`clr`](https://asm-editor.specy.app/documentation/m68k/instruction/clr.md), [`bsr`](https://asm-editor.specy.app/documentation/m68k/instruction/bsr.md), [`lea`](https://asm-editor.specy.app/documentation/m68k/instruction/lea.md), [`add`](https://asm-editor.specy.app/documentation/m68k/instruction/add.md), [`subq`](https://asm-editor.specy.app/documentation/m68k/instruction/subq.md), [`blt`](https://asm-editor.specy.app/documentation/m68k/instruction/blt.md), [`dbra`](https://asm-editor.specy.app/documentation/m68k/instruction/dbra.md), [`andi`](https://asm-editor.specy.app/documentation/m68k/instruction/andi.md), [`lsr`](https://asm-editor.specy.app/documentation/m68k/instruction/lsr.md), [`tst`](https://asm-editor.specy.app/documentation/m68k/instruction/tst.md), [`cmp`](https://asm-editor.specy.app/documentation/m68k/instruction/cmp.md), [`bgt`](https://asm-editor.specy.app/documentation/m68k/instruction/bgt.md), [`lsl`](https://asm-editor.specy.app/documentation/m68k/instruction/lsl.md), [`or`](https://asm-editor.specy.app/documentation/m68k/instruction/or.md), [`bne`](https://asm-editor.specy.app/documentation/m68k/instruction/bne.md), [`addq`](https://asm-editor.specy.app/documentation/m68k/instruction/addq.md), [`bge`](https://asm-editor.specy.app/documentation/m68k/instruction/bge.md), [`bra`](https://asm-editor.specy.app/documentation/m68k/instruction/bra.md), [`rts`](https://asm-editor.specy.app/documentation/m68k/instruction/rts.md), [`mulu`](https://asm-editor.specy.app/documentation/m68k/instruction/mulu.md), [`divu`](https://asm-editor.specy.app/documentation/m68k/instruction/divu.md), [`swap`](https://asm-editor.specy.app/documentation/m68k/instruction/swap.md), [`eor`](https://asm-editor.specy.app/documentation/m68k/instruction/eor.md)

Previous: [Move a square with the keyboard](https://asm-editor.specy.app/learn/courses/m68k/examples/keyboard-control.md)

Next: [Flappy ASM](https://asm-editor.specy.app/learn/courses/m68k/examples/flappy-asm.md)
