• they/he

a new site for me to be a furry?

icon credit: https://angiewolf.carrd.co/

header credit: https://fosbat.art/


my fursona gallery!!
toyhou.se/13995473.zanesona

rgmechex
@rgmechex

There are a couple things I didn't touch on in the Tetris video since it was already pretty lengthy. I got a lot of comments asking about the level that lasts for 800+ lines, so I figure I should explain how this happens.

Let this be a lesson to you to never mix your data types unless you really know what you're doing.

Level 255 in NES TetrisHow long could a level like this last anyway, the pieces fall so quickly!


As you probably know, you level up in NES Tetris every time you clear 10 lines. So given that you start on level 0 (we'll come back to starting at different levels later, you can start at up to level 19), when you hit 10 lines cleared you level up to level 1. When you hit 50 lines cleared you reach level 5. Simple enough.

You can presume then that if you take your current line count and divide it by 10, you'll get what level you are on. For example, at 48 lines cleared, you are currently on level 4. This is how it was intended to be, and for all casual players this is indeed how it works. It breaks down at level 219 however.

When you clear your 2190th line, you level up to level 219, but you won't get to level 220 until you clear your 3000th line. From there onward, your standard calculation of converting line count to level is always off by 80 levels.

Level 219 in NES TetrisGet used to this color scheme.

What causes this? It's an error due to mixing standard binary formatted numbers with binary-coded decimal numbers.


The current level number is stored as a standard unsigned 8-bit byte. This goes from 0 to 255. Not much to say about it!

The current line count is... weird. It's stored in binary-coded decimal. Well, the ones and tens digits are. Those two digits are stored in one 8-bit byte, taking up the lower and upper nybble respectively. This is similar to visual level display that shows up in the bottom right corner.

The hundreds digit is stored as a plain unsigned 8-bit byte however. BCD and standard binary are identical for the digits 0 thru 9, and bumping this digit up to 10 would require the player to clear 1000 lines, which would put them at level 100. You can see why the developers didn't bother to do anything about exceeding 999 lines. It's not handled for BCD calculations, and it isn't even capped. So getting 1000 lines will result in the counter displaying A00 lines.

Level 100 in NES TetrisNo this is not level 0 or level 20.

The digits 0 thru 9 use tile IDs of $00 through $09, so when you go past the numbers with $0A you overflow into all the letters. And eventually you cycle through all 256 different tile IDs if you get enough lines.


When the game checks if you should level up, it compares the level number against the line clear count. One is in (half) binary-coded decimal, while one is in standard binary. This doesn't work for all calculations, but it does work for smaller numbers. In the case of NES Tetris, it was considered "good enough" because again, you had to reach level 219 before this became an issue.

Let's take a look at the code:

; $9BA6: add lines to the line count
    LDX completedLines ; lines cleared with this piece
.completedLinesLoop:
    INC lineCount      ; add 1 to line count
    
    LDA lineCount      ; \ check if this overflowed
    AND #%00001111     ; | the ones digit
    CMP #$0A           ; |
    BMI .noOverflow    ; /
    
    LDA lineCount      ; \ if it did,
    CLC                ; | add 6 to carry to
    ADC #6             ; | the tens digit while
    STA lineCount      ; / restoring BCD
    AND #%11110000     ; \ check if this overflowed
    CMP #$A0           ; | the tens digit
    BCC .noOverflow    ; /
    
    LDA lineCount      ; \ if it did,
    AND #%00001111     ; | clear tens digit
    STA lineCount      ; | and increment
    INC lineCount+1    ; / hundreds digit

.noOverflow:
    LDA lineCount      ; \ if we hit a multiple
    AND #%00001111     ; | of ten lines,
    BNE .noLevelUp     ; | prepare to level up
    JMP .levelUp       ; /

This code takes the form of a loop--each line in the line clear (single, double, triple, tetris) is added one by one. This way we don't have to worry about "skipping over" a level up transition by going from, say, 28 lines to 32 lines with a tetris.

This part of the code works correctly. It's just a lot of work to deal with BCD values manually in software.

Since we are adding lines one by one, the carry to the hundreds digit could be simplified by just setting the tens + ones byte to zero instead of ANDing out the top nybble, but that's just a nitpick. You can also see why the hundreds digit ends up in the plain binary format, since all there is is a single INC instruction and no special handling for anything.

At this point the game can correctly detect that a level up should occur, but not exactly what level to level up to yet. It uses the line count to determine what the next level will be:

; $9BD0: determine what level to level up to, if any
.levelUp:
    LDA lineCount+1       ; \ copy the
    STA $A9               ; | line count to
    LDA linesCount        ; | a temporary
    STA $A8               ; / location
    
    LSR $A9               ; \ shift the line
    ROR $A8               ; | count to the right
    LSR $A9               ; | four times--
    ROR $A8               ; | this effectively
    LSR $A9               ; | counts as a division
    ROR $A8               ; | by ten
    LSR $A9               ; |
    ROR $A8               ; /
    
    LDA levelNumber       ; \ if the level number
    CMP $A8               ; | is lagging behind the
    BPL .noLevelUp        ; / line count, level up
    
    INC levelNumber       ; \ process leveling up
    LDA #SOUND_LEVEL_UP   ; | by incrementing
    STA soundEffect1Init  ; | level number, playing
    LDA renderFlags       ; | sound effect, and
    ORA #RENDER_LEVEL     ; | marking the level number
    STA renderFlags       ; / to be redrawn
    
.noLevelUp:
    DEX
    BNE .completedLinesLoop

This calculation requires some manipulation of the line counter, so it gets copied to a familiar pair of scratch memory locations for temporary holding (I wonder what the next article will be about?).

It then gets shifted to the right four places. This shifts out the ones nybble in the BCD byte and moves the tens nybble into the lower 4 bits. This nybble is still in BCD format! The hundreds+ byte gets shifted partially into the upper half of the remaining byte. This nybble is not in BCD format! This is where the problem lies.

The following comparison against the level number is expecting both numbers to be in the same format (I mean when would that ever be a bad thing). In fact, in theory this comparison is either comparing two equal numbers or two numbers that are off by one, since each line clear is added one at a time. (Either the level number matches the line count properly, or it is lagging behind by one--this is what indicates the level up.) This is why even though we are comparing two unsigned numbers, a BPL instruction would have been fine here (it's normally used for comparing signed bytes1).

However in practice, the level number and the line count end up drifting farther and farther away from each other over time. Eventually they become so far apart from one another, the comparison wraps around and the level number is determined to be greater than the line count for some time. Whenever the level number is greater than the line count, there is no level up. This persists until the comparison wraps around a second time to correct itself. This happens on level 219.

Now what the heck do I mean when I say "the comparison wraps around?" A comparison (via the CMP instruction) is just a subtraction in disguise. The exact result of this subtraction is not necessary, but certain aspects of it and how it was calculated are. If you subtract y from x and the result is negative, that means x < y. It's the result of this hidden subtraction that overflows. Suppose you subtract y from x and the result is negative, but is too big to fit in a signed 8-bit byte, so it overflows to being positive. Even though x < y, the comparision will tell you that x > y instead. That's what happens here.


Maybe some examples will help. Say we've got 68 lines cleared--that puts us on level 6. We get a double, so first the line count is incremented to 69. This isn't a multiple of 10 so we add the next one. The line count gets incremented to 70. This is a multiple of 10, so the game tries to figure out what level to level up to. 70 divided by 10 is 7. Our current level number of 6 is less than this intermediate figure--the difference between the two is -1. This means the level is incremented to 7.

Now say we've got 99 lines cleared--level 9. Just a single brings our line count to 100. 100 divided by 10 is 10. But remember, this is in our weird half BCD format. The value of $10 in BCD is actually 16 in standard decimal. Our current level number of 9 is less than this intermediate figure--the difference between the two is -7. This means the level is incremented to 10.

Notice how the difference between these two changed from -1 to -7. Every ten levels, this difference offsets by 6 due to the weird carry between the tens and hundreds digit of the BCD part of the line count.

Now we're at 1599 lines cleared--level 159. Line count increments to 1600. This is interesting because in the mixed BCD format, this is stored in memory as $1000. When this is divided by 10 by shifting it right 4 times, we get $100, which is truncated to zero. Now, since this is a BPL instruction, we are doing a signed comparison. So even though our level number of 159 is greater than our intermediate figure, when treated as a signed number, this is -97. This means the level is incremented to 160.

2189 lines cleared--level 218. Line count increments to 2190. Our intermediate value is $159 which is truncated to $59 or 89. The difference between the level number and this intermediate figure is 129, but treated as a signed byte this is -127. This means the level is incremented to 219.

And finally 2199 lines cleared--still on level 219. Line count increments to 2200. Intermediate value is $160 which is truncated to $60 or 96. The difference between the level number and this is 123. This is the first instance where this result is positive when treated as a signed 8-byte. Therefore, the level remains 219.

This resolves itself eventually after 810 lines cleared. Funny enough it happens on the transition from 2999 lines to 3000 lines--the rollover on the tens digit causes a jump of six for the line count. The intermediate value is $1E0 truncated to $E0 or 224. The level number 219 minus this 224 is -5 which marks the first recurring instance of this intermediate figure turning negative again, so the level increments from 219 to 220.

And here's a table summarizing all this information in a way more compact space:

LinesLine Count in MemoryIntermediate FigureLevelDifferenceNext Level
10$0010$01 = 10-11
20$0020$02 = 21-12
100$0100$10 = 169-710
1590$0F90$F9 = 249158-91159
1600$1000$00 = 0159-97160
2190$1590$59 = 89218-127219
2200$1600$60 = 96219+123219
2990$1D90$D9 = 217219+2219
3000$1E00$E0 = 224219-5220
3350$2150$15 = 21254-23255
3360$2160$16 = 22255-230

Why is this so dang complicated? This algorithm checks for the level up in such a roundabout way because of the fact you can start on a level other than 0. A simplified rule of "level up on any line count divisible by 10" would let you level up from level 9 to 10 after only ten lines cleared if you started on level 9. In reality, they want you to work all the way up to 100 lines cleared on level 9 before leveling up to level 10.

Now in the original game, starting on levels 0 through 9 doesn't really change anything. By holding the A button while selecting a level, you can add ten to the selected level number, allowing you to start on levels 10 through 19. This does have an effect on the line counts, since the line count suffers from the weird BCD rollovers before even reaching the first true level up.

You would expect the level up from 18 to 19 to occur at 190 lines cleared. However, it happens much earlier than that. Starting at level 18, suppose you have 129 lines cleared. Getting one more puts you at 130. This is stored in memory as $0130 so your intermediate figure is $13 or 19. The difference between these two is -1, so a level up occurs. You're six levels ahead of schedule.

Due to the level number being higher than before, you can get to a higher level while having a lower line clear count. This means the comparison will overflow much faster into the run while also being at a higher level. Here's an adjusted table for starting at level 18:

LinesLine Count in MemoryIntermediate FigureLevelDifferenceNext Level
10$0010$01 = 118+1718
20$0020$02 = 218+1618
120$0120$12 = 1818018
130$0130$13 = 1918-119
1590$0F90$F9 = 249164-85165
1600$1000$00 = 0165-91166
2290$1690$99 = 105234-127235
2300$1700$70 = 112235+123235
3090$1E90$E9 = 233235+2235
3100$1F00$F0 = 240235-5236
3290$2090$09 = 9254-11255
3300$2100$10 = 16255-170

You can support Retro Game Mechanics Explained on Patreon here! Any and all support is greatly appreciated!



  1. Signed comparisons are weird and require both a combination of the negative flag and overflow flag to properly detect. In fact since the 65xx processors' CMP instructions don't modify the overflow flag, in order to do a proper signed comparison, you have to manually use a SEC : SBC setup.


You must log in to comment.

in reply to @rgmechex's post:

(we'll come back to starting at different levels later, you can start at up to level 9, and pros often use a modified version of the game to start at level 18 or 19),

Just hold A when selecting a level to bump it up by 10 :P

I didn't know about it until 15 years after playing the original, so don't feel too bad

(though I knew of the game boy version's hold-down-for-heart-levels trick when it was out)