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.
Objectives:
- Playfield the same size as GB version
- in color?
- same or similar scoring system to GB version
- same rotation style as GB version
- easily portable to different platforms (that are C like)
- somewhat documented
This ended up being a sort of 24 hour challenge; I started this project at about 11pm yesterday, and by about the time of this page's creation I had created an Arduino version of Tetris that met most of these objectives.
Theory of Operation
The basics of the game's functioning can be summed up with just a few components: a part that generates the tetrominoe shapes, a playfield that contains already played pieces and open spots where the active piece can move and collision detection that says where the active piece can and can't go.
The playfield size is adjustable, but in the gameboy version it's 10 wide by 18 tall. In my version I added an extra 4 lines on top (which are not to be displayed) for the new pieces to appear within. Within the program, it appears as a big 2D matrix called playfield[][]
.
Rather than figure out all the complex rotations of the part, I hardcoded them (using the Nintendo rotation system as described at https://tetris.fandom.com/wiki/Nintendo_Rotation_System).
The Code (Arduino version)
#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
}