Tag Archives: tetris

Tetris

This was my first serious Javascript project which was written some time in 2009. I chose to write a Tetris clone because it was a well defined project that would teach me how to use Javascript. In addition to Tetris I also made Tritris and Pentris to see how well balanced the three games are compared to each other. It turns out that Tetris is about right, with Tritris being too easy and Pentris being too hard.

Links

Live page
GitHub repository

Overview

The user plays the game with the arrow keys, and the game gets slowly faster as their score increases. There is a MySQL and PHP backend to save scores on the server. As much as possible, the three games have been harmonised so that they use the same page, the same Javascript library, and the same PHP page for interaction with the server. This is outlined in a previous post where I discussed how the code was refactored. This is one of my favourite projects, as it’s one of the few “complete” projects that touches on almost all of Javascript, with some HTML, CSS, PHP, MySQL, httpxml, and cookies also thrown in there. This project taught me so much about Javascript and was an excellent start with the language.

Challenges

Challenge: This project required learning how to use Javascript.
Solution: What a challenge! Having worked previously with C++, I found that Javascript was rather easy to learn, and quickly came across its peculiarities and limitations. (Resolved)
Challenge: The project required detailed manipulation of the DOM.
Solution: It was with this project that I learned how to use the DOM, which helped me to better understand the heirarchical structure of XML in general. I also wanted the HTML to be semantically pure, so while I used the DOM to store some information about the state of the game, I also ensured that it was semantically consistent. (Resolved)
Challenge: This game required careful control of Javascript events and synchronisation.
Solution: This was probably the most difficult and instructive part of the project. I had to learn how to register event handlers in a manner which worked across browsers. I still use the same style of event handling today that I developed when I wrote this project. It took a while to get used to the issues of synchronisation using the window.setTimeout method, which I still use frequently today. (Resolved)
Challenge: I had to store some data on the server.
Solution: I had had plenty of experience with PHP and MySQL before this project, including sanitising input to the database, so the PHP side of this challenge was easy to implement. However making the httpxml requests was not so easy and took some practice. After a few iterations I got a working model, although this is something I should improve further, as httpxml requests tend to be rather messy. (Resolved)
Challenge: One of the users wanted a feature that required cookies.
Solution: One user spent so long playing the game that he wanted to be able to “block” himself. As a result I had to implement a feature hat sets a cookie that prevents the user from playing. This was the first time I had set and read cookies using Javascript, and not something I have had much use for since. (Resolved)
Challenge: The game has a soundtrack.
Solution: Having used so many feature of Javascript, I wanted to add some music. This is far from trivial in the world of Javascript, and not so easy in the days befre embedded YouTube videos. Although support is a little shaky, the music was added and an interface included. (Resolved)
Challenge: The game had to have cross browser support.
Solution: This game was initially developed using Firefox, but one of the users wanted it to work with Chrome. This was the first time I met the frustration of cross browser event handling, which has been something of a pain ever since, but it was not too hard to overcome. (Resolved)

Screenshot

Screenshot of the Tetris game
Screenshot of the Tetris game

Code update: Tetris

The first major Javascript project I took on was a clone of Tetris. Every time I look back at it I’m actually surprised an impressed at how many features it has. For a first project it has DOM manipulation, httpxml requests, timeout functions, cookies, audio, and a MySQL backend. After making the Tetris clone I then tried to make Pentris, which is the same game but with five blocks per piece. In going from Tetris to Pentris I made some interesting optimisations.

The Tetris clone
The Tetris clone

For example, here is the function tryRotate:

