Skip to main content

Gerbtris : Coding Tetris in Bash

Coding Tetris in Bash


Hi peeps.
So you've come here because you've shown some interest in coding Tetris in bash. Goodness knows why, but we'll get straight on it.

Firstly though, let me just say that this is MY implementation of the game. I'm aware that the implementations and methods used could probably be enhanced or improved, but they were used as they were the first solution I concocted for the puzzle at hand, and I had a limit of about fie hours (two motorway journeys) to get this coded from start to finish.

Lets get into it.

To break down what I needed for the very basic model (which ended up roughly 300 lines) I needed to write functions for the following:

  • shape painter - a routine is needed to paint the shape at any point on the screen
  • shape rotation - a routine is needed to rotate the shape
  • shape collision - the shapes have to be "stackable" and not cross over any other shapes or the walls of the playing field
  • shape mover - the user has to be able to move the shape
  • line checker - a routine is needed to check for "full" lines to be deleted.


  • And that's about it. Now onto the nitty gritty.

    BARE BONES

    The first thing I did was to set up a routine that would set up a few variables, enter an infinite loop and accept and process user input.
    This was very trivial - the following loop can achieve this:

          X_VAL=0           # This is a "global" variable to store X.
          Y_VAL=0           # This is a "global" variable to store Y.
          CONTROL_LIMIT=1   # Another variable.
          
          while :           # This starts an infinite loop. 
          do         
             read -sN1 -t ${CONTROL_LIMIT} key   # This accepts a user input (-s silently, -N1 character only, and times out after -t seconds (1)) 
                                                 # and stores in the variable "key".
    
             # catch multi-char special key sequences
             read -sN1 -t 0.0001 k1        # Same options as above but a smaller time limit to capture trailing data.  
             read -sN1 -t 0.0001 k2        # This is to allow for cursor keys being used.
             read -sN1 -t 0.0001 k3
             key+=${k1}${k2}${k3}
    
             case "$key" in
                z|$'\e[D'|$'\e0D')         # z or left cursor key pressed
                ((X_VAL--));;
    
                x|$'\e[C'|$'\e0C')         # x or right cursor key pressed
                ((X_VAL++));;
    
                $'\e[A'|$'\e0A')           # up cursor key pressed 
                ((Y_VAL--));;
    
                a|$'\e[B'|$'\e0B')         # a or down cursor key pressed
                ((Y_VAL++));;
    
                q)                         # q pressed to exit loop
                echo "BYE BYE PEEPS!" && exit;;
             esac
          done                             # End of infinite loop
    
    
    NOTE:
    The above case command doesn't have a "default" case which would look like the following:
                *)
                echo "invalid key press";;
    
    

    Although in general it is good practise to use a default case, I wanted to ignore all other key presses so I opted not to have on.

    TIP:
    In the snippet above you will see ((VAR--)) or ((VAR++)). This is literally decreasing or increasing the variable respectively. In bash we put variables in double round brackets to directly alter them. So if x was 10, ((x+=7+6)) would result in x being 23. Easy and very useful!

    Once my basic loop had been created I decided to print a shape on the screen and move it around, but first I needed to create a shape.

    ON THE DANCE FLOOR, MAKING SHAPES

    Ok. With a motorway as my dance floor (the missus was driving, not me) I needed to create shapes. And how would the shapes be stored? They would each be stored as a two-dimensional array!
    WRONG!
    Unfortunately, bash does not support two dimensional arrays. You can't make them. Bummer eh! Fortunately though, bash DOES support associative arrays! And due to this we can tell bash lies and fool it into thinking it is a multi-dimensional array and making it perform as such. And how do we do this? It's really quite simple!

    First though, I'll briefly explain the basic difference between an indexed array and an associative array. Skip this bit if you already know and you don't want to get bored. I'll colour the boring bit in blue.
    An indexed array stores data in cells that are indexed using a number as an index. And these indexes (usually) start at 0. So if you had an array declared as follows:
          declare -a array      # Use -a for indexed      
          array[0]=1
          array[1]=2
          array[2]=3
    
    
    then calling array[2] would give us 3 and calling array[10] error as it does not exist.
    An associative array differs as they can index array cells using text and symbols as well as numbers. So therefore you could create an array as such:

          declare -A capital_array      # Use -A for associative arrays
          capital_array[England]="London"
          capital_array[France]="Paris"
          capital_array[Spain]="Madrid"
    
    
    If we was to call capital_array["France"] then we would get the value "Paris". And that's all there is to it. End of boring bit.

    Using an associative array we can do the following:
          declare -A grid
          grid[0,0]=1
          grid[0,1]=2
          grid[1,0]=3
          grid[1,1]=4
    
    
    What we are doing here is not specifying the cells of a 2D array but actually giving the names 0,0 0,1 1,0 and 1,1 to each cell! Furthermore, we can use variables within these names, grid[${x},${y}] to enhance the effect of a 2D array being used! It's legit, it works and it is bloody brilliant! And this is how I created the shapes and the playfield of the game.

    Tetris has seven shapes. All but one can be fit into a 3x3 grid square, the 'long piece' in a 4x4 square. I say square because they need to be rotated, and rotating a square is a hell of a lot easier than rotating a rectangle, as any mathematician, computer programmer or politician would tell you. Actually, a politician would probably lie and say it is easier.
    I didn't want the shape creation to be too cumbersome so I just put the shape data as a 16 character string (4 rows of 4 digits) and wrote a routine that would place these values as separate 4x4 arrays. For example:
          shape1data="0100111000000000" # T shape
          shape2data="0220220000000000" # s shape
          shape3data="3300033000000000" # z shape
          shape4data="0040444000000000" # L shape
          shape5data="5000555000000000" # ¬ shape
          shape6data="0660066000000000" # █ shape
          shape7data="0000777700000000" # | shape
    
          declare -A shape1=()
          declare -A shape2=()
          declare -A shape3=()
          declare -A shape4=()
          declare -A shape5=()
          declare -A shape6=()
          declare -A shape7=()
    
          for shapenum in $( seq 1 7 )
          do
             i=0
             for y in $( seq 0 3 ) 
             do
                for x in $( seq 0 3 )
                do    
                   eval "shape${shapenum}[\${x},\${y}]=\${shape${shapenum}data:\${i}:1}"
                   ((i++))
                done
             done
          done
    
    
    Simple eh?
    TIP:
    In the above routine you will see the eval command. What this does is evaluate what is inside the double-quotes before running that line as a command. As I have declared the arrays as the word 'shape' followed by a number, then I can directly assign to that using eval.
    For example, using shapenum = 3, x = 2 and y = 1:
          eval "shape${shapenum}[\${x},\${y}]=\${shape${shapenum}data:\${i}:1}"
    
    
    would become:
          shape3[${x},${y}]=${shape3data:${i}:1}
    
    
    and this is the line that would get "executed" as if it was a line in the shell script.

    Now we have the structure of each shape stored in its own array. And to paint them? Simply read each cell using a nested for loop and print where they need to go on the screen:
             printshape(){
                shapenum=$1
                posX=$2
                posY=$3
                penX=${posX}
                penY=${posY}
                gridwidth=2
                
                [[ ${shapenum} -eq 7 ]] && gridwidth=3
                
                for y in $( seq 0 ${gridwidth} ) 
                do         
                   for x in $( seq 0 ${gridwidth} )
                   do    
                      block=${shape[${x},${y}]}
                         if [ ${block} -gt 0 ]
                      then
                         echo -ne "\033[${penY};${penX}H" #reposition cursor  
                         echo -ne "\033[0;3${block}m█\033[0m" 
                      fi
                      ((penX++))
                   done
                   ((penY++))
                   penX=${posX}
                done;
                echo -ne "\033[23;0H" #reposition cursor to bottom of playfield   
             }
    
    

                                  0100         ██
    Using this routine, the shape 1110 becomes ███
                                  0000         ████
                                  0000         ████

    Now it's time to move the shape.

    MOVE ON THE DANCE FLOOR

    Moving a shape is pretty easy. You simply listen for the key press and either increment or decrement the axis of the shape that the keypress corresponds to.
    Noting that the top left character is position (1,1), the up and down keys decrement and increment the position of the Y axis of the shape position respectively; and the left and right keys decrement and increment the X axis of the shape position respectively.
    But there is a catch. And that is 'you cannot move into a space that is already occupied'. We need to identify collisions.
    So how is that achieved?
    Firstly lets take a look at the playing field. This is simply a 2D array that is initially filled with 0s but with 8s for walls and the floor. This is used to keep track of the shapes as they fall to the bottom. I have chosen the value of 8 for the solid walls and floor because the values 1 - 7 are used for the shapes and the  
    value 0 is used to denote "empty" or "blank" cells. We can use this to move the shape. You may have already guessed it but the text at the bottom is a mock up of the playing field.
    This crude example is only 10x12 "squares" but for the game I have chosen 10x20.

    800000000008
    800000000008
    800000000008

    800000000008 <-----The initial playing field
    800000000008

    800000000008
    800000000008

    800000000008
    800000000008

    800000000008
    800000000008

    800000000008
    888888888888

     

    800000000008
    800000000008
    800000000008

    800001000008 <-----The playing field with
    800001100008       a "T" piece floating

    800001000008       down it
    800000000008

    800000000008
    800000000008

    800000000008
    800000000008

    800000000008
    888888888888

    As mentioned we can use this to move the shape.
    It is used to act as a guide so that we know if any playing field piece are occupied when we move or rotate the shape.
    This is done by implementing a check routine that checks the shape's new position for collisions and moves it there if there are none, thus updating the playing field with the new data. The great thing is this will also work for previous shapes that have settled too. Once the shape has moved then the graphics are sorted out - the shapes is "deleted" from the screen and repainted at its new position.
    It's actually very simple!


    YOU SPIN ME RIGHT 'ROUND BABY, RIGHT 'ROUND LIKE A RECORD BABY, RIGHT 'ROUND 'ROUND 'ROUND.

    The shapes need to be rotated, otherwise the game would be really crap.
    how did I achieve this? Well, most shapes are "based" around a 3x3 grid, except for the "straight line" shape.
    I wanted the shapes to rotate around the middle cell as shown by an * below, and where the * is shown on the long piece. The square piece purposely had the rotate function disabled as it was a square:

    010   022   330   004   500   066   0000
    1*1   2*0   0*3   4*4   5*5   066   7*77
    000   000   000   000   000   000   0000
                                        0000

    Even though the shapes were defined as 4x4 squares, the cells marked as X in the following square would be 0, so these would be ignored in rotations except for the "long" shape.

         010X                   010X
         111X rotates to become 011X
         000X                   010X
         XXXX                   XXXX


    By basing the rotation function around the top-left 9 cells, we can rotate just this section and the shape stays good as the rest of it (denoted by Xs) is ignored.


    What would
    help here is a program routine that will do this for us.
    Oh, I have one to show you here!

       rotateshape(){
          shapenum=$1
          n=3
          declare -A temp=()
          
          [[ ${shapenum} -eq 6 ]] && return #no need to rotate a square!
          [[ ${shapenum} -eq 7 ]] && n=4 #change n if shape is |
             
          for x in $( seq 0 3 ) #copy shape to temp
          do
             for y in $( seq 0 3 )
             do
                temp[${x},${y}]=${shape[${x},${y}]}
             done
          done   
    
          for x in $( seq 0 $((${n}/2)) ) #make temporary copy and check for collisions...
          do
             for y in $( seq ${x} $((${n}-${x}-2)) )
             do
                i=$((${n}-${x}-1))
                j=$((${n}-${y}-1))
                tmp=${shape[${x},${y}]}
                temp[${x},${y}]=${shape[${y},${i}]}
                [[ ${temp[${x},${y}]} -gt 0 ]] && [[ ${playfield[$(($SHAPE_X+${x}-1)),$(($SHAPE_Y+${y}-1))]} -gt 0 ]] && return #return if there's a collision
                temp[${y},${i}]=${shape[${i},${j}]}
                [[ ${temp[${y},${i}]} -gt 0 ]] && [[ ${playfield[$(($SHAPE_X+${y}-1)),$(($SHAPE_Y+${i}-1))]} -gt 0 ]] && return #return if there's a collision
                temp[${i},${j}]=${shape[${j},${x}]}
                [[ ${temp[${i},${j}]} -gt 0 ]] && [[ ${playfield[$(($SHAPE_X+${i}-1)),$(($SHAPE_Y+${j}-1))]} -gt 0 ]] && return #return if there's a collision
                temp[${j},${x}]=${tmp}
                [[ ${temp[${j},${x}]} -gt 0 ]] && [[ ${playfield[$(($SHAPE_X+${j}-1)),$(($SHAPE_Y+${x}-1))]} -gt 0 ]] && return #return if there's a collision
             done
          done   
          # If we got to here then there is no collision, so we can set the new position of the shape in the shape array. And repaint it.
          clearshape ${SHAPE_X} ${SHAPE_Y}
          for x in $( seq 0 ${n} ) #...if we got to here, no collisions. Make permanent.
          do
             for y in $( seq 0 ${n} )
             do
                shape[${x},${y}]=$((${temp[${x},${y}]}+0))
             done
          done   
       }
    
    

    The size of the main part of this has doubled as we are storing the new position in a temporary array to check for collisions. Ordinarily, we can directly rotate the shape and have done with it.

    I SEE A RED DOOR AND I WANT IT PAINTED BLACK

    The graphics are really really easy to do. To create colours in back you basically follow the following rules:
       printf "\033[0;3${block}m█\033[0m"
    
    \033[0;3 denotes that there is going to be a colour change.
    ${block}m is the value of the colour. This is why I have set each different shape with a specific value.
    Anything after the m is printed, so having a solid block will be printed to whatever was coloured previously.
    \033[0m This sets the "text" back to normal.

    So when we move a shape which is allowed to be moved we first paint the current "solid" cells black (to match the background) then repaint it at the new position. This gives it the effect that it is moving.
    If a "full line" has been made then this needs to be deleted so naturally the full playing field will change. The repainting of the full playing field will involve checking values of the playing field array, cell by cell, and painting that particular position on the screen at that particular colour.
    Easy.

    Speaking of lines, this takes me to the last bit of this blog...

    WHITE LINES, BLOW AWAY...

    Tetris is a game where shapes drop from the top and they have to be stacked in a way that they all fit leaving no gaps. A bit like packing your boot when you go camping. When a solid horizontal line is made it disappears and all the blocks above drop down a level, or more, depending on how many lines were made.

    How was this achieved? Quite simple really.
    A temporary array was created to match the current playing field, starting from the bottom.
    As each cell was being copied a check was made to see if the value was a 0 or not.
    If a zero was encountered then the row had gaps so the row would be copied to the end.
    If no zeros were encountered, then this would be flagged as a "full line" and a "full-line" variable would be incremented. The next line of the playing field would then be copied and checked, but the line marker of the temporary array would not be incremented so that the previous ("full") line in the temporary array would be overwritten, and the score would be incremented.
    Once the full playing field has been copied and checked, if the "full line" variable is greater than 0 then the temporary array is copied to the playing field. This will leave a gap at the top due to this explanation, so a "full line" amount of "empty" rows will be added to the top of the playing field.
    This may sound a bit complicated, so read this again slowly or ask somebody to help you out.

    And that's it.

    How to make Tetris in Bash.

    The full source code can be found at https://github.com/gerbilbyte/gerbtris.git

    Comments

    Popular posts from this blog

    Dissecting WannaCry

    Below is  brief overview of the inner workings of WannaCry. It is by no means a complete indepth account of what it does, but the inquisitive will learn a little bit without touching any code debuggers. Enjoy the read! gerbil (follow me on Twitter: @gerbil ) Dissecting WannaCry Hi guys. Before I continue to bore you to death, just a few points: Firstly, before you read this page thinking you're going to unlock the mysteries of the world or even find the arc of the covenant, that isn't going to happen. This page is basically a reformatted version of a text dump, i.e. a few of my notes that I took when I examined WannaCry. And I'm not prepared to write an indepth, detailed account with them notes. So, that means it contains holes, either because I've missed it, didn't think it relevant (at the time), or because I was too lazy to include it, which is probably the main reason. I am only human after all! Cynics will probably read this document and ...

    Published Article in 2600 Magazine: Take Your Work Home After Work

    Below is one of the first articles that I had published. It appeared in the Winter 2014 issue of 2600 Magazine, an awesome magazine that publishes awesome things. The idea behind the article was to provide an insight into mixing encrypted data into a normal .jpg image and pushing it through a firewall. Enjoy the read! gerbil (follow me on Twitter: @gerbil ) Taking Your Work Home After Work. GerbilByte, 2014 So there I was. I was drafted in to work for a small company (who shall remain nameless, but for this article we will call the company Bumble Bee Internet Security Services) for several months. At the end, as well as a juicy pay-check, I realised that I had written a load of little scripts that I wanted to keep. I zipped up my folder of goodies to email to myself and encrypted it for obvious reasons then attached it to an internal email to send it. DENIED! Bumble Bee Internet Security Services (BBISS from now on) was a company whose email sys...