OOTetris Design

From CSSEMediaWiki
(Difference between revisions)
Jump to: navigation, search
 
(14 intermediate revisions by one user not shown)
Line 5: Line 5:
 
My final tetris at the end could be played by many players on different computers in a network. Playing the game works well and is fun although the software design in general is quite messy. The design we came up with was more or less guided by our progress on programming different parts of the game on following the weekly assignments. So we started out with the Server-Client Communication. After the basic communication was achieved we started implementing the tetris game itself with its graphical user interface and its game logic (game control). As I said before the software design of the tetris works but is not really nice. Because of that I think it is a nice use-case to apply different OO-Design refactoring methods and other OO-Design principles to change the old design to a new much nicer design.
 
My final tetris at the end could be played by many players on different computers in a network. Playing the game works well and is fun although the software design in general is quite messy. The design we came up with was more or less guided by our progress on programming different parts of the game on following the weekly assignments. So we started out with the Server-Client Communication. After the basic communication was achieved we started implementing the tetris game itself with its graphical user interface and its game logic (game control). As I said before the software design of the tetris works but is not really nice. Because of that I think it is a nice use-case to apply different OO-Design refactoring methods and other OO-Design principles to change the old design to a new much nicer design.
  
At the beginning of my design study I was trying to figure out, what would be the best way to start redesigning and refactoring the old design. So I figured it might be smart to go over every part of the design in detail and explain how it works. Doing that seems to as a good starting point to find out what has to be done differently. Just by describing different parts I noticed things I needed to change. Also describing in detail how things work help other guys to give me better feedback.
+
At the beginning of my design study I was trying to figure out, what would be the best way to start redesigning and refactoring the old design. So I figured it might be smart to go over every part of the design in detail and explain how it works. Doing that seems to as a good starting point to find out what has to be done differently. Just by describing different parts I noticed things I needed to change. Also describing in detail how things work help other guys to give me better feedback.  
 +
 
 +
By the way Sorry for any typos but I am a foreigner!!
  
 
==The old Design==
 
==The old Design==
Line 78: Line 80:
 
===S_Command===
 
===S_Command===
 
The interface S_Command gets implemented by all "S_"-Commands. All S_Command objects are created on the server side by S_toClient, send to Client_fromServer and then executed there. When they get executed in Client_fromServer, it puts itself as parameter into the command's execute(Client_fromServer)-method. Therefore S_Commands are sent from the server but execute on the client side.
 
The interface S_Command gets implemented by all "S_"-Commands. All S_Command objects are created on the server side by S_toClient, send to Client_fromServer and then executed there. When they get executed in Client_fromServer, it puts itself as parameter into the command's execute(Client_fromServer)-method. Therefore S_Commands are sent from the server but execute on the client side.
 +
 +
S_Command commandObject = (S_Command)objectFromServer.readObject();
 +
commandObject.execute(this);
 +
 +
[[Image:S_Command.jpeg]]
  
 
===C_Command===
 
===C_Command===
 
In contrast to the S-Command, C_Command gets implemented by all "C_"-Commands. Those Commands get sent from the Client side to the server side. Thus when they get executed on the server side by the receiving thread S_fromClient, S_fromClient puts a reference of itself into the execute(S_fromClient)-method, so for example it can change attributes.
 
In contrast to the S-Command, C_Command gets implemented by all "C_"-Commands. Those Commands get sent from the Client side to the server side. Thus when they get executed on the server side by the receiving thread S_fromClient, S_fromClient puts a reference of itself into the execute(S_fromClient)-method, so for example it can change attributes.
 +
 +
C_Command commandObject = (C_Command)objectFromServer.readObject();
 +
commandObject.execute(this);
 +
 +
[[Image:C_Command.jpeg]]
  
 
Furthermore each of those two interfaces and their concrete classes of the command package realizes the [[Strategy]] design pattern. However, I just found out it could also be a [[Command]] patter. I am not totally sure.
 
Furthermore each of those two interfaces and their concrete classes of the command package realizes the [[Strategy]] design pattern. However, I just found out it could also be a [[Command]] patter. I am not totally sure.
  
 
==S_Common & C_Common==
 
