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:
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
${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...
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
Post a Comment