Lab 03 - 6502 Program Lab (Revised)

Worm Game

In this lab, I coded a game that would work within the 6520 Emulator while meeting specific criteria, including output to both the character and graphic screens, accepting keyboard input, and utilizing mathematical operations like addition and subtraction. 


While building a classic worm game in Assembly, I ran into a few tricky errors that took some time to figure out. I want to share those mistakes and how I fixed them so you can avoid the same headaches. 


On one particularly long night of coding, I decided to take on the challenge of re-building a classic Worm game! This game would be simple yet engaging, utilizing both the character screen and graphic screen for a fun experience. I found that 

Specification

  1. The game starts with a worm moving across the screen, which is controlled using the keyboard.
  2. The worm body grows every time it "eats" a fruit, which is randomly placed on the screen.
  3. The user can control the worm's movement by pressing the W, A, S, or D keys on the keyboard, which correspond to moving the worm up, left, down, and right, respectively.
  4. The worm must avoid colliding with itself and the screen borders; if it does, the game ends.
  5. A fruit will appear randomly on the graphic screen, and the worm must eat it to grow longer.
  6. The game speed will gradually increase as the worm grows, adding a level of challenge.


 

Direction Reversal Bug

I noticed that if I tried to reverse direction (e.g., move right after moving left), the worm wouldn’t move. The culprit was in the processInput section. 

rightKey:

  lda #movingLeft

  bit movementDirection

  bne illegalMove

This checks if the current direction is the opposite of the intended direction and prevents the change — which is good. But it also meant the worm stayed stuck if I made a mistake. I realized I needed a smoother way to handle invalid moves.

Instead of leaving the worm stuck on an invalid move, I modified illegalMove to keep the worm moving in the last valid direction.

illegalMove:

  rts ; Keep moving in the current direction

Now, if I accidentally hit the wrong key, the worm doesn’t freeze — it keeps going.

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:
     Reflection

    Designing a Worm game with graphical output on the 6520 Emulator presented unique challenges. I had to manage the movement of the worm, detect collisions, and handle the fruit collection logic, all while utilizing low-level assembly language. The graphics screen proved to be tricky, as I had to carefully manipulate the pixels to draw the worm's body and fruit. It was rewarding to see how I could efficiently use memory locations and perform operations such as addition and subtraction to handle game logic. Additionally, controlling the game speed and adjusting it as the worm grew was a fun challenge, adding an extra layer of complexity to the gameplay. This lab helped me strengthen my skills in assembly, particularly with input/output operations and the interaction between the character and graphic screens. I also learned how to structure a game loop and manage game states effectively, which will be valuable in future projects. Debugging Assembly games can feel brutal one wrong instruction, and everything falls apart. But breaking down the problem, one piece at a time, helped me understand what was going wrong and build better solutions.

    Comments

    Popular posts from this blog

    Project Stage 1

    Lab05 - x86_64