==S_Common & C_Common==
To be able to have access to all attributes over different threads I used two classes, S_Common and C_Common. Both are mainly responsible for keeping track of different data fields, whereas S_Common is doing that on the server side, C_Common is doing it on the client side. There are a couple of arguments which speak against the use of those two classes. I think the main reason why you shouldn't do that is because you should [[Keep related data and behavior in one place| keep related data and behavior in one place]]. Since both classes pretty much don't have any behavior it doesn't make sense to store those attributes in there. Besides  because of those classes call up operation can have really nasty nesting that clearly disagrees with the [[Law of demeter]] and [[Tell, don't ask]]:
+
To be able to have access to all attributes over different threads I used two classes, S_Common and C_Common. Both are mainly responsible for keeping track of different data fields, whereas S_Common is doing that on the server side, C_Common is doing it on the client side. There are a couple of arguments which speak against the use of those two classes. I think the main reason why you shouldn't do that is because you should [[Keep related data and behavior in one place| keep related data and behavior in one place]]. Since both classes pretty much don't have any behavior it doesn't make sense to store those attributes in there. Besides  because of those classes call-up operation can have really nasty nestings which clearly disagrees with the [[Law of demeter]] and [[Tell, don't ask]]:
  
 
  cCommon.objectToServer.writeObject(new C_gameOverCommand());
 
  cCommon.objectToServer.writeObject(new C_gameOverCommand());
  
Therefore in a first step of the refactoring all attribute by using the [[Move Field]] refactor method will be transfered to the objects they belong to. In most cases thez will be moved into the client-server threads.
+
===C_common===
 +
Therefore in a first step of the refactoring, using the [[Move Field]] refactor method, all attributes of the C_common Class were transfered to the objects on the client side they actually belong to. In this case most of the fields were either not needed or just moved into the Client_fromServer thread where they actually belong. Since Client_fromServer is handling the client sided network communication it makes sense to have the needed attributes in there.
 +
 
 +
===S_common===
 +
On the server side it turned out to be a bit different. S_common had from the start on some functionality. By adding the S_newStonesCommand it even got more functionality. S_common encapsulates all the behavior and attributes all server threads need to have. Since the server has two threads for each client, attributes like player names and setting flags for sending newStones, newScore or fallCommands are handled in S_common. So instead of removing this class I actually extended its behavior. However, it doesn't contain that customized SList with stones anymore. Each client has a list of its own stones as said before. SO there is no need for SList anymore.
 +
 
 +
S_common has an object called S_clock. S_clock is a thread which creates a "tick" every half second. This is the game pace. S_toClient sends a S_fallCommand to all clients every single time S_clock ticks. This assures all clients have the same pace.
  
 
==Tetrisfield & Stones==
 
==Tetrisfield & Stones==
*current way of moving down the stones on the tetrisfield quite elegant although no OO Design => [[Switch statement smell]] also violation of the [[Open closed principle]]
+
Every client has GUI. This GUI consists of the C_GUI class which contains a tetrisfield TetPanel (TP) and a preview of the next stone in NextStoneLabel (NSL). The TP as well as the NSL have a stone object S_Stone. A normal tetris game as introduced in the 80's has seven different colored stones I, J, L, O, S, T, and Z - due to their resembling letters of the alphabet:
*explain how it works: Tetrisfield + Stones
+
*S_I - blue
*1st solution making tetmat to a pointer of stones. although problem because of grey/white fields => violation of [[Model the real world]]???
+
*S_J - red
*2nd solution making tetmat point of new objects blocks. every stone consists of blocks as well => Composite!
+
*S_L - green
 +
*S_O - yellow
 +
*S_S - magenta
 +
*S_T - cyan
 +
*S_Z - orange
 +
Those stones have an abstract super-class S_Stone. Every stone encapsulates a different state of the superclass stone. It has different fields, such as color, name and matrix and also has different behavior, since each stones implements its own rotation. Therefore the stone package implements a [[State]] pattern.
 +
 
 +
