Skip to content
Game 1 Unit 5 of 64 1 hr learning time

Grid-Based Movement

Hop in discrete steps. Smooth animation. The Frogger feel.

8% of Signal

What You’re Building

The Frogger feel.

Real Frogger doesn’t glide—it hops. Each joystick press moves the frog exactly one grid cell. The movement is animated over 8 frames, giving that distinctive chunky-but-smooth feel that makes Frogger satisfying to play.

By the end of this unit:

  • Frog hops in discrete 16-pixel steps
  • Smooth 8-frame animation during each hop
  • Grid: 20 columns × 13 rows
  • Edge detection prevents holding the joystick

Unit 5 Screenshot

Why Grid Movement?

Frogger isn’t an action game—it’s a puzzle game with timing elements. The grid serves several purposes:

  1. Predictability: You know exactly where you’ll land
  2. Collision zones: Each lane is one row; cars and logs occupy grid cells
  3. Strategic planning: You can count cells to plan safe routes
  4. Timing windows: Hops take fixed time, creating rhythm

Smooth, analogue movement would make the game feel floaty and imprecise. The grid creates clarity.

The State Machine

Our frog now has two states:

; Frog State Machine
; Two states: waiting for input, or animating a hop

STATE_IDLE      equ 0           ; Waiting for input
STATE_HOPPING   equ 1           ; Currently animating a hop

DIR_UP          equ 0           ; Direction codes
DIR_DOWN        equ 1
DIR_LEFT        equ 2
DIR_RIGHT       equ 3

update_frog:
            tst.w   frog_state
            beq     .idle           ; STATE_IDLE = 0
            bra     .hopping        ; Must be STATE_HOPPING

.idle:
            ; Check for input, validate move, start hop
            bsr     read_joystick_edge
            tst.w   d0
            beq     .done           ; No input
            ; ... check direction bits, validate move ...
            move.w  #STATE_HOPPING,frog_state
            clr.w   frog_anim_frame
            bra     .done

.hopping:
            ; Animate position, check completion
            addq.w  #1,frog_anim_frame
            ; ... move pixel position ...
            cmp.w   #HOP_FRAMES,frog_anim_frame
            blt.s   .done
            ; Hop complete - update grid, return to idle
            move.w  #STATE_IDLE,frog_state

.done:      rts

This prevents the frog from changing direction mid-hop or moving continuously when the joystick is held.

Grid Coordinates

We track position two ways:

; Grid position (which cell)
frog_grid_x:    dc.w    9       ; Column (0-19)
frog_grid_y:    dc.w    12      ; Row (0-12)

; Pixel position (for rendering)
frog_pixel_x:   dc.w    0       ; Screen X
frog_pixel_y:   dc.w    0       ; Screen Y

Grid coordinates are the “true” position—which cell the frog occupies. Pixel coordinates are calculated from grid coordinates for rendering and animation.

Converting Grid to Pixels

; Grid to Pixel Conversion
; Convert cell coordinates to screen position

grid_to_pixels:
            ; pixel_x = grid_x * CELL_SIZE + GRID_ORIGIN_X
            move.w  frog_grid_x,d0
            mulu    #CELL_SIZE,d0
            add.w   #GRID_ORIGIN_X,d0
            move.w  d0,frog_pixel_x

            ; pixel_y = grid_y * CELL_SIZE + GRID_ORIGIN_Y
            move.w  frog_grid_y,d0
            mulu    #CELL_SIZE,d0
            add.w   #GRID_ORIGIN_Y,d0
            move.w  d0,frog_pixel_y

            rts

; Example: grid(9, 12) → pixel(192, 236)
;   192 = 9 * 16 + 48
;   236 = 12 * 16 + 44

The grid origin (48, 44) positions the grid within our playfield.

Edge Detection

Holding the joystick shouldn’t make the frog hop repeatedly. We want one hop per press. This requires edge detection—detecting the transition from “not pressed” to “pressed”.

; Joystick Edge Detection
; Triggers only on new presses, not held directions

read_joystick_edge:
            ; Read and decode joystick
            move.w  JOY1DAT(a5),d0
            move.w  d0,d1
            lsr.w   #1,d1
            eor.w   d1,d0           ; D0 = decoded current state

            ; Edge detection: current AND NOT previous
            ; This gives us bits that just became 1
            move.w  joy_prev,d1
            not.w   d1              ; D1 = NOT previous
            and.w   d0,d1           ; D1 = newly pressed only

            ; Save current for next frame
            move.w  d0,joy_prev

            move.w  d1,d0           ; Return newly pressed
            rts

; Without edge detection: holding UP would cause
; multiple hops. With edge detection: one press = one hop.

The key insight: current AND NOT previous gives us bits that are set now but weren’t set before—exactly the “just pressed” events we want.

Animation Timing

Each hop takes 8 frames at 2 pixels per frame:

HOP_FRAMES      equ 8           ; Animation duration
PIXELS_PER_FRAME equ 2          ; Movement per frame

8 × 2 = 16 pixels = one cell. At 50fps (PAL), each hop takes 8/50 = 0.16 seconds. This feels snappy but visible.

The Hop Animation

.hopping:
    addq.w  #1,frog_anim_frame

    ; Move based on direction
    move.w  frog_dir,d0
    cmp.w   #DIR_UP,d0
    bne.s   .hop_not_up
    sub.w   #PIXELS_PER_FRAME,frog_pixel_y
    bra.s   .check_done
.hop_not_up:
    ; ... check other directions ...

.check_done:
    cmp.w   #HOP_FRAMES,frog_anim_frame
    blt.s   .done

    ; Hop complete: update grid, snap pixels
    ; ... update frog_grid_x/y ...
    bsr     grid_to_pixels
    move.w  #STATE_IDLE,frog_state

