A Simple Maze Game using 6502 Emulator Part 2

seungwooji

Seung Woo (Paul) Ji

Posted on February 14, 2022

A Simple Maze Game using 6502 Emulator Part 2

Introduction

In the last post, we created a simple 6502 assembly code that generates a maze for a player to explore. Today, we are going to build codes on top of it in order to implement the rest of objectives.

Objectives

For this game, we need to accomplish 3 more objectives as following:

1. The game must draw the maze in the bitmapped screen. (Done!)

  1. A player must be able to use the keyboard to control.
  2. A player must find a route to reach to the goal within the maze in order to win the game.
  3. A player cannot goes through the wall.

Code

; zero-page variables
define  ROW     $20 ; current row
define  COL     $21 ; current column
define  DRAWN_ROW   $22 ; number of drawn rows
define  MAZE_L      $14 ; a pointer that points to where the maze will 
define  MAZE_H      $15 ; be drawn
define  PLAYER_L    $10 ; a pointer that points to the player in the 
                ; screen
define  PLAYER_H    $11
define  TARGET_L    $12 ; a pointer that points to the target position
define  TARGET_H    $13 ; where the player wants to proceed

; constants
define  PATH        $03 ; path color
define  PLAYER      $0e ; player color
define  HEIGHT      7   ; height of the maze 
define  WIDTH       7   ; width of the maze

; ROM routine
define  SCINIT      $ff81 ; initialize/clear screen

        jsr printHelp
        jsr drawMaze
        jsr gameInit
        jsr gameLoop

printHelp:  ldy #$00    ; print instructions on the screen
pHelpLoop:  lda help,y
        beq done
        sta $f000,y
        iny
        bne pHelpLoop

gameInit:   lda #$01    ; initialize ROW, COL to make the player 
        sta ROW     ; starting at $0221 of the screen
        sta COL
        rts

gameLoop:   jsr updatePosition
        jsr getkey
        jsr checkCollision
        ldx #$00    ; clear out the key buffer
        stx $ff
        jmp gameLoop

updatePosition: ldy ROW     ; load PLAYER pointer with ROW 
        lda table_low,y
        sta PLAYER_L
        lda table_high,y
        sta PLAYER_H

        ldy COL     ; place the player at (POINTER + COL)
        lda #PLAYER
        sta (PLAYER_L),y
        rts

getkey:     lda $ff     ; get the input key

        cmp #$80    ; allow arrow keys only
        bmi getkey
        cmp #$84
        bpl getkey

        pha     ; save the accumulator
        lda #PATH   ; set color of the current position to PATH
        sta (PLAYER_L),y
        pla     ; restore accumulator

        cmp #$80    ; check key is up
        bne checkRight

        dec ROW     ; ... if yes, decrement ROW
        rts

checkRight: cmp #$81    ; check if key is right
        bne checkDown
        inc COL     ; ... if yes, increment COL
        rts

checkDown:  cmp #$82    ; check if key is down
        bne checkLeft
        inc ROW     ; ... if yes, increment ROW
        rts

checkLeft:  cmp #$83    ; check if key is left
        bne done
        dec COL     ; ... if yes, decrement COL
        rts

done:       rts     ; break out of a loop or subroutine

checkCollision: ldy ROW     ; load TARGET pointer with ROW 
        lda table_low,y
        sta TARGET_L
        lda table_high,y
        sta TARGET_H

        ldy COL     ; load the color from the target
        lda (TARGET_L),y; at (POINTER + COL)

        cmp #$01
        beq done
        cmp #$03
        beq done
        cmp #$0a
        beq gameComplete

        lda #$00
        sta (TARGET_L),y

        lda $ff
        cmp #$80    ; if input key was up...
        bne ifRight

        inc ROW     ; ... if yes, increment ROW
        rts

ifRight:    cmp #$81    ; if input key was right...
        bne ifDown

        dec COL     ; ... if yes, decrement COL
        rts

ifDown:     cmp #$82    ; if input key was down...
        bne ifLeft

        dec ROW     ; ... if yes, decrement ROW
        rts

