Dive In: JSameGame
In this post I'll create a small game project in Java from scratch based on the game SameGame. There are numerous of ways to create this game. I'll try to make it as simple as possible and use Good Practices as much as I can. We'll go step by step all the way from designing the idea to creating and testing it. Because this is a small project, we will be using the Waterfall Model. This post is dedicated to a friend of mine, Niko (see, it's that simple! ;) ). The game along with the source code can be found at pek's Java Corner. So, let's begin!
The first step in creating a project is by defining it. We will start by defining what the game is and it's gameplay. Basically, we will create some Use Cases. Our project is a small one so the depth of the use case is only a matter of few lines. The important part in this step is defining the importance of a use case. This way, when we start implementing the project, we will start from the most important ones. Probably we will leave out some less important for future improvements or features. For this project there are (luckily) only a few use cases to describe.
- Use Case: Game Logic Importance: Required Description: The game is a classic SameGame version in Java. The game starts with a board of 8x8 squares. The squares are colored randomly in 4 different colors. When the player clicks on a square of a certain color all squares on top, left, right and bottom of it will disappear if they are of the same color. When a square disappears all squares on top of it will shift down. This applies recursively for all disappearing colors, in other words, if the left square is the same color, then the left square must be checked for the same color around it and disappear appropriately. If a column completely cleans (all squares are disappeared) then any square from the left of the column will shift to the right. The goal of the game is to clear the board and collect the highest score. The score is calculated with the following formula: (n - 1)² where n = squares disappeared.
- Use Case: New Game Importance: Required Description: The game starts when the program first runs or whenever the player wishes to. If a game hasn't finished and the player wishes to start a new game he must first verify that we wants to clear the board.
- Use Case: Second Click to Disappear Squares Importance: Future Feature Description: When a player clicks on a square, the game will first calculate the score and highlight all the square it will disappear. The player then decides whether he would like to actually disappear them or select a different square.
- Use Case: Level of Difficulty Use Case Importance: Future Feature Description: When creating a new game the player can select the level of difficulty. Basically, the higher the level the more colors and bigger board the game will have.
- Use Case: Hall of Fame Use Case Importance: Future Feature Description: The Hall of Fame is a place where all high scores are kept. When a user wins or loses a game, the game will calculate if it is bigger than any other score in the list and add it to the appropriate position.
Now, our next step is to design the game. We will split this step into two "sub steps". The first will be the design of the game logic and the second the design of the Graphical User Interface.
We will use a Flow Diagram to design each step of the game logic. We can create a flow diagram by using a sophisticated application such as MagicDraw (which also includes a lot of other features like Use Cases, Class Diagrams etc.), a freeware utility such as Meesoft's Diagram Designer or simply by drawing it in MSPaint. I'll be using Meesoft's Diagram Designer because the interface is easy and it's free. Flow diagrams depend mostly on how you think the program will be progressing. There isn't always a one size fits all solution. Here is my Flow Diagram (click for full size):
By studying our flow diagram we can start defining what variables and classes we will be needing. So let's start explaining it.
When the player starts the game, the first thing the program will do is check to see if there is already a game that has already been started. This means that we need a variable that tells us if a game has started or not. Let's define a boolean for this called started. If there is an existing game, the player will be asked to clear the board in order to continue.
The next step is the initialization phase. This looks like a method! Let's call it initializeGame. This method will add squares in a 8x8 board, which means that we also need somehow to represent this. Let's use a two dimensional array of Squares called board. The parent array will be our rows and each child would be a cell inside the row.
What is a Square? It's an Abstract Data Type that we define to hold the color of the square and the x,y position of it. We will simply use integers to describe each colors. To make this more robust, we will define constants for each color: 0 = empty, 1 = red, 2 = blue, 3 = green and 4 = pink.
Next we need an integer to keep our score and initialize it to 0 in every new game. Finally the board will be filled with Squares of random color. The initialization phase is done.
Now we wait until the player selects a square. Once a square is selected we first verify that it is not an empty square (it has been already disappeared). We will define a method called squareSelected for this. If the square isn't empty and at least one square next to it has the same color, we start our Recursive Cycle. Of course, this cycle would be inside a method called calculatePoints. This method will have one parameter, a Square called currentSquare. This Square will be used recursively in this method to fetch it's left, top, bottom and right Squares.
But before we start we need to define some variables just for the scope of this cycle. We need an integer to set how many squares we disappeared, let's say pointsFactor and initialize it to 0. Now we need the color of the square that was clicked to check for the other squares. So we define an integer called currentColor which equals to the color of the selected square.
Now, the phase is ready to start. We call the method calculatePoints and give the square that's been clicked. The first thing the method will do is looking at the top, left, bottom and right Squares of our currentSquare. We do this by getting the x and y of the currentSquare and subtracting or increasing them by 1 having in mind that when x or y is 0 or 8 we don't need to subtract or increase (respectively). Once we get the square and make sure it is not an empty cell we check if it's color equals to currentColor. If it is we increase our pointsFactor by 1, set the Square's color to 0 (empty) and call the same method using the square we just disappeared as the parameter. The recursive cycle is done. It will simply end by itself!
When it does we add to the total score the score for this click with the formula score = score + ((pointsFactor -1)*(pointsFactor-1)). Our next step is to check all the disappeared Squares to see if they had a Square on top of them. If they did we must shift them one step down. This calls for another method! Let's call this applyGravity. What this method will do is start at the bottom of each column and search each cell all the way to the top. Once it finds an empty cell it will hold the y of that cell to a variable called floor. Now, when the searching continues, every time it finds a Square that isn't empty it will change the Square at the floor position, increase the floor by 1 and set the found Squares color to empty. This will go on for every column.
Next step is to look for any empty columns. Aha! Another method. What about applyWind!? This method will start from the top of each column and search all the way down for Squares. Once it finds at least one it will increase the column number it looks for and search the next. If it doesn't, it will shift all the columns that are in the left side one position right. Now the board is set for the next round of click.
But wait! What if there aren't any squares left? Let's use isBoardEmpty method to check for that. The only thing this method will do is find at least one square. Once it does, it will return true otherwise false. If this method returns false, it basically means that the player has won! If not, we continue playing (waiting for the next click).
Or do we? What if there isn't any other pair of squares to click? This calls for another method! Let's say isPairsAvailable. This method will return a boolean that indicates that at least one pair inside the board exists. The method would check each cell of the board for a cell next to it with the same color. Once it finds at least one, it will return true, otherwise false. Returning false basically means the player has lost! That it! The Game Logic is ready!
Now, before we move further, we must collect all the variables and methods we will be using and split them into appropriate classes. So, let's see.
The variables started, score and board look like variables that will be used for holding all the data of the game. So, we create a class called GameDate and add the variables as members of this class.
The rest of the variables and methods could easily be added to the main class that all the game logic will be held. They could be split into more classes, but lets just stay simple. Let's call this class TheGame and add all the methods and remaining variables in it. currentSquare and floor are variables that are in the scope of calculatePoints and applyGravity respectively, so we won't declare them in this class. Also, TheGame will need a GameData to hold the data.
What we have just done in this step is create a simple Class Diagram which looks like this:
Graphical User Interface
So, now we're done with the design of the game logic. Our next step is designing our interface. Let's start by drawing it. I tried doing this using MSPaint (pen and paper would probably be a better idea) and this is my result:
OK, let's pretend that the lines are straight. What we first need is to define each section of the game and select the appropriate Layout Managers. There are two sections in this GUI. The top, which holds the button "Start" and the label "Score", and the center, which holds the board. For the center, it is easy to identify that the appropriate Layout Manager will be the GridLayout. For the top I'll use the BorderLayout because it's easier.
That's it! We are done with the Design step. We also successfully divided our project into a Three-Tier Architecture. It looks like this:
Dividing our project into a Three-Tier Architecture helps us divide our project into 3 main layers: the Presentation Layer, the Business Layer and the Data Layer. Each layer can be changed (or improved) without harming the other. Also, we can add more elements to a layer by simply making changes to the appropriate one. For example, we can simply add another class to the Presentation Layer that will display our game in a console! We would only create a Console class that will be using the TheGame class in a different manner and that's it.
We are finally at the step that every programmer likes: the coding step! Here we will actually code our game and start implementing the design we have just done. EVERY method and class should be documented appropriately using JavaDoc comments. I tried writing the code here, but WordPress just makes it hard, so instead, I'll create an html page using Java2HTML and give a link that will send you there. Be warned, the code is really really simple. A lot of improvements can (and must) be made in order to make the game more robust and error free. You will also notice that most of the attributes are public. This is not a good practice. You should encapsulate them using getters and setter in order to avoid various bugs (such as setting a Squares position outside of the board). Also, the implementation of starting a game when one already exists will be implemented in the GUI since the GUI will pop up a dialog for this. Finally, calculatePoints will be called from the GUI too.
Java programs usually are tested using Unit Tests. You could also create a more simple set of tests. The general thumbs up is creating a test class for a particular class and applying various tests on each method. For example, if we did had encapsulated our Data Types, we would test them by trying to add abnormal board positions (such as settings x = 100 or y = -1). In this case, our Abstract Data Types should yell an exception. Since our GameData is exposed to the public, we could set our own board and call applyGravity or applyWind and test to see if the results of the method are correct. Unit Tests are very useful when improving our program. We create a Test Suite with our collection of Unit Tests and every time we add a feature or fix a bug, we run the Test Suite. If the Test Suite had errors, then something is going wrong.
Applications never end. There is always space for improvements or a bug to get fixed or a new feature you have thought of. As long as you are interested in this project, you will also continue modifying it.
I know this isn't the best way to create a project. There are many Software Development Models out there. I find Waterfall fairly simple for this kind of project. Also, I know there are many things I sacrificed in order to make this project as small as I could. The goal of this post was mainly to point out how to start a project from scratch and dividing it into steps (and then dividing these steps to even smaller ones etc.) giving as much knowledge as I could for further reading. I'm no guru! I'm just a guy that loves Java. ;)