| |
Minesweeper - Startup screenshot
| Minesweeper - New game screenshot
|
| |
Minesweeper - Game won screenshot
| Minesweeper - Game lost screenshot
|
Contents
Introduction
Minesweeper is a single player game. The object of the game is to clear a minefield without hitting a mine. Minesweeper is available not only for
Windows but also for other platforms (including most of the Linux variants). Minesweeper is very popular in the Windows world, it has been bundled with Windows since Windows 3.1.
In this article, we will create a Minesweeper clone for Android. We will try to implement most of the features which are available in the Windows Minesweeper.
This article is targeted at Intermediate-Advanced level developers and expects familiarity with Java and development for Android.
About the game
In Minesweeper, we are presented with a grid of blocks and some of them randomly contain mines. In our implementation, we will limit to typical Beginner Level implementation.
The number of rows and columns in our implementation will be 9 and the total number of mines will be 10. Extending the game for Intermediate, Advanced,
is easy though (just require a change in value of 3 variables in our code).
Well, in this article, we will not talk much about how to play the game but we will talk about some of the features that we should think about before implementing it (Windows version):
- Left click on a block opens the block.
- Right click on a block allows to mark a block as flagged (confirmed mine underneath); flagged blocks can be marked
with question marks (doubt about presence of mine), and question marked blocks can be un-marked as well.
- First block never contains a mine underneath; this reduces the pain of guessing even the first block.
- If an uncovered block is blank, nearby blocks are recursively opened till a numbered block is opened; simulating a ripple effect.
- Clicking left-right/middle button on a block, where all mines in nearby blocks are already flagged, uncovers all nearby covered blocks.
- Timer starts on clicking the first block and not after selecting a new game.
Enough talking now, let's start working on it.
One step at a time
The best way to explain or implement any complex system is to take one step at a time. We will follow that approach here. First of all, we will talk about the GUI,
layout, and look-and-feel of the application. We will also talk about some techniques used while creating the layout. After that we will talk about keeping track of time
in an Android application. Next we will discuss the differences between mouse, click, and touch events and how we will respond to user actions. The last step will be about
implementing the complete game and most of the features discussed in the About the game section.
The look and feel
Let's talk about some of the aspects of designing a GUI for a Minesweeper game. We will talk about the overall application layout and also a few techniques used in creating the game.
Application layout
For Minesweeper, we will use a TableLayout
. We add three rows to the TableLayout
:
- The first row contains three columns for timer, new game button, and mine count display. For timer and mine count display, we have used a
TextView
.
For the new game button, we have used an ImageButton
. - The second row contains an empty
TextView
with a height of 50 pixels. It just helps as a spacer between the top row and the mine field. - This row contains another
TableLayout
, which is used for displaying the minefield. We add rows of buttons to this TableLayout
dynamically.
The code for the layout looks like (removed some additional attributes to save space):
="1.0"="utf-8"
<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:stretchColumns="0,2"
android:background="@drawable/back">
<TableRow>
<TextView
android:id="@+id/Timer"
android:layout_column="0"
android:text="000" />
<ImageButton android:id="@+id/Smiley"
android:layout_column="1"
android:background="@drawable/smiley_button_states"
android:layout_height="48px"/>
<TextView
android:id="@+id/MineCount"
android:layout_column="2"
android:text="000" />
</TableRow>
<TableRow>
<TextView
android:layout_column="0"
android:layout_height="50px"
android:layout_span="3"
android:padding="10dip"/>
</TableRow>
<TableRow>
<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/MineField"
android:layout_width="260px"
android:layout_height="260px"
android:gravity="bottom"
android:stretchColumns="*"
android:layout_span="3"
android:padding="5dip" >
</TableLayout>
</TableRow>
</TableLayout>
Using external fonts
For timer and mine count display, we have used an external font. We have used the LCD mono font (specified in the Resources section).
Using external fonts is fairly easy in Android and is a two-step process:
- Create a fonts folder under the assets folder of the project. Copy the TTF (True Type Font) file to the fonts folder.
- Create an object of
Typeface
by calling createFromAsset
and passing the TTF file name and set the
Typeface
of the TextView
to this object. The code for doing this looks like:
private TextView txtMineCount;
private TextView txtTimer;
txtMineCount = (TextView) findViewById(R.id.MineCount);
txtTimer = (TextView) findViewById(R.id.Timer);
Typeface lcdFont = Typeface.createFromAsset(getAssets(),
"fonts/lcd2mono.ttf");
txtMineCount.setTypeface(lcdFont);
txtTimer.setTypeface(lcdFont);
Use styles
In our Minesweeper application, the smiley on the new game button changes to a nervous smiley when the button is clicked. Basically, we want a different
image when the button is in pressed state (nervous smiley) and want another image when it is in normal state (smiley). In order to achieve this, we have used styles. The effect looks like:
Using styles is also a two-step process:
- Create a new XML file (style definition file) which specifies the images to be used with the corresponding button states. For example, in pressed state, we want to use an
image named surprise, and in normal state, we want to use the image named smile. Of course both these images are already copied to our res/drawable folder. The style file looks like:
="1.0"="utf-8"
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_focused="true"
android:state_pressed="false"
android:drawable="@drawable/smile" />
<item android:state_focused="true"
android:state_pressed="true"
android:drawable="@drawable/surprise" />
<item android:state_focused="false"
android:state_pressed="true"
android:drawable="@drawable/surprise" />
<item android:drawable="@drawable/smile" />
</selector>
- Update/add the background property attribute for the New Game button and set its values to the above created style file. The updated
ImageButton
code looks like:
<ImageButton android:id="@+id/Smiley"
android:layout_column="1"
android:background="@drawable/smiley_button_states"
android:layout_height="48px"/>
Dynamic rows to TableLayout
Adding blocks (Block
is a class derived from the Button
class with added functionality to support the implementation) dynamically works the same way
we think it should work or expect it to work. We can divide it into the following steps:
- Create an object of
TableRow
and set its layout parameters. - Add blocks (extended buttons) to the row object created above.
- Get the instance of
TableLayout
(minefield) using the findViewById
method. - Add the row created above to the
TableLayout
.
The final code resembles:
private TableLayout mineField;
private Block blocks[][];
public void onCreate(Bundle savedInstanceState)
{
...
mineField = (TableLayout)findViewById(R.id.MineField);
}
private void showMineField()
{
for (int row = 1; row < numberOfRowsInMineField + 1; row++)
{
TableRow tableRow = new TableRow(this);
tableRow.setLayoutParams(new LayoutParams((blockDimension + 2 * blockPadding) *
numberOfColumnsInMineField, blockDimension + 2 * blockPadding));
for (int column = 1; column < numberOfColumnsInMineField + 1; column++)
{
blocks[row][column].setLayoutParams(new LayoutParams(
blockDimension + 2 * blockPadding,
blockDimension + 2 * blockPadding));
blocks[row][column].setPadding(blockPadding, blockPadding,
blockPadding, blockPadding);
tableRow.addView(blocks[row][column]);
}
mineField.addView(tableRow,new TableLayout.LayoutParams(
(blockDimension + 2 * blockPadding) * numberOfColumnsInMineField,
blockDimension + 2 * blockPadding));
}
}
Set Minesweeper icon
Changing the application icon (this icon is displayed at the Home screen and the Launcher window, commonly called Launcher icon) is simple. It is simple because
there is a default icon already supplied with the project, named icon.png in the res/drawable folder. Change/Update/Replace the icon.png provided with the project
and the change will be reflected in the Launcher window. For more details about icon design for Android, read
Icon Design Guidelines.
Images for multiple resolutions
As we all know, Android supports and runs on a wide range of devices. Devices may differ in screen size, aspect ratio, resolution, density, and pixel support.
In order to support all these devices, we have to customize images according to each device. Now, this is really not possible. The recommended approach by Google
is to have a separate set of images for three generalized screen densities, namely low DPI, medium DPI, and high DPI. Images should be copied to the res/drawable-hdpi,
res/drawable-mdpi, res/drawable-ldpi folders for the respective densities. If we want Android to take care of the image adjustments (which is not always the best option),
then we should just create a single set of images (we have used this approach in Minesweeper) and should copy them to the res/drawable-nodpi folder. For more information,
read Supporting Multiple Screens.
Measure time
When you are developing a game, the most important aspect is keeping track of time. Using java.util.Timer
or java.util.TimerTask
is a standard approach
for time keeping scenarios in Java world. The only thing that bothers here is that this way we create a new thread. This is not what we may be looking for,
in some cases. Android has a better solution for this scenario. We can use the android.os.Handler
class for our purpose.
Handler in place of timer
A handler can be used in two ways, either by sending a message to the handler and performing specific actions when the message is received, or by scheduling
a Runnable object with the handler. We have used the second approach in Minesweeper. The benefit of using a handler is that it is associated with the thread/message queue
of the creator thread. Read more about handler here. The code for implementing a handler is similar to:
private Handler timer = new Handler();
private int secondsPassed = 0;
public void startTimer()
{
if (secondsPassed == 0)
{
timer.removeCallbacks(updateTimeElasped);
timer.postDelayed(updateTimeElasped, 1000);
}
}
public void stopTimer()
{
timer.removeCallbacks(updateTimeElasped);
}
private Runnable updateTimeElasped = new Runnable()
{
public void run()
{
long currentMilliseconds = System.currentTimeMillis();
++secondsPassed;
txtTimer.setText(Integer.toString(secondsPassed));
timer.postAtTime(this, currentMilliseconds);
timer.postDelayed(updateTimeElasped, 1000);
}
};
Handle user actions
In Minesweeper, we have implemented listeners for the Click and Long Click events. The Click event serves the purpose of mouse left click and the Long Click event serves
as the mouse right click event. We haven't implemented the Touch event for the same, we could have achieved the same functionality with Touch events also but that way
it would be limited to Touch capable devices only. On the contrary, on Touch enabled devices, Click and Long Click events are generated by touching also, and
on non-Touch enabled devices, these events can be generated by Trackball or Enter keys.
Understand mouse click and phone click events
Java offers functionality to implement mouse button Click/Press/Release, Mouse move/drag, Mouse enter/exit, and Scroller event support.
In Android, there is no concept of mouse over (Enter/Exit) events and Scroller events. Android provides Click and Touch capabilities only.
Click can be a Click or a Long Click. The Click event doesn't support drag functionality. The Touch event offers drag capabilities.
Understand phone click and phone touch events
Click events are generated when the user either touches the item (when in touch mode), or focuses upon the item with the navigation-keys or trackball
and presses the suitable "enter" key or presses down on the trackball. Touch events are called when the user performs an action qualified
as a touch event, including a press, a release, or any movement gesture on the screen (within the bounds of the item).
Understand phone click and phone long click events
Click events are generated when user either touches the item (when in touch mode), or focuses upon the item with the navigation-keys or trackball
and presses the suitable "enter" key or presses down on the trackball. Long Click events are generated when the user either touches and holds
the item (when in touch mode), or focuses upon the item with the navigation-keys or trackball and presses and holds the suitable "enter"
key or presses and holds down on the trackball (for one second).
Left-Right button simulation
Android doesn't support the middle button click event; in order to implement this functionality, we have simply used the Long Click event.
If Long Click is received on an open block with a number on it, then we trigger the related functionality. The code for this part looks like:
blocks[row][column].setOnLongClickListener(new OnLongClickListener()
{
public boolean onLongClick(View view)
{
if (!blocks[currentRow][currentColumn].isCovered()
&& (blocks[currentRow][currentColumn].getNumberOfMinesInSorrounding() > 0) && !isGameOver)
{
int nearbyFlaggedBlocks = 0;
for (int previousRow = -1; previousRow < 2; previousRow++)
{
for (int previousColumn = -1; previousColumn < 2; previousColumn++)
{
if (blocks[currentRow + previousRow][currentColumn + previousColumn].isFlagged())
{
nearbyFlaggedBlocks++;
}
}
}
if (nearbyFlaggedBlocks == blocks[currentRow][currentColumn].getNumberOfMinesInSorrounding())
{
for (int previousRow = -1; previousRow < 2; previousRow++)
{
for (int previousColumn = -1; previousColumn < 2; previousColumn++)
{
if (!blocks[currentRow + previousRow][currentColumn + previousColumn].isFlagged())
{
rippleUncover(currentRow + previousRow, currentColumn + previousColumn);
...
}
}
}
}
return true;
}
...
}
return true;
}
});
Events for disabled buttons
If a button is disabled we can't receive events for that button. In order to overcome this limitation, we mark the button as disabled and change its background.
In reality, the button is never disabled, it always remains enabled. This way the button is still enabled but pretends to be disabled and thus can accept events.
Decide the strategy
Let's concentrate on the most important aspect of the game, implementing it. In this section, we will talk about some of the features discussed
in the About the game section. We will talk about almost each one of them one by one (one of them is already discussed above
in the Left-Right button simulation section).
Complete cycle
The most important part of the Minesweeper game is handling user actions. It starts from waiting and receiving user inputs and processing them appropriately.
I believe that in place of explaining it in words, it would be better if I can present a pictorial representation for the same. After all it is rightly said,
a picture speaks louder than words. The whole cycle can be summarized in this flowchart:
Start handle with first click
As we already described, the timer should start with the first click (opening of first block) and not with the New Game button click. This is really necessary
to keep proper control over time. In order to do this, we just create a boolean variable and as soon as we receive the Click event, we check that variable and start
the Handler and then flip the value of the variable. The code for this part looks like:
private boolean isTimerStarted;
blocks[row][column].setOnClickListener(new OnClickListener()
{
@Override
public void onClick(View view)
{
if (!isTimerStarted)
{
startTimer();
isTimerStarted = true;
}
...
}
});
No mines on first click
The user should never get a mine at the first click as it saves the user from guessing the first block. In order to implement this feature, we set mines
after the first click. We set mines in blocks excluding the block that the user just clicked. Mines are planted in blocks randomly (by generating random row
and column numbers). After planting mines, we update the nearby mine count for all blocks. The code for this part looks like:
private boolean areMinesSet;
blocks[row][column].setOnClickListener(new OnClickListener()
{
@Override
public void onClick(View view)
{
...
if (!areMinesSet)
{
areMinesSet = true;
setMines(currentRow, currentColumn);
}
}
});
private void setMines(int currentRow, int currentColumn)
{
Random rand = new Random();
int mineRow, mineColumn;
for (int row = 0; row < totalNumberOfMines; row++)
{
mineRow = rand.nextInt(numberOfColumnsInMineField);
mineColumn = rand.nextInt(numberOfRowsInMineField);
if ((mineRow + 1 != currentColumn) || (mineColumn + 1 != currentRow))
{
if (blocks[mineColumn + 1][mineRow + 1].hasMine())
{
row--;
}
blocks[mineColumn + 1][mineRow + 1].plantMine();
}
else
{
row--;
}
}
int nearByMineCount;
...
}
Ripple effect for opening blocks
When a user opens the block, the user should get some hint about the next step. The user cannot make a guess or decision about the next step if the opened block
is empty/blank. In order to avoid this situation, we open nearby blocks and keep opening them recursively till we receive a block with the number beneath.
This creates a ripple effect. The code for recursive uncovering of blocks (ripple effect) looks like:
private void rippleUncover(int rowClicked, int columnClicked)
{
if (blocks[rowClicked][columnClicked].hasMine() ||
blocks[rowClicked][columnClicked].isFlagged())
{
return;
}
blocks[rowClicked][columnClicked].OpenBlock();
if (blocks[rowClicked][columnClicked].getNumberOfMinesInSorrounding() != 0 )
{
return;
}
for (int row = 0; row < 3; row++)
{
for (int column = 0; column < 3; column++)
{
if (blocks[rowClicked + row - 1][columnClicked + column - 1].isCovered()
&& (rowClicked + row - 1 > 0) && (columnClicked + column - 1 > 0)
&& (rowClicked + row - 1 < numberOfRowsInMineField + 1)
&& (columnClicked + column - 1 < numberOfColumnsInMineField + 1))
{
rippleUncover(rowClicked + row - 1, columnClicked + column - 1 );
}
}
}
return;
}
Blank block to flagged to question mark to blank
We discussed one more aspect about the game, marking blocks as Flagged, Question Mark, or clearing the mark again. The feature is straightforward to implement.
When we receive a Long Click event and it is not a Left-Right click, we check the current status of the block. If the block is Blank we mark it
as Flagged (mines inside), if it is flagged we can mark it as Question (doubt about presence of mine), and if it is marked as Question we can clear the mark.
I hope anyone can figure out the fact that we just need a few conditional statements:
blocks[row][column].setOnLongClickListener(new OnLongClickListener()
{
public boolean onLongClick(View view)
{
...
if (blocks[currentRow][currentColumn].isClickable() &&
(blocks[currentRow][currentColumn].isEnabled() ||
blocks[currentRow][currentColumn].isFlagged()))
{
if (!blocks[currentRow][currentColumn].isFlagged() &&
!blocks[currentRow][currentColumn].isQuestionMarked())
{
blocks[currentRow][currentColumn].setBlockAsDisabled(false);
blocks[currentRow][currentColumn].setFlagIcon(true);
blocks[currentRow][currentColumn].setFlagged(true);
minesToFind--;
updateMineCountDisplay();
}
else if (!blocks[currentRow][currentColumn].isQuestionMarked())
{
blocks[currentRow][currentColumn].setBlockAsDisabled(true);
blocks[currentRow][currentColumn].setQuestionMarkIcon(true);
blocks[currentRow][currentColumn].setFlagged(false);
blocks[currentRow][currentColumn].setQuestionMarked(true);
minesToFind++;
updateMineCountDisplay();
}
else
{
blocks[currentRow][currentColumn].setBlockAsDisabled(true);
blocks[currentRow][currentColumn].clearAllIcons();
blocks[currentRow][currentColumn].setQuestionMarked(false);
if (blocks[currentRow][currentColumn].isFlagged())
{
minesToFind++;
updateMineCountDisplay();
}
blocks[currentRow][currentColumn].setFlagged(false);
}
updateMineCountDisplay();
}
return true;
}
});
Check game win/loss at each step
Yes, this step is very important; we need to check the status of the game after each click. This is essential to make sure we don't miss any click or any block.
We lose the game if we click on a block with a mine underneath. We win the game if all the blocks marked with mines are flagged. The code for this part looks like:
if (blocks[currentRow + previousRow][currentColumn + previousColumn].hasMine())
{
finishGame(currentRow + previousRow, currentColumn + previousColumn);
}
if (checkGameWin())
{
winGame();
}
private boolean checkGameWin()
{
for (int row = 1; row < numberOfRowsInMineField + 1; row++)
{
for (int column = 1; column < numberOfColumnsInMineField + 1; column++)
{
if (!blocks[row][column].hasMine() && blocks[row][column].isCovered())
{
return false;
}
}
}
return true;
}
Testing/How to play
Testing/Playing the game is straightforward and resembles the way we play it on Windows. Some key points to mention are:
- Click the smiley icon to start a new game.
- Click or Touch the block to open it. This is same as left click on Windows.
- Click and hold (for one second) the block to mark it as Flagged, Question Mark, or clear all marks. This is same as right click on Windows.
- Click and hold a numbered block to open all covered blocks (if all mines are already flagged). This is same as middle click on Windows.
- There is no icon for flagged blocks. Flagged blocks are marked with an F symbol.
- Go ahead, play it, and give your feedback.
Summary
Explaining a full flexed game in the boundaries of an article is not really very easy. I have tried my best to explain the working of Minesweeper and how to handle some
of the important parts of the game. The full source code for the game is attached. We kept ourselves limited to Beginner mode of the game but implementing Intermediate,
Expert, and Custom modes is also very easy. It just needs a few modifications to the code. Feel free to use it and extend it. Please provide your feedback and suggestions.
Resources
The images used for creating the GUI for Minesweeper belong to the respective owners. The sources for the images are:
History
- Sep 28, 2010: Initial draft.