function tryRotate(){
  switch(color){
    case "red":
    switch(orientation){
      case 0:
      case 2:
        rotatedC[0] = C[0] + 1 ; rotatedR[0] = R[0] + 0 ;
        rotatedC[1] = C[1] + 0 ; rotatedR[1] = R[1] + 1 ;
        rotatedC[2] = C[2] - 1 ; rotatedR[2] = R[2] + 0 ;
        rotatedC[3] = C[3] - 2 ; rotatedR[3] = R[3] + 1 ;
      break ;
      case 1:
      case 3:
        rotatedC[0] = C[0] - 1 ; rotatedR[0] = R[0] + 0 ;
        rotatedC[1] = C[1] + 0 ; rotatedR[1] = R[1] - 1 ;
        rotatedC[2] = C[2] + 1 ; rotatedR[2] = R[2] + 0 ;
        rotatedC[3] = C[3] + 2 ; rotatedR[3] = R[3] - 1 ;
      break ;
    }
    break ;
    
    case "green":
    switch(orientation){
      case 0:
        rotatedC[0] = C[0] + 1 ; rotatedR[0] = R[0] + 1 ;
        rotatedC[1] = C[1] + 1 ; rotatedR[1] = R[1] - 1 ;
        rotatedC[2] = C[2] + 0 ; rotatedR[2] = R[2] + 0 ;
        rotatedC[3] = C[3] - 1 ; rotatedR[3] = R[3] + 1 ;
      break ;
      case 1:
        rotatedC[0] = C[0] - 1 ; rotatedR[0] = R[0] + 1 ;
        rotatedC[1] = C[1] + 1 ; rotatedR[1] = R[1] + 1 ;
        rotatedC[2] = C[2] + 0 ; rotatedR[2] = R[2] + 0 ;
        rotatedC[3] = C[3] - 1 ; rotatedR[3] = R[3] - 1 ;
      break ;
      case 2:
        rotatedC[0] = C[0] - 1 ; rotatedR[0] = R[0] - 1 ;
        rotatedC[1] = C[1] - 1 ; rotatedR[1] = R[1] + 1 ;
        rotatedC[2] = C[2] + 0 ; rotatedR[2] = R[2] + 0 ;
        rotatedC[3] = C[3] + 1 ; rotatedR[3] = R[3] - 1 ;
      break ;
      case 3:
        rotatedC[0] = C[0] + 1 ; rotatedR[0] = R[0] - 1 ;
        rotatedC[1] = C[1] - 1 ; rotatedR[1] = R[1] - 1 ;
        rotatedC[2] = C[2] + 0 ; rotatedR[2] = R[2] + 0 ;
        rotatedC[3] = C[3] + 1 ; rotatedR[3] = R[3] + 1 ;
      break ;
    }
    break ;
    
    case "blue":
    switch(orientation){
      case 0:
        rotatedC[0] = C[0] + 1 ; rotatedR[0] = R[0] + 0 ;
        rotatedC[1] = C[1] + 0 ; rotatedR[1] = R[1] + 1 ;
        rotatedC[2] = C[2] - 1 ; rotatedR[2] = R[2] + 2 ;
        rotatedC[3] = C[3] - 2 ; rotatedR[3] = R[3] + 1 ;
      break ;
      case 1:
        rotatedC[0] = C[0] + 1 ; rotatedR[0] = R[0] + 1 ;
        rotatedC[1] = C[1] + 0 ; rotatedR[1] = R[1] + 0 ;
        rotatedC[2] = C[2] - 1 ; rotatedR[2] = R[2] - 1 ;
        rotatedC[3] = C[3] + 0 ; rotatedR[3] = R[3] - 2 ;
      break ;
      case 2:
        rotatedC[0] = C[0] - 1 ; rotatedR[0] = R[0] + 1 ;
        rotatedC[1] = C[1] + 0 ; rotatedR[1] = R[1] + 0 ;
        rotatedC[2] = C[2] + 1 ; rotatedR[2] = R[2] - 1 ;
        rotatedC[3] = C[3] + 2 ; rotatedR[3] = R[3] + 0 ;
      break ;
      case 3:
        rotatedC[0] = C[0] - 1 ; rotatedR[0] = R[0] - 2 ;
        rotatedC[1] = C[1] + 0 ; rotatedR[1] = R[1] - 1 ;
        rotatedC[2] = C[2] + 1 ; rotatedR[2] = R[2] + 0 ;
        rotatedC[3] = C[3] + 0 ; rotatedR[3] = R[3] + 1 ;
      break ;
    }
    break ;
    
    case "cyan":
    switch(orientation){
      case 0:
        rotatedC[0] = C[0] - 1 ; rotatedR[0] = R[0] + 2 ;
        rotatedC[1] = C[1] + 0 ; rotatedR[1] = R[1] + 1 ;
        rotatedC[2] = C[2] + 1 ; rotatedR[2] = R[2] + 0 ;
        rotatedC[3] = C[3] + 0 ; rotatedR[3] = R[3] - 1 ;
      break ;
      case 1:
        rotatedC[0] = C[0] - 1 ; rotatedR[0] = R[0] - 1 ;
        rotatedC[1] = C[1] + 0 ; rotatedR[1] = R[1] + 0 ;
        rotatedC[2] = C[2] + 1 ; rotatedR[2] = R[2] + 1 ;
        rotatedC[3] = C[3] + 2 ; rotatedR[3] = R[3] + 0 ;
      break ;
      case 2:
        rotatedC[0] = C[0] + 1 ; rotatedR[0] = R[0] - 1 ;
        rotatedC[1] = C[1] + 0 ; rotatedR[1] = R[1] + 0 ;
        rotatedC[2] = C[2] - 1 ; rotatedR[2] = R[2] + 1 ;
        rotatedC[3] = C[3] + 0 ; rotatedR[3] = R[3] + 2 ;
      break ;
      case 3:
        rotatedC[0] = C[0] + 1 ; rotatedR[0] = R[0] + 0 ;
        rotatedC[1] = C[1] + 0 ; rotatedR[1] = R[1] - 1 ;
        rotatedC[2] = C[2] - 1 ; rotatedR[2] = R[2] - 2 ;
        rotatedC[3] = C[3] - 2 ; rotatedR[3] = R[3] - 1 ;
      break ;
    }
    break ;
    
    case "magenta":
    switch(orientation){
      case 0:
      case 2:
        rotatedC[0] = C[0] - 1 ; rotatedR[0] = R[0] + 0 ;
        rotatedC[1] = C[1] + 0 ; rotatedR[1] = R[1] + 1 ;
        rotatedC[2] = C[2] + 1 ; rotatedR[2] = R[2] + 0 ;
        rotatedC[3] = C[3] + 2 ; rotatedR[3] = R[3] + 1 ;
      break ;
      case 1:
      case 3:
        rotatedC[0] = C[0] + 1 ; rotatedR[0] = R[0] + 0 ;
        rotatedC[1] = C[1] + 0 ; rotatedR[1] = R[1] - 1 ;
        rotatedC[2] = C[2] - 1 ; rotatedR[2] = R[2] + 0 ;
        rotatedC[3] = C[3] - 2 ; rotatedR[3] = R[3] - 1 ;
      break ;
    }
    break ;
    
    case "yellow":
    switch(orientation){
      case 0:
      case 1:
      case 2:
      case 3:
        rotatedC[0] = C[0] + 0 ; rotatedR[0] = R[0] + 0 ;
        rotatedC[1] = C[1] + 0 ; rotatedR[1] = R[1] + 0 ;
        rotatedC[2] = C[2] + 0 ; rotatedR[2] = R[2] + 0 ;
        rotatedC[3] = C[3] + 0 ; rotatedR[3] = R[3] + 0 ;
      break ;
    }
    break ;
    
    case "white":
    switch(orientation){
      case 0:
      case 2:
        rotatedC[0] = C[0] + 1 ; rotatedR[0] = R[0] - 1 ;
        rotatedC[1] = C[1] + 0 ; rotatedR[1] = R[1] + 0 ;
        rotatedC[2] = C[2] - 1 ; rotatedR[2] = R[2] + 1 ;
        rotatedC[3] = C[3] - 2 ; rotatedR[3] = R[3] + 2 ;
      break ;
      case 1:
      case 3:
        rotatedC[0] = C[0] - 1 ; rotatedR[0] = R[0] + 1 ;
        rotatedC[1] = C[1] + 0 ; rotatedR[1] = R[1] + 0 ;
        rotatedC[2] = C[2] + 1 ; rotatedR[2] = R[2] - 1 ;
        rotatedC[3] = C[3] + 2 ; rotatedR[3] = R[3] - 2 ;
      break ;
    }
  break ;
  }
  for(var i=0 ; i<4 ; i++){
    if(rotatedR[i]>nRows) return false ;
    if(rotatedR[i]<1)     return false ;
    if(rotatedC[i]>nCols) return false ;
    if(rotatedC[i]<1)     return false ;
    var skip = 0 ;
    for(var j=0 ; j<4 ; j++){ if(rotatedR[i]==R[j] && rotatedC[i]==C[j]) skip = 1 ; }
    if(skip==0){
      var className = getClass(rotatedR[i],rotatedC[i]) ;
      if(className!="empty") return false ;
    }
  }
  for(var i=0 ; i<4 ; i++){
    set(R[i],C[i], "empty") ;
    R[i] = rotatedR[i] ;
    C[i] = rotatedC[i] ;
  }
  for(var i=0 ; i<4 ; i++){
    set(R[i],C[i], color) ;
  }
  orientation++ ;
  if(orientation==4) orientation = 0 ;
}

