Soviet Bloc Game (C version): Difference between revisions

From Alnwlsn - Projects Repository
Jump to navigation Jump to search
 
(38 intermediate revisions by the same user not shown)
Line 1: Line 1:
[[File:Wilson-tetris-trs80.png|thumb|Soviet Bloc Game v1.1 running in a trs-80 emulator]]
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.
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.
However, I have written few games, and never done a Tetris-like clone before, so I approach this mostly as a challenge.
 
Logal notice: "Tetris" is a trademark of The Tetris Company (who have been known to take down unauthorized trademark versions before: https://github.com/jdah/tetris-os), so I have decided to rename this clone "Soviet Bloc Game"
 
'''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 wellishly 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 Soviet Bloc Game that met most of these objectives.


==Theory of Operation==
==Theory of Operation==
===Basics===
The basics of the game's functioning can be summed up with just a few components: a part that generates the  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 <code>playfield[][]</code>.
====Drawing the pieces====
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 parts appear in a 4x4 piece container <code>pieceC[][]</code> matrix. This is then overlaid onto the playfield, and can be moved around. Nonzero cells of the matrix define the shape of the piece, like:
{| class="wikitable"
|-
| 0|| 0|| 0|| 0
|-
| 0|| 5|| 0|| 0
|-
| 5|| 5|| 0|| 0
|-
| 0|| 5|| 0|| 0
|}
The numbers used to define the piece are unique to each piece, so if you write the display section to use each number as a color, you can have a color game.
====Collision detection====
In the collision detection section, the overlaid piece carrier is moved as if you were to make that move, and then we check if any of the cells overlap with non-blank sections of the playfield, or if any part of the active piece lies outside the playfield area. If so, the move is undone, and you can decide what to do from there. If no collision occurs, the move is done for real and displayed the next time the screen is updated.
   
   
Usually, the collision detection is used to prevent motion if a piece or wall is in the way, but if the piece is attempting to move downwards, the piece is instead copied onto the playfield, and the numbers making up the piece are increased by 8. This is how the collision detection can differentiate between the active piece and dead pieces in the playfield.
====Input conditioning====
I added some input conditioning as well. For rotations of the active piece, the move is not repeating, and you must release the rotation input and press it again for a rotation to happen again. For moves left, right, and down, a system similar to repeating keys on a computer keyboard is used, holding down the direction for a brief moment will make the action repeat. In the gameboy version, dropping a piece quickly will increase the score based on the number of tiles dropped while moving fast. This is also replicated (but probably not perfectly) in my version.
===Details===
====Levels and speed====
I only update the screen every few moments. Call the time of one of these updates a '''frame'''. While the screen is waiting to be updated, it's a good time to check the controls and see what we need to do the next time the screen needs updating. All the processing of the moves and collisions happens just before the screen updates. Every few frames, the active piece drops one row. This is connected to the levels, which in my version go from 0-20, which I think is in line with the gb version. The drop happens every 20 frames at level 0, up to a delay of 0 frames at level 20. For every 10 lines that the user clears, the level is increased by 1, so that the game gets progressively faster, just like the gb version.
====Game over detection====
This one is pretty simple. If any non-active piece sections are found on the line above the top of the visible playfield, the game ends. Check this every frame.
====Line completion detection====
This is a little more complicated than the game over, but basically I check to see if any rows are completely filled in with dead pieces. I keep track of which lines are full (and how many of them there are), and animate the corresponding lines by having the whole row(s) flash for a moment. At this point I also add onto the number of completed lines counter displayed on the scoreboard. Then, I copy all the lines above the filled line down by one tile. Since this is done for each filled line in sequence, the effect moves the playfield down for all cases of any number of filled lines. 
====Scoring====
The number of filled lines cleared in one move (calculated above) is used in the scoring formula to add to the score. The full formula (according to the Nintendo scoring system) is:
* 1 line: 40*(level+1)
* 2 lines: 100*(level+1)
* 3 lines: 300*(level+1)
* 4 lines: 1200*(level+1)
Additionally, the number of grid tiles that are dropped by the user pressing "down" (ie, soft drop), which I kept track of earlier, are also added to the score.
====New game====
I have a new game button also. When pressed (and the controls are checked), it picks new pieces, clears the level, score, and lines counters, and empties the playfield of dead pieces. Thus, a new game begins.
==Hardware (Arduino version)==
I do most of my software development on arduino-like embedded systems, and am pretty familiar with I/O options on these devices (more so than on a stndard computer), so I used an arduino nano with a 128x64 st7920 monochrome LCD and the u8g2 libray for the display. For input, I wired up 3 buttons, 2 rotate and one new game, and an analog joystick (which I am using as a digital one) for directional moves.
===Porting===
With any luck, my code can be ported to different platforms just my changing the section that displays the playfield on a screen, the input sections that take in the button presses, and maybe the delay sections. On the Arduino, my code takes up about 50% of the prgram storage space, and 50% of ram, with most of that being due to the u8g2 library. I'm not an expert on the super low-level processes on the arduino, nor do I think my game is the most effceiently coded, but my guess is that if the game works well on a system with as low specifications as the atmega328, it should be able to run on nearly anything else.
==The Code (Arduino version)==
==The Code (Arduino version)==
<pre>
<pre>
Line 483: Line 546:
}
}
</pre>
</pre>
==The Code (TRS-80 version)==
I used the z88dk C compiler for the port. To compile, use <code>zcc  +trs80 -lndos -lm -create-app soviet-bloc-game.c -o soviet-bloc-game</code> which will create a <code>.CAS</code> file. Use <code>zcc  +trs80 -lndos -lm -create-app -subtype=disk soviet-bloc-game.c -o soviet-bloc-game</code> to create a <code>.CMD</code>
Porting this to the TRS-80 turned out to be a much harder challange, mostly because the TRS-80 is a very slow, low power machine. Some of the improvements were obvious, like only checking the lines and game over only when an active piece becomes inactive. However, this isn't enough to make a playable version. The TRS-80 is slow enough that every comparison, operation, and graphics plot add noticable delay. Therefore, I only update parts of the display when needed (ie, the area around the active piece, 1 block around the piece carrier); the background is filled back in to erase piece moves, and the screen is only redrawn at the end when the active piece becomes inactive. This is possible because unlike the arduino version, the whole screen doesn't need to be rewitten every time it changes. I also had to take out any frame delay and reduce the levels to 10 because the machine is not fast enough to keep up. The version below is therefore clunky, but playable. The obvious next steps are to make an assembler version of Soviet Bloc Game.
<pre>
//soviet-bloc-game.c - Soviet Bloc Game for TRS-80 - 05/28/2019
#include <stdio.h>
#include <stdlib.h>
#include <graphics.h>
#define pfsizeX 10 //playfield size (in cells)
#define pfsizeY 22 //note that top 4 lines are not drawn
#define pfX 8 //upper left corner of playfield location (pixel)
#define pfY 4
#define npX 60 //upper left corner of next piece indicator
#define npY 2
#define clspX 4 //spacing between cell elements (pixels)
#define clspY 2
void controls(void);
unsigned char playfield[pfsizeX][pfsizeY];  //the game area
unsigned char cscreen[pfsizeX][pfsizeY-4];  //a copy of what gets drawn on the screen (game area)
unsigned char pieceC[4][4]; //piece container
signed char pieceCX;  //location of upper left corner of piece container
signed char pieceCY;
unsigned char pieceT; //type of piece
unsigned char pieceR; //rotation of piece
unsigned char nextpieceT;
unsigned char nextpieceR;
unsigned char dcontrol; //locks in control every frame
unsigned char rcontrol;
unsigned char lastdcontrol=0; //for "debouncing" of inputs
unsigned char lastrcontrol=0;
unsigned char drepeatframe=0;
#define drepeatframes 3 //wait _ frames before repeatedly going in one direction
unsigned char dropframe=0;    //counter for number of frames between block drops
unsigned char level=0; //LEVEL decreases frame drop from maxlevel frames to 0 frames (levels 0 to MAXLEVEL)
#define maxlevel 10
unsigned char ngame=1; //when set, starts new game
unsigned char newpc=0; //when set, get a new piece
unsigned int lines=0; //NUMBER OF LINES CLEARED
long score=0; //TOTAL SCORE (using NES rules)
unsigned char fdrop;  //number of blocks that piece has been fast dropped
unsigned char lslvi=0; //lines since level increase (when this gets to 10, increase the level)
char lvladd=0;
void setblock(char x, char y){
for(char i=0; i<8; i++){
controls();
    plot(x+i%4,y+i/4);
}
}
void clearblock(char x, char y){
for(char i=0; i<8; i++){
controls();
    unplot(x+i%4,y+i/4);
}
}
void cdraw(char x, char y, char color){//only draws on screen if it needs to (uses cell coordinates)
if(cscreen[x][y]!=color){
cscreen[x][y]=color;
if(color!=0){
setblock( (x)*clspX+pfX, (y)*clspY+pfY );
}else{
clearblock( (x)*clspX+pfX, (y)*clspY+pfY);
}
}
}
void recopyscreen(){ //copies playfield to screen
  for(char i=0; i<pfsizeX; i++){ //manually redraw the entire screen
  for(char j=4; j<pfsizeY; j++){
  cdraw(i,j-4,playfield[i][j]);
  }}
  printf("%cY%c%c%ld", 27, 32+3, 32+47, score);
  printf("%cY%c%c%d", 27, 32+5, 32+47, level);
  printf("%cY%c%c%d", 27, 32+7, 32+47, lines);
}
void controls(){
/*
  if(bpeek(0x3802)&0b00000010){plot(126,46);}else{unplot(126,46);} //up button (I)
  if(bpeek(0x3802)&0b00000100){plot(125,47);}else{unplot(125,47);} //left button (J)
  if(bpeek(0x3802)&0b00001000){plot(126,47);}else{unplot(126,47);} //down button (K)
  if(bpeek(0x3802)&0b00010000){plot(127,47);}else{unplot(127,47);} //right button (L)
  if(bpeek(0x3801)&0b00000010){plot(121,47);}else{unplot(121,47);} //a button (A)
  if(bpeek(0x3804)&0b00001000){plot(122,47);}else{unplot(122,47);} //b button (S)
  if(bpeek(0x3801)&0b00000001){plot(119,47);}else{unplot(119,47);} //newgame (@)
  */
  if(bpeek(0x3840)&0b00000100){exit(0);}
  if(bpeek(0x3810)&0b00000010){lvladd=1;}
  if(bpeek(0x3810)&0b00000001){lvladd=-1;}
 
  if(bpeek(0x3801)&0b00000001){ngame=1;}
  if(bpeek(0x3808)&0b00000010){newpc=1;}
  if(dcontrol==0){
    if(bpeek(0x3802)&0b00000100){dcontrol=4;}
    if(bpeek(0x3802)&0b00010000){dcontrol=6;}
    if(bpeek(0x3802)&0b00000010){dcontrol=8;}
    if(bpeek(0x3802)&0b00001000){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(bpeek(0x3801)&0b00000010){rcontrol=1;}
    if(bpeek(0x3804)&0b00001000){rcontrol=2;}
    if(rcontrol==lastrcontrol){rcontrol=3;}
  }
}
void clearControls(){
  if(rcontrol!=3){lastrcontrol=rcontrol;}
  if(dcontrol!=3){lastdcontrol=dcontrol;}
  rcontrol=0;
  dcontrol=0;
}
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 nextpiece(){//generate the next piece and move PC back to the top
  unsigned char tempT=nextpieceT;
  unsigned char tempR=nextpieceR;
  nextpieceT = rand()%7 + 1;
  nextpieceR = rand()%4 + 1;
  pieceT=nextpieceT;
  pieceR=nextpieceR;
  loadpiece();
  for(char i=0; i<4; i++){
  for(char j=0; j<4; j++){
  if(pieceC[i][j]!=0){
  setblock((i)*clspX+npX, (j)*clspY+npY);
  }else{
  clearblock((i)*clspX+npX, (j)*clspY+npY);
  }
  }}
 
  pieceT=tempT;
  pieceR=tempR;
 
  pieceCY=0; //move piece carrier back to the top of screen
  pieceCX=3;
}
void mdraw(){
  //printf("%cY%c%c%d", 27, 32+1, 32+55, fff); fff++;
  loadpiece(); //load piece into the piece carrier
  for(char i=0; i<6; i++){ //check piece carrier and one block offset outside it
  for(char j=0; j<6; j++){
  controls();
  if((pieceCX-1+i)>=0&&(pieceCX-1+i)<pfsizeX&&(pieceCY-1+j)>=4&&(pieceCY-1+j)<(pfsizeY)){//check if this segment be drawn on screen
if(i>=1&&i<=4&&j>=1&&j<=4){//accessing piece carrier section here}
if(pieceC[i-1][j-1]>0){//if piece carrier block is confirmed should be white
cdraw(i-1+pieceCX,j-5+pieceCY,pieceC[i-1][j-1]);
}else{
cdraw(i-1+pieceCX,j-5+pieceCY,playfield[i-1+pieceCX][j-1+pieceCY]); //color it in with the existing playfield background
}
}else{
cdraw(i-1+pieceCX,j-5+pieceCY,playfield[i-1+pieceCX][j-1+pieceCY]);
}
  }
  }}
}
unsigned char checkCollide(){ //move the piece carrier first, then check if anything collides
  loadpiece(); //load piece into the piece carrier
  unsigned char nonvalidity=0;
  for(char i=0; i<4; i++){ //run through all piece carrier cells
    for(char 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 newgamemon(){
  if(ngame==0){return;}
  //Serial.println(F("NEW GAME"));
  printf("%cY%c%c        ", 27, 32+14, 32+40);
  printf("%cY%c%c        ", 27, 32+3, 32+47);
  printf("%cY%c%c        ", 27, 32+5, 32+47);
  printf("%cY%c%c        ", 27, 32+7, 32+47);
  ngame=0;
  for(unsigned char i=0; i<pfsizeX; i++){  //clear any cells with active piece parts (will be written again with new pieceC
    for(unsigned char j=0; j<pfsizeY; j++){
      playfield[i][j]=0;
    }
  }
  lines=0;
  level=0;
  score=0;
  lslvi=0;
  fdrop=0;
  nextpiece();
  nextpiece();
  recopyscreen();
}
void piece2bg(){ //will copy active piece into background and check for lines and game over
  for(unsigned char i=0; i<4; i++){ //copy piece onto background
  for(unsigned char 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();
 
  //check for line clears
  unsigned char clearline[pfsizeY];
  unsigned char clearedlines=0;
  unsigned char templines=0;
  for(unsigned char j=4; j<pfsizeY; j++){
  clearline[j]=1; //assume line is cleared
  for(unsigned char 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(unsigned char f=0; f<=4; f++){
    for(unsigned char j=4; j<pfsizeY; j++){
        if(clearline[j]==1){
          for(unsigned char i=0; i<pfsizeX; i++){
  cdraw(i,j-4,f%2);
          }
        }
      }
      t_delay(2000);
    }
    for(unsigned char j=4; j<pfsizeY; j++){ //accutally clear the lines
      if(clearline[j]==1){
        for(unsigned char t=j; t>=4; t--){
        for(unsigned char 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(unsigned char i=1; i<=templines; i++){ //see if the level needs increasing
    lslvi++;
    if(lslvi>=10){lslvi=0;level++;}
    if(level>=maxlevel){level=maxlevel;}
  }
  }
  //check gameover (scan through line 3 and see if there are any non-active pieces in it)
  for(unsigned char i=0; i<pfsizeX; i++){
    if(playfield[i][3]>7){
      //*********************GAME OVER********************
      //Serial.println(F("GAME OVER"));
  printf("%cY%c%cGAME OVER", 27, 32+14, 32+40);
      while(ngame==0){
        controls();
      }
      newgamemon();
    }
  }
 
  recopyscreen();
 
}
void main(){
printf("%cE", 27);
printf("%cY%c%cSoviet Bloc Game 2019", 27, 32+1, 32+40);
printf("%cY%c%cScore:", 27, 32+3, 32+40);
printf("%cY%c%cLevel:", 27, 32+5, 32+40);
printf("%cY%c%cLines:", 27, 32+7, 32+40);
printf("%cY%c%cNext", 27, 32+4, 32+30);
printf("%cY%c%cJ K L to move", 27, 32+10, 32+32);
printf("%cY%c%c A S  to rotate", 27, 32+11, 32+32);
printf("%cY%c%c  @  to start new game", 27, 32+12, 32+32);
drawb(pfX-2, pfY-1, (pfsizeX+1)*clspX, ((pfsizeY-3)*clspY));
ngame=1;
newgamemon();
while(1){ //*********************MAIN*PROGRAM*LOOP**************************
 
  //normally a frame delay would go here, but the trs-80 is slow and we need all the speed we can get.
 
  unsigned char droppiece=0;
  dropframe++;
  if(dropframe>=(maxlevel-level)){
    dropframe=0;
    droppiece=1;
  }
  newgamemon();
 
  if(lvladd!=0){
  level+=lvladd;
  lvladd=0;
  recopyscreen();
  }
 
  if(newpc!=0){
  newpc=0;
  nextpiece();
  recopyscreen();
  }
 
  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
    }
  }
 
  clearControls();
  mdraw();
}
}
</pre>
==Nintendo DS version==
'''This is ridiculous. We took the TRS-80 version and developed it for a platform made 27 years later. And it works.'''
[[File:Wilson-tetris-NDS.png|thumb|Nintendo DS Version running. Note the split screens, one text, one graphics. The DS gives you almost nothing built in, it expects all game data to be provided in the game (including fonts and images), and there are only a handful included with ndslib. For me, that means one screen with graphics, and one with monochrome text.]]
Sure, there are already plenty of games available for the Nintendo DS, but if all you have is a Soviet Bloc Game clone, everything starts to look like a nail. The NDS defines handheld gaming for the children of my generation, so when I found one at my local rummage sale and paired it up with a generic China Special flash card so it could run programs off a micro sd card, I knew it had to be done.
This is honestly the first time I have worked/played with an actual games machine since I was first learning to hack my Wii around 2010, and will be the first time I write a homebrew app of any kind.
My port uses libnds (or is that ndslib?), compiled with the devkitPro enviroment. There isn't much development on the NDS nowadays, but even in 2019 I found updated instructions for installing an up to date development enviroment.
I learned that the nds has a ton of fancy game featrues (as you might expect), like sprites and special memory hardware that can make really fast copies of some things. It also has two seperate processors, which do completely diferent things (update the screen and check the inputs, for example) yet still make a complete system work. This is, needless to say, much more complicated than the things I usually work on.
However, I decided to forgo all the low level work and keep things simple. There is one video mode which, instead of making up sprites or loading in complex background images, is just that you write to a certain spot in memory with a certain value, and it turns a certain pixel of the screen a certain color (aka framebuffer mode). This is really easy to work with, although there are no included commands to draw lines or crcles or anything like that, so I added a few functions for this (although all I need to draw is boxes, which does make it somewhat easier.) This mode is also a lot slower than what the Ds is truely caplable of doing apparently, but I found it was more than fast enough to make Soviet Bloc Game work correctly.
The DS is also equipped with color screens, so I got to try coloring the blocks for the first time; this too was very easy, since I had kept track of which block belongs to which piece type, all I had to do was assign a color to each piece when going through and printing the playfield matrix. In doing this, I left out a bit of the code from the TRS-80 version that only plots things on the screen where it needs to be changed (this was absolutely nessacary in the TRS-80 version to get a playable game), but I found that the DS was so fast that it didn't matter at all. So now the entire piece carrier gets redrawn instead of just those tiles which have changed, but I could probably redraw the entire playfield too without any issue at all because speed is very high.
Speaking of speed, this ended up being a problem. In my two older versions, gameplay was quite slow. On the DS, I needed to introduce a lockout so that the next piece would not immediatly drop down while still holding the DOWN key. Then, it was a matter of adding frame delays to make a game that was not too fast (though I did increase the number of levels (speedup as lines are cleared) for faster gameplay as you keep playing).
Other than that, the port went smoothly, and as usual the hardest parts were trying to figure out how to get inputs from the buttons and how to draw blocks on the screen. Once I got that done I had a working version of Soviet Bloc Game about 30 seconds later, and the rest was fine tuning it.
The last step is, hey, the DS has speakers. The example code includes a way to play MOD-like tracker files (XM, S3M, and others). So, why not finish it out with a poorly rendered Midi version of All Star by Smash Mouth? This was literally a matter of adding about 5 lines to my existing code, and disabling a couple things in the setup that it turned out I wasn't using anyways. The player takes everything in the background, while playing Soviet Bloc Game still. It uses some hardware (interrupts, timers and the like) so you can't drop it into just any source code you've got, but for one like Soviet Bloc Game that barely uses any special hardware it works fine.
==IBM-PC/DOS Version==
I got a bunch of 5-1/4 floppy drives recently and used them to fix the first computer I learned to program - the Sanyo MBC-550. This 1983 computer comes with MS-DOS and BASIC, but is not IBM-PC compatible (no CGA card, for instance, instead having it's own internal color graphics). As such, it has very little games availible for it.
Since I had never written any code for a 8088 based system, and I have five computers that use a pre-286 CPU, I decided to port my Soviet Bloc Game to it. GCC does not work for anything below a 386, but Open WATCOM does, so that's what I used.
This C comiler comes with modes for choosing text modes, but like most IBM PC software, it directly accesses the hardware to make it work. This makes things incompatible with the Sanyo. The Sanyo does support standard BIOS Interrupt calls, which are slower than the direct hardware calls but are fast enough for Soviet Bloc Game. However, I discovered that C functions to do text manipulation are too slow for Soviet Bloc Game, at least on a slow system like the Sanyo. So, the sections to write graphics and also read keys on the keyboard are done in Assembly. For proper timing between system of different clock speeds, I used the system tick - Int 1aH, which increments a counter about 18.2 times per second.
So now, there is a version of my Soviet Bloc Game that works on pretty much any computer that runs MS-DOS.
==Files==
Here is a link to the source files and a compiled version: [[File:Wilson-tetris-NDS-t2.zip]]. You can build this just by running <code>make</code> from within the devkitPro console.
[[File:Wilson-tetris-PC.zip]] - the DOS/PC version
[[File:alnwlsn-sbg-trs80.dmk]] - (by popular demand) TRS-80 disk image with /CMD binary

Latest revision as of 15:26, 20 September 2023

Soviet Bloc Game v1.1 running in a trs-80 emulator

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.

However, I have written few games, and never done a Tetris-like clone before, so I approach this mostly as a challenge.

Logal notice: "Tetris" is a trademark of The Tetris Company (who have been known to take down unauthorized trademark versions before: https://github.com/jdah/tetris-os), so I have decided to rename this clone "Soviet Bloc Game"

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 wellishly 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 Soviet Bloc Game that met most of these objectives.

Theory of Operation

Basics

The basics of the game's functioning can be summed up with just a few components: a part that generates the 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[][].

Drawing the pieces

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 parts appear in a 4x4 piece container pieceC[][] matrix. This is then overlaid onto the playfield, and can be moved around. Nonzero cells of the matrix define the shape of the piece, like:

0 0 0 0
0 5 0 0
5 5 0 0
0 5 0 0

The numbers used to define the piece are unique to each piece, so if you write the display section to use each number as a color, you can have a color game.

Collision detection

In the collision detection section, the overlaid piece carrier is moved as if you were to make that move, and then we check if any of the cells overlap with non-blank sections of the playfield, or if any part of the active piece lies outside the playfield area. If so, the move is undone, and you can decide what to do from there. If no collision occurs, the move is done for real and displayed the next time the screen is updated.

Usually, the collision detection is used to prevent motion if a piece or wall is in the way, but if the piece is attempting to move downwards, the piece is instead copied onto the playfield, and the numbers making up the piece are increased by 8. This is how the collision detection can differentiate between the active piece and dead pieces in the playfield.

Input conditioning

I added some input conditioning as well. For rotations of the active piece, the move is not repeating, and you must release the rotation input and press it again for a rotation to happen again. For moves left, right, and down, a system similar to repeating keys on a computer keyboard is used, holding down the direction for a brief moment will make the action repeat. In the gameboy version, dropping a piece quickly will increase the score based on the number of tiles dropped while moving fast. This is also replicated (but probably not perfectly) in my version.

Details

Levels and speed

I only update the screen every few moments. Call the time of one of these updates a frame. While the screen is waiting to be updated, it's a good time to check the controls and see what we need to do the next time the screen needs updating. All the processing of the moves and collisions happens just before the screen updates. Every few frames, the active piece drops one row. This is connected to the levels, which in my version go from 0-20, which I think is in line with the gb version. The drop happens every 20 frames at level 0, up to a delay of 0 frames at level 20. For every 10 lines that the user clears, the level is increased by 1, so that the game gets progressively faster, just like the gb version.

Game over detection

This one is pretty simple. If any non-active piece sections are found on the line above the top of the visible playfield, the game ends. Check this every frame.

Line completion detection

This is a little more complicated than the game over, but basically I check to see if any rows are completely filled in with dead pieces. I keep track of which lines are full (and how many of them there are), and animate the corresponding lines by having the whole row(s) flash for a moment. At this point I also add onto the number of completed lines counter displayed on the scoreboard. Then, I copy all the lines above the filled line down by one tile. Since this is done for each filled line in sequence, the effect moves the playfield down for all cases of any number of filled lines.

Scoring

The number of filled lines cleared in one move (calculated above) is used in the scoring formula to add to the score. The full formula (according to the Nintendo scoring system) is:

  • 1 line: 40*(level+1)
  • 2 lines: 100*(level+1)
  • 3 lines: 300*(level+1)
  • 4 lines: 1200*(level+1)

Additionally, the number of grid tiles that are dropped by the user pressing "down" (ie, soft drop), which I kept track of earlier, are also added to the score.

New game

I have a new game button also. When pressed (and the controls are checked), it picks new pieces, clears the level, score, and lines counters, and empties the playfield of dead pieces. Thus, a new game begins.

Hardware (Arduino version)

I do most of my software development on arduino-like embedded systems, and am pretty familiar with I/O options on these devices (more so than on a stndard computer), so I used an arduino nano with a 128x64 st7920 monochrome LCD and the u8g2 libray for the display. For input, I wired up 3 buttons, 2 rotate and one new game, and an analog joystick (which I am using as a digital one) for directional moves.

Porting

With any luck, my code can be ported to different platforms just my changing the section that displays the playfield on a screen, the input sections that take in the button presses, and maybe the delay sections. On the Arduino, my code takes up about 50% of the prgram storage space, and 50% of ram, with most of that being due to the u8g2 library. I'm not an expert on the super low-level processes on the arduino, nor do I think my game is the most effceiently coded, but my guess is that if the game works well on a system with as low specifications as the atmega328, it should be able to run on nearly anything else.

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

  
}

The Code (TRS-80 version)

I used the z88dk C compiler for the port. To compile, use zcc +trs80 -lndos -lm -create-app soviet-bloc-game.c -o soviet-bloc-game which will create a .CAS file. Use zcc +trs80 -lndos -lm -create-app -subtype=disk soviet-bloc-game.c -o soviet-bloc-game to create a .CMD

Porting this to the TRS-80 turned out to be a much harder challange, mostly because the TRS-80 is a very slow, low power machine. Some of the improvements were obvious, like only checking the lines and game over only when an active piece becomes inactive. However, this isn't enough to make a playable version. The TRS-80 is slow enough that every comparison, operation, and graphics plot add noticable delay. Therefore, I only update parts of the display when needed (ie, the area around the active piece, 1 block around the piece carrier); the background is filled back in to erase piece moves, and the screen is only redrawn at the end when the active piece becomes inactive. This is possible because unlike the arduino version, the whole screen doesn't need to be rewitten every time it changes. I also had to take out any frame delay and reduce the levels to 10 because the machine is not fast enough to keep up. The version below is therefore clunky, but playable. The obvious next steps are to make an assembler version of Soviet Bloc Game.

//soviet-bloc-game.c - Soviet Bloc Game for TRS-80 - 05/28/2019
#include <stdio.h>
#include <stdlib.h> 
#include <graphics.h>

#define pfsizeX 10 //playfield size (in cells)
#define pfsizeY 22 //note that top 4 lines are not drawn
#define pfX 8 //upper left corner of playfield location (pixel)
#define pfY 4
#define npX 60 //upper left corner of next piece indicator
#define npY 2
#define clspX 4 //spacing between cell elements (pixels)
#define clspY 2

void controls(void);

unsigned char playfield[pfsizeX][pfsizeY];  //the game area
unsigned char cscreen[pfsizeX][pfsizeY-4];  //a copy of what gets drawn on the screen (game area)
unsigned char pieceC[4][4]; //piece container
signed char pieceCX;  //location of upper left corner of piece container
signed char pieceCY;
unsigned char pieceT; //type of piece
unsigned char pieceR; //rotation of piece 
unsigned char nextpieceT; 
unsigned char nextpieceR; 

unsigned char dcontrol; //locks in control every frame
unsigned char rcontrol;

unsigned char lastdcontrol=0; //for "debouncing" of inputs
unsigned char lastrcontrol=0; 
unsigned char drepeatframe=0;
#define drepeatframes 3 //wait _ frames before repeatedly going in one direction

unsigned char dropframe=0;    //counter for number of frames between block drops
unsigned char level=0; //LEVEL decreases frame drop from maxlevel frames to 0 frames (levels 0 to MAXLEVEL)
#define maxlevel 10

unsigned char ngame=1; //when set, starts new game
unsigned char newpc=0; //when set, get a new piece

unsigned int lines=0; //NUMBER OF LINES CLEARED
long score=0; //TOTAL SCORE (using NES rules)

unsigned char fdrop;   //number of blocks that piece has been fast dropped
unsigned char lslvi=0; //lines since level increase (when this gets to 10, increase the level)

char lvladd=0;


void setblock(char x, char y){
	for(char i=0; i<8; i++){
		controls();
     	plot(x+i%4,y+i/4);
	}
}
void clearblock(char x, char y){
	for(char i=0; i<8; i++){
		controls();
    	unplot(x+i%4,y+i/4);
	}
}
void cdraw(char x, char y, char color){//only draws on screen if it needs to (uses cell coordinates)
	if(cscreen[x][y]!=color){
		cscreen[x][y]=color;
		if(color!=0){
			setblock( (x)*clspX+pfX, (y)*clspY+pfY );
		}else{
			clearblock( (x)*clspX+pfX, (y)*clspY+pfY);
		}
	}
}
void recopyscreen(){ //copies playfield to screen 
  for(char i=0; i<pfsizeX; i++){ //manually redraw the entire screen
  for(char j=4; j<pfsizeY; j++){
	  cdraw(i,j-4,playfield[i][j]);
  }}
  printf("%cY%c%c%ld", 27, 32+3, 32+47, score);
  printf("%cY%c%c%d", 27, 32+5, 32+47, level);
  printf("%cY%c%c%d", 27, 32+7, 32+47, lines);
}
void controls(){
	/*
   if(bpeek(0x3802)&0b00000010){plot(126,46);}else{unplot(126,46);} //up button (I)
   if(bpeek(0x3802)&0b00000100){plot(125,47);}else{unplot(125,47);} //left button (J)
   if(bpeek(0x3802)&0b00001000){plot(126,47);}else{unplot(126,47);} //down button (K)
   if(bpeek(0x3802)&0b00010000){plot(127,47);}else{unplot(127,47);} //right button (L)
   if(bpeek(0x3801)&0b00000010){plot(121,47);}else{unplot(121,47);} //a button (A)
   if(bpeek(0x3804)&0b00001000){plot(122,47);}else{unplot(122,47);} //b button (S)
   if(bpeek(0x3801)&0b00000001){plot(119,47);}else{unplot(119,47);} //newgame (@)
	   */
  if(bpeek(0x3840)&0b00000100){exit(0);}
  if(bpeek(0x3810)&0b00000010){lvladd=1;}
  if(bpeek(0x3810)&0b00000001){lvladd=-1;}
  
  if(bpeek(0x3801)&0b00000001){ngame=1;}
  if(bpeek(0x3808)&0b00000010){newpc=1;}
  if(dcontrol==0){
    if(bpeek(0x3802)&0b00000100){dcontrol=4;}
    if(bpeek(0x3802)&0b00010000){dcontrol=6;}
    if(bpeek(0x3802)&0b00000010){dcontrol=8;}
    if(bpeek(0x3802)&0b00001000){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(bpeek(0x3801)&0b00000010){rcontrol=1;}
    if(bpeek(0x3804)&0b00001000){rcontrol=2;}
    if(rcontrol==lastrcontrol){rcontrol=3;}
  }
}
void clearControls(){
  if(rcontrol!=3){lastrcontrol=rcontrol;}
  if(dcontrol!=3){lastdcontrol=dcontrol;}
  rcontrol=0;
  dcontrol=0;
}


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 nextpiece(){//generate the next piece and move PC back to the top
  unsigned char tempT=nextpieceT;
  unsigned char tempR=nextpieceR;
  nextpieceT = rand()%7 + 1;
  nextpieceR = rand()%4 + 1;
  pieceT=nextpieceT;
  pieceR=nextpieceR;
  loadpiece();
  for(char i=0; i<4; i++){
  for(char j=0; j<4; j++){
	  if(pieceC[i][j]!=0){
		  setblock((i)*clspX+npX, (j)*clspY+npY);
	  }else{
		  clearblock((i)*clspX+npX, (j)*clspY+npY);
	  }
  }}
  
  pieceT=tempT;
  pieceR=tempR;
  
  pieceCY=0; //move piece carrier back to the top of screen
  pieceCX=3;
}

void mdraw(){
  //printf("%cY%c%c%d", 27, 32+1, 32+55, fff); fff++;
  loadpiece(); //load piece into the piece carrier
  for(char i=0; i<6; i++){ //check piece carrier and one block offset outside it
  for(char j=0; j<6; j++){
	  controls();
	  if((pieceCX-1+i)>=0&&(pieceCX-1+i)<pfsizeX&&(pieceCY-1+j)>=4&&(pieceCY-1+j)<(pfsizeY)){//check if this segment be drawn on screen
		if(i>=1&&i<=4&&j>=1&&j<=4){//accessing piece carrier section here}
			if(pieceC[i-1][j-1]>0){//if piece carrier block is confirmed should be white
				cdraw(i-1+pieceCX,j-5+pieceCY,pieceC[i-1][j-1]);
			}else{
				cdraw(i-1+pieceCX,j-5+pieceCY,playfield[i-1+pieceCX][j-1+pieceCY]); //color it in with the existing playfield background
			}
		}else{
			cdraw(i-1+pieceCX,j-5+pieceCY,playfield[i-1+pieceCX][j-1+pieceCY]);
		}
	  }
  }}
}

unsigned char checkCollide(){ //move the piece carrier first, then check if anything collides
  loadpiece(); //load piece into the piece carrier
  unsigned char nonvalidity=0;
  for(char i=0; i<4; i++){ //run through all piece carrier cells
    for(char 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 newgamemon(){
  if(ngame==0){return;}
  //Serial.println(F("NEW GAME"));
  printf("%cY%c%c         ", 27, 32+14, 32+40);
  printf("%cY%c%c        ", 27, 32+3, 32+47);
  printf("%cY%c%c        ", 27, 32+5, 32+47);
  printf("%cY%c%c        ", 27, 32+7, 32+47);
  ngame=0;
  for(unsigned char i=0; i<pfsizeX; i++){   //clear any cells with active piece parts (will be written again with new pieceC
    for(unsigned char j=0; j<pfsizeY; j++){
      playfield[i][j]=0;
    }
  }
  lines=0;
  level=0;
  score=0;
  lslvi=0;
  fdrop=0;
  nextpiece();
  nextpiece();
  recopyscreen();
}

void piece2bg(){ //will copy active piece into background and check for lines and game over
  for(unsigned char i=0; i<4; i++){ //copy piece onto background
  for(unsigned char 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();
  
  //check for line clears
  unsigned char clearline[pfsizeY];
  unsigned char clearedlines=0;
  unsigned char templines=0;
  for(unsigned char j=4; j<pfsizeY; j++){
  clearline[j]=1; //assume line is cleared
  for(unsigned char 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(unsigned char f=0; f<=4; f++){
    for(unsigned char j=4; j<pfsizeY; j++){
        if(clearline[j]==1){
          for(unsigned char i=0; i<pfsizeX; i++){
			  cdraw(i,j-4,f%2);
          }
        }
      }
      t_delay(2000);
    }
	
    for(unsigned char j=4; j<pfsizeY; j++){ //accutally clear the lines
       if(clearline[j]==1){
        for(unsigned char t=j; t>=4; t--){
         for(unsigned char 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(unsigned char i=1; i<=templines; i++){ //see if the level needs increasing
    lslvi++;
    if(lslvi>=10){lslvi=0;level++;}
    if(level>=maxlevel){level=maxlevel;}
   }
  }
  //check gameover (scan through line 3 and see if there are any non-active pieces in it)
  for(unsigned char i=0; i<pfsizeX; i++){
    if(playfield[i][3]>7){
      //*********************GAME OVER********************
      //Serial.println(F("GAME OVER"));
	  printf("%cY%c%cGAME OVER", 27, 32+14, 32+40);
      while(ngame==0){
        controls();
      }
      newgamemon();
    }
  }
  
  recopyscreen();
	  
}

void main(){
	printf("%cE", 27);
	printf("%cY%c%cSoviet Bloc Game 2019", 27, 32+1, 32+40);
	printf("%cY%c%cScore:", 27, 32+3, 32+40);
	printf("%cY%c%cLevel:", 27, 32+5, 32+40);
	printf("%cY%c%cLines:", 27, 32+7, 32+40);
	printf("%cY%c%cNext", 27, 32+4, 32+30);
	
	printf("%cY%c%cJ K L to move", 27, 32+10, 32+32);
	printf("%cY%c%c A S  to rotate", 27, 32+11, 32+32);
	printf("%cY%c%c  @   to start new game", 27, 32+12, 32+32);
	
	drawb(pfX-2, pfY-1, (pfsizeX+1)*clspX, ((pfsizeY-3)*clspY));
	

	ngame=1;
	newgamemon();
	while(1){ //*********************MAIN*PROGRAM*LOOP**************************
  
  //normally a frame delay would go here, but the trs-80 is slow and we need all the speed we can get.
  
  unsigned char droppiece=0;
  dropframe++;
  if(dropframe>=(maxlevel-level)){
    dropframe=0;
    droppiece=1;
  }
  newgamemon(); 
  
  if(lvladd!=0){
	  level+=lvladd;
	  lvladd=0;
	  recopyscreen();
  }
  
  if(newpc!=0){
	  newpc=0;
	  nextpiece();
	  recopyscreen();
  }
  
  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
    }
  }
  
  clearControls();
  mdraw();
}
}

Nintendo DS version

This is ridiculous. We took the TRS-80 version and developed it for a platform made 27 years later. And it works.

Nintendo DS Version running. Note the split screens, one text, one graphics. The DS gives you almost nothing built in, it expects all game data to be provided in the game (including fonts and images), and there are only a handful included with ndslib. For me, that means one screen with graphics, and one with monochrome text.

Sure, there are already plenty of games available for the Nintendo DS, but if all you have is a Soviet Bloc Game clone, everything starts to look like a nail. The NDS defines handheld gaming for the children of my generation, so when I found one at my local rummage sale and paired it up with a generic China Special flash card so it could run programs off a micro sd card, I knew it had to be done.

This is honestly the first time I have worked/played with an actual games machine since I was first learning to hack my Wii around 2010, and will be the first time I write a homebrew app of any kind.

My port uses libnds (or is that ndslib?), compiled with the devkitPro enviroment. There isn't much development on the NDS nowadays, but even in 2019 I found updated instructions for installing an up to date development enviroment.

I learned that the nds has a ton of fancy game featrues (as you might expect), like sprites and special memory hardware that can make really fast copies of some things. It also has two seperate processors, which do completely diferent things (update the screen and check the inputs, for example) yet still make a complete system work. This is, needless to say, much more complicated than the things I usually work on.

However, I decided to forgo all the low level work and keep things simple. There is one video mode which, instead of making up sprites or loading in complex background images, is just that you write to a certain spot in memory with a certain value, and it turns a certain pixel of the screen a certain color (aka framebuffer mode). This is really easy to work with, although there are no included commands to draw lines or crcles or anything like that, so I added a few functions for this (although all I need to draw is boxes, which does make it somewhat easier.) This mode is also a lot slower than what the Ds is truely caplable of doing apparently, but I found it was more than fast enough to make Soviet Bloc Game work correctly.

The DS is also equipped with color screens, so I got to try coloring the blocks for the first time; this too was very easy, since I had kept track of which block belongs to which piece type, all I had to do was assign a color to each piece when going through and printing the playfield matrix. In doing this, I left out a bit of the code from the TRS-80 version that only plots things on the screen where it needs to be changed (this was absolutely nessacary in the TRS-80 version to get a playable game), but I found that the DS was so fast that it didn't matter at all. So now the entire piece carrier gets redrawn instead of just those tiles which have changed, but I could probably redraw the entire playfield too without any issue at all because speed is very high.

Speaking of speed, this ended up being a problem. In my two older versions, gameplay was quite slow. On the DS, I needed to introduce a lockout so that the next piece would not immediatly drop down while still holding the DOWN key. Then, it was a matter of adding frame delays to make a game that was not too fast (though I did increase the number of levels (speedup as lines are cleared) for faster gameplay as you keep playing).

Other than that, the port went smoothly, and as usual the hardest parts were trying to figure out how to get inputs from the buttons and how to draw blocks on the screen. Once I got that done I had a working version of Soviet Bloc Game about 30 seconds later, and the rest was fine tuning it.

The last step is, hey, the DS has speakers. The example code includes a way to play MOD-like tracker files (XM, S3M, and others). So, why not finish it out with a poorly rendered Midi version of All Star by Smash Mouth? This was literally a matter of adding about 5 lines to my existing code, and disabling a couple things in the setup that it turned out I wasn't using anyways. The player takes everything in the background, while playing Soviet Bloc Game still. It uses some hardware (interrupts, timers and the like) so you can't drop it into just any source code you've got, but for one like Soviet Bloc Game that barely uses any special hardware it works fine.

IBM-PC/DOS Version

I got a bunch of 5-1/4 floppy drives recently and used them to fix the first computer I learned to program - the Sanyo MBC-550. This 1983 computer comes with MS-DOS and BASIC, but is not IBM-PC compatible (no CGA card, for instance, instead having it's own internal color graphics). As such, it has very little games availible for it.

Since I had never written any code for a 8088 based system, and I have five computers that use a pre-286 CPU, I decided to port my Soviet Bloc Game to it. GCC does not work for anything below a 386, but Open WATCOM does, so that's what I used.

This C comiler comes with modes for choosing text modes, but like most IBM PC software, it directly accesses the hardware to make it work. This makes things incompatible with the Sanyo. The Sanyo does support standard BIOS Interrupt calls, which are slower than the direct hardware calls but are fast enough for Soviet Bloc Game. However, I discovered that C functions to do text manipulation are too slow for Soviet Bloc Game, at least on a slow system like the Sanyo. So, the sections to write graphics and also read keys on the keyboard are done in Assembly. For proper timing between system of different clock speeds, I used the system tick - Int 1aH, which increments a counter about 18.2 times per second.

So now, there is a version of my Soviet Bloc Game that works on pretty much any computer that runs MS-DOS.

Files

Here is a link to the source files and a compiled version: File:Wilson-tetris-NDS-t2.zip. You can build this just by running make from within the devkitPro console.

File:Wilson-tetris-PC.zip - the DOS/PC version

File:Alnwlsn-sbg-trs80.dmk - (by popular demand) TRS-80 disk image with /CMD binary