Every single stones consists of 4 blocks. Each stone has a different way of rotating. Thus stones can be in different states and can also have a different amount of states. For example, the O-stone doesn't rotate at all and therefore just has one state. In contrast the T-stone has 4 different states. Depending on its state it is ordered differently in the tetrisfield.
 +
 
 +
===Elegant===
 +
In the old design every stone object has 4 x 4 matrix (2-dimensional int array) containing numbers. Every number stands for a color. So for example if we look at the T-stone. The T-stone is cyan. Therefore its 4 fields which represent the shape of the stone have the number which stands for color cyan (e.g. 3). The rest of the matrix is filled with 0. To turn/rotate the stone just the fields which change the color need to be changed. In the diagram below you can see how the switching between different states of a stone is working:
 +
 
 +
[[Image:StoneStates.jpg|500px]]
 +
 
 +
To [[Keep it simple]] the TP consists also of 2-dimensional integer array. All white field are filled with 0 which represents the color white. The stone on the TP is moving down as soon as the Client_fromServer has executed a S_fallCommand. Each time the stones moves down on the tetrisfield, it is checked if a collision occurred. The checkCollision() method is in my opinion done in an elegant way. The number of every single field in the matrix of the stone is multiplied with the corresponding number of the TP's matrix. If the result of this is 0 no collision happened and the stone can move to that position and can be painted. If a collision occurred the stone will placed at the last position.
 +
 
 +
[[Image:Tetrisfield.jpg|400px]]
 +
 
 +
As soon as the stone can not move down anymore, its containing numbers are transfered into the TP's matrix and the next stone from the NSL comes into play and starts to move down the tetrisfield.
 +
 
 +
===but smelly===
 +
Even though on first sight that seemed to me a nice way of doing the graphical game logic it actually creates a couple of problems with respect to OODesign:
 +
 
 +
for(int i = 0; i < 26; i++)
 +
{
 +
  for(int j = 0; j < 15; j++)
 +
  {
 +
  int col = tetMat[j][i];
 +
  switch(col)
 +
  {
 +
    case 0: g.setColor(Color.white); break;
 +
    case 1: g.setColor(Color.blue); break;
 +
    case 2: g.setColor(Color.red); break;
 +
    case 3: g.setColor(Color.green); break;
 +
    case 4: g.setColor(Color.yellow); break;
 +
    case 5: g.setColor(Color.magenta); break;
 +
    case 6: g.setColor(Color.cyan); break;
 +
    case 7: g.setColor(Color.orange); break;
 +
    case 8: g.setColor(Color.lightGray); break;
 +
  }
 +
  if(col==0 || col==8)
 +
  g.fillRect(j*width, i*height, width, height);
 +
  else
 +
  g.fill3DRect(j*width, i*height, width, height, true);
 +
  }
 +
}
 +
