Back to Hardware

Tetris and 3D Transforms

22 Dec 2018
Progress: Complete

This is page 9 of the Games Console project.

Tetris was one of the first games I made for the console, and it was early enough in the process that I didn't think to make a javascript prototype of it. The logic is, after all, fairly linear. Instead, you can feast your eyes on this 3D demo:

As I mentioned in the video, the 3D stuff was written without a particular use in mind, and only towards the end of the project did I think to do something with it.

The 3D effect is extremely simple (compared to most 3D implementations, I mean). It's just two rotations, one in the X-Y plane and then one in the Y-Z plane. The Z coordinate is then added to the Y coordinate to render a 2D image. Since there's no perspective, it often appears to do a weird depth inversion as it crosses the boundary (at least to my eyes). I actually did implement perspective in the JS version, but took it out as it would be too hard to implement in assembly. Really we needed a tangent lookup table, but I wasn't sure how to scale that, since it goes between infinity and minus-infinity. Alternatively, we could have attempted a division routine after sine and cosine lookups.

A rotation matrix looks like this.

So to rotate an (x, y) coordinate in a plane, we multiply by the rotation matrix. If you're not familiar with matrix multiplication, it translates to procedural code like this:

nx = cos(theta)*x - sin(theta)*y;
ny = sin(theta)*x + cos(theta)*y;
x = nx;
y = ny;

This works nicely for the theta rotation in the X, Y plane. Every coordinate of our model has to go through the transform. For the next rotation, we basically just swap x with y, y with z, and theta with phi, in the code above. As it happens, we can simplify the code a bit after that.

Most importantly, for the entirety of a frame, theta and phi do not change. So, sin(theta) and so on will always return the same numbers, and we can cache them at the start of the frame and save a huge amount of time that would have been spent doing lookups.

Simplifying, caching the trig values, and not bothering with perspective, translated into a one-instruction-per-line format, the javascript transform looks like this.

function transform1(x,y,z,st,ct,cp,sp){
    nx=ct*x
    nx-=st*y
    ny=st*x
    ny+=ct*y
    ny=ny*cp
    ny+=sp*z
  return [nx, ny];
}

From here, paying close attention to where the "point" in the fixed-point multiplication lies, it's a fairly easy, if tedious, task to translate this to assembly.

/* Function transform3D (x,y,z, sintheta, costheta, sinphi, cosphi)
 * Applies transform matrices: x/y rotation (theta) followed
 * by y/z rotation (phi). Returns x,y in r18,r19
 */
transform3D:
    ; Function arguments 
    ; z = r13   ; signed
    ; x = r14   ; signed
    ; y = r15   ; signed


    ; Returns
.def nx = r18   ; unsigned
.def ny = r19   ; unsigned

    ; fmuls only works on registers r16 - r23
    push x0 ; r16
    push y0 ; r17

    movw r17:r16, r15:r14       ; move two registers at once

    ; Lots of fixed-point multiplication - output goes into r1:r0. 
    ; We throw away r0 but use its MSB to round up answer

    fmuls costheta,x0
    mov nx, r1
    sbrc r0,7               ; round it up
    inc nx

    fmuls sintheta,y0
    sub nx, r1              ; subtract! x row is cos theta minus sin theta
    sbrc r0,7
    dec nx
    
    fmuls sintheta,x0
    mov ny, r1
    sbrc r0,7
    inc ny

    fmuls costheta,y0
    add ny, r1
    sbrc r0,7
    inc ny

    fmuls ny,cosphi         ; Now apply phi matrix
    mov ny, r1
    sbrc r0,7
    inc ny
    
    mov x0, r13             ; load z value, move into x because we're
                            ; so short on registers
    fmuls sinphi, x0
    add ny, r1
    sbrc r0,7
    inc ny

    subi nx, 128            ; and finally, 
    subi ny, 128            ; shift origin to center of unsigned range

    pop y0
    pop x0
    ret

With that in place, all we have to do is extend our polygon-drawing routines to pass everything they read from program memory through the transform routine. For the entire 3D library, that's pretty much it! Here's the full file, most of it you've seen before.


### 3Dfunctions.asm ###



/* Function drawPolygonPM(Z pointer)
 * Draw from a list of coords in program memory
 * First byte is number of vertices, followed by pairs of x,y bytes
 */
drawPolygonPM:
    push r20
.def vertexCount=r20

    lpm vertexCount, Z+
    lpm r16,Z+              ; load starting point
    lpm r17,Z+              ; all polygons must have at least 2 vertices
    dec vertexCount 

polyPMnextVert:
    lpm r18,Z+              ; load next vertex
    lpm r19,Z+
    rcall drawLine          ; drawLine updates r16, r17 for us

    dec vertexCount
    brne polyPMnextVert     ; repeat until end of polygon

    pop r20
    ret


/* Function drawPolygonListPM(Z pointer)
 * Draws lots of polygons from program memory.
 * First byte is number of polygons
 */
drawPolygonListPM:
    push r20
    lpm r20, Z+         ; load number of polygons

polyListPMnext:
    rcall drawPolygonPM ; just call drawPolygonPM that many times
    dec r20
    brne polyListPMnext

    pop r20
    ret



/* Function drawModelListPM(Z pointer, theta, phi)
 * basically a 3D version of drawPolygonListPM
 */
drawModelListPM:
    ; arguments
.def theta= r24
.def phi  = r25

.def sintheta=r20   ; These values are going to be the same for all
.def costheta=r21   ; transforms, so we may as well cache them
.def sinphi = r22
.def cosphi = r23
    push r20
    push r21
    push r22
    push r23
    push ZH
    push ZL

    ; Load trig values. Table is backwards so we can use subtract immediate
    ldi ZH, HIGH(sineTable*2+255)
    ldi ZL,  LOW(sineTable*2+255)
    sub ZL, theta
    sbci ZH, $00
    lpm sintheta, Z

    ; Cos is just an offset, but we have to rewind 
    ; to the start of the table or it could overflow
    ldi ZH, HIGH(sineTable*2+255)
    ldi ZL,  LOW(sineTable*2+255)
    subi theta, 192                 ; subtract pi/2 from theta
    sub ZL, theta
    sbci ZH, $00
    lpm costheta, Z
    subi theta, 64                  ; return theta to its orig value

    ; Now do the same for phi
    ldi ZH, HIGH(sineTable*2+255)
    ldi ZL,  LOW(sineTable*2+255)
    sub ZL, phi
    sbci ZH, $00
    lpm sinphi, Z

    ; cos phi
    ldi ZH, HIGH(sineTable*2+255)
    ldi ZL,  LOW(sineTable*2+255)
    subi phi, 192                   ; subtract pi/2 from phi
    sub ZL, phi
    sbci ZH, $00
    lpm cosphi, Z
    subi phi, 64                    ; return phi to its orig value

    pop ZL
    pop ZH  ; Now we can start to read coordinates from program mem

    lpm r11, Z+                     ; Number of polygons

dModelListPMnext:
    rcall draw3DModelPM
    dec r11
    brne dModelListPMnext           ; loop until we've drawn them all

    pop r20
    pop r21
    pop r22
    pop r23
    ret

/* Function draw3DModelPM(Z pointer, sintheta, costheta, sinphi, cosphi)
 * rotates and then draws a list of vertices from prog mem
 */
draw3DModelPM:
    lpm r12, Z+                     ; Number of vertices

    ; Load starting coordinate
    lpm r14, Z+             ; x
    lpm r15, Z+             ; y
    lpm r13, Z+             ; z
    rcall transform3D       ; this returns in r18 and r19, we want r16, r17
    movw r17:r16,r19:r18    ; they're not used as a word but movw still works
    dec r12

d3DModelPMnext:             ; There will always be at least 2 vertices
    lpm r14, Z+             ; x
    lpm r15, Z+             ; y
    lpm r13, Z+             ; z
    rcall transform3D
    rcall drawLine          ; draws a line from r16,r17 to r18,r19

    dec r12
    brne d3DModelPMnext             ; loop for each vertex

    ret



/* Function transform3D (x,y,z, sintheta, costheta, sinphi, cosphi)
 * Applies transform matrices: x/y rotation (theta) followed
 * by y/z rotation (phi). Returns x,y in r18,r19
 */
transform3D:
    ; Function arguments 
    ; z = r13   ; signed
    ; x = r14   ; signed
    ; y = r15   ; signed


    ; Returns
