Lab 3 The Worm Game
Lab 3 - 6502 Assembly: The Worm Game Adventure
For this lab, I decided to take a leap into creating a simple but super fun Worm Game in 6502 Assembly language. And let me tell you – this was by far the most fun lab I've worked on so far! Not only did I get to learn more about how Assembly interacts with both text and graphic screens, but I also had to dig into game mechanics like collision detection, random fruit generation, and managing user input, all with very low-level code.
The Game Plan
The goal was to create a basic version of the classic "Worm" game, which starts with a small worm moving around the screen. The player controls the worm with the keyboard (W, A, S, D to move up, left, down, and right) and tries to eat randomly appearing fruits, which makes the worm grow longer. The challenge? Avoiding crashing into walls or its own body!
Key Features:
Moving the Worm: Control the worm's movement using the keyboard (WASD keys).
Growing the Worm: Every time the worm eats a piece of fruit, it grows in size.
Avoiding Collisions: The worm must not crash into itself or the edges of the screen.
Random Fruit Placement: Fruits appear randomly on the screen, and the worm must eat them to grow.
Increasing Speed: As the worm grows, the game speed increases, making things more challenging.
Challenges and Fixes
One of the trickiest parts of this project was figuring out the direction reversal bug. Initially, when I tried to reverse direction (like moving right after moving left), the worm wouldn’t move at all. The problem was in the direction-checking logic. The code was designed to prevent moving directly opposite to the current direction, but this also meant the worm got stuck if I made a mistake.
Fix:
I made it so that when an invalid move was made (like trying to move right after left), the worm would continue in the last valid direction rather than freezing. This small tweak made a huge difference in gameplay!
Here's the code adjustment:
rightKey:
lda #movingLeft
bit movementDirection
bne illegalMove
This change allowed the worm to continue moving in the previous direction on invalid inputs, rather than stopping completely.
Reflection: The Fun Factor
Designing the Worm game in Assembly was an absolute blast! It pushed my skills in managing memory, handling user input, and dealing with low-level graphics. I had to figure out how to update the worm’s position on the screen, deal with fruit placement, and even adjust the game speed as the worm grew longer. The interaction between the text and graphic screens was a great exercise in controlling multiple outputs in a limited environment.
Debugging Assembly was brutal at times — one wrong instruction and the whole game could break. I spent a lot of time isolating issues, checking memory addresses, and testing changes in the emulator. The process felt incredibly rewarding, especially when everything finally clicked, and the game worked as expected. A big part of this was learning to efficiently manage memory and how the game loop ties all of these small instructions together.
I can definitely say this was the most enjoyable lab yet, and I’m excited to continue exploring the world of low-level programming!
The Final Code:
define fruitLocationLow $00 ; screen location of fruit, low byte define fruitLocationHigh $01 ; screen location of fruit, high byte define headPositionLow $10 ; screen location of worm head, low byte define headPositionHigh $11 ; screen location of worm head, high byte define bodyStart $12 ; start of worm body byte pairs define movementDirection $02 ; direction (possible values are below) define wormSize $03 ; worm length, in bytes define fruitColor $04 ; define wormSegments $0900 ; define gameSpeedCycles $05 ; ; page 0 (@ 0000) used for worm locations ; page 1 (@ 0100) used for stack ; page 2-5 screen ; page 6-? program opcodes. ; thus, use page @0900 -> page 9 for storing worm colors. Not using zero page incurs many cycles for drawing the worm at every loop. ; set worm body colors lda #$00 sta wormSegments ; trail color lda #$05 ldx #$02 sta wormSegments,x ; tail color ldx #$20 ; set game speed. stx gameSpeedCycles ; ; note: When gameSpeedCycles reaches 0, game cannot run any faster. Game starts to slow down at this point. ; Directions (each using a separate bit) define movingUp 1 define movingRight 2 define movingDown 4 define movingLeft 8 ; ASCII values of keys controlling the worm define ASCII_w $77 define ASCII_a $61 define ASCII_s $73 define ASCII_d $64 ; System variables define sysRandom $fe define sysLastKey $ff jsr initialize jsr gameLoop initialize: jsr setupWorm jsr createFruitPosition rts setupWorm: lda #movingRight ;start direction sta movementDirection lda #4 ;start length (2 segments) sta wormSize lda #$11 sta headPositionLow lda #$10 sta bodyStart lda #$0f sta $14 ; body segment 1 lda #$04 sta headPositionHigh sta $13 ; body segment 1 sta $15 ; body segment 2 rts createFruitPosition: ;load a new random byte into $00 lda sysRandom and #$0f ; discard high bit cmp #$00 ; dissallow black beq addOne cmp #$05 ; dissallow green beq addOne bne storeColor addOne: adc #$01 storeColor: sta fruitColor lda sysRandom sta fruitLocationLow ;load a new random number from 2 to 5 into $01 lda sysRandom and #$03 ;mask out lowest 2 bits clc adc #2 sta fruitLocationHigh rts gameLoop: jsr processInput jsr detectCollisions jsr updateWormPosition jsr renderFruit jsr renderWorm jsr controlGameSpeed jmp gameLoop processInput: lda sysLastKey cmp #ASCII_w beq upKey cmp #ASCII_d beq rightKey cmp #ASCII_s beq downKey cmp #ASCII_a beq leftKey rts upKey: lda #movingDown bit movementDirection bne illegalMove lda #movingUp sta movementDirection rts rightKey: lda #movingLeft bit movementDirection bne illegalMove lda #movingRight sta movementDirection rts downKey: lda #movingUp bit movementDirection bne illegalMove lda #movingDown sta movementDirection rts leftKey: lda #movingRight bit movementDirection bne illegalMove lda #movingLeft sta movementDirection rts illegalMove: rts detectCollisions: jsr checkFruitCollision jsr checkWormCollision rts checkFruitCollision: lda fruitLocationLow cmp headPositionLow bne doneCheckingFruitCollision lda fruitLocationHigh cmp headPositionHigh bne doneCheckingFruitCollision ;eat fruit lda gameSpeedCycles cmp #$00 beq maxSpeedReached dec gameSpeedCycles ; remove ~24 wasted cycles maxSpeedReached: ; cannot decrease beyond 0 ldy wormSize lda fruitColor sta wormSegments,Y inc wormSize inc wormSize ;increase length jsr createFruitPosition doneCheckingFruitCollision: rts checkWormCollision: ldx #2 ;start with second segment wormCollisionLoop: lda headPositionLow,x cmp headPositionLow bne continueCollisionLoop maybeCollided: lda headPositionHigh,x cmp headPositionHigh beq didCollide continueCollisionLoop: inx inx cpx wormSize ;got to last section with no collision beq didntCollide jmp wormCollisionLoop didCollide: jmp gameOver didntCollide: rts updateWormPosition: ldx wormSize dex txa updateloop: lda headPositionLow,x sta bodyStart,x dex bpl updateloop lda movementDirection lsr bcs up lsr bcs right lsr bcs down lsr bcs left up: lda headPositionLow sec sbc #$20 sta headPositionLow bcc upup rts upup: dec headPositionHigh lda #$1 cmp headPositionHigh beq collision rts right: inc headPositionLow lda #$1f bit headPositionLow beq collision rts down: lda headPositionLow clc adc #$20 sta headPositionLow bcs downdown rts downdown: inc headPositionHigh lda #$6 cmp headPositionHigh beq collision rts left: dec headPositionLow lda headPositionLow and #$1f cmp #$1f beq collision rts collision: jmp gameOver renderFruit: ldy #0 lda fruitColor ; sysRandom sta (fruitLocationLow),y sta fruitColor rts renderWorm: ldx #0 lda #5 ; green sta (headPositionLow,x) ; paint head ;paint body and trail. ldx wormSize ldy #$00 drawBelly: lda wormSegments,y ; 5 cycles sta (headPositionLow,x) ; 6 cycles dex ; 2 cycles dex ; ... iny ; ... iny ; ... cpx #$00 ; 2 cycles bne drawBelly ; 3 cycles ; -1 if branch fails ; $0766 d0 f3 BNE $075b -> no page crossing ; total ~24 cycles per segment rts controlGameSpeed: ldx gameSpeedCycles beq noSpin ; don't spin if gameSpeedCycles = 0. spinCycle: ldy #$02 spinloop: nop ; nop ; nop ; nop ; 2*4 dey ; 2 bne spinloop ; 3 first time , 2 on exit ;2*4*2+2*2+3+2 = 25 cycles. dex bne spinCycle noSpin: rts gameOver:
Comments
Post a Comment