For example the code above shows part of the [[Switch statement smell| switch-statement smelly]] paintComponent() method in TP. Basically that method just goes through the whole matrix and depending on the number in the field draws a little block in the correct color. The other problem with that statement is that as soon as somebody wants to extend the tetris, for example with a new stone shape, you have to go through the code and add new lines (cases) to those switch-statements. This violates a couple design maxims such as the [[Open closed principle]], [[Don't repeat yourself]] and [[Once and only once]]. Whenever a stone or the TP has to be painted this kind of switch-statement appears. That means there is at least one in NSL, S_Stone and TP. This [[Duplicate code smell | smells like the same code all over the place]].
 +
 
 +
To get rid of those smelly switch-case-statments I thought I need to get every field in the TP, NSL and stone to draw itself. Therefore every field would have to have a paint() method and would need to know e.g. its own color. That means each field in a matrix should be an object by itself. First I thought I convert the integer matrix of the TP into a stonepointer matrix where every pointer is a reference to its corresponding stone object. In the end every stone object should draw itself. However this didn't seem to be a good solution, since for example the white fields in the TP's matrix are not part of a stone. So I would need to create blank stone object which would just draw a white block. This seemed to me as violation of the [[Model the real world]] principle since there is no blank white stone.
 +
 
 +
In my second approach I created a new object called S_ColorBlock. Since every stone consists of 4 blocks in a 4 x 4 matrix with 12 white blocks I figured it might be smart to split up a stone into blocks. Every single block has its own paintBlock() method. Also each block knows its own color. Thus every S_Stone object consists of 16 S_ColorBlock objects. In this case I made use of the [[Model the real world]] rule since every tetris stone by definition consists of 4 separate blocks anyways. The tetrisfield and the preview field as well are 2-dimensional arrays of S_ColorBlock objects. Every single block object is able to paint itself. Therefore I got rid of those switch-case statements. Also it is quite simple to add a newly shaped tetris stone. All you have to do is create a new class for the new stone and define its shape and color.
  
 
==Conclusion==
 
==Conclusion==
 +
In general the design now is still not the perfect object oriented design although it is way better than before. There is still some coupling going on and also many field are still public. I have created numerous [[Getters and setters|getters and setters]] but there is still more refactoring involved to get rid of the coupling. Also in my tetris implementation the GUI and Model are quite tightly coupled at least on the client side. Also I didn't change the names of my classes but I think it would make sense to use JAVA conventions. I removed many fields which were not needed anymore.
 +
 +
[[Image:Newtetris.jpeg|1024px]]
 +
 +
In the end I have to say I am quite happy with the result of my design study especially the Stone/TetrisPanel problem and the Commands problem are solved in a nice way. In the future I might actually end up adding some more functionality. I think it would be really nice to have an even more competing mode to play against each other. I remembered on my GameBoy's Tetris when we played against each other, as soon as somebody removed a line the other person got random blocks added to their tetrisfield.
 +
 +
 +
==Sourcecode==
 +
 +
[[Image:OOTetris.zip]]
 +
 +
To start the game you have to be in the OOTetris/bin directory and type in your shell: java GUIPack/S_GUI
 +
Then the S_GUI comes up and you have to choose if you want to be the server. By clicking the start-button the C_GUI comes up. Now you just type in the server's IP and your game name and press connect.
 +
Have fun playing OOTetris!
  
 
==Log of Constructionsites==
 
==Log of Constructionsites==
Line 109: Line 191:
 
*adding game ideas (sendLines/no timelimit) => sendLinesCommand and change timelimit in S_GUI
 
*adding game ideas (sendLines/no timelimit) => sendLinesCommand and change timelimit in S_GUI
 
*GUI classes refactoring => observer/observable
 
*GUI classes refactoring => observer/observable
*public-private -> getters/setters and [[Move field]] (done)
+
*public-private -> getters/setters and [[Move field]] (some done)
 +
 
 +
==See also==
 +
used links in the design study

Latest revision as of 04:00, 1 October 2008

Contents

Introduction

During my Bachelor studies in Germany I had to implement a network based multiplayer Tetris. Usually every person, who has touched a computer, has played or at least knows the computer game Tetris. It is one of the most popular games of all times. Programming this tetris game was one of my first projects during my bachelor studies. We were guided by weekly assignments of the lecturer/tutors. The difficulty of that project was probably not the tetris game and its game control itself, it was rather that the game was supposed to be played over a network by multiple players. So therefore we had to establish a server-client communication. Whereas the server is supposed to control the game pace. It also should assure that each client gets the same stones, especially in the same order. Hence all clients have the same conditions. The server also keeps track of the different clients scores.

My final tetris at the end could be played by many players on different computers in a network. Playing the game works well and is fun although the software design in general is quite messy. The design we came up with was more or less guided by our progress on programming different parts of the game on following the weekly assignments. So we started out with the Server-Client Communication. After the basic communication was achieved we started implementing the tetris game itself with its graphical user interface and its game logic (game control). As I said before the software design of the tetris works but is not really nice. Because of that I think it is a nice use-case to apply different OO-Design refactoring methods and other OO-Design principles to change the old design to a new much nicer design.

At the beginning of my design study I was trying to figure out, what would be the best way to start redesigning and refactoring the old design. So I figured it might be smart to go over every part of the design in detail and explain how it works. Doing that seems to as a good starting point to find out what has to be done differently. Just by describing different parts I noticed things I needed to change. Also describing in detail how things work help other guys to give me better feedback.

By the way Sorry for any typos but I am a foreigner!!

The old Design

Class-Diagram (package overview)

TetrisDesign1.jpg

The old design consists out of 5 different packages as you can see in the UML-Diagram above:

  • The green package in the UML-Diagram is responsible for the Server-Client communication.
  • The pink package controls the game. That's where the game intelligence is implemented.
  • The blue package is obviously the graphical user interface (GUI).
  • The red package contains the different stones of the tetris game.
  • The yellow package is a customized list which is used to store the stones of the game.

State-of-the-art Class-Diagram

To be able to understand the whole old design, I think it makes sense to have a look at the more detailed version of the Class-Diagram:

OldTetris.jpeg

This diagram shows the whole structure of all classes and their relationships a bit more in detail.

Network Structure

As I said before this is a multi player tetris which can be played over the network. To be able to do that a client-server structure is necessary. As you can see in the diagram there is a GUI for the Client as well as for the Server. The Server GUI (S_GUI) gets started. Here the player chooses either to setup his/her computer as server or just as client. In case it is setup as server, the server structure as well as the client structure are built. In case it is just a client, only the client (C_GUI) is started. To understand the server-client communication I attached a diagram, which describes the essentials of the network communication.

CScommunication.jpeg

So basically what does the diagram tell us. There is a server-sided Thread called Server_connectClient. This thread waits until all clients are connected to it. In the server GUI the user defines how many players the game is going to have. As soon as all clients are connected the game starts. That means on the server side for every client there are two threads constructed, S_toClient and S_fromClient. S_toClient is responsible for sending stone- and command-objects to the client. On the client's side a thread Client_fromServer is constructed which handles the client communication. It receives the stone- and command-objects from the server and handles those in certain ways. Besides handling the receiving objects this thread can also send commands to the server (more detail about the different commands later on). So on the server side the thread S_fromClient treats the receiving commands. So in general the server has 2 threads for every single client to communicate with it. Every client has just one thread which handles the communication.

Commands

The command objects get sent over the network from the client to the server and vice versa. On the serve side, as said before, the thread S_fromClient is handling receiving command objects and the thread S_toClient is doing the sending of command objects to the client. Commands starting with an "S_" are sent from the server to the client; commands starting with a "C_" are sent from the client.

So far there is a command-package with 5 different command objects:

  • S_printScoreCommand is sent from the server to the clients as soon as the score of players has changed and the list of players and their scores were updated/sorted, the server sends a S_printScoreCommand to the clients. By calling up the execute()-method the new score list is updated on the GUI.
  • S_fallCommand is responsible for the falling/moving down of the stones on the gamefield. On every tick of the S_clock object a S_fallCommand is sent to all clients to have the same game pace for players.
  • C_newScoreCommand is the counterpart of the S_printScoreCommand on the client side. As soon as a client removes lines and therefore gets new points the score list has to be updated. THat's when the client sends this command to the server.
  • C_getStoneCommand requests a new stone. In the S_fallCommand the stone gets moved down. By doing that it is checked if there is a collision. As soon as there is a collision the stone gets transfered into the gamefield and a new stone has to be requested.
  • C_gameOverCommand is sent when a client is game over.

Commands.jpeg

As you can see there is an interface called CS_Command, which has two execute() methods, one for commands executed on the server side and one for the commands executed on the client side. There are two methods because they have different parameters. So all "C_"-commands just implement the execute(S_fromClient) method while all "S_"-commands just implement the execute(C_fromServer) method. This in Jason's and my understanding is a violation of the No fat interfaces principle because this interface is designed for two different aims. In this case there should probably either be two interfaces one for the server commands and one for the client commands or I think the nicer solution would be just to add an interface, for example called CSCommunication, which would be implemented by the server-client-threads, S_fromClient, S_toClient and Client_fromServer. Thus you wouldn't need two execute()-methods anymore. You would end up with one method execute(CSCommunication).

CSCommunication.jpeg

Also so far when the client sends the C_getStoneCommand the server sends the client just one stone-object. The server has a list of stones called SList. This list consists of SElements which contain stone objects. The stone objects are randomly created. To assure that every client gets the same stones and in particular in the same order the list memorizes which clients has requested which SElement's stone. So every client has basically just 2 stones, one on the gamefield and the next stone in the preview field. According to that when the client asks for a new stone by sending the C_getStoneCommand, the server sends a specific stone object back to client. To receive the different objects sent by the server the Client_fromServer thread has to check from which type those objects are:

Object serverObjectOutput = objectFromServer.readObject();
if (serverObjectOutput instanceof S_Stone)
{
  knowsAll.actualStone = (S_Stone)serverObjectOutput;
  doSomething();
}
else if (serverObjectOutput instanceof CS_Command)
{
  CS_Command commandObject = (CS_Command)serverObjectOutput;
  commandObject.execute(this);
}

So first the object gets checked of which type it is (instance of) and then has to be downcasted. This violates the Avoid downcasting principle. So it would be way nicer to just have commands getting send over the network. All the receiver has to do is read the received object and execute it:

CS_Command commandObject = (CS_Command)objectFromServer.readObject();
commandObject.execute(this);

So I am going to add a new command class. This command will send stone objects to the clients. To simplify the whole organization of keeping track of the stones, every client will have its own list of stones. Thus instead of saving the stones on the server side, every client is going to do that job from now on on its own. The stones are just created on the server to make sure that every clients gets the same stones in the same order. The new command, S_newStonesCommand, contains an array of 5 randomly created stone objects and gets sent to all clients. They just have to add the incoming stones to their stone list. As soon as one of the clients' list gets smaller than 3 a C_getStoneCommand is sent to request new stones. This simplifies a lot.

CommandsNew.jpeg

First, as I stated above, I thought it would be a nice way to add a new interface "CSCommunication", which gets implemented by S_fromClient, S_toClient and Client_fromServer. So I could avoid having a Fat interface with those two different execute() methods in the CS_Command interface. However, I looked into that problem again. I think the solution I proposed first, having two separate interface for different commands makes more sense. If you look at the Commands again, you can see that they basically consist of two groups, "S_"-Commands" and "C_"-Commands". To keep those two groups encapsulated and separated by following the Separation of concerns-principle I decided to have two interfaces for the stone objects and no interface for the different communication threads.

S_Command

The interface S_Command gets implemented by all "S_"-Commands. All S_Command objects are created on the server side by S_toClient, send to Client_fromServer and then executed there. When they get executed in Client_fromServer, it puts itself as parameter into the command's execute(Client_fromServer)-method. Therefore S_Commands are sent from the server but execute on the client side.

S_Command commandObject = (S_Command)objectFromServer.readObject();
commandObject.execute(this);

S Command.jpeg

C_Command

In contrast to the S-Command, C_Command gets implemented by all "C_"-Commands. Those Commands get sent from the Client side to the server side. Thus when they get executed on the server side by the receiving thread S_fromClient, S_fromClient puts a reference of itself into the execute(S_fromClient)-method, so for example it can change attributes.

C_Command commandObject = (C_Command)objectFromServer.readObject();
commandObject.execute(this);

C Command.jpeg

Furthermore each of those two interfaces and their concrete classes of the command package realizes the Strategy design pattern. However, I just found out it could also be a Command patter. I am not totally sure.

S_Common & C_Common

To be able to have access to all attributes over different threads I used two classes, S_Common and C_Common. Both are mainly responsible for keeping track of different data fields, whereas S_Common is doing that on the server side, C_Common is doing it on the client side. There are a couple of arguments which speak against the use of those two classes. I think the main reason why you shouldn't do that is because you should keep related data and behavior in one place. Since both classes pretty much don't have any behavior it doesn't make sense to store those attributes in there. Besides because of those classes call-up operation can have really nasty nestings which clearly disagrees with the Law of demeter and Tell, don't ask:

cCommon.objectToServer.writeObject(new C_gameOverCommand());

C_common

Therefore in a first step of the refactoring, using the Move Field refactor method, all attributes of the C_common Class were transfered to the objects on the client side they actually belong to. In this case most of the fields were either not needed or just moved into the Client_fromServer thread where they actually belong. Since Client_fromServer is handling the client sided network communication it makes sense to have the needed attributes in there.

S_common

On the server side it turned out to be a bit different. S_common had from the start on some functionality. By adding the S_newStonesCommand it even got more functionality. S_common encapsulates all the behavior and attributes all server threads need to have. Since the server has two threads for each client, attributes like player names and setting flags for sending newStones, newScore or fallCommands are handled in S_common. So instead of removing this class I actually extended its behavior. However, it doesn't contain that customized SList with stones anymore. Each client has a list of its own stones as said before. SO there is no need for SList anymore.

S_common has an object called S_clock. S_clock is a thread which creates a "tick" every half second. This is the game pace. S_toClient sends a S_fallCommand to all clients every single time S_clock ticks. This assures all clients have the same pace.

Tetrisfield & Stones

Every client has GUI. This GUI consists of the C_GUI class which contains a tetrisfield TetPanel (TP) and a preview of the next stone in NextStoneLabel (NSL). The TP as well as the NSL have a stone object S_Stone. A normal tetris game as introduced in the 80's has seven different colored stones I, J, L, O, S, T, and Z - due to their resembling letters of the alphabet:

  • S_I - blue
  • S_J - red
  • S_L - green
  • S_O - yellow
  • S_S - magenta
  • S_T - cyan
  • S_Z - orange

Those stones have an abstract super-class S_Stone. Every stone encapsulates a different state of the superclass stone. It has different fields, such as color, name and matrix and also has different behavior, since each stones implements its own rotation. Therefore the stone package implements a State pattern.

Every single stones consists of 4 blocks. Each stone has a different way of rotating. Thus stones can be in different states and can also have a different amount of states. For example, the O-stone doesn't rotate at all and therefore just has one state. In contrast the T-stone has 4 different states. Depending on its state it is ordered differently in the tetrisfield.

Elegant

In the old design every stone object has 4 x 4 matrix (2-dimensional int array) containing numbers. Every number stands for a color. So for example if we look at the T-stone. The T-stone is cyan. Therefore its 4 fields which represent the shape of the stone have the number which stands for color cyan (e.g. 3). The rest of the matrix is filled with 0. To turn/rotate the stone just the fields which change the color need to be changed. In the diagram below you can see how the switching between different states of a stone is working:

StoneStates.jpg

To Keep it simple the TP consists also of 2-dimensional integer array. All white field are filled with 0 which represents the color white. The stone on the TP is moving down as soon as the Client_fromServer has executed a S_fallCommand. Each time the stones moves down on the tetrisfield, it is checked if a collision occurred. The checkCollision() method is in my opinion done in an elegant way. The number of every single field in the matrix of the stone is multiplied with the corresponding number of the TP's matrix. If the result of this is 0 no collision happened and the stone can move to that position and can be painted. If a collision occurred the stone will placed at the last position.

Tetrisfield.jpg

As soon as the stone can not move down anymore, its containing numbers are transfered into the TP's matrix and the next stone from the NSL comes into play and starts to move down the tetrisfield.

but smelly

Even though on first sight that seemed to me a nice way of doing the graphical game logic it actually creates a couple of problems with respect to OODesign:

for(int i = 0; i < 26; i++)
{
 for(int j = 0; j < 15; j++)
 {
  int col = tetMat[j][i];
  switch(col)
  {
   case 0: g.setColor(Color.white); break;
   case 1: g.setColor(Color.blue); break;
   case 2: g.setColor(Color.red); break;
   case 3: g.setColor(Color.green); break;
   case 4: g.setColor(Color.yellow); break;
   case 5: g.setColor(Color.magenta); break;
   case 6: g.setColor(Color.cyan); break;
   case 7: g.setColor(Color.orange); break;
   case 8: g.setColor(Color.lightGray); break;
  }
 if(col==0 || col==8)
  g.fillRect(j*width, i*height, width, height);
 else
  g.fill3DRect(j*width, i*height, width, height, true);
 }
}

For example the code above shows part of the switch-statement smelly paintComponent() method in TP. Basically that method just goes through the whole matrix and depending on the number in the field draws a little block in the correct color. The other problem with that statement is that as soon as somebody wants to extend the tetris, for example with a new stone shape, you have to go through the code and add new lines (cases) to those switch-statements. This violates a couple design maxims such as the Open closed principle, Don't repeat yourself and Once and only once. Whenever a stone or the TP has to be painted this kind of switch-statement appears. That means there is at least one in NSL, S_Stone and TP. This smells like the same code all over the place.

To get rid of those smelly switch-case-statments I thought I need to get every field in the TP, NSL and stone to draw itself. Therefore every field would have to have a paint() method and would need to know e.g. its own color. That means each field in a matrix should be an object by itself. First I thought I convert the integer matrix of the TP into a stonepointer matrix where every pointer is a reference to its corresponding stone object. In the end every stone object should draw itself. However this didn't seem to be a good solution, since for example the white fields in the TP's matrix are not part of a stone. So I would need to create blank stone object which would just draw a white block. This seemed to me as violation of the Model the real world principle since there is no blank white stone.

In my second approach I created a new object called S_ColorBlock. Since every stone consists of 4 blocks in a 4 x 4 matrix with 12 white blocks I figured it might be smart to split up a stone into blocks. Every single block has its own paintBlock() method. Also each block knows its own color. Thus every S_Stone object consists of 16 S_ColorBlock objects. In this case I made use of the Model the real world rule since every tetris stone by definition consists of 4 separate blocks anyways. The tetrisfield and the preview field as well are 2-dimensional arrays of S_ColorBlock objects. Every single block object is able to paint itself. Therefore I got rid of those switch-case statements. Also it is quite simple to add a newly shaped tetris stone. All you have to do is create a new class for the new stone and define its shape and color.

Conclusion

In general the design now is still not the perfect object oriented design although it is way better than before. There is still some coupling going on and also many field are still public. I have created numerous getters and setters but there is still more refactoring involved to get rid of the coupling. Also in my tetris implementation the GUI and Model are quite tightly coupled at least on the client side. Also I didn't change the names of my classes but I think it would make sense to use JAVA conventions. I removed many fields which were not needed anymore.

Newtetris.jpeg

In the end I have to say I am quite happy with the result of my design study especially the Stone/TetrisPanel problem and the Commands problem are solved in a nice way. In the future I might actually end up adding some more functionality. I think it would be really nice to have an even more competing mode to play against each other. I remembered on my GameBoy's Tetris when we played against each other, as soon as somebody removed a line the other person got random blocks added to their tetrisfield.


Sourcecode

File:OOTetris.zip

To start the game you have to be in the OOTetris/bin directory and type in your shell: java GUIPack/S_GUI Then the S_GUI comes up and you have to choose if you want to be the server. By clicking the start-button the C_GUI comes up. Now you just type in the server's IP and your game name and press connect. Have fun playing OOTetris!

Log of Constructionsites

  • getting rid of C_Common class, because no functionality, perhaps getting remove S_common as well
  • change the handling of stones to the client side. Create stones on the server and send via command stones to the clients
  • add a new Command S_newStonesObjectCommand which contains a list of stones
  • getting rid of Command Objects Fat interface
  • change tetrisfield and stones because of Switch statement smell, although a nice elegant way has to be found first... current way quite elegant but not really oo
  • change class names to Java conventions
  • Extract class out of Client_fromServer, not sure if necessary
  • adding game ideas (sendLines/no timelimit) => sendLinesCommand and change timelimit in S_GUI
  • GUI classes refactoring => observer/observable
  • public-private -> getters/setters and Move field (some done)

See also

used links in the design study

Personal tools