Hi there and welcome to a new part of this series. We are going to look at hi-scores!
Note
As usual, the code can be found on github. Warning! The code can be a bit unstable at the moment because the final bits are tweaked to get rid of the minor annoyances that still remain, and I am tweaking all kinds of stuff at this moment.
Here is a screenshot of the old Tetris code, as seen in VisAss in VICE, and the code being ported to kick assembler.
Lots of code follows! :) I decided to not add new all code to this post as it is already a very long one. The complete files can be found on GitHub as I said. I have posted the most interesting bits here and it already is quite a lot.
Hi-score Table
As Tetris is a score attack game it is essential that progress is saved. A hi-score table is mandatory for this type of game. It’s even better if it is saved and re-loaded as well. So let’s get to it.
The table itself is very simple. It is a data area, and each entry has a score length of 3 bytes (as we use decimal mode for scoring, we can have a score up to 999999) and then we have a name length of 7. Why? Seven bytes fit in the window that holds the score display. Here is the table definition:
.const TABLE_ENTRIES = 3
.const TABLE_NAMELENGTH = 7
.const TABLE_SCORELENGTH = 3
.const ENTRY_LENGTH = TABLE_NAMELENGTH + TABLE_SCORELENGTH
hiscore_table_start:
.fill ENTRY_LENGTH*TABLE_ENTRIES, 0
hiscore_table_end:
.fill ENTRY_LENGTH, 0
We add an extra entry at the back of the table to ensure we have a buffer when moving data. This might not be required, but at least it guarantees we do not overwrite any code or data we put behind it. As you might have guessed, this moving entries code might require a rewrite to ensure only valid data is moved. We’ll get to that later on, I don’t like loose ends. Loose ends in assembly code tend to have consequences!! :)
Initializing the table goes as follows: We go through the data area, and we add a default entry for as often as defined by TABLE_ENTRIES
:
Printing the table requires quite a few steps. We are re-using the level select screen when printing the hiscore table, so when that is done, we add the table information over the data in that screen. We only need to print the scores and the names, as the level select screen already has an empty table in it with an index.
We set the .x
and .y
registers to the correct screen location and call this routine:
Game Over And New Hiscore?
Looking back at the code from 23 years ago, I quite liked the approach. Instead of using sorting methods, like bubble sort, an entry comparison is done on all the bytes in each table entry.
Each byte is checked against the new score and the difference is logged in a corresponding flag. This flag is set to $ff
if the new score was lower, $00
if it was the same and $01
if it was higher. Depending on the byte checked a new hi-score is detected or rejected. If a new score is detected a new entry is inserted at the current location, else the next entry is tested:
Some data manipulation is required to insert a new entry in the list:
When this is all done we print the level select screen and we overwrite the level select text with a happy or sad message to indicate a new hiscore or a loser attempt:
Code to enter the name is also added, in the file controlled_input.asm
. It uses the kernal routine at $FFE4
(GETIN) to scan for keyboard entry and only the accepted characters are added to the name buffer:
The label input_done
is jumped to when the user presses the return key. A flag is set so the main program can exit this game mode, or keep waiting for characters. The complete code for the input routes can be found in controlled_input.asm
.
Pfew!!!!
Load and Save
Saving and loading data on the C64 is relatively easy. It is memory based, so you need to know which memory parts need to be written to the open device. The open device is #8 (the disk drive)
First, we need to open the device and set the file name to load:
Then we point to the memory address we want to load the data before calling the load KERNAL routine:
load_destination
must point to hiscore_table_start
.
Don’t forget to close the file afterwards. We need to do this as we might save several times during one play session.
Saving is done as loading, except we also have to set the end memory address. This is easy, as we labeled it in the source: data_start
should point to hiscore_table_start
and data_end
should point to hiscore_table_end
.
// set pointers to the memory block to save
lda #<data_start
sta startsave
lda #>data_start
sta startsave+1
// save up until to the end address
lda #<startsave
ldx #<data_end
ldy #>data_end
jsr SAVE
rts
Conclusion
Adding a hi-score to a game is not trivial. It requires quite a bit of code, certainly more than I expected when I started this addition. But it gives the game an important feature and it is appreciated by everyone who plays the game, we can be sure of that.
Next time we will start adding the final touches to the game. We will be adding more robust sound features, some colour options and tweaks to try and finalise the game.
Note
Remember: to view the full code —this post contains only excerpts— visit my github page linked at the top of this post.
Happy coding and see you later, in Tetris in 6502 - Part 14.