.def nx = r18   ; unsigned
.def ny = r19   ; unsigned

    ; fmuls only works on registers r16 - r23
    push x0 ; r16
    push y0 ; r17

    movw r17:r16, r15:r14       ; move two registers at once

    ; Lots of fixed-point multiplication - output goes into r1:r0. 
    ; We throw away r0 but use its MSB to round up answer

    fmuls costheta,x0
    mov nx, r1
    sbrc r0,7               ; round it up
    inc nx

    fmuls sintheta,y0
    sub nx, r1              ; subtract! x row is cos theta minus sin theta
    sbrc r0,7
    dec nx
    
    fmuls sintheta,x0
    mov ny, r1
    sbrc r0,7
    inc ny

    fmuls costheta,y0
    add ny, r1
    sbrc r0,7
    inc ny

    fmuls ny,cosphi         ; Now apply phi matrix
    mov ny, r1
    sbrc r0,7
    inc ny
    
    mov x0, r13             ; load z value, move into x because we're
                            ; so short on registers
    fmuls sinphi, x0
    add ny, r1
    sbrc r0,7
    inc ny

    subi nx, 128            ; and finally, 
    subi ny, 128            ; shift origin to center of unsigned range

    pop y0
    pop x0
    ret


cancelDrawLine:
    ret
    
/*  Function drawLine (x0,y0,x1,y1)
 *  My implementation of Bresenham line algorithm
 *  based on pseudocode/description from wikipedia
 */
drawLine:

    cp r16,r18
    cpc r17,r19
    breq cancelDrawLine

    ;Function arguments
.def x0=r16
.def y0=r17
.def dx=r18
.def dy=r19
    
    ;Local variables
    push r20
    push r21
    push r22
    push r23
    push r24
    push r25
.def slowx=r20
.def slowy=r21
.def fastx=r22
.def fasty=r23
.def err = r24
.def t   = r25

    ; First we split the magnitude and direction
    ; Need to be careful/explicit because mag could be >127

    cp dx,x0
    breq lineXeq
    brcs lineXneg       ; Carry flag set if it overflowed
    
    ldi slowx,1         ; slowx is the direction
    sub dx,x0           ; dx is now the magnitude
    rjmp lineY

lineXneg:
    ldi slowx,-1
    mov t, x0           ; t as temp var
    sub t, dx
    mov dx, t
    rjmp lineY

lineXeq:
    clr slowx
    clr dx
    
lineY:                  ; Now do the same for y
    cp dy,y0
    breq lineYeq
    brcs lineYneg
    
    ldi slowy,1
    sub dy,y0
    rjmp lineCheckDistance

lineYneg:
    ldi slowy,-1
    mov t, y0           ; t as temp var
    sub t, dy
    mov dy, t
    rjmp lineCheckDistance

lineYeq:
    clr slowy
    clr dy

lineCheckDistance:      ; Now to find which direction is "faster"
    cp dx, dy
    brcs lineDxLessthanDy
    
    mov fastx, slowx    ; x is the fast direction
    clr fasty

    eor dx, dy          ; Tripple XOR, the classy way to
    eor dy, dx          ; swap two registers
    eor dx, dy

    rjmp lineInit   

lineDxLessthanDy:
    mov fasty, slowy    ; y is the fast direction, no need to swap
    clr fastx

lineInit:
    mov err, dy         ; set err to half the furthest distance
    lsr err
    
    out PORTA, x0       ; We are now ready to draw the line
    out PORTC, y0
    rcall delayPixel



    mov t, dy

lineNextPixel:
    sub err, dx         ; Update error term
    brcc lineStepParallel

    add err, dy         ; Make error term positive again
    add x0,slowx        ; Step diagonally
    add y0,slowy
    
    rjmp lineLoopBack

lineStepParallel:
    add x0,fastx        ; Step parallel
    add y0,fasty

lineLoopBack:
    out PORTA, x0       ; Plot point
    out PORTC, y0
    rcall delayPixel

    dec t
    brne lineNextPixel

    pop r25
    pop r24
    pop r23
    pop r22
    pop r21
    pop r20

    ret                 ; End of drawLine



delayPixel:
    push r16
    ldi r16,8
delayPixelLoop:
    dec r16
    brne delayPixelLoop
    pop r16
    ret



/* Function sinS(coordinate, dir)
 * Scales 8bit signed coordinate by sine of the direction
 */
sinS:
    .def cor=r16
    .def dir=r17

    push ZH
    push ZL
    ; As there is no add-immediate-with-carry, it is faster to
    ; go to the end of the table and walk backwards
    ldi ZH, HIGH(sineTable*2+255)
    ldi ZL,  LOW(sineTable*2+255)
    sub ZL, dir
    sbci ZH, $00            ; sub-with-carry 0 from high byte
    lpm dir, Z
    fmuls cor,dir
    mov cor,r1              ; result is in r1:r0
    sbrc r0,7               ; round number up if necessary
    inc cor
    
    pop ZL
    pop ZH
    ret

/* Function cosS(coordinate, dir)
 * Scales 8bit signed coordinate by cosine of the direction
 */
cosS:
    .def cor=r16
    .def dir=r17

    push ZH
    push ZL
    ldi ZH, HIGH(sineTable*2+255)
    ldi ZL,  LOW(sineTable*2+255)   ; reuse table, but shift by pi/2
    subi dir, 192                   ; which in our notation is 64 (-192)
    sub ZL, dir
    sbci ZH, $00
    lpm dir, Z
    fmuls cor,dir
    mov cor,r1              ; result is in r1:r0
    sbrc r0,7               ; round number up if necessary
    inc cor

    pop ZL
    pop ZH
    ret

; signed values, in reverse order
sineTable: 
.db $FD,$FA,$F7,$F4,$F0,$ED,$EA,$E7,$E4,$E1,$DE,$DB,$D8,$D5,$D2,$CF,$CD,$CA,$C7,$C4,$C1,$BF,$BC,$B9,$B7,$B4,$B2,$AF,$AD,$AB,$A8,$A6,$A4,$A2,$A0,$9E,$9C,$9A,$98,$96,$95,$93,$91,$90,$8F,$8D,$8C,$8B,$8A,$88,$87,$86,$86,$85,$84,$83,$83,$82,$82,$82,$81,$81,$81,$81,$81,$81,$81,$82,$82,$82,$83,$83,$84,$85,$86,$86,$87,$88,$8A,$8B,$8C,$8D,$8F,$90,$91,$93,$95,$96,$98,$9A,$9C,$9E,$A0,$A2,$A4,$A6,$A8,$AB,$AD,$AF,$B2,$B4,$B7,$B9,$BC,$BF,$C1,$C4,$C7,$CA,$CD,$CF,$D2,$D5,$D8,$DB,$DE,$E1,$E4,$E7,$EA,$ED,$F0,$F4,$F7,$FA,$FD,$00,$03,$06,$09,$0C,$10,$13,$16,$19,$1C,$1F,$22,$25,$28,$2B,$2E,$31,$33,$36,$39,$3C,$3F,$41,$44,$47,$49,$4C,$4E,$51,$53,$55,$58,$5A,$5C,$5E,$60,$62,$64,$66,$68,$6A,$6B,$6D,$6F,$70,$71,$73,$74,$75,$76,$78,$79,$7A,$7A,$7B,$7C,$7D,$7D,$7E,$7E,$7E,$7F,$7F,$7F,$7F,$7F,$7F,$7F,$7E,$7E,$7E,$7D,$7D,$7C,$7B,$7A,$7A,$79,$78,$76,$75,$74,$73,$71,$70,$6F,$6D,$6B,$6A,$68,$66,$64,$62,$60,$5E,$5C,$5A,$58,$55,$53,$51,$4E,$4C,$49,$47,$44,$41,$3F,$3C,$39,$36,$33,$31,$2E,$2B,$28,$25,$22,$1F,$1C,$19,$16,$13,$10,$0C,$09,$06,$03,$00



I have this to say about 3D. It's all about confidence. I was able to casually implement this because I've implemented 3D routines half a dozen times before. The first time I implemented a 3D routine was when I was 14, so I think that's before I'd even covered pythagoras in school, let alone trig functions (I went to really bad school). It was all new to me but I managed it for just one reason. My online friend Atrius had said that he'd implemented a 3D rotating cube on a TI-83 calculator when he was younger than me! I'm not kidding when I say that I didn't know what trigonometry was, the whole thing was a huge stack of gamemaker's built-in lengthdir_ functions (just a wrapper for sine and cosine). Perhaps that's part of it, not knowing that what I was doing was actually this scary "trigonometry" thing.

If you balk when you see matrix transforms, you shouldn't! It's just equations with an in and an out, and you can treat them like a black box, stack them up, build a huge ugly script and it'll all be dandy. Unlike with "real" maths, you have immediate confirmation of whether it's working or not because you can see when it runs whether you've got a beautiful 3D render or a psychedelic mass of wires. So give it a go. Believe in yourself!

Tetris

I'll now hand the narrative over to my younger self, as the part that follows was written in 2014, shortly after finishing the project. Apparently I anticipated the writeup having more screenshots.

* * *

'Paint' was the first thing I made. Actually the very first thing I made was just a cursor you could control with the NES pad.

The control pad is just a shift register. The pinout and some info is given here. I managed to get it working with the SPI port of the ATmega128 by using the data-out line to control the latch signal. We didn't want to send any data out of the SPI port anyway, so I send zeros, which means that for the duration of the readout the latch signal is low. It unfortunately means that the latch is high between readouts, so the buttons are latched immediately at the end of the readout. I suppose there's a delay to the controller because of that, but it's less than the length of one frame/game loop (16ms). Doing it this way means that just calling an SPI read gives us the button states with no mucking about.