We update pixel position each frame for smooth animation, then snap to exact grid position when done. The snap prevents cumulative rounding errors.

Boundary Checking

Before starting a hop, we check if the target cell exists:

btst    #8,d0               ; Up pressed?
beq.s   .not_up
move.w  frog_grid_y,d1
tst.w   d1                  ; Already at row 0?
beq.s   .not_up             ; Can't go higher
move.w  #DIR_UP,frog_dir
bra.s   .start_hop

We check grid coordinates, not pixel coordinates. This is cleaner and prevents the frog from starting a hop it can’t complete.

The Complete Flow

  1. Frame starts: Wait for VBlank
  2. Update frog: Run state machine
  3. Update sprite: Write new position to sprite data
  4. Loop: Repeat

The frog logic is now significantly more complex than our previous smooth movement, but the result feels much better. The grid creates that classic arcade rhythm.

Key Takeaways

  • State machine separates idle and hopping behaviour
  • Grid coordinates are the source of truth
  • Pixel coordinates are derived for rendering
  • Edge detection gives one hop per press
  • Snapping to grid prevents drift
  • 8-frame animation balances speed and visibility

The Code

;══════════════════════════════════════════════════════════════════════════════
; SIGNAL - A Frogger-style game for the Commodore Amiga
; Unit 5: Grid-Based Movement
;
; Real Frogger doesn't glide—it hops. Each press moves the frog exactly one
; grid cell, with smooth animation over 8 frames. This unit implements that
; distinctive movement feel using a simple state machine.
;══════════════════════════════════════════════════════════════════════════════

;══════════════════════════════════════════════════════════════════════════════
; GRID CONSTANTS
;══════════════════════════════════════════════════════════════════════════════

; Grid dimensions
GRID_COLS       equ 20          ; 20 columns (320 ÷ 16)
GRID_ROWS       equ 13          ; 13 rows (matching our playfield)
CELL_SIZE       equ 16          ; Each cell is 16×16 pixels

; Grid origin (pixel coordinates of top-left cell)
GRID_ORIGIN_X   equ 48          ; Left margin
GRID_ORIGIN_Y   equ 44          ; Top of playfield

; Starting position (grid coordinates)
START_GRID_X    equ 9           ; Middle column (0-19)
START_GRID_Y    equ 12          ; Bottom row (0-12)

; Animation timing
HOP_FRAMES      equ 8           ; Animation lasts 8 frames
PIXELS_PER_FRAME equ 2          ; 2 pixels per frame × 8 frames = 16 pixels

; Movement states
STATE_IDLE      equ 0           ; Waiting for input
STATE_HOPPING   equ 1           ; Currently animating a hop

; Direction codes
DIR_UP          equ 0
DIR_DOWN        equ 1
DIR_LEFT        equ 2
DIR_RIGHT       equ 3

FROG_HEIGHT     equ 16

; Zone colours
COLOUR_BLACK    equ $0000
COLOUR_HOME     equ $0282
COLOUR_HOME_LIT equ $03a3
COLOUR_WATER1   equ $0148
COLOUR_WATER2   equ $026a
COLOUR_MEDIAN   equ $0383
COLOUR_ROAD1    equ $0333
COLOUR_ROAD2    equ $0444
COLOUR_START    equ $0262
COLOUR_START_LIT equ $0373

; Sprite palette
COLOUR_FROG     equ $00f0
COLOUR_EYES     equ $0ff0
COLOUR_OUTLINE  equ $0000

;══════════════════════════════════════════════════════════════════════════════
; HARDWARE REGISTERS
;══════════════════════════════════════════════════════════════════════════════

CUSTOM      equ $dff000

DMACONR     equ $002
DMACON      equ $096
INTENA      equ $09a
INTREQ      equ $09c
VPOSR       equ $004
JOY1DAT     equ $00c
COP1LC      equ $080
COPJMP1     equ $088
COLOR00     equ $180
SPR0PTH     equ $120
SPR0PTL     equ $122

;══════════════════════════════════════════════════════════════════════════════
; CODE SECTION
;══════════════════════════════════════════════════════════════════════════════

            section code,code_c

start:
            lea     CUSTOM,a5

            ; --- System takeover ---
            move.w  #$7fff,INTENA(a5)
            move.w  #$7fff,INTREQ(a5)
            move.w  #$7fff,DMACON(a5)

            ; --- Initialise game state ---
            move.w  #START_GRID_X,frog_grid_x
            move.w  #START_GRID_Y,frog_grid_y
            move.w  #STATE_IDLE,frog_state
            move.w  #0,frog_anim_frame
            clr.w   joy_prev

            ; --- Calculate initial pixel position ---
            bsr     grid_to_pixels

            ; --- Set sprite pointer in copper list ---
            lea     frog_data,a0
            move.l  a0,d0
            swap    d0
            lea     sprpth_val,a1
            move.w  d0,(a1)
            swap    d0
            lea     sprptl_val,a1
            move.w  d0,(a1)

            ; --- Update sprite control words ---
            bsr     update_sprite

            ; --- Install copper list ---
            lea     copperlist,a0
            move.l  a0,COP1LC(a5)
            move.w  d0,COPJMP1(a5)

            ; --- Enable DMA ---
            move.w  #$83a0,DMACON(a5)

;══════════════════════════════════════════════════════════════════════════════
; MAIN LOOP
;══════════════════════════════════════════════════════════════════════════════

mainloop:
            bsr     wait_vblank
            bsr     update_frog
            bsr     update_sprite
            bra.s   mainloop

