Our website is made possible by displaying online advertisements to our visitors. Please consider supporting us by disabling your ad blocker.

Create A Minesweeper Game With Native Android

TwitterFacebookRedditLinkedInHacker News

So recently I was presented with a challenge. Make a Minesweeper game using native Android with no additional frameworks such as Unity3D or similar.

Minesweeper via Wikipedia:

A single-player puzzle video game. The objective of the game is to clear a rectangular board containing hidden “mines” without detonating any of them, with help from clues about the number of neighboring mines in each field.

This task can be accomplished many ways. For example we could choose to use OpenGL, a 2D canvas, or something else. In this particular tutorial we’re going to be using a 2D canvas because it is simple and acceptable for a game with minimal to no animations.;

Let’s start by creating a fresh Android project with the command line.

android create project --activity GameActivity --package com.nraboy.minesweeper --path ./Minesweeper --target android-19 --gradle --gradle-version 0.11.+

You can see from the above command that we’ll be using Gradle in this project rather than Apache Ant.

Before we dive into the source code, let’s take a look at the graphics that we’re going to use in the project.

Minesweeper Game Sprite

Above is an image that should be split into two sprite sheets. One sprite sheet will represent all the states of a cell and the other sheet will represent all the buttons of the HUD.

When it comes to game design, it is always a good idea to use a sprite sheet rather than trying to load many image files because of the following via the Stack Exchange:

A very important reason to use sprite-sheets is to reduce the amount of draw-calls on your GPU, which can have a notable impact on performance.

These sprite sheets will go in your projects src/main/res/drawable-mdpi directory. Now let’s take a look now at the file structure for our project:

src
    main
        java
            com
                nraboy
                    minesweeper
                        Board.java
                        Cell.java
                        GameActivity.java
                        GameLoopThread.java
                        GameView.java
                        HUD.java
                        Sprite.java

Sure there are other files in our project created by Gradle, but they don’t really matter. Just leave them alone.

GameActivity.java is where we start when the Android application is opened, so we’re going to start coding that file:

package com.nraboy.minesweeper;

import android.app.Activity;
import android.os.Bundle;

public class GameActivity extends Activity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(new GameView(this));
    }

}

The above code is incredibly simple. We’re just telling our activity to set the content view as a new GameView which we’ll define in a moment.

Before we look at the GameView class, let’s jump straight into the parent Sprite class found in the Sprite.java file:

package com.nraboy.minesweeper;

import android.graphics.*;

public class Sprite {

    private int x;
    private int y;
    private GameView gameView;
    private Bitmap sheet;
    private int width;
    private int height;

    public Sprite(GameView gameView, Bitmap bmp, int x, int y, int columns, int rows) {
        this.gameView = gameView;
        this.sheet = bmp;
        this.x = x;
        this.y = y;
        this.width = bmp.getWidth() / columns;
        this.height = bmp.getHeight() / rows;
    }

    public void onDraw(Canvas canvas, int spriteColumn, int spriteRow) {
        Rect src = new Rect((spriteColumn * this.width), (spriteRow * this.height), (spriteColumn * this.width) + width, (spriteRow * this.height) + height);
        Rect dst = new Rect(this.x, this.y, this.x + this.width, this.y + this.height);
        canvas.drawBitmap(this.sheet, src, dst, null);
    }

    public boolean hasCollided(float otherX, float otherY) {
        return this.x < otherX && this.y < otherY && this.x + this.width > otherX && this.y + this.height > otherY;
    }

    public void setX(int x) {
        this.x = x;
    }

    public void setY(int y) {
        this.y = y;
    }

    public int getX() {
        return this.x;
    }

    public int getY() {
        return this.y;
    }

    public int getWidth() {
        return this.width;
    }

    public int getHeight() {
        return this.height;
    }

}

So what is going on in the above Sprite class? There are three main things occurring in this class:

  • Split the sprite sheet into a grid of rows and columns representing one sprite each.
  • Drawing one sprite to the canvas at a time.
  • Detecting if there is a collision between a point and the bounds of the sprite.