With a moveable cursor on the screen (D-pad just increments/decrements the coordinates sent to the DACs) I made a simple system to push coordinates into the SRAM while A is held and draw all the points each frame. Easy. Paint.

After that I proceeded to try out all kinds of ideas, not all of which made it into games, but most of them did in some form: first, drawing straight lines via bressenham, then polygon lists from program memory, then text, the 3D stuff, bitmap sprites, and so on. I started and didn't finish a few 'simple' games like breakout because my progress was making me more ambitious. When I got the sound working the first midi file I played was the tetris theme. And so, the first game I decided to make into a finished form was Tetris.

I am very meticulous when it comes to these things, and there's nothing I hate more than a fan game that doesn't feel right. Luckily, for Tetris there were great resources, the main one I turned to was the tetrisconcept wiki. I stuck to the physics of TGM (Tetris The Grand Master). Believe it or not there a many different versions of the Tetris physics, but the most important thing is to be consistent.

I spent quite some time getting the delayed-auto-shift correct. Press left once, it moves one step left. Hold it, and after a certain time, it autoshifts at a faster speed than the initial delay. In my experience most flash games get this wrong.

Choosing which block appears next follows quite strict rules. I took the 'bag' approach, described here. To actually generate the randomness I used the pseudorandom number generator in the bootloader.

Lots of difficulties surround rotation. The main problem is dealing with wall kicks, and I again tried to be consistent and went by TGM rotation. Useful resource, that site is.

Scoring may seem simple, but fast-fall (holding down on the d-pad) awards one point, and scoring a tetris on level 9 gives 12000 points. Quite a range, so unfortunately a 16-bit number won't cut it. It's quite possible to score over 65,535... I had no choice but to make it a 24-bit number. At the end of a game, if you've scored high enough, this will go on the highscore table (stored to the cartridge) which I added right at the end of the project.

[2018 edit: You should have done the scoring in BCD, idiot.]

Now, before making any games I had prepared the 3D code, right after doing the bressenham algorithm. But near the end of the project I still hadn't used it for anything, and so, I decided to make a splash screen for Tetris with it.

Watching it is weird. Without any sense of perspective, it's impossible to tell which way it's spinning, and in my mind it keeps changing back and forth. Maybe there's something to do with being left- or right-brained.

Once you're bored of watching it, you get to select a level.

And then play begins.

Getting ready for a tetris...

here it comes....

Woo 1200 points!

But that was only on level 0 of course. I mean, I had to operate the camera at the same time. Normally I'd start on level 9.

That's your lot. But wait, before you rush ahead to page 10, check out the full Tetris source code right here:



### Tetris.asm ###



;//////////////////////////////
; Title screen
;//////////////////////////////
    ldi r24,0
    ldi r20,0 ; level select - check unused
    ldi XH,0  ; counter
    ldi XL,0
    call allNotesOff
showTetrisLogo:

    rcall readController
    sbrc r3,ctrlStart
    rjmp chooseLevel

    ;increase theta, oscillate phi
    inc r24
    adiw X,63
    ldi r16,32
    mov r17,XH
    rcall sinS
    mov r25,r16
    subi r25,-64

    ldi ZH, HIGH(TetrisLogo*2)
    ldi ZL,  LOW(TetrisLogo*2)
    rcall drawPolygonListPM

    ldi ZH, HIGH(Tetris3DBlocks*2)
    ldi ZL,  LOW(Tetris3DBlocks*2)
    rcall drawModelListPM

    ldi r16, (127+66)
    ldi r17, (254)
    ldi ZH, HIGH(msgPressStart*2)
    ldi ZL,  LOW(msgPressStart*2)
    rcall drawTextPM
    
    call BootRandomNumber       ; Continuously seed the RNG
    
    rjmp showTetrisLogo

chooseLevel:
  
    ldi r16,240
    mov r5,r16              ; Disable DAS
    rcall readController
    sbrc r3,ctrlLeft
    dec r20
    sbrc r3,ctrlRight
    inc r20
    sbrc r3,ctrlUp
    subi r20,5
    sbrc r3,ctrlDown
    subi r20,-5

    sbrc r3,ctrlStart
    rjmp startLevel

chLevelCheckRange:  ; Wrap to range 0 - 9
    cpi r20,10
    brpl chLevelSub10
    cpi r20,0
    brmi chLevelAdd10

    ldi r17, 139
    ldi r16,24
    mul r16,r20
    mov r16,r0
    cpi r20, 5
    brpl chLevelLowerLine
    rjmp chLevelDraw
  
chLevelAdd10:
    subi r20,-10
    rjmp chLevelCheckRange
chLevelSub10:
    subi r20,10
    rjmp chLevelCheckRange

chLevelLowerLine:
    subi r16,120
    subi r17,-30
    rjmp chLevelDraw

chLevelDraw:
    neg r16
    subi r16, 97

    ; draw rect at r16,r17
    ldi r18,24
    ldi r19,20
    rcall drawRectangle

    ldi ZH, HIGH(TetrisLogo*2)
    ldi ZL,  LOW(TetrisLogo*2)
    rcall drawPolygonListPM  
    
    ldi r16,230
    ldi r17,130
    ldi ZH, HIGH(msgChooseALevel*2)
    ldi ZL,  LOW(msgChooseALevel*2)
    rcall drawTextPM
    ldi r16,(255-80)
    ldi r17,156
    rcall drawTextPM
    ldi r16,(255-80)
    ldi r17,186
    rcall drawTextPM
    
    
    rjmp chooseLevel

startLevel:

;//////////////////////////////
; Main Tetris Game code
;//////////////////////////////

    ;init frame counter
    ldi r16,0
    out TCNT0, r16
    
    ldi r16, (1<<CS02)|(1<<CS01)|(1<<CS00)
    out TCCR0, r16
    
    ldi r16, 128
    out OCR0, r16


    ldi r16, HIGH(Music*2)
    sts MUSIC_START_H, r16
    ldi r16,  LOW(Music*2)
    sts MUSIC_START_L, r16
    rcall resetMusic


.equ BLOCKSIZE = 11
.equ GRIDX = 100
.equ GRIDY = 20
.equ DAS_TIME = 248
.equ ANIM_TIME = 20

.equ HISTORY_LENGTH= 4
.equ HISTORY_TRIES = 6

.equ FALLTIME = D_START+1 ;$011A
.equ NUMLINES = D_START+2 ;$011B
.equ CURLEVEL = D_START+3 ;$011C
.equ INCLEVEL = D_START+4 ;$011D
.equ HISTORY  = D_START+5
.equ GRID     = D_START+10

.def nextPiece     = r9
.def currentPiece  = r10
.def currentPieceX = r11
.def currentPieceY = r12
.def AnimTimer     = r23
.def fallTimer     = r25

    sts CURLEVEL, r20       ; r20 set on the level select screen
    rcall setLevelFallSpeed
    
    rcall emptyGrid

    ; Initially fill the history with S and Z pieces
    ldi r16,4
    sts (HISTORY), r16
    sts (HISTORY+2), r16
    ldi r16,5
    sts (HISTORY+1), r16
    sts (HISTORY+3), r16
    
    rcall loadNewPiece      ; Set currentPiece and nextPiece
    rcall loadNewPiece
    


    
    
    
    ldi r16,0
    sts SCORE_H,r16
    sts SCORE_M,r16
    sts SCORE_L,r16
    sts NUMLINES, r16
    ldi r16,9
    sts INCLEVEL, r16
    
    ldi AnimTimer,0
    lds fallTimer,FALLTIME
    

Main:
    cpi AnimTimer,0
    brne drawAnimation      ; If we're animating, skip the control sequence
    
    
    rcall readController
    
    sbrc r3,ctrlLeft
    rcall moveLeft
    sbrc r3,ctrlRight
    rcall moveRight
    sbrc r3,ctrlDown
    rcall fastDrop
    
    sbrc r3,ctrlB
    rcall rotateClockwise
    sbrc r3,ctrlA
    rcall rotateWithershins
    
    sbrc r3,ctrlStart
    rcall PauseGame
    
    
    dec fallTimer
    brne drawScreen
    
    lds fallTimer,FALLTIME  ; Reset Timer
    
    inc currentPieceY
    rcall checkCollision    ; Have we hit the bottom?
    breq drawScreen         ; No - move along...

    dec currentPieceY       ; Else move back again and
    rcall placeOntoGrid     ; add it onto the grid
    rcall loadNewPiece
    
    ldi r16, DAS_TIME       ; Important - reset the DAS timer
    mov r5, r16

    
    rcall checkForFullRows
    breq drawScreen         ; If no rows are full, go to draw
    
    ldi AnimTimer,ANIM_TIME
    ldi r16, HIGH(soundLineClear*2)
    sts EFFECT_H, r16
    ldi r16,  LOW(soundLineClear*2)
    sts EFFECT_L, r16
    
    
