Skip to main content

Tetris meets the Java bean

Break down the Tetris game elements into Java objects to form a reusable Java game component

Scott Clee (Scott_Clee@uk.ibm.com), Software Engineer, IBM
Photo of Scott Clee
Scott Clee currently works as an FV Tester for IBM's CICS product. He's been a Java programmer for four years and enjoys dabbling in fun projects involving Java. While his name is in lights, he'd like to say hello to his parents, Stephen and Susan, and his brothers, Craig and Christian. Contact Scott at Scott_Clee@uk.ibm.com.

Summary:  IBM Software Engineer -- and gamer at heart -- Scott Clee provides a simple way to take the Tetris game model and wrap it up as a reusable Java bean component. Once the game elements have been broken down into Java objects, they can be reassembled to form the complete game model bean, enabling it to be plugged into virtually any Tetris GUI.

This updated version lets you see the TetrisBean in use. Just click Start and use the arrow keys on your keyboard to maneuver the tetris pieces.

Date:  11 Apr 2002 (Published 01 Mar 2002)
Level:  Introductory
Activity:  1866 views
Comments:  

A friend once told me that whenever he learned a new programming language, he would challenge himself to write a Tetris game using that language. So in this tradition, I took it upon myself to do the very same when I first learned to program using the Java language. My first attempt, although a complete game, was very basic and very ugly. As time went on and I gained greater experience in Java design and development, I knew that I could separate the game model from the GUI (similar to Swing components) and create a Tetris bean. So I set about doing just that.

In this article, I'll guide you through the construction and implementation of Tetris bean. You can see the TetrisBean in use. Just click Start and use the arrow keys on your keyboard to maneuver the tetris pieces. Pressing the up-arrow changes the orientation of the piece; pressing the left- and right-arrows moves the piece sideways; and pressing the down-arrow moves the piece to the bottom. Give it a try!

Pieces and parts

Tetris has several components that can be expressed as Java objects:

  • The Tetris pieces
  • The Tetris board that holds the pieces
  • The game that controls the pieces on the board, manages scoring, and so on.

We'll look at each of these elements more closely.


The Tetris pieces: Class TetrisPiece

The key elements of a Tetris piece, shown in Figure 1, are as follows:

  • Each piece is made up of exactly four blocks.
  • Each block in the piece has an (x,y) coordinate within the Tetris board.
  • Each piece has a rotation factor of either 0, 2, or 4.
  • Each piece can be either an L, J, S, O, I, Z, or T shape.

Figure 1. Four elements are needed for each Tetris piece: blocks, an (x,y) coordinate, a rotation factor, and its shape.
Four elements of each Tetris piece

What can you do with a Tetris bean?

Here are a few things I've done since creating the Tetris bean:

  • Connected two instances of the bean together to create a player-vs-player game
  • Created the first-ever Tetris game for the Psion Netpad, called Netris
  • Incorporated the bean into an applet to create a browser-compatible Tetris game

The first two items can be implemented using a very simple system. By choosing a centre block for each piece and storing its (x,y) position, you can store the rest of the blocks in the piece as coordinates relative around it. This allows you to describe any style of piece by simply storing its shape as blocks relative around a centre piece. The centre point can be stored as a java.awt.Point and the relative coordinates in a java.awt.Point array. To make it easier for coordinate calculations, you can also store the centre piece as a block with relative coordinates (0,0).

Rotating pieces

This system also makes things easier when it comes to calculating the rotation of the pieces. Using simple matrix manipulation, you can rotate a piece 90 degrees clockwise by simply swapping the y-coordinate with the x-coordinate and the x-coordinate with the negative of the y-coordinate. Since we happen to be using relative coordinates around a centre point, we can do the same here:

temp = x;
x = -y;
y = temp;

