Soviet Bloc Game (C version)
While thinking about ways to make my TRS-80 model I more useable in 2019, I realized that I could not easily find a version of Tetris for it (probably because the TRS-80 came out about 10ish years before Tetris was invented), so I thought I might try to write my own after watching an episode of the 8-bit Guy where David does the same. I used to play the Gameboy version of Tetris all the time on my Ti-nSpire calculator thanks to the nspire hacking scene and an emulator port, so that's the style of Tetris I will try to recreate.
This ended up being a sort of 24 hour challenge, where I created a color version of Tetris with basic animations, scoring, and levels, just like the GameBoy version.
Theory of Operation
The Code
- define pfsizeX 10
- define pfsizeY 22 //note that top 4 lines are not drawn
uint8_t playfield[pfsizeX][pfsizeY]; //the game area
uint8_t pieceC[4][4]; //piece container
int8_t pieceCX; //location of upper left corner of piece container
int8_t pieceCY;
uint8_t pieceT; //type of piece
uint8_t pieceR; //rotation of piece
uint8_t nextpieceT;
uint8_t nextpieceR;
uint8_t dcontrol; //locks in control every frame
uint8_t rcontrol;
uint8_t lastdcontrol=0; //for "debouncing" of inputs
uint8_t lastrcontrol=0;
uint8_t drepeatframe=0;
- define drepeatframes 3 //wait _ frames before repeatedly going in one direction
uint8_t dropframe=0; //counter for number of frames between block drops
uint8_t level=0; //LEVEL decreases frame drop from 20 frames to 0 frames (levels 0 to 20)
boolean ngame=0; //when set, starts new game
uint16_t lines=0; //NUMBER OF LINES CLEARED
uint32_t score=0; //TOTAL SCORE (using NES rules)
uint8_t fdrop; //number of blocks that piece has been fast dropped
uint8_t lslvi=0; //lines since level increase (when this gets to 10, increase the level)
- include <U8g2lib.h>
U8G2_ST7920_128X64_1_HW_SPI u8g2(U8G2_R0, /* CS=*/ 12, /* reset=*/ 8);
void dispscreen(){
u8g2.firstPage();
do {
for(uint8_t j=4; j<pfsizeY; j++){ //draw screen
for(uint8_t i=0; i<pfsizeX; i++){
if(playfield[i][j]!=0){
u8g2.drawBox(5*(j-4),5*(pfsizeX-1)-5*i,6,6);
}else{
u8g2.drawFrame(5*(j-4),5*(pfsizeX-1)-5*i,6,6);
//if(j==3){u8g2.drawLine(5*j,5*(pfsizeX-1)-5*i,5*j+5,5*(pfsizeX-1)-5*i+5);}
}
}}
uint8_t temppieceT=pieceT;
uint8_t temppieceR=pieceR;
pieceT=nextpieceT;
pieceR=nextpieceR;
loadpiece();
for(uint8_t j=0; j<4; j++){ //draw next piece
for(uint8_t i=0; i<4; i++){
if(pieceC[i][j]!=0){
u8g2.drawBox(5*(j+pfsizeY-3),5*(pfsizeX-1)-5*i,6,6);
}else{
u8g2.drawFrame(5*(j+pfsizeY-3),5*(pfsizeX-1)-5*i,6,6);
}
}}
pieceT=temppieceT;
pieceR=temppieceR;
loadpiece();
u8g2.setFont(u8g2_font_6x10_tf);
char zbuffer[32];
sprintf(zbuffer, "N%d", lines);
u8g2.drawStr(0,60, zbuffer);
sprintf(zbuffer, "L%d", level);
u8g2.drawStr(25,60, zbuffer);
sprintf(zbuffer, "S%d", score);
u8g2.drawStr(50,60, zbuffer);
} while( u8g2.nextPage() );
}
void controls(){
if(digitalRead(A3)==0){ngame=1;}
if(dcontrol==0){
if(analogRead(A7)<10){dcontrol=4;}
if(analogRead(A7)>1014){dcontrol=6;}
if(analogRead(A6)>1014){dcontrol=8;}
if(analogRead(A6)<10){dcontrol=2;}
if(dcontrol==0){drepeatframe=0;}
if(dcontrol==lastdcontrol){ //short delay before fast motion
if(drepeatframe<=drepeatframes){
drepeatframe++;
dcontrol=3; //lockout if within lockout period
}
}
}
if(rcontrol==0){
if(digitalRead(A4)==0){rcontrol=1;}
if(digitalRead(A5)==0){rcontrol=2;}
if(rcontrol==lastrcontrol){rcontrol=3;}
}
}
void clearControls(){
if(rcontrol!=3){lastrcontrol=rcontrol;}
if(dcontrol!=3){lastdcontrol=dcontrol;}
rcontrol=0;
dcontrol=0;
}
void draw(){
loadpiece(); //load piece into the piece carrier
for(uint8_t i=0; i<pfsizeX; i++){ //clear any cells with active piece parts (will be written again with new pieceC
for(uint8_t j=0; j<pfsizeY; j++){
if(playfield[i][j]<=7){playfield[i][j]=0;}
}
}
for(uint8_t i=0; i<4; i++){ //copy active piece onto the playfield
for(uint8_t j=0; j<4; j++){
if(pieceCX+i>=0&&pieceCX+i<pfsizeX&&pieceCY+j>=0&&pieceCY+j<pfsizeY){//check if piece segment can be drawn on screen
if(pieceC[i][j]!=0){playfield[i+pieceCX][j+pieceCY]=pieceC[i][j];}
}
}
}
}
boolean checkCollide(){ //move the piece carrier first, then check if anything collides
loadpiece(); //load piece into the piece carrier
boolean nonvalidity=0;
for(uint8_t i=0; i<4; i++){ //run through all piece carrier cells
for(uint8_t j=0; j<4; j++){
if(pieceCX+i>=0&&pieceCX+i<pfsizeX&&pieceCY+j>=0&&pieceCY+j<pfsizeY){ //check if piece carrier segment can be drawn on screen
if(pieceC[i][j]!=0&&playfield[i+pieceCX][j+pieceCY]>7){ //if both background and nonzero piece carrier segment collide
nonvalidity=1;
}
}else{ //this segment of PC can't be drawn on the screen
if(pieceC[i][j]!=0){ //a filled in segment would be drawn offscreen
nonvalidity=1;
}
}
}
}
return nonvalidity;
}
void piece2bg(){
for(uint8_t i=0; i<4; i++){ //copy active piece onto the screen
for(uint8_t j=0; j<4; j++){
if(pieceC[i][j]!=0){playfield[i+pieceCX][j+pieceCY]=pieceC[i][j]+8;} //copy the piece into the playfield/background
}}
nextpiece();
}
void nextpiece(){//generate the next piece and move PC back to the top
pieceT=nextpieceT;
pieceR=nextpieceR;
nextpieceT = rand()%7 + 1;
nextpieceR = rand()%4 + 1;
pieceCY=0; //move piece carrier back to the top of screen
pieceCX=3;
}
void newgamemon(){
if(ngame==0){return;}
Serial.println(F("NEW GAME"));
ngame=0;
for(uint8_t i=0; i<pfsizeX; i++){ //clear any cells with active piece parts (will be written again with new pieceC
for(uint8_t j=0; j<pfsizeY; j++){
playfield[i][j]=0;
}
}
lines=0;
level=0;
score=0;
lslvi=0;
fdrop=0;
nextpiece();
nextpiece();
}
void loadpiece(){ //hardcoded all pieces
switch(pieceT){
case 1: //long one
switch(pieceR){
case 1:
case 3:
pieceC[0][0]=0; pieceC[1][0]=0; pieceC[2][0]=0; pieceC[3][0]=0;
pieceC[0][1]=0; pieceC[1][1]=0; pieceC[2][1]=0; pieceC[3][1]=0;
pieceC[0][2]=1; pieceC[1][2]=1; pieceC[2][2]=1; pieceC[3][2]=1;
pieceC[0][3]=0; pieceC[1][3]=0; pieceC[2][3]=0; pieceC[3][3]=0;
break;
case 2:
case 4:
pieceC[0][0]=0; pieceC[1][0]=0; pieceC[2][0]=1; pieceC[3][0]=0;
pieceC[0][1]=0; pieceC[1][1]=0; pieceC[2][1]=1; pieceC[3][1]=0;
pieceC[0][2]=0; pieceC[1][2]=0; pieceC[2][2]=1; pieceC[3][2]=0;
pieceC[0][3]=0; pieceC[1][3]=0; pieceC[2][3]=1; pieceC[3][3]=0;
break;
}
break;
case 2: //backwards L
switch(pieceR){
case 1:
pieceC[0][0]=0; pieceC[1][0]=0; pieceC[2][0]=0; pieceC[3][0]=0;
pieceC[0][1]=0; pieceC[1][1]=0; pieceC[2][1]=0; pieceC[3][1]=0;
pieceC[0][2]=2; pieceC[1][2]=2; pieceC[2][2]=2; pieceC[3][2]=0;
pieceC[0][3]=0; pieceC[1][3]=0; pieceC[2][3]=2; pieceC[3][3]=0;
break;
case 2:
pieceC[0][0]=0; pieceC[1][0]=0; pieceC[2][0]=0; pieceC[3][0]=0;
pieceC[0][1]=0; pieceC[1][1]=2; pieceC[2][1]=0; pieceC[3][1]=0;
pieceC[0][2]=0; pieceC[1][2]=2; pieceC[2][2]=0; pieceC[3][2]=0;
pieceC[0][3]=2; pieceC[1][3]=2; pieceC[2][3]=0; pieceC[3][3]=0;
break;
case 3:
pieceC[0][0]=0; pieceC[1][0]=0; pieceC[2][0]=0; pieceC[3][0]=0;
pieceC[0][1]=2; pieceC[1][1]=0; pieceC[2][1]=0; pieceC[3][1]=0;
pieceC[0][2]=2; pieceC[1][2]=2; pieceC[2][2]=2; pieceC[3][2]=0;
pieceC[0][3]=0; pieceC[1][3]=0; pieceC[2][3]=0; pieceC[3][3]=0;
break;
case 4:
pieceC[0][0]=0; pieceC[1][0]=0; pieceC[2][0]=0; pieceC[3][0]=0;
pieceC[0][1]=0; pieceC[1][1]=2; pieceC[2][1]=2; pieceC[3][1]=0;
pieceC[0][2]=0; pieceC[1][2]=2; pieceC[2][2]=0; pieceC[3][2]=0;
pieceC[0][3]=0; pieceC[1][3]=2; pieceC[2][3]=0; pieceC[3][3]=0;
break;
}
break;
case 3: //L
switch(pieceR){
case 1:
pieceC[0][0]=0; pieceC[1][0]=0; pieceC[2][0]=0; pieceC[3][0]=0;
pieceC[0][1]=0; pieceC[1][1]=0; pieceC[2][1]=0; pieceC[3][1]=0;
pieceC[0][2]=3; pieceC[1][2]=3; pieceC[2][2]=3; pieceC[3][2]=0;
pieceC[0][3]=3; pieceC[1][3]=0; pieceC[2][3]=0; pieceC[3][3]=0;
break;
case 2:
pieceC[0][0]=0; pieceC[1][0]=0; pieceC[2][0]=0; pieceC[3][0]=0;
pieceC[0][1]=3; pieceC[1][1]=3; pieceC[2][1]=0; pieceC[3][1]=0;
pieceC[0][2]=0; pieceC[1][2]=3; pieceC[2][2]=0; pieceC[3][2]=0;
pieceC[0][3]=0; pieceC[1][3]=3; pieceC[2][3]=0; pieceC[3][3]=0;
break;
case 3:
pieceC[0][0]=0; pieceC[1][0]=0; pieceC[2][0]=0; pieceC[3][0]=0;
pieceC[0][1]=0; pieceC[1][1]=0; pieceC[2][1]=3; pieceC[3][1]=0;
pieceC[0][2]=3; pieceC[1][2]=3; pieceC[2][2]=3; pieceC[3][2]=0;
pieceC[0][3]=0; pieceC[1][3]=0; pieceC[2][3]=0; pieceC[3][3]=0;
break;
case 4:
pieceC[0][0]=0; pieceC[1][0]=0; pieceC[2][0]=0; pieceC[3][0]=0;
pieceC[0][1]=0; pieceC[1][1]=3; pieceC[2][1]=0; pieceC[3][1]=0;
pieceC[0][2]=0; pieceC[1][2]=3; pieceC[2][2]=0; pieceC[3][2]=0;
pieceC[0][3]=0; pieceC[1][3]=3; pieceC[2][3]=3; pieceC[3][3]=0;
break;
}
break;
case 4: //s shape
switch(pieceR){
case 1:
case 3:
pieceC[0][0]=0; pieceC[1][0]=0; pieceC[2][0]=0; pieceC[3][0]=0;
pieceC[0][1]=0; pieceC[1][1]=0; pieceC[2][1]=0; pieceC[3][1]=0;
pieceC[0][2]=0; pieceC[1][2]=4; pieceC[2][2]=4; pieceC[3][2]=0;
pieceC[0][3]=4; pieceC[1][3]=4; pieceC[2][3]=0; pieceC[3][3]=0;
break;
case 2:
case 4:
pieceC[0][0]=0; pieceC[1][0]=0; pieceC[2][0]=0; pieceC[3][0]=0;
pieceC[0][1]=0; pieceC[1][1]=4; pieceC[2][1]=0; pieceC[3][1]=0;
pieceC[0][2]=0; pieceC[1][2]=4; pieceC[2][2]=4; pieceC[3][2]=0;
pieceC[0][3]=0; pieceC[1][3]=0; pieceC[2][3]=4; pieceC[3][3]=0;
break;
}
break;
case 5: //T shape
switch(pieceR){
case 1:
pieceC[0][0]=0; pieceC[1][0]=0; pieceC[2][0]=0; pieceC[3][0]=0;
pieceC[0][1]=0; pieceC[1][1]=0; pieceC[2][1]=0; pieceC[3][1]=0;
pieceC[0][2]=5; pieceC[1][2]=5; pieceC[2][2]=5; pieceC[3][2]=0;
pieceC[0][3]=0; pieceC[1][3]=5; pieceC[2][3]=0; pieceC[3][3]=0;
break;
case 2:
pieceC[0][0]=0; pieceC[1][0]=0; pieceC[2][0]=0; pieceC[3][0]=0;
pieceC[0][1]=0; pieceC[1][1]=5; pieceC[2][1]=0; pieceC[3][1]=0;
pieceC[0][2]=5; pieceC[1][2]=5; pieceC[2][2]=0; pieceC[3][2]=0;
pieceC[0][3]=0; pieceC[1][3]=5; pieceC[2][3]=0; pieceC[3][3]=0;
break;
case 3:
pieceC[0][0]=0; pieceC[1][0]=0; pieceC[2][0]=0; pieceC[3][0]=0;
pieceC[0][1]=0; pieceC[1][1]=5; pieceC[2][1]=0; pieceC[3][1]=0;
pieceC[0][2]=5; pieceC[1][2]=5; pieceC[2][2]=5; pieceC[3][2]=0;
pieceC[0][3]=0; pieceC[1][3]=0; pieceC[2][3]=0; pieceC[3][3]=0;
break;
case 4:
pieceC[0][0]=0; pieceC[1][0]=0; pieceC[2][0]=0; pieceC[3][0]=0;
pieceC[0][1]=0; pieceC[1][1]=5; pieceC[2][1]=0; pieceC[3][1]=0;
pieceC[0][2]=0; pieceC[1][2]=5; pieceC[2][2]=5; pieceC[3][2]=0;
pieceC[0][3]=0; pieceC[1][3]=5; pieceC[2][3]=0; pieceC[3][3]=0;
break;
}
break;
case 6: //reverse s
switch(pieceR){
case 1:
case 3:
pieceC[0][0]=0; pieceC[1][0]=0; pieceC[2][0]=0; pieceC[3][0]=0;
pieceC[0][1]=0; pieceC[1][1]=0; pieceC[2][1]=0; pieceC[3][1]=0;
pieceC[0][2]=6; pieceC[1][2]=6; pieceC[2][2]=0; pieceC[3][2]=0;
pieceC[0][3]=0; pieceC[1][3]=6; pieceC[2][3]=6; pieceC[3][3]=0;
break;
case 2:
case 4:
pieceC[0][0]=0; pieceC[1][0]=0; pieceC[2][0]=0; pieceC[3][0]=0;
pieceC[0][1]=0; pieceC[1][1]=0; pieceC[2][1]=6; pieceC[3][1]=0;
pieceC[0][2]=0; pieceC[1][2]=6; pieceC[2][2]=6; pieceC[3][2]=0;
pieceC[0][3]=0; pieceC[1][3]=6; pieceC[2][3]=0; pieceC[3][3]=0;
break;
}
break;
case 7: //square
switch(pieceR){
default:
pieceC[0][0]=0; pieceC[1][0]=0; pieceC[2][0]=0; pieceC[3][0]=0;
pieceC[0][1]=0; pieceC[1][1]=7; pieceC[2][1]=7; pieceC[3][1]=0;
pieceC[0][2]=0; pieceC[1][2]=7; pieceC[2][2]=7; pieceC[3][2]=0;
pieceC[0][3]=0; pieceC[1][3]=0; pieceC[2][3]=0; pieceC[3][3]=0;
break;
}
break;
}
}
void setup(){
Serial.begin(115200);
u8g2.begin();
pinMode(A4, INPUT_PULLUP);
pinMode(A5, INPUT_PULLUP);
pinMode(A3, INPUT_PULLUP); //new game button
ngame=1;
newgamemon();
}
void loop() {
for(uint32_t h=0;h<=5;h++){//frame delay //DELAY SECTION (BETWEEN FRAMES)
delay(1);
controls();
newgamemon();
}
boolean droppiece=0;
dropframe++;
if(dropframe>=(20-level)){
dropframe=0;
droppiece=1;
}
if(rcontrol==1){
pieceR++; if(pieceR>=5){pieceR=1;} //try to rotate piece
if(checkCollide()){
pieceR--; if(pieceR<=0){pieceR=4;} //undo rotation
}
}
if(rcontrol==2){
pieceR--; if(pieceR<=0){pieceR=4;}
if(checkCollide()){
pieceR++; if(pieceR>=5){pieceR=1;}
}
}
if(dcontrol==4){
pieceCX--; //try and see what happens if we move the piece left
if(checkCollide()){//piece move is not valid
pieceCX++; //take piece back
}
}
if(dcontrol==6){
pieceCX++; //try and see what happens if we move the piece left
if(checkCollide()){//piece move is not valid
pieceCX--; //take piece back
}
}
if(dcontrol==8){
pieceCY--; //try and see what happens if we move the piece up
if(checkCollide()){//piece move is not valid
pieceCY++; //take piece back
}
}
if(dcontrol==2||droppiece==1){
pieceCY++; //try and see what happens if we move the piece down
if(!(drepeatframe<=drepeatframes)){ //is in fast mode
fdrop++;
}else{
fdrop=0;
}
if(checkCollide()){//piece move is not valid
pieceCY--; //take piece back
score+=fdrop; //add # of fast dropped blocks to score
fdrop=0;
piece2bg(); //copy piece to background and reset to next piece
}
}
draw();
dispscreen();
clearControls();
//check for line clears
boolean clearline[pfsizeY];
uint8_t clearedlines=0;
uint8_t templines=0;
for(uint8_t j=4; j<pfsizeY; j++){
clearline[j]=1; //assume line is cleared
for(uint8_t i=0; i<pfsizeX; i++){
if(playfield[i][j]<=7){clearline[j]=0;break;} //line is not full
}
clearedlines+=clearline[j];
templines+=clearline[j];
}
if(clearedlines>0){//breifly animate the cleared lines, then clear them
for(uint8_t f=0; f<=6; f++){
for(uint8_t j=4; j<pfsizeY; j++){
if(clearline[j]==1){
for(uint8_t i=0; i<pfsizeX; i++){
if(f%2==0){playfield[i][j]=8;}else{playfield[i][j]=0;}
}
}
}
draw();
dispscreen();
delay(200);
}
for(uint8_t j=4; j<pfsizeY; j++){ //accutally clear the lines
if(clearline[j]==1){
for(uint8_t t=j; t>=4; t--){
for(uint8_t i=0; i<pfsizeX; i++){
playfield[i][t]=playfield[i][t-1];
}
}
}
}
lines+=templines; //add number of lines to lines counter
switch(templines){ //calculate score
case 1:
score+=40*(level+1);
break;
case 2:
score+=100*(level+1);
break;
case 3:
score+=300*(level+1);
break;
default:
score+=1200*(level+1);
break;
}
for(uint8_t i=1; i<=templines; i++){ //see if the level needs increasing
lslvi++;
if(lslvi>=10){lslvi=0;level++;}
if(level>=20){level=20;}
}
}
//check gameover (scan through line 3 and see if there are any non-active pieces in it)
for(uint8_t i=0; i<pfsizeX; i++){
if(playfield[i][3]>7){
//*********************GAME OVER********************
Serial.println(F("GAME OVER"));
while(ngame==0){
controls();
}
newgamemon();
}
}
//check for completed lines
}