drawAnimation:
    ldi r21,0               ; r21 will count the number of lines cleared
    dec AnimTimer
    breq animationEnd
    
    mov r16,AnimTimer
    lsr r16
    sbrc r16,0              ; To achieve a flicker effect, only
    rjmp drawScreen         ; draw if timer/2 is even
    
    ldi YH, HIGH(GRID+6)
    ldi YL,  LOW(GRID+6)
    ldi r19, GRIDY-BLOCKSIZE
    ldi r18,$FF
    ldi r21,20
drawAnimLoop:
    ld r16,Y+
    ld r17,Y+
    subi r19, (-BLOCKSIZE)

    cp r16,r18              ; If both bytes of 
    cpc r17,r18             ; the row are full...
    brne drawAnimLoop2
    rcall drawBar           ; draw a bar
    
drawAnimLoop2:
    dec r21
    brne drawAnimLoop
    
    

    
drawScreen: ; Continue
    
    rcall drawPiece
    rcall drawGrid
    
    push currentPiece           ; drawPiece uses these registers
    push currentPieceX          ; so to draw the "next piece"
    push currentPieceY          ; we need to push and pop them all
    mov currentPiece,nextPiece
    ldi r16,6
    mov currentPieceY,r16
    ldi r16,-3
    mov currentPieceX,r16
    rcall drawPiece
    pop currentPieceY   
    pop currentPieceX
    pop currentPiece
    
    rcall drawScore
    ldi r16,0           ; Just move the beam off screen
    out PORTA, r16      ; for the rest of the frame
    out PORTC, r16
    
    call processAudio
    
waitforframe:
    in r16, TIFR
    sbrs r16, OCF0
    rjmp waitforframe
    ldi r16, (1<<OCF0)
    out TIFR, r16
    ldi r16, 0
    out TCNT0, r16
    rjmp Main


    
    
    
    
    
animationEnd:
    ; Now remove the completed lines and increase score
    ldi YH, HIGH(GRID+6)
    ldi YL,  LOW(GRID+6)
    ldi r18,$FF
    