You can also rotate a piece 90 degrees counterclockwise by applying three clockwise rotations. (Try it out for yourself if you don't believe me!)

Finally, not all the pieces have the same amount of rotation factor, so some checks will need to be implemented in the rotation method to compensate for this.

Give me an L

Because we use the same TetrisPiece class to represent all the types of pieces, we need a way to distinguish among them. To do this, we use some static int constants to represent the different types and have a local variable to store the piece's type. Here is an example of one of the constants:

public static final int L_PIECE = 0;

Moving pieces

Because we'll be moving the pieces around the Tetris board, we need to provide a move method that allows us to do this. Notice that some moves may be illegal (such as an attempt to move right when you are already as far right as possible), so we'll need to validate all move requests. We'll do this against the Tetris board, which we'll store a reference to. So from this the constructor of our class will take two parameters: first the type of piece created, and second a reference to the board. In the constructor, we'll call a private utility method named initalizeBlocks(), which will set the values of the piece's relative coordinates to what the respective piece type is.

A simple approach of checking move legality is to remove the piece from the board, then move it in the desired direction and see if it fits. If it does, then put the piece into the new board position. Otherwise, undo the move and put it back where it was. Depending on the result, the returned value will either be true (if the move was fine) or false (if it wasn't).

One type of move that needs a bit more consideration is that of a piece fall. A fall means the piece will immediately drop to its lowest position in the board. To do this we need a while loop that will continue moving the piece down until it can't move any further. The piece will then be placed at that position.

To differentiate the various moves that can be applied to a piece we'll use some extra static int constants, as shown in the following example:

public static final int LEFT = 10;

The willFit() method for checking if a piece fits will be implemented in the TetrisBoard class later.

Finalizing TetrisPiece

Finally, to wrap up the TetrisPiece class we have a few getters and setters for variables such as the centre point and the relative coordinates, and a static method called getRandomPiece() that will return a TetrisPiece instance of random type.

The complete source, which includes the TetrisPiece class, is available for download in Resources.


The Tetris board: Class TetrisBoard

A Tetris board can be thought of as a 2D grid containing empty blocks and coloured blocks. Because the various types of Tetris pieces are differentiated by int constants, all we need to do is define a value for an empty block and we can store the board as a 2D int array. Using this approach, an empty block in the board will be represented by:

public static final int EMPTY_BLOCK = -1;

To keep things flexible, we'll allow the size of the board to be variable but define this in the constructor. So the constructor will accept two ints representing the number of rows and columns. It will then call a resetBoard() method that will default all values in the 2D array to the empty block.

Adding and removing pieces

Because pieces are added to and removed from the board, we provide addPiece() and removePiece() methods. The addPiece() method works by taking a TetrisPiece() and setting the values of all the positions on the board it occupies to that of its type. The removePiece() method is similar except the board values are set to that of the empty block.

Notifying users of change

To keep users updated when something happens on the board, a BoardEvent will be fired when pieces are added or moved. For classes to listen to this event, we need a BoardListener interface, whose boardChange() method gets called when the event is fired. These events can be used by a Tetris board GUI to be notified when the screen needs updating. To store the listeners, we'll use a java.util.Vector and provide relevant methods for adding and removing the listeners and firing events.

Occasionally, it might be inappropriate to fire BoardEvents when you're adding and removing pieces; for example, when a piece fall is requested (remember this move uses a while loop to drop the piece). In this case, an event is only required when the piece hits the bottom. To facilitate this, we'll create the addPiece() method so it takes a boolean parameter, which will cause events to be fired only if the value is true.

Removing completed rows

One important factor in the game of Tetris is that when a line is completed it disappears and all rows above it drop down. To do this, we'll provide a removeRow() method that accepts the index of the row to remove as a parameter. After the row has been removed, a BoardEvent will get fired.

Finalizing TetrisBoard

A hundred-foot wall of Tetris?

One day I'd like to connect this bean to the lighting system of a tower block and play Tetris down the side of the building. I've been wanting to do this ever since I read in a newspaper about someone doing the same.

In addition to the necessary getters and setters to access private variables, we need one more method: the willFit() method discussed earlier. This method takes TetrisPiece as a parameter and returns a boolean value to determine whether the piece fits in this board. By fitting I mean that the piece is within the boundaries of the board and the blocks on the board corresponding to where the piece would fit are set to the empty block value. If this is the case, then a value of true is returned.

That completes the TetrisBoard class. Again, the complete source, which includes this class is available for download in Resources.


The Tetris game model: Class TetrisGame

Now that we've created the two main components used in a Tetris game, all we have to do is put them together with a little game logic and we're away.

The GameThread inner class

A nice way to control the flow of play is to embed it in an inner class extending java.lang.Thread. One advantage of this approach is that we can add a thread sleep call to control the speed of play. Another is that because the main application thread is now freed up we shouldn't have any painting problems when a GUI is attached, which can happen sometimes if the main thread gets tied up and doesn't have enough time to paint.

The logic in the thread will be implemented within a while loop inside the run() method. The loop will continually create pieces and drop them within the game board until no more pieces can fit. At this point a local boolean variable called fPlaying will be set to false, ending the loop and firing a GameEvent to signify the end of the game.

Within the while loop is an if statement that checks on the value of the boolean fPaused. If this is set to true, the loop will continue running but all the game logic will be bypassed, giving the impression of a pause. When the boolean is changed back to false the game will continue.

Because we're concerned only with one piece dropping at a time, we'll create a variable called fCurrPiece that will store a reference to it. If this variable is set to null, it means the previous piece could not move down any further and has been left in its final position on the board. At this point we need to create a new piece and position it at the centre of the top of the board. For all the time that the fCurrPiece variable is non-null, all we have to do is drop it by one position and sleep the thread for a given amount of time.

When a piece cannot move any further, we need to determine whether we have any completed lines. An easy way to do this is to use a pair of nested for loops where the outer one works up through the row indexes and the inner one checks across them. If we find a completed line, we can call the removeRow() method implemented in the TetrisBoard class and pass it the index of the completed line. Because all rows above it will now be shifted down by one, we'll need to check that row again. To encourage the completion of multiple lines at once we'll store a count of the lines completed and award a high score increment respectively.

Another major element of Tetris game play is the fact that as more lines are completed the pieces fall faster. This feature can be implemented by checking the number of lines completed so far and decrementing the thread sleep delay accordingly.

That's all there is to the GameThread inner class, but there is another inner class we'll need to implement. Because there will be a load of events fired that in turn will require listeners to be stored, it would be neat for us to keep them all in one place. We'll do this using the EventHandler inner class.

The EventHandler inner class

This class will store references to listeners that are interested in the events we fire. In addition to providing add and remove methods for the listeners, there'll also be utility methods for firing the events.

This class takes care of the following types of events:

  • GameEvent, which gets fired whenever a game starts or stops. It has a value to signify a game START or END.

  • BoardEvent, which gets fired whenever something has changed on the board. In the EventHandler class the add/remove listener calls are passed through to the TetrisBoard class.

  • ScoreEvent, which gets fired whenever the score changes.

There are many other types of events that could get fired, but to keep things simple I've stuck to the ones above. One other obvious event we could implement is LineEvent, which would get fired when a line or multiple lines are completed and potentially could be used to trigger an on-screen animation.

Finalizing TetrisGame

Now that we've finished the inner classes, we need to describe the rest of the TetrisGame class. As with all Java beans, we need a zero argument constructor. In this constructor we'll create an instance of the EventHandler class and an instance of the TetrisBoard class. The TetrisBoard class will have a default size of 10x20.

To control the state of the game we'll use some start, stop, and pause methods. The startGame() method will reset all game variables and fire both a ScoreEvent (as it has now been reset to zero) and a GameEvent (with a parameter type of START). It will also create and kick off a GameThread. The stopGame() method will change the fPlaying variable to false, causing the GameThread to end and fire a GameEvent with parameter type END. The setPause() method will only pause a game if one is actually being played.

Apart from all the necessary getters and setters, there's only one more method to implement, that is the move() method. This method takes as a parameter the direction of the move, which is a constant from the TetrisPiece class. Assuming that a game is in play and not paused, the move() method attempts to make the move. If the move is a drop or a fall request and it is unsuccessful, then fCurrPiece is set to null and the piece is left in its current board position. The GameThread then takes care of creating a new piece for us.

And that's it for the TetrisGame. Again, this class is available as part of the complete source for download in Resources.

Now for completeness sake we could create a BeanInfo class and jar these classes up with an appropriate manifest file, but for now we don't need to. What we do need, though, is a simple GUI to test our bean out, and one is provided in Resources. This is a very simple class that uses an inner class to draw the Tetris board and contains some logic for coordinating key presses. The GUI itself is displayed in a javax.swing.JFrame, although it could alternatively be a java.applet.Applet.

To test out the Tetris bean, unzip the source code, keeping the directory structure intact. (This is necessary as I have placed the bean classes in a TetrisBean package so they need to be in a TetrisBean directory.) Add the path where you unzipped the source files to your java classpath and compile the files. Now all you have to do is run "java Scottris" and you're on your way!

My own little disclaimer: I understand that there are many ways this Tetris bean could be implemented, but what I've given you here is a very simple approach that I hope will stoke your creative fire. Please feel free to improve on it.



Download

NameSizeDownload method
j-tetris.zip10KB HTTP

Information about download methods


Resources

  • Check out JavaGaming.org for more game developer resources.

  • Robocode wraps up Java gaming and Java education in one great program. And don't miss Sing Li's excellent guide "Rock 'em, sock 'em Robocode" (developerWorks, January 2002).

  • You'll find hundreds of articles about every aspect of Java programming in the IBM developerWorks Java technology zone.

About the author

Photo of Scott Clee

Scott Clee currently works as an FV Tester for IBM's CICS product. He's been a Java programmer for four years and enjoys dabbling in fun projects involving Java. While his name is in lights, he'd like to say hello to his parents, Stephen and Susan, and his brothers, Craig and Christian. Contact Scott at Scott_Clee@uk.ibm.com.

Comments



Trademarks

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Java technology
ArticleID=10642
ArticleTitle=Tetris meets the Java bean
publish-date=04112002
author1-email=Scott_Clee@uk.ibm.com
author1-email-cc=