In this chapter, we will perform a major restructuring of the code to allow drawing multiple tetriminos at the same time. We’re going to create a variable called the state grid that will maintain the state of each grid location in the game canvas. A zero will represent an empty location and the numbers 1-7 will represent one of the tetrimino brick types. We’ll start by creating another global variable.
1 |
var grid; //Game state grid |
We need to initialize the grid to be a 2D array containing all zeros. In the initialize() function, add the following lines.
1 2 3 4 5 6 7 |
//Create an empty game state grid grid = new Array(20); for(i = 0; i < 20; i++) { grid[i] = new Array(10); for(j = 0; j < 10; j++) grid[i][j] = 0; } |
The first line creates a new Array object containing 20 elements and assigns it to the variable grid. The next line begins a for loop block. Just like functions and if-else statements, curly braces are used to define the scope of the for loop. Loops are one of the basic control structures in every programming language, repeating a block of code until a certain condition is met. A for loop executes the code inside the block a certain number of times. In JavaScript, a for loop is defined by the three statements separated by semicolons inside of the parentheses. The first statement defines a loop variable i that we initialize to zero. The second statement defines the stop condition for the loop, which we set as i < 20, meaning that the program should continue to loop as long as the loop variable i is less than 20. The third statement increments the loop variable each time through the loop. Altogether, these three statements create a loop that will execute 20 times, once for each row of the game state grid.
The first line inside the loop block assigns a 10-element Array object to the grid variable at one of the 20 row locations defined by the loop variable i. Notice that we access an individual Array element by using an integer in square brackets. Arrays in JavaScript are 0-indexed, meaning that the first element is accessed with an index of zero. We define another for loop, inside the first, with a different loop variable j to iterate over the 10 grid columns. This loop has no curly braces since it only repeats a single line. The indexing scheme on this line uses both loop variables, accessing a specific row and column of the grid and setting the value to zero.
Now that we have a game state grid defined, we need a function that will draw whatever it contains to the canvas. We define a new function drawGrid() that loops over each grid cell as before, and calls the drawBlock function.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
/************************************************ Draws the current game state grid ************************************************/ function drawGrid() { //Clear the canvas ctx.clearRect(0,0,200,400); //Loop over each grid cell for(i = 0; i < 20; i++) { for(j = 0; j < 10; j++) drawBlock(j, i, grid[i][j]); } } |
You may notice that I said that grid contains an integer between 0 and 7 representing the block type, but our current drawBlock function takes a color parameter directly. We could change our representation so that grid holds a color value, but it will ultimately make things easier in the long run, if we modify our existing code to conform to this new representation. To do that, we need to add a block of code to the drawBlock function that will pick the appropriate color based on the block type. That should look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
//Get the block color var c; if(t == 1) //I type c = 180; //Cyan else if(t == 2) //J type c = 240; //Blue else if(t == 3) //L type 40; //Orange else if(t == 4) //O type c = 60; //Yellow else if(t == 5) //S type c = 120; //Green else if(t == 6) //T type c = 280; //Purple else //Z type c = 0; //Red |
We’ll also put an additional if statement at the start of the function to only perform the drawing commands if the block type t > 0. This also changes our tetrimino indexing scheme, so we’ll need to change the drawTetrimino function as well. In addition to modifying the tetrimino index values, we need to change this function to modify the game state grid, rather than calling the drawBlock function directly. We also need the function to be able to erase a specific tetrimino, so an additional function parameter will be needed to indicate whether we want to draw or erase. This represents a rather significant change to the longest function we currently have. To make the change easier, we define a helper function that will mimic the form of the current drawBlock function, but will instead write to the game state grid. This will also allow us to make sure that the x and y values are valid.
1 2 3 4 5 6 7 8 9 10 |
/************************************************* Sets a grid cell in the game state grid x = [0,9] x-coordinate y = [0,19] y-coordinate t = [0,7] block type *************************************************/ function setGrid(x, y, t) { if(x >= 0 && x < 10 && y >= 0 && y < 20) grid[y][x] = t; } |
The if statement in this function checks four different conditions. The && operators mean logical AND, so the whole condition will only be true if each of the four inequalities is true. Now in the drawTetrimino function, we can set the variable c = t*d, where t is the tetrimino type using the new indexing scheme, and d is 1 for drawing and 0 for erasing. We can then erase all of the existing lines that set the variable c. Then, using the find and replace command, we can change all of the drawBlock calls in this function with setGrid. Do NOT use replace all, as it is easy to accidentally replace something that shouldn’t be changed, causing errors.
The last thing we need to change is the keyDown function. Now that drawing is handled by the drawGrid function, we don’t need to clear the canvas each time a key is pressed. Instead, we will erase the current tetrimino using the current variable values, then update the values and redraw the tetrimino. If we don’t erase the old tetrimino when a new one is added, we can retain the position of the old blocks in the game state grid. This is how we can have multiple tetrimino blocks on the screen at the same time. The last two changes we need to make to this function are to add 1 to the new tetrimino type to account for the new indexing scheme, and to call the drawGrid function at the end to refresh the display. The complete code should look like this.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 |
<!DOCTYPE html> <html> <head> <title>Tetris</title> <script> //Golbal variables var ctx; //Canvas object var t; //Tetrimino type var x, y; //Tetrimino position var o; //Tetrimino orientation var grid; //Game state grid /************************************************ Initialize the drawing canvas ************************************************/ function initialize() { //Get the canvas context object from the body c = document.getElementById("myCanvas"); ctx = c.getContext("2d"); //Initialize tetrimino variables t = 1 + Math.floor((Math.random()*7)); x = 4; y = 18; o = 0; //Create an empty game state grid grid = new Array(20); for(i = 0; i < 20; i++) { grid[i] = new Array(10); for(j = 0; j < 10; j++) grid[i][j] = 0; } } /************************************************ Draws the current game state grid ************************************************/ function drawGrid() { //Clear the canvas ctx.clearRect(0,0,200,400); //Loop over each grid cell for(i = 0; i < 20; i++) { for(j = 0; j < 10; j++) drawBlock(j, i, grid[i][j]); } } /************************************************ Draws a block at the specified game coordinate x = [0,9] x-coordinate y = [0,19] y-coordinate t = [0,7] block type ************************************************/ function drawBlock(x, y, t) { //Check if a block needs to be drawn if(t > 0) { //Get the block color var c; if(t == 1) //I type c = 180; //Cyan else if(t == 2) //J type c = 240; //Blue else if(t == 3) //L type c = 40; //Orange else if(t == 4) //O type c = 60; //Yellow else if(t == 5) //S type c = 120; //Green else if(t == 6) //T type c = 280; //Purple else //Z type c = 0; //Red //Convert game coordinaes to pixel coordinates pixelX = x*20; pixelY = (19-y)*20; /**** Draw the center part of the block ****/ //Set the fill color using the supplied color ctx.fillStyle = "hsl(" + c + ",100%,50%)"; //Create a filled rectangle ctx.fillRect(pixelX+2,pixelY+2,16,16); /**** Draw the top part of the block ****/ //Set the fill color slightly lighter ctx.fillStyle = "hsl(" + c + ",100%,70%)"; //Create the top polygon and fill it ctx.beginPath(); ctx.moveTo(pixelX,pixelY); ctx.lineTo(pixelX+20,pixelY); ctx.lineTo(pixelX+18,pixelY+2); ctx.lineTo(pixelX+2,pixelY+2); ctx.fill(); /**** Draw the sides of the block ****/ //Set the fill color slightly darker ctx.fillStyle = "hsl(" + c + ",100%,40%)"; //Create the left polygon and fill it ctx.beginPath(); ctx.moveTo(pixelX,pixelY); ctx.lineTo(pixelX,pixelY+20); ctx.lineTo(pixelX+2,pixelY+18); ctx.lineTo(pixelX+2,pixelY+2); ctx.fill(); //Create the right polygon and fill it ctx.beginPath(); ctx.moveTo(pixelX+20,pixelY); ctx.lineTo(pixelX+20,pixelY+20); ctx.lineTo(pixelX+18,pixelY+18); ctx.lineTo(pixelX+18,pixelY+2); ctx.fill(); /**** Draw the bottom part of the block ****/ //Set the fill color much darker ctx.fillStyle = "hsl(" + c + ",100%,30%)"; //Create the bottom polygon and fill it ctx.beginPath(); ctx.moveTo(pixelX,pixelY+20); ctx.lineTo(pixelX+20,pixelY+20); ctx.lineTo(pixelX+18,pixelY+18); ctx.lineTo(pixelX+2,pixelY+18); ctx.fill(); } } /************************************************* Draws a tetrimino at the specified game coordinate with the specified orientation x = [0,9] x-coordinate y = [0,19] y-coordinate t = [1,7] tetrimino type o = [0,3] orientation d = [0,1] draw or erase *************************************************/ function drawTetrimino(x,y,t,o,d) { //Set the drawing value c = t*d; /**** Pick the appropriate tetrimino type ****/ if(t == 1) { //I Type //Get orientation if(o == 0) { setGrid(x-1,y,c); setGrid(x,y,c); setGrid(x+1,y,c); setGrid(x+2,y,c); } else if(o == 1) { setGrid(x+1,y+1,c); setGrid(x+1,y,c); setGrid(x+1,y-1,c); setGrid(x+1,y-2,c); } else if(o == 2) { setGrid(x-1,y-1,c); setGrid(x,y-1,c); setGrid(x+1,y-1,c); setGrid(x+2,y-1,c); } else if(o == 3) { setGrid(x,y+1,c); setGrid(x,y,c); setGrid(x,y-1,c); setGrid(x,y-2,c); } } if(t == 2) { //J Type //Get orientation if(o == 0) { setGrid(x-1,y+1,c); setGrid(x-1,y,c); setGrid(x,y,c); setGrid(x+1,y,c); } else if(o == 1) { setGrid(x+1,y+1,c); setGrid(x,y+1,c); setGrid(x,y,c); setGrid(x,y-1,c); } else if(o == 2) { setGrid(x-1,y,c); setGrid(x,y,c); setGrid(x+1,y,c); setGrid(x+1,y-1,c); } else if(o == 3) { setGrid(x,y+1,c); setGrid(x,y,c); setGrid(x,y-1,c); setGrid(x-1,y-1,c); } } if(t == 3) { //L Type //Get orientation if(o == 0) { setGrid(x-1,y,c); setGrid(x,y,c); setGrid(x+1,y,c); setGrid(x+1,y+1,c); } else if(o == 1) { setGrid(x,y+1,c); setGrid(x,y,c); setGrid(x,y-1,c); setGrid(x+1,y-1,c); } else if(o == 2) { setGrid(x-1,y-1,c); setGrid(x-1,y,c); setGrid(x,y,c); setGrid(x+1,y,c); } else if(o == 3) { setGrid(x-1,y+1,c); setGrid(x,y+1,c); setGrid(x,y,c); setGrid(x,y-1,c); } } if(t == 4) { //O Type //Orientation doesn’t matter setGrid(x,y,c); setGrid(x+1,y,c); setGrid(x,y+1,c); setGrid(x+1,y+1,c); } if(t == 5) { //S Type //Get orientation if(o == 0) { setGrid(x-1,y,c); setGrid(x,y,c); setGrid(x,y+1,c); setGrid(x+1,y+1,c); } else if(o == 1) { setGrid(x,y+1,c); setGrid(x,y,c); setGrid(x+1,y,c); setGrid(x+1,y-1,c); } else if(o == 2) { setGrid(x-1,y-1,c); setGrid(x,y-1,c); setGrid(x,y,c); setGrid(x+1,y,c); } else if(o == 3) { setGrid(x-1,y+1,c); setGrid(x-1,y,c); setGrid(x,y,c); setGrid(x,y-1,c); } } if(t == 6) { //T Type //Get orientation if(o == 0) { setGrid(x-1,y,c); setGrid(x,y,c); setGrid(x+1,y,c); setGrid(x,y+1,c); } else if(o == 1) { setGrid(x,y+1,c); setGrid(x,y,c); setGrid(x,y-1,c); setGrid(x+1,y,c); } else if(o == 2) { setGrid(x-1,y,c); setGrid(x,y,c); setGrid(x+1,y,c); setGrid(x,y-1,c); } else if(o == 3) { setGrid(x,y+1,c); setGrid(x,y,c); setGrid(x,y-1,c); setGrid(x-1,y,c); } } if(t == 7) { //Z Type //Get orientation if(o == 0) { setGrid(x-1,y+1,c); setGrid(x,y+1,c); setGrid(x,y,c); setGrid(x+1,y,c); } else if(o == 1) { setGrid(x+1,y+1,c); setGrid(x+1,y,c); setGrid(x,y,c); setGrid(x,y-1,c); } else if(o == 2) { setGrid(x-1,y,c); setGrid(x,y,c); setGrid(x,y-1,c); setGrid(x+1,y-1,c); } else if(o == 3) { setGrid(x,y+1,c); setGrid(x,y,c); setGrid(x-1,y,c); setGrid(x-1,y-1,c); } } } /************************************************* Sets a grid cell in the game state grid x = [0,9] x-coordinate y = [0,19] y-coordinate t = [0,7] block type *************************************************/ function setGrid(x, y, t) { if(x >= 0 && x < 10 && y >= 0 && y < 20) grid[y][x] = t; } /************************************************* Responds to a key press event *************************************************/ function keyDown(e) { if(e.keyCode == 37) { //Left arrow drawTetrimino(x,y,t,o,0); //Erase the current tetrimino x -= 1; } else if(e.keyCode == 38) { //Up arrow drawTetrimino(x,y,t,o,0); //Erase the current tetrimino o = (o + 1) % 4; } else if(e.keyCode == 39) { //Right arrow drawTetrimino(x,y,t,o,0); //Erase the current tetrimino x += 1; } else if(e.keyCode == 40) { //Down arrow drawTetrimino(x,y,t,o,0); //Erase the current tetrimino y -= 1; } else { //Create a new tetrimino t = 1 + Math.floor((Math.random()*7)); x = 4; y = 18; o = 0; } //Draw the current tetrimino drawTetrimino(x,y,t,o,1); //Redraw the grid drawGrid(); } </script> <body onload="initialize();" onkeydown="keyDown(event);" style="background-color:#EEEEEE"> <canvas id="myCanvas" height="400px" width="200px" style="background-color:#444444"></canvas> <div style="width:200px;background-color:#CCCCCC"> Score: 0 </div> </body> </html> |
If you load the page now, you should be able to add new tetriminos with any key except the arrow keys, and move them with the arrow keys. You can stack them as you would when actually playing, however you’ll notice that if you move a piece over some existing blocks, they will be overwritten and will disappear when you move again. We’ll look at how to prevent this from happening next time when we introduce basic collision detection.