Compare that to the streamlined version I had for Pentris:

function tryRotate(){
  rotatePiece() ;
  for(var i=0 ; i<n ; i++){
    if(rotatedR[i]>nRows) return false ;
    if(rotatedR[i]<1)     return false ;
    if(rotatedC[i]>nCols) return false ;
    if(rotatedC[i]<1)     return false ;
    var skip = 0 ;
    for(var j=0 ; j<n ; j++){ if(rotatedR[i]==R[j] && rotatedC[i]==C[j]) skip = 1 ; }
    if(skip==0){
      var className = getClass(rotatedR[i],rotatedC[i]) ;
      if(className!="empty") return false ;
    }
  }
  for(var i=0 ; i<n ; i++){
    set(R[i],C[i], "empty") ;
    R[i] = rotatedR[i] ;
    C[i] = rotatedC[i] ;
  }
  for(var i=0 ; i<n ; i++){ set(R[i],C[i], color) ;}
}
function rotatePiece(){
  for(var i=0 ; i<n ; i++){
    rotatedR[i] =  (Y) + C[i] - (X) ;
    rotatedC[i] =  (X) - R[i] + (Y) ;
  }
}

The optimisation here is quite impressive, and very much needed when you consider that the move from Tetris to Pentris means moving from five pieces of four blocks each, to eighteeen pieces of five blocks each, with the space needed for ratotions scaling from sixteen to twenty five.