;══════════════════════════════════════════════════════════════════════════════
; UPDATE_FROG - Main frog logic
;══════════════════════════════════════════════════════════════════════════════
; State machine:
;   IDLE: Check for joystick input, start hop if valid
;   HOPPING: Animate position, return to IDLE when complete

update_frog:
            tst.w   frog_state
            beq     .idle
            bra     .hopping

.idle:
            ; --- Read joystick with edge detection ---
            bsr     read_joystick_edge
            tst.w   d0
            beq     .done                   ; No new input

            ; --- Check each direction ---
            btst    #8,d0                   ; Up?
            beq.s   .not_up
            move.w  frog_grid_y,d1
            tst.w   d1
            beq.s   .not_up                 ; Already at top
            move.w  #DIR_UP,frog_dir
            bra.s   .start_hop
.not_up:
            btst    #0,d0                   ; Down?
            beq.s   .not_down
            move.w  frog_grid_y,d1
            cmp.w   #GRID_ROWS-1,d1
            beq.s   .not_down               ; Already at bottom
            move.w  #DIR_DOWN,frog_dir
            bra.s   .start_hop
.not_down:
            btst    #9,d0                   ; Left?
            beq.s   .not_left
            move.w  frog_grid_x,d1
            tst.w   d1
            beq.s   .not_left               ; Already at left
            move.w  #DIR_LEFT,frog_dir
            bra.s   .start_hop
.not_left:
            btst    #1,d0                   ; Right?
            beq     .done
            move.w  frog_grid_x,d1
            cmp.w   #GRID_COLS-1,d1
            beq     .done                   ; Already at right
            move.w  #DIR_RIGHT,frog_dir
            ; Fall through to start_hop

.start_hop:
            ; --- Begin hopping animation ---
            move.w  #STATE_HOPPING,frog_state
            move.w  #0,frog_anim_frame
            bra.s   .done

.hopping:
            ; --- Advance animation frame ---
            addq.w  #1,frog_anim_frame

            ; --- Move pixel position based on direction ---
            move.w  frog_dir,d0

            cmp.w   #DIR_UP,d0
            bne.s   .hop_not_up
            sub.w   #PIXELS_PER_FRAME,frog_pixel_y
            bra.s   .check_hop_done
.hop_not_up:
            cmp.w   #DIR_DOWN,d0
            bne.s   .hop_not_down
            add.w   #PIXELS_PER_FRAME,frog_pixel_y
            bra.s   .check_hop_done
.hop_not_down:
            cmp.w   #DIR_LEFT,d0
            bne.s   .hop_not_left
            sub.w   #PIXELS_PER_FRAME,frog_pixel_x
            bra.s   .check_hop_done
.hop_not_left:
            ; Must be RIGHT
            add.w   #PIXELS_PER_FRAME,frog_pixel_x

.check_hop_done:
            ; --- Check if animation complete ---
            cmp.w   #HOP_FRAMES,frog_anim_frame
            blt.s   .done

            ; --- Hop complete: update grid position ---
            move.w  frog_dir,d0

            cmp.w   #DIR_UP,d0
            bne.s   .end_not_up
            subq.w  #1,frog_grid_y
            bra.s   .snap_to_grid
.end_not_up:
            cmp.w   #DIR_DOWN,d0
            bne.s   .end_not_down
            addq.w  #1,frog_grid_y
            bra.s   .snap_to_grid
.end_not_down:
            cmp.w   #DIR_LEFT,d0
            bne.s   .end_not_left
            subq.w  #1,frog_grid_x
            bra.s   .snap_to_grid
.end_not_left:
            ; Must be RIGHT
            addq.w  #1,frog_grid_x

.snap_to_grid:
            ; --- Snap pixel position to grid (prevents drift) ---
            bsr     grid_to_pixels

            ; --- Return to idle state ---
            move.w  #STATE_IDLE,frog_state

.done:
            rts

;══════════════════════════════════════════════════════════════════════════════
; GRID_TO_PIXELS - Convert grid coordinates to pixel coordinates
;══════════════════════════════════════════════════════════════════════════════
; Input: frog_grid_x, frog_grid_y
; Output: frog_pixel_x, frog_pixel_y

grid_to_pixels:
            move.w  frog_grid_x,d0
            mulu    #CELL_SIZE,d0
            add.w   #GRID_ORIGIN_X,d0
            move.w  d0,frog_pixel_x

            move.w  frog_grid_y,d0
            mulu    #CELL_SIZE,d0
            add.w   #GRID_ORIGIN_Y,d0
            move.w  d0,frog_pixel_y

            rts

;══════════════════════════════════════════════════════════════════════════════
; READ_JOYSTICK_EDGE - Read joystick with edge detection
;══════════════════════════════════════════════════════════════════════════════
; Returns only newly-pressed directions (not held directions)
; Output: D0 = direction bits (only set on transition from not-pressed)

read_joystick_edge:
            ; --- Read and decode joystick ---
            move.w  JOY1DAT(a5),d0
            move.w  d0,d1
            lsr.w   #1,d1
            eor.w   d1,d0               ; D0 = decoded current state

            ; --- Edge detection: current AND NOT previous ---
            move.w  joy_prev,d1
            not.w   d1                  ; D1 = NOT previous
            and.w   d0,d1               ; D1 = newly pressed only

            ; --- Save current state for next frame ---
            move.w  d0,joy_prev

            move.w  d1,d0               ; Return newly pressed in D0
            rts

;══════════════════════════════════════════════════════════════════════════════
; SUBROUTINES
;══════════════════════════════════════════════════════════════════════════════

wait_vblank:
            move.l  #$1ff00,d1
.wait:
            move.l  VPOSR(a5),d0
            and.l   d1,d0
            bne.s   .wait
            rts