ifLeft:     cmp #$83    ; if input key was left...
        bne done

        inc COL     ; ... if yes, increment COL
        rts

gameComplete:   jsr SCINIT
        ldy #$00    ; print game completion message on the screen 
pGameComplete:  lda complete,y
        beq done
        sta $f000,y
        iny
        bne pGameComplete
        brk

drawMaze:   lda #$21    ; a pointer pointing to the first pixel
        sta MAZE_L  ; of the screen
        lda #$02
        sta MAZE_H

        lda #$00    ; number of drawn rows
        sta DRAWN_ROW

        ldx #$00    ; maze data index
        ldy #$00    ; column index

draw:       lda maze_data,x
        sta (MAZE_L), y
        inx
        iny
        cpy #WIDTH  ; compare with the number of WIDTH
        bne draw    ; if not, keep drawing the column

        inc DRAWN_ROW   ; increment the number of row
        lda #HEIGHT
        cmp DRAWN_ROW   ; compare with the number of HEIGHT
        beq done

        lda MAZE_L
        clc
        adc #$20    ; add 32(0x0020) to increment the row
        sta MAZE_L  ; of the pixel
        lda MAZE_H
        adc #$00
        sta MAZE_H

        ldy #$00    ; reset the column index for the new row
        beq draw            

; help text message
help:
dcb "P","l","a","y",32,"w","i","t","h",32,"a","r","r","o","w"
dcb 32,"k","e","y","s",32,"t","o",32,"c","o","n","t","r","o","l",10
dcb 00

; game complete message
complete:
dcb "Y","o","u",32,"b","e","a","t",32
dcb "t","h","e",32,"g","a","m","e","!"
dcb 00

; maze map data
maze_data:
dcb 01,00,01,00,01,01,01
dcb 01,01,01,00,00,00,01
dcb 00,00,01,00,01,00,01
dcb 01,00,01,00,01,01,01
dcb 01,00,01,00,01,00,01
dcb 01,00,01,00,01,00,01
dcb 01,01,01,01,01,00,10

; these two tables contain the high and low bytes
; of the addresses of the start of each row
table_high:
dcb $02,$02,$02,$02,$02,$02,$02,$02
dcb $03,$03,$03,$03,$03,$03,$03,$03
dcb $04,$04,$04,$04,$04,$04,$04,$04
dcb $05,$05,$05,$05,$05,$05,$05,$05

table_low:
dcb $00,$20,$40,$60,$80,$a0,$c0,$e0
dcb $00,$20,$40,$60,$80,$a0,$c0,$e0
dcb $00,$20,$40,$60,$80,$a0,$c0,$e0
dcb $00,$20,$40,$60,$80,$a0,$c0,$e0
Enter fullscreen mode Exit fullscreen mode

game_screen

Let's walk through the code together. First of all, we print out the helpful instruction on the text screen with printHelp subroutine. Then, we draw a maze using the drawMaze subroutine we created from the last post. Having a maze for a player to explore on the screen, we need to first set the game state with the initial player position on the screen $#0221. After that, we call the subroutine called gameLoop which constantly loops itself.
text_help_message

The gameLoop itself consists of a number of subroutines. The first one is updatePosition. This subroutine loads the player pointer with the given the row and column information so that we can place the player on the screen. Afterwards, we call the getKey subroutine to receive the player input from the keyboard. We limit the keyboard input by only accepting arrow keystrokes. Once we receive a key input, we update the number of column and row accordingly. Then, we check if the position the player wants to move is a wall by using the checkCollision subroutine. If the player hits by the wall, we simply retract the move.
game_screen_2

Once reaching to the goal, the screen will congratulate the player with the text message.
game_screen_3n
text_game_completed

Conclusion

Making a simple maze using the 6502 assembly language definitely is harder and more time-consuming as compared to other high-level languages. The game we explored together is also not polished and needs a lot of improvements as well (such as having an alert when the player wants to proceed into the wall). we could use. Yet, this experience gives us a very meaningful insight as to how the game really works under the hood.

💖 💪 🙅 🚩
seungwooji
Seung Woo (Paul) Ji

Posted on February 14, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related