This Sprite class is pretty generic. It works for just about every usage scenario. Now it is time to look into the classes which inherit from this parent class starting with the HUD class found in the HUD.java file:

package com.nraboy.minesweeper;

import android.graphics.*;

public class HUD extends Sprite {

    public HUD(GameView gameView, Bitmap spriteSheet, int x, int y) {
        super(gameView, spriteSheet, x, y, 1, 3);
    }

}

You can see in the above code that we’re defining the dimensions of the sprite sheet and assigning it the Bitmap graphic to be used. Not much to it, yet it inherits all the great properties from the Sprite class.

We’re now going to look at the Cell class found in the Cell.java file. It too inherits the Sprite class, but it has a little more to it than the HUD class.

package com.nraboy.minesweeper;

import java.util.*;
import android.graphics.*;

public class Cell extends Sprite {

    public boolean isRevealed;
    public boolean isBomb;
    public boolean isCheat;
    private ArrayList<Cell> neighbors;
    private int bombNeighborCount;

    public Cell(GameView gameView, Bitmap spriteSheet, int x, int y, boolean isBomb) {
        super(gameView, spriteSheet, x, y, 3, 4);
        this.isBomb = isBomb;
        this.isRevealed = false;
        this.isCheat = false;
        this.bombNeighborCount = 0;
        this.neighbors = new ArrayList<Cell>();
    }

    public void addNeighbor(Cell neighbor) {
        this.neighbors.add(neighbor);
        if(neighbor.isBomb) {
            this.bombNeighborCount++;
        }
    }

    public ArrayList<Cell> getNeighbors() {
        return this.neighbors;
    }

    public int getBombNeighborCount() {
        return this.bombNeighborCount;
    }

    public void reveal() {
        this.isRevealed = true;
    }

}

In addition to defining the sprite sheet dimensions, we need to keep track of a few other things. We need to know if the cell is a bomb and if it is not, then we need to determine how many cells that neighbor contain bombs. Although we won’t directly determine this in the Cell class, the information is still stored here.

We’re going to continue to work our way backwards. We’ve got our cell and button sprites, now it is time to make a board that houses multiple sprites with additional logic:

package com.nraboy.minesweeper;

import java.util.*;
import android.graphics.*;
import android.content.*;
import android.view.*;

public class Board {

    public Cell[][] grid;
    private GameView gameView;
    private Bitmap cellSpriteSheet;
    private int boardSize;
    private int bombCount;
    private int cellsRevealed;

    public Board(GameView gameView, int boardSize, int bombCount) {
        this.grid = new Cell[boardSize][boardSize];
        this.gameView = gameView;
        this.cellSpriteSheet = BitmapFactory.decodeResource(this.gameView.context.getResources(), R.drawable.cell_spritesheet_md);
        this.boardSize = boardSize;
        this.bombCount = bombCount;
    }

    public void draw(Canvas canvas) {
        for(int i = 0; i < this.grid.length; i++) {
            for(int j = 0; j < this.grid.length; j++) {
                if(this.grid[i][j].isRevealed) {
                    if(this.grid[i][j].isCheat) {
                        this.grid[i][j].onDraw(canvas, 2, 3);
                    } else if(this.grid[i][j].isBomb) {
                        this.grid[i][j].onDraw(canvas, 1, 0);
                    } else {
                        switch(this.grid[i][j].getBombNeighborCount()) {
                            case 0:
                                this.grid[i][j].onDraw(canvas, 2, 0);
                                break;
                            case 1:
                                this.grid[i][j].onDraw(canvas, 0, 1);
                                break;
                            case 2:
                                this.grid[i][j].onDraw(canvas, 1, 1);
                                break;
                            case 3:
                                this.grid[i][j].onDraw(canvas, 2, 1);
                                break;
                            case 4:
                                this.grid[i][j].onDraw(canvas, 0, 2);
                                break;
                            case 5:
                                this.grid[i][j].onDraw(canvas, 1, 2);
                                break;
                            case 6:
                                this.grid[i][j].onDraw(canvas, 2, 2);
                                break;
                            case 7:
                                this.grid[i][j].onDraw(canvas, 0, 3);
                                break;
                            case 8:
                                this.grid[i][j].onDraw(canvas, 1, 3);
                                break;
                            default:
                                this.grid[i][j].onDraw(canvas, 0, 0);
                                break;
                        }
                    }
                } else {
                    this.grid[i][j].onDraw(canvas, 0, 0);
                }
            }
        }
    }

