Introduction
Welcome to Lab 3! 😄
In this lab, I will be creating a simple program in the 6502 assembly language. Before I tell you what program I am writing, there are some requirements:
- Must work in 6502 Emulator
- Must output to character screen and graphics screen
- Must input from user keyboard
- Must use arithmetic operations
Fulfilling these requirements, the program I am writing is a random number guessing game!
How the game plays: the player tries to guess a random number between 1 and 9. They get 5 tries, and the outcome is a bitmap display filled with a color (green for a win, red for a loss).
✅ The source code is provided at the end of this blog post after the whole thing has been walked through!
Let's start!
📌 Setup & ROM Routines
Before we get into the main game loop, I need to define a few ROM routines. These routines will handle things like screen clearing, keyboard input, and text output.
I also allocate zero page variables for fast access.
; ROM routines entry points
define SCINIT $ff81 ; initialize/clear screen
define CHRIN $ffcf ; input character from keyboard
define CHROUT $ffd2 ; output character to screen
; Zero Page Variables
define TEXT_PTR $40 ; pointer for text (low byte)
define TEXT_PTR_H $41 ; pointer for text (high byte)
define GUESS $42 ; user guess
define ATTEMPTS $43 ; remaining attempts (5 tries)
define ANSWER $45 ; correct answer (ASCII value)
* = $0600 ; Program starts here
JSR SCINIT ; Clear screen
📌 Random Number Generation
Let's start with this part as I want to make sure the random number is generating properly before continuing on.
We know that 6502
does not have true randomness. So we use pseudo-random behavior from system memory. I used the value at $FE
(a common pseudo-random location), but filtered it to keep only ASCII '1' to '9'.
RANDOM:
getRandom:
LDA $FE ; Load a value from memory address $FE
CMP #$31 ; Compare with ASCII '1' (value = $31)
BCC getRandom ; If it's less than '1', try again
CMP #$3A ; Compare with ASCII ':' (value = $3A, one past '9')
BCS getRandom ; If it's '9' or more, try again
RTS ; Return with a valid value in A
It's quite simple. We compare the $FE
value so that it can be only between 1 and 9.
This loop continues until value is between $31
and $39
, which are the ASCII values for '1' to '9'.
Then I use it in the main code (under that JSR SCINIT
line):
JSR RANDOM
STA ANSWER
📌 Welcome Message!
While we're at the main code, let's set the stage for the game. I want to print a nice welcome message and give the player 5 tries. I also reused a simple PRINT
subroutine that walks over null-terminated strings.
I want to show something like:
SELECT A NUMBER ( 1 - 9 )
YOU HAVE 5 CHANCES!
This should be straightforward. Please feel free to read the comments to see what it does in details.
Referenced from SPO600: Place a Message on the Character Display
LDA #$05 ; Load 5 into A (the number of tries)
STA ATTEMPTS ; Store it in ATTEMPTS variable
LDA #TITLE ; Load the high byte of TITLE address
STA TEXT_PTR_H ; Store it in TEXT_PTR_H (high part of pointer)
LDY #$00 ; Set index Y = 0
JSR PRINT ; Call the PRINT subroutine to display the message
PRINT:
LDA (TEXT_PTR), Y ; Load byte at address (TEXT_PTR + Y)
BEQ DONE ; If byte = 0 (null terminator), end of string
JSR CHROUT ; Otherwise, print the character
INY ; Move to next character
BNE PRINT ; Loop until we hit null (BNE is always true unless Y wraps)
DONE:
RTS
TITLE:
DCB "S","E","L","E","C","T",32,"A",32,"N","U","M","B","E","R",32
DCB "(",32,"1",32,"-",32,"9",32,")",13
DCB "Y","O","U",32,"H","A","V","E",32,"5",32,"C","H","A","N","C","E","S","!",0
At this point, if you run the code, you should get something like this:
📌 The Main Game Loop
Now let's finish defining the main loop: it prints the input prompt, takes input, validates it, then compares it to the answer.
Printing the input prompt is basically the same as printing the title:
START:
LDA #INPUT
STA TEXT_PTR_H
LDY #$00
JSR PRINT
INPUT:
DCB 13,13
DCB "E","N","T","E","R",32,"A",32,"N","U","M","B","E","R",32
DCB "(",32,"1",32,"-",32,"9",32,")",":",32,0
After that, wait for input and then validate it.
WAIT_INPUT:
JSR CHRIN ; Wait for player keypress (returns ASCII in A)
CMP #$31 ; Compare with ASCII '1'
BMI WAIT_INPUT ; If below, invalid → wait again
CMP #$3A ; Compare with ASCII ':'
BPL WAIT_INPUT ; If 9 or above (':'), invalid → wait again
STA GUESS ; Save valid guess to GUESS variable
JSR CHROUT ; Echo the guess back to screen (user feedback)
CMP ANSWER ; Compare guess with the correct answer
BEQ WIN ; If equal → player wins!
BCC HIGHER ; If less → guess is lower → suggest to go higher
BCS LOWER ; If more → guess is higher → suggest to go lower
We validate the input to prevent invalid characters. If it's a match, we jump to WIN
. Otherwise, we give feedback.
📌 Feedback: Higher or Lower
You might have noticed HIGHER
and LOWER
when validating user input. It's basically just printing to the screen whether the number needs to be higher or not.
The condition is already done in the main loop. So just define the text and the printing logic that we have used already before:
HIGHER_TEXT:
DCB 13,"I","T","'", "S",32,"H","I","G","H","E","R",0
LOWER_TEXT:
DCB 13,"I","T","'", "S",32,"L","O","W","E","R",0
HIGHER:
LDA #HIGHER_TEXT
STA TEXT_PTR_H
LDY #$00
JSR PRINT
JMP UPDATE_ATTEMPTS
LOWER:
LDA #LOWER_TEXT
STA TEXT_PTR_H
LDY #$00
JSR PRINT
JMP UPDATE_ATTEMPTS
📌 Track Attempts
Now, what else is left? We need to track the user's tries. After every wrong guess, we decrement the number of attempts and decide whether to continue or end the game.
We just reuse START
if they still have guesses left.
UPDATE_ATTEMPTS:
DEC ATTEMPTS
LDA ATTEMPTS
BEQ LOSE ; Defining this below
JMP START
📌 Lose State
Okay, almost done. Let's define what happens when a player loses. The game shows a message, reveals the correct answer, then fills the screen with red.
Printing is the same as always. I want to also show what the answer was:
LOSE:
LDA #LOSE_TEXT
STA TEXT_PTR_H
LDY #$00
JSR PRINT
LDA #ANSWER_TEXT
STA TEXT_PTR_H
LDY #$00
JSR PRINT
LDA ANSWER
JSR CHROUT
LOSE_TEXT:
DCB 13,"Y","O","U",32,"L","O","S","T",32,"T","H","E",32,"G","A","M","E",0
ANSWER_TEXT:
DCB 13,"T","H","E",32,"N","U","M","B","E","R",32,"W","A","S",32,0
And then, call the subroutine to fill the screen with red, after which you can jump to game over.
JSR FILL_RED
JMP GAME_OVER
Let's define FILL_RED
, which is the function that fills the bitmap display with red when the player loses.
FILL_RED:
LDA #$00
STA $10 ; Low byte of screen memory pointer
LDA #$02
STA $11 ; High byte → $0200 = screen start
LDX #$06 ; Fill up to $0600
LDY #$00
LDA #$02 ; Red color code (system-dependent)
red_loop:
STA ($10), y ; Store red color at screen location
INY ; Next byte
BNE red_loop ; If Y didn't wrap → loop again
INC $11 ; Move to next screen row (high byte of address)
LDY #$00 ; Reset Y
CPX $11 ; Compare to target memory limit
BNE red_loop ; Keep going until we hit limit
RTS
📌 Win State
Printing to text display:
WIN_TEXT:
DCB 13,"Y","O","U",32,"W","I","N","!",32,"I","T","'", "S",32,0
WIN:
LDA #WIN_TEXT
STA TEXT_PTR_H
LDY #$00
JSR PRINT
LDA ANSWER
JSR CHROUT
JSR FILL_GREEN
Green fill function:
FILL_GREEN:
LDA #$00
STA $10
LDA #$02
STA $11
LDX #$06
LDY #$00
LDA #$0D
green_loop:
STA ($10), y
INY
BNE green_loop
INC $11
LDY #$00
CPX $11
BNE green_loop
RTS
📌 Game Reset
After a win or loss, wait for a key press, then restart the game from the beginning:
GAME_OVER:
JSR CHRIN
JMP $0600
✅ SOURCE CODE ✅
; Number Guessing Game for 6502
; Guess the correct number (1-9). The answer is randomly generated each game.
; On win, the display fills with green; on loss, it fills with red.
; You get 5 tries to guess the number.
; ROM routines entry points
define SCINIT $ff81 ; initialize/clear screen
define CHRIN $ffcf ; input character from keyboard
define CHROUT $ffd2 ; output character to screen
; Zero Page Variables
define TEXT_PTR $40 ; pointer for text (low byte)
define TEXT_PTR_H $41 ; pointer for text (high byte)
define GUESS $42 ; user guess
define ATTEMPTS $43 ; remaining attempts (5 tries)
define ANSWER $45 ; correct answer (ASCII value)
* = $0600 ; Program starts here
JSR SCINIT ; Clear screen
; Generate random answer (1-9) using hardware randomness.
; The RANDOM subroutine returns an ASCII digit ('1'-'9') directly.
JSR RANDOM
STA ANSWER ; Store the ASCII value
LDA #$05 ; Set 5 attempts
STA ATTEMPTS
; Print the title
LDA #TITLE
STA TEXT_PTR_H
LDY #$00
JSR PRINT
START:
; Print the input prompt
LDA #INPUT
STA TEXT_PTR_H
LDY #$00
JSR PRINT
WAIT_INPUT:
JSR CHRIN ; Wait for user input (result in A)
CMP #$31 ; Must be between ASCII '1' and '9'
BMI WAIT_INPUT
CMP #$3A
BPL WAIT_INPUT
STA GUESS ; Store the user's guess
JSR CHROUT ; Echo the input on screen
; Compare the guess to the random answer
CMP ANSWER
BEQ WIN ; Correct guess
BCC HIGHER ; If guess < answer, show "Higher"
BCS LOWER ; If guess > answer, show "Lower"
HIGHER:
LDA #HIGHER_TEXT
STA TEXT_PTR_H
LDY #$00
JSR PRINT
JMP UPDATE_ATTEMPTS
LOWER:
LDA #LOWER_TEXT
STA TEXT_PTR_H
LDY #$00
JSR PRINT
JMP UPDATE_ATTEMPTS
UPDATE_ATTEMPTS:
DEC ATTEMPTS ; Decrease attempt count by 1
LDA ATTEMPTS
BEQ LOSE ; If no attempts left, game over
JMP START
LOSE:
; Print lose message
LDA #LOSE_TEXT
STA TEXT_PTR_H
LDY #$00
JSR PRINT
; Show the correct answer
LDA #ANSWER_TEXT
STA TEXT_PTR_H
LDY #$00
JSR PRINT
LDA ANSWER
JSR CHROUT ; Print the answer
JSR FILL_RED ; Fill display with red
JMP GAME_OVER
WIN:
; Print win message
LDA #WIN_TEXT
STA TEXT_PTR_H
LDY #$00
JSR PRINT
; Show the correct answer
LDA ANSWER
JSR CHROUT ; Print the answer
JSR FILL_GREEN ; Fill display with green
GAME_OVER:
JSR CHRIN ; Wait for any key press
JMP $0600 ; Restart the game (generates new random answer)
; ---------------------------------------------------
; Subroutine RANDOM
; Generates a pseudo-random number using hardware randomness.
; Reads from memory location $FE until a valid candidate is found.
; Returns a valid ASCII digit in the range '1' to '9' (i.e., $31 to $39) in A.
RANDOM:
getRandom:
LDA $FE ; Get a pseudo-random candidate from hardware RNG
CMP #$31 ; Check if candidate is >= '1' (ASCII $31)
BCC getRandom ; If candidate is less than '1', try again
CMP #$3A ; Check if candidate is less than ':' (ASCII $3A)
BCS getRandom ; If candidate is not less than $3A, try again
RTS ; Return valid candidate in A
; ---------------------------------------------------
; Subroutine PRINT
; Prints a null-terminated string pointed to by TEXT_PTR.
PRINT:
LDA (TEXT_PTR), Y
BEQ DONE ; Zero terminator reached; stop printing
JSR CHROUT ; Output character
INY
BNE PRINT
DONE:
RTS
; ---------------------------------------------------
; Subroutine FILL_GREEN
; Fills the display with green (color code $0D).
FILL_GREEN:
LDA #$00
STA $10 ; Set pointer low byte to 0
LDA #$02
STA $11 ; Set pointer high byte to 2 (base address $0200)
LDX #$06 ; X = 6 (max for high-byte pointer)
LDY #$00 ; Reset column index (Y) to 0
LDA #$0D ; Green color code
green_loop:
STA ($10), y ; Write green to display at pointer + Y
INY ; Increment column index
BNE green_loop ; Loop until Y wraps (256 pixels)
INC $11 ; Move to next page (increment high-byte pointer)
LDY #$00 ; Reset Y for new page
CPX $11 ; Compare X with current pointer high byte
BNE green_loop ; Continue filling until done
RTS
; ---------------------------------------------------
; Subroutine FILL_RED
; Fills the display with red (color code $02).
FILL_RED:
LDA #$00
STA $10 ; Set pointer low byte to 0
LDA #$02
STA $11 ; Set pointer high byte to 2 (base address $0200)
LDX #$06 ; X = 6
LDY #$00 ; Reset Y to 0
LDA #$02 ; Red color code
red_loop:
STA ($10), y ; Write red to display at pointer + Y
INY ; Increment column index
BNE red_loop ; Loop until Y wraps (256 pixels)
INC $11 ; Move to next page (increment high-byte pointer)
LDY #$00 ; Reset Y for new page
CPX $11 ; Compare X with current pointer high byte
BNE red_loop ; Continue filling until finished
RTS
; ---------------------------------------------------
; Text Strings
TITLE:
DCB "G","U","E","S","S",32
DCB "A",32
DCB "N","U","M","B","E","R",32
DCB "B","E","T","W","E","E","N",32
DCB "1",32
DCB "T","O",32
DCB "9",13
DCB "Y","O","U",32
DCB "H","A","V","E",32
DCB "5",32
DCB "T","R","I","E","S","!",0
INPUT:
DCB 13,13
DCB "E","N","T","E","R",32
DCB "A",32
DCB "N","U","M","B","E","R",32
DCB "(", "1","-","9",")",":",32,0
HIGHER_TEXT:
DCB 13
DCB "H","I","G","H","E","R",".",".",".",0
LOWER_TEXT:
DCB 13
DCB "L","O","W","E","R",".",".",".",0
WIN_TEXT:
DCB 13
DCB "Y","O","U",32
DCB "W","I","N","!",32
DCB "T","H","E",32
DCB "A","N","S","W","E","R",32
DCB "I","S",32,0
LOSE_TEXT:
DCB 13
DCB "Y","O","U",32
DCB "L","O","S","T",".",32
DCB "G","A","M","E",32
DCB "O","V","E","R","!",0
ANSWER_TEXT:
DCB 13
DCB "T","H","E",32
DCB "A","N","S","W","E","R",32
DCB "W","A","S",32,0
Testing it
Let's run the whole game to make sure that it is running correctly.
On run:
Low number should prompt 'HIGHER...':
High number should prompt 'LOWER...' :
A clip of getting it correct:
A clip of getting it wrong:
As you can see the code is running as intended! 😄
Conclusion
Tackling this lab reminded me how brutal and rewarding the 6502 assembly can be. There's no high-level logic, so much so that even printing can take a while to figure out. You really learn to appreciate every instruction.
It was fun though to figure out the game loop. Making subroutine is extremely helpful in keeping everything neat. My challenge was in figuring out the logic for generating random numbers. It was so simple in retrospect but I had to change my mindset on another logic to achieve it.
I hope this breakdown helped you follow the logic and maybe even inspired you to make something of your own in assembly.
See you next time~ 👋