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 a 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!
'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.