    public void reset() {
        for(int i = 0; i < this.grid.length; i++) {
            for(int j = 0; j < this.grid.length; j++) {
                this.grid[i][j] = new Cell(this.gameView, this.cellSpriteSheet, i, j, false);
            }
        }
        this.shuffleBombs(this.bombCount);
        this.calculateCellNeighbors();
        this.setPositions();
        this.cellsRevealed = 0;
    }

    public void shuffleBombs(int bombCount) {
        boolean spotAvailable = true;
        Random random = new Random();
        int row;
        int column;
        for(int i = 0; i < bombCount; i++) {
            do {
                column = random.nextInt(8);
                row = random.nextInt(8);
                spotAvailable = this.grid[column][row].isBomb;
            } while (spotAvailable);
            this.grid[column][row].isBomb = true;
        }
    }

    public void calculateCellNeighbors() {
        for(int x = 0; x < this.grid.length; x++) {
            for(int y = 0; y < this.grid.length; y++) {
                for(int i = this.grid[x][y].getX() - 1; i <= this.grid[x][y].getX() + 1; i++) {
                    for(int j = this.grid[x][y].getY() - 1; j <= this.grid[x][y].getY() + 1; j++) {
                        if(i >= 0 && i < this.grid.length && j >= 0 && j < this.grid.length) {
                            this.grid[x][y].addNeighbor(this.grid[i][j]);
                        }
                    }
                }
            }
        }
    }

    public void setPositions() {
        int horizontalOffset = (320 - (this.boardSize * 25)) / 2;
        for(int i = 0; i < this.grid.length; i++) {
            for(int j = 0; j < this.grid.length; j++) {
                this.grid[i][j].setX(horizontalOffset + i * 25);
                this.grid[i][j].setY(90 + j * 25);
            }
        }
    }

    public boolean reveal(Cell c) {
        c.reveal();
        if(!c.isBomb) {
            this.cellsRevealed++;
            if(c.getBombNeighborCount() == 0) {
                ArrayList<Cell> neighbors = c.getNeighbors();
                for(int i = 0; i < neighbors.size(); i++) {
                    if(!neighbors.get(i).isRevealed) {
                        reveal(neighbors.get(i));
                    }
                }
            }
        }
        return c.isBomb;
    }

    public void showBombs(Cell c) {
        if(c.isBomb) {
            c.reveal();
        }
    }

    public int getRevealedCount() {
        return this.cellsRevealed;
    }

}

A lot of stuff is happening in this Board class that you see above so let’s break it down and figure out what each piece does.

When a user clicks on a cell we want to reveal it:

public boolean reveal(Cell c) {
    c.reveal();
    if(!c.isBomb) {
        this.cellsRevealed++;
        if(c.getBombNeighborCount() == 0) {
            ArrayList<Cell> neighbors = c.getNeighbors();
            for(int i = 0; i < neighbors.size(); i++) {
                if(!neighbors.get(i).isRevealed) {
                    reveal(neighbors.get(i));
                }
            }
        }
    }
    return c.isBomb;
}

However, the laws of Minesweeper have a little more depth. If we reveal a cell and it is not a bomb and there are zero neighbors that have bombs, we want to reveal all neighbors to the current cell including those that touch bombs. This will happen recursively for as long as a cell has zero bomb neighbors. It is a method for unlocking massive parts of the board in a single click. The neighbors are calculated with the following:

public void calculateCellNeighbors() {
    for(int x = 0; x < this.grid.length; x++) {
        for(int y = 0; y < this.grid.length; y++) {
            for(int i = this.grid[x][y].getX() - 1; i <= this.grid[x][y].getX() + 1; i++) {
                for(int j = this.grid[x][y].getY() - 1; j <= this.grid[x][y].getY() + 1; j++) {
                    if(i >= 0 && i < this.grid.length && j >= 0 && j < this.grid.length) {
                        this.grid[x][y].addNeighbor(this.grid[i][j]);
                    }
                }
            }
        }
    }
}

We’re essentially looping through each cell on the board and adding its neighbors to a list. Kind of like a graph.

Another important method we have in the Board class is the shuffleBombs() method.

public void shuffleBombs(int bombCount) {
    boolean spotAvailable = true;
    Random random = new Random();
    int row;
    int column;
    for(int i = 0; i < bombCount; i++) {
        do {
            column = random.nextInt(8);
            row = random.nextInt(8);
            spotAvailable = this.grid[column][row].isBomb;
        } while (spotAvailable);
        this.grid[column][row].isBomb = true;
    }
}

Given the total bombs required to be on the board, loop through a randomizer until all bombs are placed. A bomb is not placed if the cell already contains a bomb.

There are of course other methods in the Board class, but they are a little more self explanatory.

This brings us to our Game class which resides in the Game.java file. It is responsible for our buttons that start new games, cheat, and validate. It also registers touch events, and determines if we’ve gotten game over or succeeded in the game. It is basically the class for managing gameplay and the code is as follows:

package com.nraboy.minesweeper;

import android.view.*;
import android.widget.*;
import android.graphics.*;

public class Game {

    private GameView gameView;
    public Board gameBoard;
    private Bitmap hudSpriteSheet;
    private int boardSize;
    private int bombCount;
    private boolean isGameOver;
    private int score;
    public HUD[] hud;

    public Game(GameView gameView, int boardSize, int bombCount) {
        this.gameView = gameView;
        this.boardSize = boardSize;
        this.bombCount = bombCount;
        this.gameBoard = new Board(gameView, boardSize, bombCount);
        this.hud = new HUD[3];
        this.hudSpriteSheet = BitmapFactory.decodeResource(this.gameView.context.getResources(), R.drawable.hud_spritesheet_md);
        this.hud[0] = new HUD(this.gameView, this.hudSpriteSheet, 0, 0);
        this.hud[1] = new HUD(this.gameView, this.hudSpriteSheet, 160, 0);
        this.hud[2] = new HUD(this.gameView, this.hudSpriteSheet, 80, 40);
    }

    public void start() {
        this.isGameOver = false;
        this.score = 0;
        this.gameBoard.reset();
    }

    public void cheat() {
        outerLoop:
        for(int i = 0; i < this.boardSize; i++) {
            for(int j = 0; j < this.boardSize; j++) {
                if(!this.gameBoard.grid[i][j].isRevealed && this.gameBoard.grid[i][j].isBomb) {
                    this.gameBoard.grid[i][j].isCheat = true;
                    this.gameBoard.grid[i][j].reveal();
                    break outerLoop;
                }
            }
        }
    }

    public void gameOver() {
        this.isGameOver = true;
        for(int i = 0; i < this.boardSize; i++) {
            for(int j = 0; j < this.boardSize; j++) {
                this.gameBoard.showBombs(this.gameBoard.grid[i][j]);
            }
        }
        Toast.makeText(this.gameView.context, "Game over!", Toast.LENGTH_LONG).show();
    }

    public void gameFinished() {
        this.isGameOver = true;
        Toast.makeText(this.gameView.context, "You've beat the game!", Toast.LENGTH_LONG).show();
    }

    public void validate() {
        if(this.score == (this.boardSize * this.boardSize) - this.bombCount) {
            this.gameFinished();
        } else {
            this.gameOver();
        }
    }