update_sprite:
            lea     frog_data,a0
            move.w  frog_pixel_y,d0
            move.w  frog_pixel_x,d1

            move.w  d0,d2
            lsl.w   #8,d2
            lsr.w   #1,d1
            or.b    d1,d2
            move.w  d2,(a0)

            add.w   #FROG_HEIGHT,d0
            lsl.w   #8,d0
            move.w  d0,2(a0)

            rts

;══════════════════════════════════════════════════════════════════════════════
; VARIABLES
;══════════════════════════════════════════════════════════════════════════════

; Grid position (which cell the frog occupies)
frog_grid_x:    dc.w    9           ; Column (0-19)
frog_grid_y:    dc.w    12          ; Row (0-12)

; Pixel position (for rendering and animation)
frog_pixel_x:   dc.w    0           ; Screen X
frog_pixel_y:   dc.w    0           ; Screen Y

; Movement state machine
frog_state:     dc.w    0           ; 0=idle, 1=hopping
frog_dir:       dc.w    0           ; Direction of current hop
frog_anim_frame: dc.w   0           ; Current frame of hop animation (0-7)

; Input state
joy_prev:       dc.w    0           ; Previous joystick state (for edge detection)

;══════════════════════════════════════════════════════════════════════════════
; COPPER LIST
;══════════════════════════════════════════════════════════════════════════════

copperlist:
            dc.w    COLOR00,COLOUR_BLACK

            ; --- Sprite palette ---
            dc.w    $01a2,COLOUR_FROG
            dc.w    $01a4,COLOUR_EYES
            dc.w    $01a6,COLOUR_OUTLINE

            ; --- Sprite 0 pointer ---
            dc.w    SPR0PTH
sprpth_val: dc.w    $0000
            dc.w    SPR0PTL
sprptl_val: dc.w    $0000

            ; ROW 1: HOME ZONE
            dc.w    $2c07,$fffe
            dc.w    COLOR00,COLOUR_HOME
            dc.w    $3407,$fffe
            dc.w    COLOR00,COLOUR_HOME_LIT
            dc.w    $3807,$fffe
            dc.w    COLOR00,COLOUR_HOME

            ; ROWS 2-6: WATER ZONE
            dc.w    $3c07,$fffe
            dc.w    COLOR00,COLOUR_WATER1
            dc.w    $4c07,$fffe
            dc.w    COLOR00,COLOUR_WATER2
            dc.w    $5c07,$fffe
            dc.w    COLOR00,COLOUR_WATER1
            dc.w    $6c07,$fffe
            dc.w    COLOR00,COLOUR_WATER2
            dc.w    $7c07,$fffe
            dc.w    COLOR00,COLOUR_WATER1

            ; ROW 7: MEDIAN
            dc.w    $8c07,$fffe
            dc.w    COLOR00,COLOUR_MEDIAN

            ; ROWS 8-12: ROAD ZONE
            dc.w    $9c07,$fffe
            dc.w    COLOR00,COLOUR_ROAD1
            dc.w    $ac07,$fffe
            dc.w    COLOR00,COLOUR_ROAD2
            dc.w    $bc07,$fffe
            dc.w    COLOR00,COLOUR_ROAD1
            dc.w    $cc07,$fffe
            dc.w    COLOR00,COLOUR_ROAD2
            dc.w    $dc07,$fffe
            dc.w    COLOR00,COLOUR_ROAD1

            ; ROW 13: START ZONE
            dc.w    $ec07,$fffe
            dc.w    COLOR00,COLOUR_START
            dc.w    $f407,$fffe
            dc.w    COLOR00,COLOUR_START_LIT
            dc.w    $f807,$fffe
            dc.w    COLOR00,COLOUR_START

            ; BOTTOM BORDER
            dc.w    $fc07,$fffe
            dc.w    COLOR00,COLOUR_BLACK

            dc.w    $ffff,$fffe

;══════════════════════════════════════════════════════════════════════════════
; SPRITE DATA
;══════════════════════════════════════════════════════════════════════════════

            even
frog_data:
            dc.w    $ec50,$fc00             ; Control words

            dc.w    $0000,$0000
            dc.w    $07e0,$0000
            dc.w    $1ff8,$0420
            dc.w    $3ffc,$0a50
            dc.w    $7ffe,$1248
            dc.w    $7ffe,$1008
            dc.w    $ffff,$2004
            dc.w    $ffff,$0000
            dc.w    $ffff,$0000
            dc.w    $7ffe,$2004
            dc.w    $7ffe,$1008
            dc.w    $3ffc,$0810
            dc.w    $1ff8,$0420
            dc.w    $07e0,$0000
            dc.w    $0000,$0000
            dc.w    $0000,$0000

            dc.w    $0000,$0000

Build It

vasmm68k_mot -Fhunkexe -kick1hunks -o signal signal.asm

What’s Next

The frog hops correctly, but there’s nothing to dodge. In Unit 6, we’ll introduce the Blitter—the Amiga’s block image transfer coprocessor—to draw cars on the road.

What Changed