In the first function the different scenarios are all checked explicitly, whereas in the second function the temporary piece is rotated and then collisions checked, before updating the main piece. The amount of code when from 82 to 27 lines, and in doing so became safer and more easily extendible.

In a similar vein, here's the updatePreview function, first in Tetris:

function updatePreview(){
  for(var i=1 ; i<3 ; i++){ for(var j=1 ; j<5 ; j++){ setPreview(i, j, "empty") ; } }
  switch(nextPiece){
    case 1:
      setPreview(1, 2, "red") ;
      setPreview(1, 3, "red") ;
      setPreview(2, 3, "red") ;
      setPreview(2, 4, "red") ;
      break ;
    case 2:
      setPreview(1, 3, "green") ;
      setPreview(2, 2, "green") ;
      setPreview(2, 3, "green") ;
      setPreview(2, 4, "green") ;
      break ;
    case 3:
      setPreview(1, 1, "blue") ;
      setPreview(1, 2, "blue") ;
      setPreview(1, 3, "blue") ;
      setPreview(2, 3, "blue") ;
      break ;
    case 4:
      setPreview(1, 3, "cyan") ;
      setPreview(1, 2, "cyan") ;
      setPreview(1, 1, "cyan") ;
      setPreview(2, 1, "cyan") ;
      break ;
    case 5:
      setPreview(1, 3, "magenta") ;
      setPreview(1, 2, "magenta") ;
      setPreview(2, 2, "magenta") ;
      setPreview(2, 1, "magenta") ;
      break ;
    case 6:
      setPreview(1, 2, "yellow") ;
      setPreview(1, 3, "yellow") ;
      setPreview(2, 3, "yellow") ;
      setPreview(2, 2, "yellow") ;
      break ;
    case 7:
      setPreview(2, 1, "white") ;
      setPreview(2, 2, "white") ;
      setPreview(2, 3, "white") ;
      setPreview(2, 4, "white") ;
      break ;
  }
}

Second in Pentris:

function updatePreview(){
  for(var i=1 ; i<4 ; i++){ for(var j=1 ; j<6 ; j++){ setPreview(i, j, "empty") ; } }
  var x = new Array(n) ;
  var y = new Array(n) ;
  var piece = getPiece(nextPiece) ;
  var counter = 0 ;
  for(var i=0 ; i<n ; i++){
    for(var j=0 ; j<n ; j++){
      if(piece[i][j]==1){
        x[counter] = i   ;
        y[counter] = j+1 ;
        counter++ ;
      }
    }
  }
  for(var i=0 ; i<n ; i++){setPreview(x[i],y[i],colors[nextPiece-1]);}
}

Such an economy of code! One of my favourite parts of code is refactoring existing blocks of code into something more elegant and effiienct. The original code, as a first attempt, and a first adventure in serious Javascript was necessarily messy and in need of a good clean, so it's no surprise that so many improvements could be had.

I've been putting off resolving these differences for a very long time, partly because the games work as they are and there's no need to fix something that's not broken, and partly because I wanted to keep some legacy code around to remind myself of how it all started. I've archived the original code and now I'm bringing the Tetris, Pentris, and Tritris into line with each other to make all three games share a single code base.