    public void draw(Canvas canvas) {
        this.hud[0].onDraw(canvas, 0, 0);
        this.hud[1].onDraw(canvas, 0, 1);
        this.hud[2].onDraw(canvas, 0, 2);
    }

    public void registerTouch(MotionEvent event) {
        if(this.hud[0].hasCollided(event.getX(), event.getY())) {
            this.start();
        }
        if(this.hud[1].hasCollided(event.getX(), event.getY())) {
            this.cheat();
        }
        if(this.hud[2].hasCollided(event.getX(), event.getY())) {
            this.validate();
        }
        if(!this.isGameOver) {
            for(int i = 0; i < this.boardSize; i++) {
                for(int j = 0; j < this.boardSize; j++) {
                    if(this.gameBoard.grid[i][j].hasCollided(event.getX(), event.getY())) {
                        if(this.gameBoard.reveal(this.gameBoard.grid[i][j])) {
                            this.gameOver();
                        } else {
                            this.score = this.gameBoard.getRevealedCount();
                            if(this.score == (this.boardSize * this.boardSize) - this.bombCount) {
                                this.gameFinished();
                            }
                        }
                        break;
                    }
                }
            }
        }
    }

}

Because a lot of stuff is happening in the Game class, we’re going to break it down to see what each function is doing.

public void cheat() {
    outerLoop:
    for(int i = 0; i < this.boardSize; i++) {
        for(int j = 0; j < this.boardSize; j++) {
            if(!this.gameBoard.grid[i][j].isRevealed && this.gameBoard.grid[i][j].isBomb) {
                this.gameBoard.grid[i][j].isCheat = true;
                this.gameBoard.grid[i][j].reveal();
                break outerLoop;
            }
        }
    }
}

In the cheat() method we are looping through our game board until we find a cell that has not been revealed and is a bomb. When one is found, we set it as a cheat cell, reveal it, then exit out of the cheat() method. Cheat cells hold the flag graphic on the game board which tells the user not to click it.

public void gameOver() {
    this.isGameOver = true;
    for(int i = 0; i < this.boardSize; i++) {
        for(int j = 0; j < this.boardSize; j++) {
            this.gameBoard.showBombs(this.gameBoard.grid[i][j]);
        }
    }
    Toast.makeText(this.gameView.context, "Game over!", Toast.LENGTH_LONG).show();
}

When gameOver() happens it means we’ve hit a mine, or tried to validate prematurely. In this case we want to show all remaining mines on the board and show a message.

Now I’m going to skip straight to the registerTouch method that we have.

public void registerTouch(MotionEvent event) {
    if(this.hud[0].hasCollided(event.getX(), event.getY())) {
        this.start();
    }
    if(this.hud[1].hasCollided(event.getX(), event.getY())) {
        this.cheat();
    }
    if(this.hud[2].hasCollided(event.getX(), event.getY())) {
        this.validate();
    }
    if(!this.isGameOver) {
        for(int i = 0; i < this.boardSize; i++) {
            for(int j = 0; j < this.boardSize; j++) {
                if(this.gameBoard.grid[i][j].hasCollided(event.getX(), event.getY())) {
                    if(this.gameBoard.reveal(this.gameBoard.grid[i][j])) {
                        this.gameOver();
                    } else {
                        this.score = this.gameBoard.getRevealedCount();
                        if(this.score == (this.boardSize * this.boardSize) - this.bombCount) {
                            this.gameFinished();
                        }
                    }
                    break;
                }
            }
        }
    }
}

It is responsible for routing all of our touch events. More importantly, it will analyze our touch collisions and determine whether or not we’ve completed the game or got game over.

This brings us back to the GameView class that we saw at the beginning of this tutorial. It is responsible for everything that has to do with the SurfaceView which is a driver for the 2D canvas. Although manipulations and drawing doesn’t happen directly in this class, it is still the baseline to our game. It is where we register touch events, construct our game and board, and determine what will be drawn to the screen during the lifetime of the game.