Unit 4 → Unit 5
+259-171
11 ;══════════════════════════════════════════════════════════════════════════════
22 ; SIGNAL - A Frogger-style game for the Commodore Amiga
3-; Unit 4: The Playfield
3+; Unit 5: Grid-Based Movement
44 ;
5-; This unit refines our Copper list to create a proper 13-row Frogger grid:
6-; - Row 1: Home zone (5 docking spots at top)
7-; - Rows 2-6: Water zone (5 lanes for logs and turtles)
8-; - Row 7: Median (safe resting zone)
9-; - Rows 8-12: Road zone (5 lanes for traffic)
10-; - Row 13: Start zone (where the frog begins)
5+; Real Frogger doesn't glide—it hops. Each press moves the frog exactly one
6+; grid cell, with smooth animation over 8 frames. This unit implements that
7+; distinctive movement feel using a simple state machine.
118 ;══════════════════════════════════════════════════════════════════════════════
129
1310 ;══════════════════════════════════════════════════════════════════════════════
14-; TWEAKABLE VALUES
11+; GRID CONSTANTS
1512 ;══════════════════════════════════════════════════════════════════════════════
1613
17-FROG_START_X equ 160 ; Centre of screen
18-FROG_START_Y equ 220 ; Bottom row (start zone)
14+; Grid dimensions
15+GRID_COLS equ 20 ; 20 columns (320 ÷ 16)
16+GRID_ROWS equ 13 ; 13 rows (matching our playfield)
17+CELL_SIZE equ 16 ; Each cell is 16×16 pixels
1918
20-MOVE_SPEED equ 2 ; Pixels per frame
19+; Grid origin (pixel coordinates of top-left cell)
20+GRID_ORIGIN_X equ 48 ; Left margin
21+GRID_ORIGIN_Y equ 44 ; Top of playfield
2122
22-; Screen boundaries match our 13-row grid
23-MIN_X equ 48 ; Left edge
24-MAX_X equ 280 ; Right edge
25-MIN_Y equ 44 ; Top of home zone
26-MAX_Y equ 220 ; Bottom of start zone
23+; Starting position (grid coordinates)
24+START_GRID_X equ 9 ; Middle column (0-19)
25+START_GRID_Y equ 12 ; Bottom row (0-12)
2726
28-FROG_HEIGHT equ 16 ; Sprite height
27+; Animation timing
28+HOP_FRAMES equ 8 ; Animation lasts 8 frames
29+PIXELS_PER_FRAME equ 2 ; 2 pixels per frame × 8 frames = 16 pixels
2930
30-; Grid layout constants
31-ROW_HEIGHT equ 16 ; Each row is 16 pixels tall
32-GRID_TOP equ 44 ; First row starts at scanline 44
33-GRID_ROWS equ 13 ; Total rows in the playfield
31+; Movement states
32+STATE_IDLE equ 0 ; Waiting for input
33+STATE_HOPPING equ 1 ; Currently animating a hop
3434
35-; Zone colours - carefully chosen for readability and atmosphere
36-COLOUR_BLACK equ $0000 ; Border
37-COLOUR_HOME equ $0282 ; Home zone: deep green
38-COLOUR_HOME_LIT equ $03a3 ; Home zone highlight
39-COLOUR_WATER1 equ $0148 ; Water: deep blue
40-COLOUR_WATER2 equ $026a ; Water: medium blue
41-COLOUR_MEDIAN equ $0383 ; Median: bright green (safe!)
42-COLOUR_ROAD1 equ $0333 ; Road: dark grey
43-COLOUR_ROAD2 equ $0444 ; Road: medium grey
44-COLOUR_START equ $0262 ; Start zone: grass green
45-COLOUR_START_LIT equ $0373 ; Start zone highlight
35+; Direction codes
36+DIR_UP equ 0
37+DIR_DOWN equ 1
38+DIR_LEFT equ 2
39+DIR_RIGHT equ 3
40+
41+FROG_HEIGHT equ 16
42+
43+; Zone colours
44+COLOUR_BLACK equ $0000
45+COLOUR_HOME equ $0282
46+COLOUR_HOME_LIT equ $03a3
47+COLOUR_WATER1 equ $0148
48+COLOUR_WATER2 equ $026a
49+COLOUR_MEDIAN equ $0383
50+COLOUR_ROAD1 equ $0333
51+COLOUR_ROAD2 equ $0444
52+COLOUR_START equ $0262
53+COLOUR_START_LIT equ $0373
4654
4755 ; Sprite palette
48-COLOUR_FROG equ $00f0 ; Bright green body
49-COLOUR_EYES equ $0ff0 ; Yellow eyes
50-COLOUR_OUTLINE equ $0000 ; Black outline
56+COLOUR_FROG equ $00f0
57+COLOUR_EYES equ $0ff0
58+COLOUR_OUTLINE equ $0000
5159
5260 ;══════════════════════════════════════════════════════════════════════════════
5361 ; HARDWARE REGISTERS
...
8189 move.w #$7fff,INTREQ(a5)
8290 move.w #$7fff,DMACON(a5)
8391
84- ; --- Initialise frog position ---
85- move.w #FROG_START_X,frog_x
86- move.w #FROG_START_Y,frog_y
92+ ; --- Initialise game state ---
93+ move.w #START_GRID_X,frog_grid_x
94+ move.w #START_GRID_Y,frog_grid_y
95+ move.w #STATE_IDLE,frog_state
96+ move.w #0,frog_anim_frame
97+ clr.w joy_prev
98+
99+ ; --- Calculate initial pixel position ---
100+ bsr grid_to_pixels
87101
88102 ; --- Set sprite pointer in copper list ---
89103 lea frog_data,a0
...
111125 ;══════════════════════════════════════════════════════════════════════════════
112126
113127 mainloop:
114- bsr.s wait_vblank
115- bsr.s read_joystick
116- bsr.s move_frog
128+ bsr wait_vblank
129+ bsr update_frog
117130 bsr update_sprite
118-
119- btst #6,$bfe001
120- bne.s mainloop
121131 bra.s mainloop
122132
123133 ;══════════════════════════════════════════════════════════════════════════════
124-; SUBROUTINES
134+; UPDATE_FROG - Main frog logic
125135 ;══════════════════════════════════════════════════════════════════════════════
136+; State machine:
137+; IDLE: Check for joystick input, start hop if valid
138+; HOPPING: Animate position, return to IDLE when complete
126139
127-wait_vblank:
128- move.l #$1ff00,d1
129-.wait:
130- move.l VPOSR(a5),d0
131- and.l d1,d0
132- bne.s .wait
140+update_frog:
141+ tst.w frog_state
142+ beq .idle
143+ bra .hopping
144+
145+.idle:
146+ ; --- Read joystick with edge detection ---
147+ bsr read_joystick_edge
148+ tst.w d0
149+ beq .done ; No new input
150+
151+ ; --- Check each direction ---
152+ btst #8,d0 ; Up?
153+ beq.s .not_up
154+ move.w frog_grid_y,d1
155+ tst.w d1
156+ beq.s .not_up ; Already at top
157+ move.w #DIR_UP,frog_dir
158+ bra.s .start_hop
159+.not_up:
160+ btst #0,d0 ; Down?
161+ beq.s .not_down
162+ move.w frog_grid_y,d1
163+ cmp.w #GRID_ROWS-1,d1
164+ beq.s .not_down ; Already at bottom
165+ move.w #DIR_DOWN,frog_dir
166+ bra.s .start_hop
167+.not_down:
168+ btst #9,d0 ; Left?
169+ beq.s .not_left
170+ move.w frog_grid_x,d1
171+ tst.w d1
172+ beq.s .not_left ; Already at left
173+ move.w #DIR_LEFT,frog_dir
174+ bra.s .start_hop
175+.not_left:
176+ btst #1,d0 ; Right?
177+ beq .done
178+ move.w frog_grid_x,d1
179+ cmp.w #GRID_COLS-1,d1
180+ beq .done ; Already at right
181+ move.w #DIR_RIGHT,frog_dir
182+ ; Fall through to start_hop
183+
184+.start_hop:
185+ ; --- Begin hopping animation ---
186+ move.w #STATE_HOPPING,frog_state
187+ move.w #0,frog_anim_frame
188+ bra.s .done
189+
190+.hopping:
191+ ; --- Advance animation frame ---
192+ addq.w #1,frog_anim_frame
193+
194+ ; --- Move pixel position based on direction ---
195+ move.w frog_dir,d0
196+
197+ cmp.w #DIR_UP,d0
198+ bne.s .hop_not_up
199+ sub.w #PIXELS_PER_FRAME,frog_pixel_y
200+ bra.s .check_hop_done
201+.hop_not_up:
202+ cmp.w #DIR_DOWN,d0
203+ bne.s .hop_not_down
204+ add.w #PIXELS_PER_FRAME,frog_pixel_y
205+ bra.s .check_hop_done
206+.hop_not_down:
207+ cmp.w #DIR_LEFT,d0
208+ bne.s .hop_not_left
209+ sub.w #PIXELS_PER_FRAME,frog_pixel_x
210+ bra.s .check_hop_done
211+.hop_not_left:
212+ ; Must be RIGHT
213+ add.w #PIXELS_PER_FRAME,frog_pixel_x
214+
215+.check_hop_done:
216+ ; --- Check if animation complete ---
217+ cmp.w #HOP_FRAMES,frog_anim_frame
218+ blt.s .done
219+
220+ ; --- Hop complete: update grid position ---
221+ move.w frog_dir,d0
222+
223+ cmp.w #DIR_UP,d0
224+ bne.s .end_not_up
225+ subq.w #1,frog_grid_y
226+ bra.s .snap_to_grid
227+.end_not_up:
228+ cmp.w #DIR_DOWN,d0
229+ bne.s .end_not_down
230+ addq.w #1,frog_grid_y
231+ bra.s .snap_to_grid
232+.end_not_down:
233+ cmp.w #DIR_LEFT,d0
234+ bne.s .end_not_left
235+ subq.w #1,frog_grid_x
236+ bra.s .snap_to_grid
237+.end_not_left:
238+ ; Must be RIGHT
239+ addq.w #1,frog_grid_x
240+
241+.snap_to_grid:
242+ ; --- Snap pixel position to grid (prevents drift) ---
243+ bsr grid_to_pixels
244+
245+ ; --- Return to idle state ---
246+ move.w #STATE_IDLE,frog_state
247+
248+.done:
133249 rts
134250
135-read_joystick:
251+;══════════════════════════════════════════════════════════════════════════════
252+; GRID_TO_PIXELS - Convert grid coordinates to pixel coordinates
253+;══════════════════════════════════════════════════════════════════════════════
254+; Input: frog_grid_x, frog_grid_y
255+; Output: frog_pixel_x, frog_pixel_y
256+
257+grid_to_pixels:
258+ move.w frog_grid_x,d0
259+ mulu #CELL_SIZE,d0
260+ add.w #GRID_ORIGIN_X,d0
261+ move.w d0,frog_pixel_x
262+
263+ move.w frog_grid_y,d0
264+ mulu #CELL_SIZE,d0
265+ add.w #GRID_ORIGIN_Y,d0
266+ move.w d0,frog_pixel_y
267+
268+ rts
269+
270+;══════════════════════════════════════════════════════════════════════════════
271+; READ_JOYSTICK_EDGE - Read joystick with edge detection
272+;══════════════════════════════════════════════════════════════════════════════
273+; Returns only newly-pressed directions (not held directions)
274+; Output: D0 = direction bits (only set on transition from not-pressed)
275+
276+read_joystick_edge:
277+ ; --- Read and decode joystick ---
136278 move.w JOY1DAT(a5),d0
137279 move.w d0,d1
138280 lsr.w #1,d1
139- eor.w d1,d0
281+ eor.w d1,d0 ; D0 = decoded current state
282+
283+ ; --- Edge detection: current AND NOT previous ---
284+ move.w joy_prev,d1
285+ not.w d1 ; D1 = NOT previous
286+ and.w d0,d1 ; D1 = newly pressed only
287+
288+ ; --- Save current state for next frame ---
289+ move.w d0,joy_prev
290+
291+ move.w d1,d0 ; Return newly pressed in D0
140292 rts
141293
142-move_frog:
143- btst #8,d0
144- beq.s .no_up
145- move.w frog_y,d1
146- sub.w #MOVE_SPEED,d1
147- cmp.w #MIN_Y,d1
148- blt.s .no_up
149- move.w d1,frog_y
150-.no_up:
151- btst #0,d0
152- beq.s .no_down
153- move.w frog_y,d1
154- add.w #MOVE_SPEED,d1
155- cmp.w #MAX_Y,d1
156- bgt.s .no_down
157- move.w d1,frog_y
158-.no_down:
159- btst #9,d0
160- beq.s .no_left
161- move.w frog_x,d1
162- sub.w #MOVE_SPEED,d1
163- cmp.w #MIN_X,d1
164- blt.s .no_left
165- move.w d1,frog_x
166-.no_left:
167- btst #1,d0
168- beq.s .no_right
169- move.w frog_x,d1
170- add.w #MOVE_SPEED,d1
171- cmp.w #MAX_X,d1
172- bgt.s .no_right
173- move.w d1,frog_x
174-.no_right:
294+;══════════════════════════════════════════════════════════════════════════════
295+; SUBROUTINES
296+;══════════════════════════════════════════════════════════════════════════════
297+
298+wait_vblank:
299+ move.l #$1ff00,d1
300+.wait:
301+ move.l VPOSR(a5),d0
302+ and.l d1,d0
303+ bne.s .wait
175304 rts
176305
177306 update_sprite:
178307 lea frog_data,a0
179- move.w frog_y,d0
180- move.w frog_x,d1
308+ move.w frog_pixel_y,d0
309+ move.w frog_pixel_x,d1
181310
182311 move.w d0,d2
183312 lsl.w #8,d2
...
195324 ; VARIABLES
196325 ;══════════════════════════════════════════════════════════════════════════════
197326
198-frog_x: dc.w 160
199-frog_y: dc.w 220
327+; Grid position (which cell the frog occupies)
328+frog_grid_x: dc.w 9 ; Column (0-19)
329+frog_grid_y: dc.w 12 ; Row (0-12)
330+
331+; Pixel position (for rendering and animation)
332+frog_pixel_x: dc.w 0 ; Screen X
333+frog_pixel_y: dc.w 0 ; Screen Y
334+
335+; Movement state machine
336+frog_state: dc.w 0 ; 0=idle, 1=hopping
337+frog_dir: dc.w 0 ; Direction of current hop
338+frog_anim_frame: dc.w 0 ; Current frame of hop animation (0-7)
339+
340+; Input state
341+joy_prev: dc.w 0 ; Previous joystick state (for edge detection)
200342
201343 ;══════════════════════════════════════════════════════════════════════════════
202344 ; COPPER LIST
203345 ;══════════════════════════════════════════════════════════════════════════════
204-; The playfield is organised as a 13-row grid, each row 16 pixels tall.
205-; Row numbers (1-13) and scanlines are:
206-;
207-; Row 1 (Home) : $2C-$3B (44-59) - 5 docking spots
208-; Row 2 (Water) : $3C-$4B (60-75) - Log/turtle lane 1
209-; Row 3 (Water) : $4C-$5B (76-91) - Log/turtle lane 2
210-; Row 4 (Water) : $5C-$6B (92-107) - Log/turtle lane 3
211-; Row 5 (Water) : $6C-$7B (108-123) - Log/turtle lane 4
212-; Row 6 (Water) : $7C-$8B (124-139) - Log/turtle lane 5
213-; Row 7 (Median) : $8C-$9B (140-155) - Safe zone
214-; Row 8 (Road) : $9C-$AB (156-171) - Car lane 1
215-; Row 9 (Road) : $AC-$BB (172-187) - Car lane 2
216-; Row 10 (Road) : $BC-$CB (188-203) - Car lane 3
217-; Row 11 (Road) : $CC-$DB (204-219) - Car lane 4
218-; Row 12 (Road) : $DC-$EB (220-235) - Car lane 5
219-; Row 13 (Start) : $EC-$FB (236-251) - Starting area
220-;
221-; The Copper changes COLOR00 at each row boundary to create the zones.
222346
223347 copperlist:
224- dc.w COLOR00,COLOUR_BLACK ; Black border at top
348+ dc.w COLOR00,COLOUR_BLACK
225349
226- ; --- Sprite palette (colours 17-19) ---
350+ ; --- Sprite palette ---
227351 dc.w $01a2,COLOUR_FROG
228352 dc.w $01a4,COLOUR_EYES
229353 dc.w $01a6,COLOUR_OUTLINE
...
234358 dc.w SPR0PTL
235359 sprptl_val: dc.w $0000
236360
237- ; ═══════════════════════════════════════════════════════════════
238- ; ROW 1: HOME ZONE (scanline $2C = 44)
239- ; ═══════════════════════════════════════════════════════════════
361+ ; ROW 1: HOME ZONE
240362 dc.w $2c07,$fffe
241363 dc.w COLOR00,COLOUR_HOME
242- dc.w $3407,$fffe ; Highlight stripe in home zone
364+ dc.w $3407,$fffe
243365 dc.w COLOR00,COLOUR_HOME_LIT
244366 dc.w $3807,$fffe
245367 dc.w COLOR00,COLOUR_HOME
246-
247- ; ═══════════════════════════════════════════════════════════════
248- ; ROWS 2-6: WATER ZONE (5 lanes)
249- ; ═══════════════════════════════════════════════════════════════
250- ; Alternating blue shades suggest rippling water
251368
252- ; Row 2: Water lane 1 (scanline $3C = 60)
369+ ; ROWS 2-6: WATER ZONE
253370 dc.w $3c07,$fffe
254371 dc.w COLOR00,COLOUR_WATER1
255-
256- ; Row 3: Water lane 2 (scanline $4C = 76)
257372 dc.w $4c07,$fffe
258373 dc.w COLOR00,COLOUR_WATER2
259-
260- ; Row 4: Water lane 3 (scanline $5C = 92)
261374 dc.w $5c07,$fffe
262375 dc.w COLOR00,COLOUR_WATER1
263-
264- ; Row 5: Water lane 4 (scanline $6C = 108)
265376 dc.w $6c07,$fffe
266377 dc.w COLOR00,COLOUR_WATER2
267-
268- ; Row 6: Water lane 5 (scanline $7C = 124)
269378 dc.w $7c07,$fffe
270379 dc.w COLOR00,COLOUR_WATER1
271380
272- ; ═══════════════════════════════════════════════════════════════
273- ; ROW 7: MEDIAN - SAFE ZONE (scanline $8C = 140)
274- ; ═══════════════════════════════════════════════════════════════
381+ ; ROW 7: MEDIAN
275382 dc.w $8c07,$fffe
276383 dc.w COLOR00,COLOUR_MEDIAN
277-
278- ; ═══════════════════════════════════════════════════════════════
279- ; ROWS 8-12: ROAD ZONE (5 lanes)
280- ; ═══════════════════════════════════════════════════════════════
281- ; Alternating grey shades suggest lane markings
282384
283- ; Row 8: Road lane 1 (scanline $9C = 156)
385+ ; ROWS 8-12: ROAD ZONE
284386 dc.w $9c07,$fffe
285387 dc.w COLOR00,COLOUR_ROAD1
286-
287- ; Row 9: Road lane 2 (scanline $AC = 172)
288388 dc.w $ac07,$fffe
289389 dc.w COLOR00,COLOUR_ROAD2
290-
291- ; Row 10: Road lane 3 (scanline $BC = 188)
292390 dc.w $bc07,$fffe
293391 dc.w COLOR00,COLOUR_ROAD1
294-
295- ; Row 11: Road lane 4 (scanline $CC = 204)
296392 dc.w $cc07,$fffe
297393 dc.w COLOR00,COLOUR_ROAD2
298-
299- ; Row 12: Road lane 5 (scanline $DC = 220)
300394 dc.w $dc07,$fffe
301395 dc.w COLOR00,COLOUR_ROAD1
302396
303- ; ═══════════════════════════════════════════════════════════════
304- ; ROW 13: START ZONE (scanline $EC = 236)
305- ; ═══════════════════════════════════════════════════════════════
397+ ; ROW 13: START ZONE
306398 dc.w $ec07,$fffe
307399 dc.w COLOR00,COLOUR_START
308- dc.w $f407,$fffe ; Highlight stripe in start zone
400+ dc.w $f407,$fffe
309401 dc.w COLOR00,COLOUR_START_LIT
310402 dc.w $f807,$fffe
311403 dc.w COLOR00,COLOUR_START
312404
313- ; ═══════════════════════════════════════════════════════════════
314405 ; BOTTOM BORDER
315- ; ═══════════════════════════════════════════════════════════════
316406 dc.w $fc07,$fffe
317407 dc.w COLOR00,COLOUR_BLACK
318408
319- ; End of copper list
320409 dc.w $ffff,$fffe
321410
322411 ;══════════════════════════════════════════════════════════════════════════════
...
325414
326415 even
327416 frog_data:
328- dc.w $dc50,$ec00 ; Control words (Y=220, X=160)
417+ dc.w $ec50,$fc00 ; Control words
329418
330- ; 16 lines of image data
331- dc.w $0000,$0000 ; ................
332- dc.w $07e0,$0000 ; .....######.....
333- dc.w $1ff8,$0420 ; ...##########...
334- dc.w $3ffc,$0a50 ; ..############..
335- dc.w $7ffe,$1248 ; .##############.
336- dc.w $7ffe,$1008 ; .##############.
337- dc.w $ffff,$2004 ; ################
338- dc.w $ffff,$0000 ; ################
339- dc.w $ffff,$0000 ; ################
340- dc.w $7ffe,$2004 ; .##############.
341- dc.w $7ffe,$1008 ; .##############.
342- dc.w $3ffc,$0810 ; ..############..
343- dc.w $1ff8,$0420 ; ...##########...
344- dc.w $07e0,$0000 ; .....######.....
345- dc.w $0000,$0000 ; ................
346- dc.w $0000,$0000 ; ................
419+ dc.w $0000,$0000
420+ dc.w $07e0,$0000
421+ dc.w $1ff8,$0420
422+ dc.w $3ffc,$0a50
423+ dc.w $7ffe,$1248
424+ dc.w $7ffe,$1008
425+ dc.w $ffff,$2004
426+ dc.w $ffff,$0000
427+ dc.w $ffff,$0000
428+ dc.w $7ffe,$2004
429+ dc.w $7ffe,$1008
430+ dc.w $3ffc,$0810
431+ dc.w $1ff8,$0420
432+ dc.w $07e0,$0000
433+ dc.w $0000,$0000
434+ dc.w $0000,$0000
347435
348- dc.w $0000,$0000 ; End marker
436+ dc.w $0000,$0000
349437