animationEndLoop:
    ld r16,Y+
    ld r17,Y+
    cp r16,r18              ; If both bytes of 
    cpc r17,r18             ; the row are full...
    breq shiftRows

    cpi YH, HIGH(GRID+46)   
    brne animationEndLoop   
    cpi YL,  LOW(GRID+46)   ; Loop until we reach the end
    brne animationEndLoop   ; of the playable grid

    
    lds r16, NUMLINES       ; Add to number of lines cleared
    add r16,r21
    sts NUMLINES, r16
    
    ; Calculate the score (formula: http://tetrisconcept.net/wiki/Scoring)  
    ldi r20, 4
    cpi r21, 1          ; 1 line = 40 * (level +1)
    breq calcScore
    ldi r20, 10
    cpi r21, 2          ; 2 lines = 100 * (level +1)
    breq calcScore
    ldi r20, 30
    cpi r21,3           ; 3 lines = 300 * (level +1)
    breq calcScore
    ldi r20, 120        ; 4 lines = 1200 * (level +1)
    
calcScore:
    lds r16, CURLEVEL
    inc r16
    mul r16,r20
    movw r17:r16,r1:r0  ; Multiply by level number
    ldi r20,10
    mul r16,r20         ; Now multiply that by 10
    movw X, r0          ; use X as temp var
    mul r17,r20         ; multiply high byte
    add XH, r0          ; Now X is equal to score
    
    ldi r20,0
    lds r16, SCORE_L
    add r16,XL
    sts SCORE_L, r16
    lds r16, SCORE_M
    adc r16,XH
    sts SCORE_M, r16
    lds r16, SCORE_H
    adc r16,r20
    sts SCORE_H, r16
    
    
    ; Every ten lines, increase level
    lds r16, INCLEVEL
    sub r16, r21
    brpl doneIncreaseLevel
    subi r16,-10
    lds r17, CURLEVEL
    inc r17
    sts CURLEVEL, r17
doneIncreaseLevel:
    sts INCLEVEL, r16
    
    
    
    rjmp drawScreen
    

shiftRows:
    inc r21                 ; Count the number of lines cleared
    sbiw Y, 2
shiftRowsLoop:
    ;sbiw Y, 2
    ld r16,-Y
    ld r17,-Y
    std Y+3,r16
    std Y+2,r17
    cpi YH, HIGH(GRID+2)    
    brne shiftRowsLoop
    cpi YL,  LOW(GRID+2)    ; Loop until we reach the start
    brne shiftRowsLoop
    rjmp animationEnd
    
    
    

; Function moveLeft
moveLeft:
    inc currentPieceX
    rcall checkCollision
    brne moveLeftCollision
    ret
moveLeftCollision:
    dec currentPieceX
    ret

; Function moveRight
moveRight:
    dec currentPieceX
    rcall checkCollision
    brne moveRightCollision
    ret
moveRightCollision:
    inc currentPieceX
    ret

; Function fastDrop
fastDrop:
    ldi fallTimer,1

    
    lds r16, SCORE_L
    subi r16,-1
    sts SCORE_L, r16
    lds r16, SCORE_M
    sbci r16,-1
    sts SCORE_M, r16
    lds r16, SCORE_H
    sbci r16,-1
    sts SCORE_H, r16
    
    ret




; Function checkForFullRows
; Just a true/false of if any are present
checkForFullRows:
    ldi YH, HIGH(GRID+6)
    ldi YL,  LOW(GRID+6)
    
    ldi r18,$FF
checkFullRowLoop:
    ld r16,Y+
    ld r17,Y+
    cp r16,r18              ; If both bytes of 
    cpc r17,r18             ; the row are full...
    breq checkFullRowFull

    cpi YH, HIGH(GRID+46)   
    brne checkFullRowLoop   
    cpi YL,  LOW(GRID+46)   ; Loop until we reach the end
    brne checkFullRowLoop   ; of the playable grid

    
    sez                     ; Set Z flag
    ret

checkFullRowFull:
    clz                     ; Clear Z flag
    ret







placeOntoGrid:
    ldi r16, HIGH(soundEffect1*2)
    sts EFFECT_H, r16
    ldi r16,  LOW(soundEffect1*2)
    sts EFFECT_L, r16
    
    rcall pointBlockMap
    ;move Y pointer to start of grid data
    ldi YH, HIGH(GRID)
    ldi YL,  LOW(GRID)
    ldi r16,0
    add YL, currentPieceY
    adc YH, r16
    add YL, currentPieceY
    adc YH, r16
    
    ldi r20, 4
placeOntoGridLoop:
    ;load byte of piece from Z+
    lpm r16,Z+
    ldi r17,0
    
    mov r21, currentPieceX
    cpi r21,0
    breq placeOntoGridNoshift
    
placeGridColShiftLoop: ;shift X times
    lsl r16
    rol r17
    
    dec r21
    brne placeGridColShiftLoop

placeOntoGridNoshift:

    ld r18, Y
    or r18,r16
    st Y+,r18
    
    ld r18, Y
    or r18,r17
    st Y+,r18
    
    dec r20
    brne placeOntoGridLoop
        
    ret
    

    




    
    

; Function rotatePiece - r18 is rotation direction/amount
rotatePiece:
    ldi r16, HIGH(soundRotate*2)
    sts EFFECT_H, r16
    ldi r16,  LOW(soundRotate*2)
    sts EFFECT_L, r16
    ; rotation = last 2 bits of piece type
    push currentPiece
    
    ldi r17, $03
    mov r16, currentPiece   ; To get the rotation state, 
    and r16, r17            ; AND the current piece with $03
    add r16,r18             ; Now do the rotation
    and r16, r17            ; null the overflow so it wraps to 0
    com r17
    and currentPiece, r17   ; Now clear the rotation state of the piece
    add currentPiece, r16   ; and set the new one
    
    ; Check for collisions
    rcall checkCollision
    breq rotNoCollision
    
    ; Try a wall kick - move left
    inc currentPieceX
    rcall checkCollision
    breq rotNoCollision
    
    ; no luck? try moving right
    dec currentPieceX
    dec currentPieceX
    rcall checkCollision
    breq rotNoCollision
    
    ; still colliding? move back and undo rotation
    inc currentPieceX
    pop currentPiece
    ret

rotNoCollision:
    pop r16     ; just to clear currentPiece from stack
    ret



; Function rotateClockwise
rotateClockwise:
    push r18
    ldi r18,-1
    rcall rotatePiece
    pop r18
    ret
; Function rotateWithershins
rotateWithershins:
    push r18
    ldi r18,1
    rcall rotatePiece
    pop r18
    ret
    

; function checkCollision
checkCollision:
    ; move Z pointer to progmem for current piece map
    rcall pointBlockMap
    
    ;move Y pointer to start of grid data
    ldi YH, HIGH(GRID)
    ldi YL,  LOW(GRID)
    ldi r16,0
    add YL, currentPieceY
    adc YH, r16
    add YL, currentPieceY
    adc YH, r16

    ldi r20, 4
checkCollisionLoop:
    ;load byte of piece from Z+
    lpm r16,Z+
    ldi r17,0
    
    mov r21, currentPieceX
    cpi r21,0
    breq checkNoshift
    
checkColShiftLoop: ;shift X times
    lsl r16
    rol r17
    
    dec r21
    brne checkColShiftLoop

checkNoshift:
    ;and it with grid data from Y+
    ;if nonzero goto collision
    ld r18, Y+
    and r18,r16
    brne collision
    ld r18, Y+
    and r18,r17
    brne collision
    
    dec r20
    brne checkCollisionLoop

    sez     ; (set zero flag for return value)
    ret
    
collision:
    clz     ; Clear Z flag
    ret





    
; Function loadNewPiece
; The TGM method of chosing a new piece is based around keeping a history
; of the last four pieces dealt. We try a certain number of times to choose
; a piece which isn't in the history. This makes receiving the same piece
; several times in a row rare but not impossible.
loadNewPiece:
    mov currentPiece, nextPiece
    ldi r16, 6
    mov currentPieceX, r16
    ldi r16, 2
    mov currentPieceY, r16
    rcall checkCollision
    brne GameOver
    
    
    ldi r21, HISTORY_TRIES

loadNewPieceRnd:            ; Repeatedly load a random number until it is
    call BootRandomNumber   ; in the correct range
    lds r16,RND_H
    lsr r16                 ; The higher bits are the "most random"
    lsr r16                 ; Divide down...
    lsr r16
    lsr r16
    lsr r16
    cpi r16, 7              ; 7 possible pieces
    brcc loadNewPieceRnd    ; If outside desired range, try again

    ldi YH, HIGH(HISTORY)
    ldi YL,  LOW(HISTORY)
    ldi r20,HISTORY_LENGTH
loadNewPieceCheck:          ; Compare byte of history (r17) 
    ld r17, Y+              ; with our chosen piece (r16)
    cp r16,r17
    breq lnpFoundInHistory

    dec r20
    brne loadNewPieceCheck
    
    ; Not in history - we're good to go.
    rjmp loadNewPieceUseit


lnpFoundInHistory:          ; If it's in the history, we only try
    dec r21                 ; so many times before using the piece anyway.
    brne loadNewPieceRnd


loadNewPieceUseit:  
    ldi YH, HIGH(HISTORY)   ; Now push our chosen piece onto the history
    ldi YL,  LOW(HISTORY)
    ldi r20,HISTORY_LENGTH
lnpUpdateHistory:
    ldd r17, Y+1            ; Move each byte one place along
    st Y+, r17

    dec r20
    brne lnpUpdateHistory
    ; Now place the new byte at the end
    sts (HISTORY+HISTORY_LENGTH-1), r16

    lsl r16                 ; Multiply by 4 since 'nextPiece' contains
    lsl r16                 ; rotation info also. ( We do NOT want to randomize
    mov nextPiece, r16      ; initial rotation)
    
    ret
    


GameOver:
    ; Show that the piece collides
    rcall placeOntoGrid
    
    ldi YH, HIGH(GRID+46)
    ldi YL,  LOW(GRID+46)
    
    call allNotesOff
    ldi r23,40
    
GameOverLoop1:
    dec r23
    mov r16,r23
    call setNote1

    ldi r16,$FF
    st -Y,r16
    st -Y,r16
    sbiw Y,2
    ldi r16,$00
    st Y+,r16
    st Y+,r16
    
    rcall drawGrid

    cpi YL,  LOW(GRID)
    brne GameOverLoop1  
    cpi YH, HIGH(GRID)
    brne GameOverLoop1
    

    
    ; go to boot Highscore table
    ;jmp 0
    call allNotesOff
    jmp BootRunHighscores
    ;////////////////
    

    
    
PauseGame:
    call allnotesoff
    ldi r16, 190 ; X
    ldi r17, 100 ; Y

    ldi r18, '*'
    rcall drawCharDouble
    ldi r18, '*'
    rcall drawCharDouble
    ldi r18, ' '
    rcall drawCharDouble
    ldi r18, 'P'
    rcall drawCharDouble
    ldi r18, 'A'
    rcall drawCharDouble
    ldi r18, 'U'
    rcall drawCharDouble
    ldi r18, 'S'
    rcall drawCharDouble
    ldi r18, 'E'
    rcall drawCharDouble
    ldi r18, ' '
    rcall drawCharDouble
    ldi r18, '*'
    rcall drawCharDouble
    ldi r18, '*'
    rcall drawCharDouble

    rcall readController
    sbrs r3, ctrlStart
    rjmp PauseGame


    ret



; Function emptyGrid - resets grid data
emptyGrid:
    ldi ZH, HIGH(GRID)
    ldi ZL,  LOW(GRID)
    ldi r17,23
emptyGridLoop:
    ldi r16,0b00000111
    st Z+,r16
    ldi r16,0b11100000
    st Z+,r16
    dec r17
    brne emptyGridLoop
    
    ldi r16,0b11111111  ; Block up the floor
    st Z+,r16
    st Z+,r16
    st Z+,r16
    st Z+,r16
    st Z+,r16
    st Z+,r16
    ret



drawGrid:
    ldi r17, (GRIDY-2)
    ldi r16, (GRIDX-2)
    ldi r18, (BLOCKSIZE*10+4)
    ldi r19, (BLOCKSIZE*20+4)
    rcall drawRectangle
    


    ldi r17,GRIDY           ; Y coord to top

    ldi ZH, HIGH(GRID+6)    ; Start of visible grid
    ldi ZL,  LOW(GRID+6)
    ldi r20,20              ; 20 rows
drawGridLoop:
    ; Draw block row
    
    .def RowByte=r24
    
drawRowRight:
    ldi r16,GRIDX           ; X coord to start of row
    ld RowByte, Z+          ; Load right hand side of row
    ldi r21, 5              ; 5 blocks to draw in this byte
drawRowRightLoop:
    sbrc RowByte, 3         ; Go through the five bits that matter
    rcall drawBlock         ; draw if set
    subi r16, (-BLOCKSIZE)  ; advance cursor X
    lsr RowByte             ; shift byte to draw next block
    
    dec r21
    brne drawRowRightLoop
    
drawRowLeft:                ; Repeat for left row
    ld RowByte, Z+
    ldi r21, 5
drawRowLeftLoop:
    sbrc RowByte, 0         ; Check bit 0 this time
    rcall drawBlock
    subi r16, (-BLOCKSIZE)
    lsr RowByte
    
    dec r21
    brne drawRowLeftLoop

drawRowEnd: 
    subi r17,(-BLOCKSIZE)
    dec r20
    brne drawGridLoop

    ret



;function drawPiece
drawPiece:
    ;move Z pointer to prog mem for current piece map
    rcall pointBlockMap

    ;move x and y cursor to coords of block
    ldi r18, BLOCKSIZE

    mul currentPieceX, r18
    mov r19,r0
    mul currentPieceY, r18
    mov r17,r0
    
    subi r19, -GRIDX +BLOCKSIZE*3       ; Offset of the grid position and
    subi r17, -GRIDY +BLOCKSIZE*3       ; the origin of the blockmap

    ldi r20,4
drawPieceLoopY:
    lpm r18,Z+          ; Load next byte (1 row)
    
    mov r16, r19
    ldi r21, 4
drawPieceLoopX:
    sbrc r18, 0
    rcall drawBlock
    subi r16, (-BLOCKSIZE)
    lsr r18
    
    dec r21
    brne drawPieceLoopX
    
    subi r17, (-BLOCKSIZE)
    dec r20
    brne drawPieceLoopY

    ret


; Function pointBlockMap
; Just moves the Z pointer to the area of prog mem for current piece
pointBlockMap:
    ldi ZH, HIGH(blockMaps*2)
    ldi ZL,  LOW(blockMaps*2)
    mov r16, currentPiece
    lsl r16                 ; Multiply by 4
    lsl r16
    add ZL, r16
    ldi r16,0
    adc ZH, r16
    ret



drawScore:
    ldi r16, 85 ; X
    ldi r17, 40 ; Y

    ; For such a small amount of text, this is faster
    ; than loading from program memory.
    ldi r18, 'N'
    rcall drawCharDouble
    ldi r18, 'E'
    rcall drawCharDouble
    ldi r18, 'X'
    rcall drawCharDouble
    ldi r18, 'T'
    rcall drawCharDouble

    ldi r16, 85
    ldi r17, 120

    ldi r18, 'S'
    rcall drawCharDouble
    ldi r18, 'C'
    rcall drawCharDouble
    ldi r18, 'O'
    rcall drawCharDouble
    ldi r18, 'R'
    rcall drawCharDouble
    ldi r18, 'E'
    rcall drawCharDouble

    ldi r16, 85
    subi r17, -16
    lds r20, SCORE_L
    lds r21, SCORE_M
    lds r22, SCORE_H
    rcall drawDec24bit

    ldi r16, 85
    subi r17, -32
    ldi r18, 'L'
    rcall drawCharDouble
    ldi r18, 'E'
    rcall drawCharDouble
    ldi r18, 'V'
    rcall drawCharDouble
    ldi r18, 'E'
    rcall drawCharDouble
    ldi r18, 'L'
    rcall drawCharDouble
    ldi r16, 85
    subi r17, -16
    lds r20, CURLEVEL
    rcall drawDecByte

    ldi r16, 85
    subi r17, -32
    ldi r18, 'L'
    rcall drawCharDouble
    ldi r18, 'I'
    rcall drawCharDouble
    ldi r18, 'N'
    rcall drawCharDouble
    ldi r18, 'E'
    rcall drawCharDouble
    ldi r18, 'S'
    rcall drawCharDouble
    ldi r16, 85
    subi r17, -16
    lds r20, NUMLINES
    rcall drawDecByte
    ret
    

; function drawBlock(x=r16,y=r17)
drawBlock:
    push r18
    ldi r18,(BLOCKSIZE-2)
drawBlockL1:
    rcall drawBlockPixel
    rcall drawBlockPixel    ; Slewrate compensation
    rcall drawBlockPixel
    inc r17
    dec r18
    brne drawBlockL1
    ldi r18,(BLOCKSIZE-2)
drawBlockL2:
    rcall drawBlockPixel
    inc r16
    dec r18
    brne drawBlockL2
    ldi r18,(BLOCKSIZE-2)
drawBlockL3:
    rcall drawBlockPixel
    dec r17
    dec r18
    brne drawBlockL3
    ldi r18,(BLOCKSIZE-2)
drawBlockL4:
    rcall drawBlockPixel
    dec r16
    dec r18
    brne drawBlockL4
    subi r17,-2
    subi r16,-1
    ldi r18,(BLOCKSIZE-5)
drawBlockL5:
    rcall drawBlockPixel
    inc r17
    dec r18
    brne drawBlockL5
    ldi r18,(BLOCKSIZE-4)
drawBlockL6:
    rcall drawBlockPixel
    inc r16
    dec r18
    brne drawBlockL6
    subi r16, (BLOCKSIZE-3)
    subi r17, (BLOCKSIZE-3)
    
    pop r18
    ret

drawBlockPixel:
    out PORTA, r16
    out PORTC, r17
    nop
;   nop
;   nop
;   nop
;   nop
;   nop
;   nop
;   nop
;   nop
;   nop
    
    ret
    
//  Function drawRectangle (x,y,width,height)
drawRectangle:
    ;Function arguments
.def x0=r16
.def y0=r17
.def width=r18
.def height=r19
    
    push height
    out PORTA,x0

drawRectDown:
    inc y0
    out PORTC,y0
    rcall dRectDel
    dec height
    brne drawRectDown
    pop height  
    push width

drawRectRight:  
    inc x0
    out PORTA,x0
    rcall dRectDel
    dec width
    brne drawRectRight
    pop width

drawRectUp:
    dec y0
    out PORTC,y0
    rcall dRectDel
    dec height
    brne drawRectUp

drawRectLeft:   
    dec x0
    out PORTA,x0
    rcall dRectDel
    dec width
    brne drawRectLeft

    ret 
    
dRectDel:
    nop
    nop
    nop
    nop
    nop
    ret

; Function drawBar (y = r19)
drawBar:
    push r18
    push r19
    ldi r20, (BLOCKSIZE>>1)
    ldi r16, GRIDX
    mov r17, r19
drawBarLoop:
    ldi r18, (BLOCKSIZE*10)
    ldi r19, 2
    rcall drawRectangle
    inc r17
    inc r17
    dec r20
    brne drawBarLoop
    
    pop r19
    pop r18
    ret


/* Function drawDecByte(x,y,byte)
 * draws a one byte number in decimal without leading zero
 */
drawDecByte:
    push r18
    ldi r18, '0'-1
    cpi r20, 100
    brcc drawByteCheckHund
    cpi r20, 10
    brcc drawByteCheckTens
    rjmp drawByteOnes
drawByteCheckHund:
    inc r18
    subi r20, 100
    brcc drawByteCheckHund
    subi r20, -100
    rcall drawCharDouble
    ldi r18, '0'-1
drawByteCheckTens:
    inc r18
    subi r20, 10
    brcc drawByteCheckTens
    subi r20, -10
    rcall drawCharDouble
drawByteOnes:
    ldi r18, '0'
    add r18, r20
    rcall drawCharDouble

    pop r18
    ret

/* Function drawDec24bit(x,y,low,med,high)
 * draws a three byte number in decimal up to 9,999,999
 */
drawDec24bit:
    ; arguments
    ; x    = r16    ; x and y pass through to drawCharDouble unchanged
    ; y    = r17
.def byteL = r20
.def byteM = r21
.def byteH = r22
    ; local
    push r18
    push r23
    ldi r18, '0'-1
    
    cpi r20, BYTE1(1000000)     ; Remove leading zeros...
    ldi r23, BYTE2(1000000)
    cpc r21, r23
    ldi r23, BYTE3(1000000)
    cpc r22, r23
    brcc d24bitChkMil
    
    cpi r20, BYTE1(100000)
    ldi r23, BYTE2(100000)
    cpc r21, r23
    ldi r23, BYTE3(100000)
    cpc r22, r23
    brcc d24bitChkHundThou
    
    cpi r20,  LOW(10000)
    ldi r23, HIGH(10000)
    cpc r21, r23
    brcc d24bitChkTenThou
    
    cpi r20,  LOW(1000)
    ldi r23, HIGH(1000)
    cpc r21, r23
    brcc d24bitChkThou
    
    cpi r20,  LOW(100)
    ldi r23, HIGH(100)
    cpc r21, r23
    brcc d24bitChkHund
    
    cpi r20,  10
    brcc d24bitChkTens
    
    rjmp d24bitRemainder

d24bitChkMil:
    inc r18
    subi byteL,BYTE1(1000000)
    sbci byteM,BYTE2(1000000)
    sbci byteH,BYTE3(1000000)
    brcc d24bitChkMil
    
    subi byteL,BYTE1(-1000000)
    sbci byteM,BYTE2(-1000000)
    sbci byteH,BYTE3(-1000000)
    rcall drawCharDouble
    
    ldi r18, '0'-1
d24bitChkHundThou:
    inc r18
    subi byteL,BYTE1(100000)
    sbci byteM,BYTE2(100000)
    sbci byteH,BYTE3(100000)
    brcc d24bitChkHundThou
    
    subi byteL,BYTE1(-100000)
    sbci byteM,BYTE2(-100000)
    sbci byteH,BYTE3(-100000)
    rcall drawCharDouble

    ldi r18, '0'-1      ; From here on we know byteH = 0
d24bitChkTenThou:
    inc r18
    subi byteL, LOW(10000)
    sbci byteM,HIGH(10000)
    brcc d24bitChkTenThou
    
    subi byteL, LOW(-10000)
    sbci byteM,HIGH(-10000)

    rcall drawCharDouble

    ldi r18, '0'-1
d24bitChkThou:
    inc r18
    subi byteL, LOW(1000)
    sbci byteM,HIGH(1000)
    brcc d24bitChkThou
    
    subi byteL, LOW(-1000)
    sbci byteM,HIGH(-1000)

    rcall drawCharDouble

    ldi r18, '0'-1
d24bitChkHund:
    inc r18
    subi byteL, LOW(100)
    sbci byteM,HIGH(100)
    brcc d24bitChkHund

    subi byteL, LOW(-100)
    sbci byteM,HIGH(-100)

    rcall drawCharDouble

    ldi r18, '0'-1
d24bitChkTens:
    inc r18
    subi byteL, LOW(10)
    sbci byteM,HIGH(10)
    brcc d24bitChkTens

    subi byteL, LOW(-10)
    sbci byteM,HIGH(-10)

    rcall drawCharDouble
d24bitRemainder:
    ldi r18, '0'
    add r18, byteL

    rcall drawCharDouble

    pop r23
    pop r18
    ret


/* Function drawCharDouble(x0,y0,char)
 * Same as drawChar but twice the size
 */
drawCharDouble:
.def x0=r16
.def y0=r17
.def char=r18
    push r19
    push ZH
    push ZL
    ; Each character is a 5 byte bitmap

    ldi ZL,  LOW(bitmapFontTable*2)
    ldi ZH, HIGH(bitmapFontTable*2)
    subi char,32                    ; Font starts at ascii 32

    ldi r19,5
    mul char,r19                    ; Multiply char by 5
    movw char,r0                    ; Result is in R1:R0

    add ZL, char                    ; Add this to Z pointer
    adc ZH, r19

    ldi r19,5

drawCharDblLoopByte:
    lpm char, Z+
    push y0
    
drawCharDblLoopBit:

    sbrs char,0
    rjmp drawCharDblNextBit
    
    out PORTC,y0
    out PORTA,x0
    rcall delayChar
    
drawCharDblNextBit:
    dec y0
    dec y0
    lsr char
    brne drawCharDblLoopBit
    dec x0
    dec x0
    pop y0

    dec r19
    brne drawCharDblLoopByte
    
    dec x0                  ; Move cursor ready to draw next character
    dec x0
    pop ZL
    pop ZH
    pop r19
    ret

delayChar:
    push r16
    ldi r16,$07
count2:
    dec r16
    brne count2
    pop r16
    ret
    
/*  Function drawTextPM(x0,y0)
 *  draws a message from Z pointer program memory
 */
drawTextPM:
    out PORTC,y0
    out PORTA,x0
    push r18

drawTextPMLoop:
    lpm r18,Z+              ; Load next character
    cpi r18,0               ; (null terminated string)
    breq drawTextPMend
    rcall drawCharDouble    ; x and y are passed straight through to drawChar
    rjmp drawTextPMLoop

drawTextPMend:
    pop r18
    ret
    
; Function readController
; r2 is buttons held down. r3 is buttons only just pressed.
; r4 and r5 as state variables
readController:
    sbis SPSR,SPIF          ; Wait for previous reception to complete
    rjmp readController
    in r2,SPDR              ; Read received data
    com r2                  ; Invert (logic low is button down)

    ; Work out what buttons have just been pressed.
    ; r4 is the button states from the previous reading
    mov r3,r2               ; Copy into r3
    eor r3,r4               ; XOR gives us the buttons which have changed
    and r3,r2               ; AND it with the the buttons currently held down

    mov r4, r2              ; Save button states into r4 for next time
    
    push r16
    push r17

    ; Delayed-Auto-shift mechanism
    ; This is made slightly more complicated because we do not want to 
    ; apply repeated presses to the rotation buttons - so screen them out first
    ldi r17,$0F
    and r17,r2
    brne readCHoldingDir
    ldi r16,DAS_TIME
    mov r5,r16              ; reset DAS timer
    
readCHoldingDir:
    inc r5                  ; increase DAS timer
    brne readControllerEnd
    
    or r3, r17              ; Load the held direction into the 'pressed' register
    dec r5                  ; but don't affect the button presses
    dec r5                  ; Rewind a bit so that repetition is every 2 frames
    
readControllerEnd:
    ldi r16,0b00000000      ; Initiate the next reading
    out SPDR,r16
    pop r17
    pop r16
    ret




; Function setLevelFallSpeed
; Numbers found at http://tetrisconcept.net/wiki/Tetris_%28NES,_Nintendo%29
setLevelFallSpeed:
    lds r16,CURLEVEL            ; Above level 29, fall speed is 1.
    cpi r16,29
    brcc setLevelGt29
    
    ldi ZH, HIGH(levelFallSpeed*2)
    ldi ZL,  LOW(levelFallSpeed*2)
    add ZL,r16
    ldi r16,0
    adc ZH,r16
    lpm r16,Z
    sts FALLTIME,r16
    
    ret

setLevelGt29:
    ldi r16,1               ; Set speed to 1
    sts FALLTIME,r16
    ret
    

.include "3Dfunctions.asm"

; Level - fallspeed relation
levelFallSpeed:
.db 48,43,38,33,28,23,18,13,8,6,5,5,5,4,4,4,3,3,3,2,2,2,2,2,2,2,2,2,2,1


; Data mapping the shape of each possible piece, padded with zeros 
; to ease collision checking. 4 bytes per rotation, 4 rotations per piece.
blockMaps:
; I
.db \
0b00000000,\
0b00001111,\
0b00000000,\
0b00000000,\
0b00000010,\
0b00000010,\
0b00000010,\
0b00000010,\
0b00000000,\
0b00001111,\
0b00000000,\
0b00000000,\
0b00000010,\
0b00000010,\
0b00000010,\
0b00000010

; T
.db \
0b00000000,\
0b00001110,\
0b00000100,\
0b00000000,\
0b00000100,\
0b00001100,\
0b00000100,\
0b00000000,\
0b00000000,\
0b00000100,\
0b00001110,\
0b00000000,\
0b00000100,\
0b00000110,\
0b00000100,\
0b00000000

;L
.db \
0b00000000,\
0b00001110,\
0b00001000,\
0b00000000,\
0b00001100,\
0b00000100,\
0b00000100,\
0b00000000,\
0b00000000,\
0b00000010,\
0b00001110,\
0b00000000,\
0b00000100,\
0b00000100,\
0b00000110,\
0b00000000

;J
.db \
0b00000000,\
0b00001110,\
0b00000010,\
0b00000000,\
0b00000100,\
0b00000100,\
0b00001100,\
0b00000000,\
0b00000000,\
0b00001000,\
0b00001110,\
0b00000000,\
0b00000110,\
0b00000100,\
0b00000100,\
0b00000000

;S
.db \
0b00000000,\
0b00000110,\
0b00001100,\
0b00000000,\
0b00001000,\
0b00001100,\
0b00000100,\
0b00000000,\
0b00000000,\
0b00000110,\
0b00001100,\
0b00000000,\
0b00001000,\
0b00001100,\
0b00000100,\
0b00000000

;Z
.db \
0b00000000,\
0b00001100,\
0b00000110,\
0b00000000,\
0b00000010,\
0b00000110,\
0b00000100,\
0b00000000,\
0b00000000,\
0b00001100,\
0b00000110,\
0b00000000,\
0b00000010,\
0b00000110,\
0b00000100,\
0b00000000

;O
.db \
0b00000000,\
0b00000110,\
0b00000110,\
0b00000000,\
0b00000000,\
0b00000110,\
0b00000110,\
0b00000000,\
0b00000000,\
0b00000110,\
0b00000110,\
0b00000000,\
0b00000000,\
0b00000110,\
0b00000110,\
0b00000000


; Polygon list for the logo.
TetrisLogo:
.db \
7, \
 9, \
  253,7, 208,7, 208,25, 221,25, 221,89, 240,89, 240,25, 253,25, 253,7, \
 13, \
  205,7, 205,89, 153,89, 163,70, 188,70, 188,45, 179,45, 171,31, 188,31, 188,24, 174,24, 165,7, 205,7, \
 9, \
  164,7, 164,25, 151,25, 151,89, 133,89, 133,25, 120,25, 120,7, 164,7, \
 12, \
  118,7, 118,89, 101,89, 101,19, 93,19, 101,33, 73,89, 51,89, 80,32, 88,32, 73,7, 118,7, \
 5, \
  67,7, 67,25, 49,25, 49,7, 67,7, \
 5, \
  67,27, 67,54, 49,89, 49,27, 67,27, \
 13, \
  47,7, 47,25, 25,70, 42,70, 47,66, 47,89, 5,89, 5,66, 29,20, 15,20, 13,25, 6,7, 47,7



Tetris3DBlocks:
.db $3B,\
        $05,\
            $0A, $9C, $00,\
            $0A, $C4, $00,\
            $0A, $C4, $28,\
            $0A, $9C, $28,\
            $0A, $9C, $00,\
        $02,\
            $0A, $B0, $00,\
            $0A, $B0, $28,\
        $02,\
            $0A, $9C, $14,\
            $0A, $C4, $14,\
        $05,\
            $F6, $9C, $00,\
            $F6, $C4, $00,\
            $F6, $C4, $28,\
            $F6, $9C, $28,\
            $F6, $9C, $00,\
        $02,\
            $F6, $B0, $00,\
            $F6, $B0, $28,\
        $02,\
            $F6, $9C, $14,\
            $F6, $C4, $14,\
        $02,\
            $0A, $B0, $00,\
            $F6, $B0, $00,\
        $02,\
            $0A, $B0, $14,\
            $F6, $B0, $14,\
        $02,\
            $0A, $B0, $28,\
            $F6, $B0, $28,\
        $02,\
            $0A, $9C, $00,\
            $F6, $9C, $00,\
        $02,\
            $0A, $9C, $14,\
            $F6, $9C, $14,\
        $02,\
            $0A, $9C, $28,\
            $F6, $9C, $28,\
        $02,\
            $0A, $C4, $00,\
            $F6, $C4, $00,\
        $02,\
            $0A, $C4, $14,\
            $F6, $C4, $14,\
        $02,\
            $0A, $C4, $28,\
            $F6, $C4, $28,\
        $05,\
            $0A, $6E, $00,\
            $0A, $32, $00,\
            $0A, $32, $14,\
            $0A, $6E, $14,\
            $0A, $6E, $00,\
        $04,\
            $0A, $46, $00,\
            $0A, $46, $28,\
            $0A, $5A, $28,\
            $0A, $5A, $00,\
        $05,\
            $F6, $6E, $00,\
            $F6, $32, $00,\
            $F6, $32, $14,\
            $F6, $6E, $14,\
            $F6, $6E, $00,\
        $04,\
            $F6, $46, $00,\
            $F6, $46, $28,\
            $F6, $5A, $28,\
            $F6, $5A, $00,\
        $02,\
            $F6, $46, $00,\
            $0A, $46, $00,\
        $02,\
            $F6, $5A, $00,\
            $0A, $5A, $00,\
        $02,\
            $F6, $6E, $00,\
            $0A, $6E, $00,\
        $02,\
            $F6, $32, $00,\
            $0A, $32, $00,\
        $02,\
            $F6, $46, $14,\
            $0A, $46, $14,\
        $02,\
            $F6, $5A, $14,\
            $0A, $5A, $14,\
        $02,\
            $F6, $6E, $14,\
            $0A, $6E, $14,\
        $02,\
            $F6, $32, $14,\
            $0A, $32, $14,\
        $02,\
            $F6, $46, $28,\
            $0A, $46, $28,\
        $02,\
            $F6, $5A, $28,\
            $0A, $5A, $28,\
        $07,\
            $6E, $0A, $00,\
            $32, $0A, $00,\
            $32, $0A, $14,\
            $5A, $0A, $14,\
            $5A, $0A, $28,\
            $6E, $0A, $28,\
            $6E, $0A, $00,\
        $03,\
            $6E, $0A, $14,\
            $5A, $0A, $14,\
            $5A, $0A, $00,\
        $02,\
            $46, $0A, $14,\
            $46, $0A, $00,\
        $07,\
            $6E, $F6, $00,\
            $32, $F6, $00,\
            $32, $F6, $14,\
            $5A, $F6, $14,\
            $5A, $F6, $28,\
            $6E, $F6, $28,\
            $6E, $F6, $00,\
        $03,\
            $6E, $F6, $14,\
            $5A, $F6, $14,\
            $5A, $F6, $00,\
        $02,\
            $46, $F6, $14,\
            $46, $F6, $00,\
        $02,\
            $46, $F6, $00,\
            $46, $0A, $00,\
        $02,\
            $46, $F6, $14,\
            $46, $0A, $14,\
        $02,\
            $5A, $F6, $00,\
            $5A, $0A, $00,\
        $02,\
            $32, $F6, $00,\
            $32, $0A, $00,\
        $02,\
            $6E, $F6, $00,\
            $6E, $0A, $00,\
        $02,\
            $5A, $F6, $14,\
            $5A, $0A, $14,\
        $02,\
            $6E, $F6, $14,\
            $6E, $0A, $14,\
        $02,\
            $32, $F6, $14,\
            $32, $0A, $14,\
        $02,\
            $5A, $F6, $28,\
            $5A, $0A, $28,\
        $02,\
            $6E, $F6, $28,\
            $6E, $0A, $28,\
        $09,\
            $92, $0A, $00,\
            $BA, $0A, $00,\
            $BA, $0A, $14,\
            $CE, $0A, $14,\
            $CE, $0A, $28,\
            $A6, $0A, $28,\
            $A6, $0A, $14,\
            $92, $0A, $14,\
            $92, $0A, $00,\
        $04,\
            $A6, $0A, $00,\
            $A6, $0A, $14,\
            $BA, $0A, $14,\
            $BA, $0A, $28,\
        $09,\
            $92, $F6, $00,\
            $BA, $F6, $00,\
            $BA, $F6, $14,\
            $CE, $F6, $14,\
            $CE, $F6, $28,\
            $A6, $F6, $28,\
            $A6, $F6, $14,\
            $92, $F6, $14,\
            $92, $F6, $00,\
        $04,\
            $A6, $F6, $00,\
            $A6, $F6, $14,\
            $BA, $F6, $14,\
            $BA, $F6, $28,\
        $02,\
            $BA, $F6, $28,\
            $BA, $0A, $28,\
        $02,\
            $A6, $F6, $28,\
            $A6, $0A, $28,\
        $02,\
            $CE, $F6, $28,\
            $CE, $0A, $28,\
        $02,\
            $BA, $F6, $14,\
            $BA, $0A, $14,\
        $02,\
            $A6, $F6, $14,\
            $A6, $0A, $14,\
        $02,\
            $CE, $F6, $14,\
            $CE, $0A, $14,\
        $02,\
            $92, $F6, $14,\
            $92, $0A, $14,\
        $02,\
            $92, $F6, $00,\
            $92, $0A, $00,\
        $02,\
            $BA, $F6, $00,\
            $BA, $0A, $00,\
        $02,\
            $A6, $F6, $00,\
            $A6, $0A, $00


msgPressStart: .db "PRESS START",0
msgChooseALevel: .db "* SELECT A LEVEL *",0,"0 1 2 3 4",0,"5 6 7 8 9",0

bitmapFontTable: 
.db $00,$00,$00,$00,$00,$00,$00,$FA,$00,$00,$00,$E0,$00,$E0,$00,$28,$FE,$28,$FE,$28,$24,$54,$FE,$54,$48,$C4,$C8,$10,$26,$46,$6C,$92,$6A,$04,$0A,$00,$00,$E0,$00,$00,$38,$44,$82,$00,$00,$00,$00,$82,$44,$38,$44,$28,$FE,$28,$44,$10,$10,$7C,$10,$10,$00,$02,$0C,$00,$00,$10,$10,$10,$10,$10,$00,$00,$02,$00,$00,$04,$08,$10,$20,$40,$7C,$8A,$92,$A2,$7C,$00,$42,$FE,$02,$00,$46,$8A,$92,$92,$62,$84,$82,$92,$B2,$CC,$18,$28,$48,$FE,$08,$E4,$A2,$A2,$A2,$9C,$3C,$52,$92,$92,$8C,$80,$8E,$90,$A0,$C0,$6C,$92,$92,$92,$6C,$62,$92,$92,$94,$78,$00,$00,$28,$00,$00,$00,$02,$2C,$00,$00,$10,$28,$44,$82,$00,$28,$28,$28,$28,$28,$00,$82,$44,$28,$10,$40,$80,$9A,$A0,$40,$7C,$82,$BA,$9A,$72,$3E,$48,$88,$48,$3E,$FE,$92,$92,$92,$6C,$7C,$82,$82,$82,$44,$FE,$82,$82,$82,$7C,$FE,$92,$92,$92,$82,$FE,$90,$90,$90,$80,$7C,$82,$82,$8A,$8E,$FE,$10,$10,$10,$FE,$00,$82,$FE,$82,$00,$04,$02,$02,$02,$FC,$FE,$10,$28,$44,$82,$FE,$02,$02,$02,$02,$FE,$40,$30,$40,$FE,$FE,$20,$10,$08,$FE,$7C,$82,$82,$82,$7C,$FE,$90,$90,$90,$60,$7C,$82,$8A,$84,$7A,$FE,$90,$98,$94,$62,$64,$92,$92,$92,$4C,$80,$80,$FE,$80,$80,$FC,$02,$02,$02,$FC,$F8,$04,$02,$04,$F8,$FE,$04,$18,$04,$FE,$C6,$28,$10,$28,$C6,$C0,$20,$1E,$20,$C0,$86,$8A,$92,$A2,$C2,$00,$FE,$82,$82,$00,$40,$20,$10,$08,$04,$00,$82,$82,$FE,$00,$20,$40,$80,$40,$20,$01,$01,$01,$01,$01,$00,$80,$40,$20,$00,$04,$2A,$2A,$2A,$1E,$FE,$22,$22,$22,$1C,$1C,$22,$22,$22,$22,$1C,$22,$22,$22,$FE,$1C,$2A,$2A,$2A,$1A,$10,$7E,$90,$90,$40,$18,$25,$25,$25,$1E,$FE,$20,$20,$20,$1E,$00,$22,$BE,$02,$00,$02,$01,$21,$BE,$00,$FE,$08,$08,$14,$22,$00,$82,$FE,$02,$00,$3E,$20,$1C,$20,$3E,$3E,$20,$20,$20,$1E,$1C,$22,$22,$22,$1C,$3F,$24,$24,$24,$18,$18,$24,$24,$24,$3F,$3E,$10,$20,$20,$20,$12,$2A,$2A,$2A,$24,$20,$FC,$22,$22,$04,$3C,$02,$02,$04,$3E,$38,$04,$02,$04,$38,$3E,$02,$0C,$02,$3E,$22,$14,$08,$14,$22,$38,$05,$05,$05,$3E,$22,$26,$2A,$32,$22,$10,$6C,$82,$82,$00,$00,$00,$FF,$00,$00,$00,$82,$82,$6C,$10,$40,$80,$C0,$40,$80,$54,$28,$54,$28,$54,$00,$00,$00,$00,$00



soundEffect1:
.db $FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,$FF,0
soundRotate:
.db 35,35,35,35,35,43,43,43,43,43,43,43,43,43,0
soundLineClear:
.db 60,60,60,60,60,60,52,52,52,52,52,52,60,60,60,60,60,60,52,52,52,52,52,52,0




.include "audio.asm"
Music:
.include "Music/TetrisMusic1.asm"


.include "Bootloader.asm"

Our voyage reaches new shores in the next section.