package com.nraboy.minesweeper;

import android.content.*;
import android.view.*;
import android.graphics.*;

public class GameView extends SurfaceView {

    private SurfaceHolder holder;
    private GameLoopThread gameLoopThread;
    public Context context;
    private Board gameBoard;
    private Game game;
    private long lastClick;

    public GameView(Context context) {
        super(context);
        this.context = context;
        this.gameLoopThread = new GameLoopThread(this);
        this.game = new Game(this, 8, 10);
        this.game.start();
        holder = getHolder();
        holder.addCallback(new SurfaceHolder.Callback() {

            @Override
            public void surfaceDestroyed(SurfaceHolder holder) {
                boolean retry = false;
                gameLoopThread.setRunning(false);
                while(retry) {
                    try {
                        gameLoopThread.join();
                        retry = false;
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }

            @Override
            public void surfaceCreated(SurfaceHolder holder) {
                gameLoopThread.setRunning(true);
                gameLoopThread.start();
            }

            @Override
            public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { }

        });
    }

    public void draw(Canvas canvas) {
        canvas.drawColor(Color.BLACK);
        this.game.draw(canvas);
        this.game.gameBoard.draw(canvas);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if(System.currentTimeMillis() - lastClick > 500) {
            lastClick = System.currentTimeMillis();
            synchronized (getHolder()) {
                this.game.registerTouch(event);
            }
        }
        return true;
    }

}

Two not so obvious things in the above code are the draw and onTouchEvent methods. Anything in this particular draw method will be rendered to the screen and then all sprites will draw on their own logic. For example, here we’re saying that the game (HUD) will be drawn to the screen as well as the board. At this point, the HUD and board will draw however it seems fit. Inside the onTouchEvent we want to avoid accidental clicks or strange results from long quicks so we set a time gap between possible clicks. This way a click can only happen once every 500 milliseconds.

As you’ve probably discovered in your Android development adventures, it is a terrible idea to do a ton of stuff on the main thread because it will cause freezing and crashes. This game is no exception. Instead, we’ll be handling all game rendering and logic via a GameLoopThread class found in the GameLoopThread.java file. Essentially, the thread will get the canvas, and pass it to the draw() function of our GameView class in which case each sprite will go off and do it’s own drawing logic still under the looping thread.

package com.nraboy.minesweeper;

import android.graphics.*;

public class GameLoopThread extends Thread {

    private GameView gameView;
    private boolean isRunning;

    public GameLoopThread(GameView gameView) {
        this.gameView = gameView;
        this.isRunning = false;
    }

    public void setRunning(boolean isRunning) {
        this.isRunning = isRunning;
    }

    @Override
    public void run() {
        while(this.isRunning) {
            Canvas canvas = null;
            try {
                canvas = this.gameView.getHolder().lockCanvas();
                synchronized(this.gameView.getHolder()) {
                    if(canvas != null) {
                        this.gameView.draw(canvas);
                    }
                }
            } finally {
                if (canvas != null) {
                    this.gameView.getHolder().unlockCanvasAndPost(canvas);
                }
            }
        }
    }
}

At this point our game is complete and we should try to build and install it.

Minesweeper Challenge

Using the command prompt or terminal, run the following:

./gradlew assemble

The binary APK file produced should now be in your build/output/apk directory.

Conclusion

We just created a fully functional Minesweeper game using nothing more than native Android. It makes use of the 2D canvas and Android SurfaceView class, an alternative to OpenGL.

The funny thing, however, is that I received a failing status on this challenge even though it was clean and it worked. If you think you could beat my implementation, please share what you’d change in the comments section.

This full project can be found in my GitHub repository.

Nic Raboy

Nic Raboy

Nic Raboy is an advocate of modern web and mobile development technologies. He has experience in C#, JavaScript, Golang and a variety of frameworks such as Angular, NativeScript, and Unity. Nic writes about his development experiences related to making web and mobile development easier to understand.