Whilst we can add items to the UI in the designer manually, sometimes it may be impractical (for example if we want to add a significant number of items to the UI), or we need to alter the UI dynamically
This tutorial will work through the process of creating a game of minesweeper
private final int width = 7;
private final int height = 5;
minePanel
, and check the 'Custom Create' optioncreateUIComponants()
method add the following code to create the layout, instantiate the JPanel and set the layout for the JPanel:GridLayout mineLayout = new GridLayout(height,width);
minePanel = new JPanel();
minePanel.setLayout(mineLayout);
private ActionListener mineButtonListener;
void setupMineButtonListener(){
mineButtonListener = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("Button " + e.getActionCommand() + " pressed");
// TODO: determine position of mine in grid and handle
}
};
}
createUIComponents()
method add the following code to instantiate the button listener://mine button listener must be created before the buttons (can't go in constructor)
setupMineButtonListener();
createUIComponents()
method and add the following code which will add the same number of buttons as there will be in the grid. Note that each button will have its 'ActionCommand' value set with a string corresponding to the number of the button (from 0 to the number in the grid). This will enable us to identify it at a later point. We will also associate each button with the ActionListener we created earlier:for (int i = 0; i < (width * height); i++) {
JButton mineButton = new JButton(" ");
mineButton.addActionListener(mineButtonListener);
mineButton.setActionCommand(String.valueOf(i));
minePanel.add(mineButton);
}
We will return to the MinesweeperUI class later, but the next stage is to build the logic for the Minesweeper game itself. We will create three class for this, the first of which will simply be to represent a point in 2D space.
class Point {
private int x, y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
int getX() {
return x;
}
int getY() {
return y;
}
}
equals
method (which allows one object to be compared to a another). As we will want to compare an instance Point to another instance of Point (and not an Object), we can test if the other object being compared is also a Point and adjust the behaviour accordingly. We test if an object is a point using the keyword instanceof
. Add the following code inside the Point class:@Override
public boolean equals(Object obj) {
if (obj instanceof Point){
Point otherPoint = (Point)obj;
return this.x == otherPoint.x && this.y == otherPoint.y ;
}
return false;
}
public class MineInfo {
private boolean isMine = false;
private int adjacentMineCount = 0;
public boolean isMine() {
return isMine;
}
public void setMine(boolean mine) {
isMine = mine;
}
public int getAdjacentMineCount() {
return adjacentMineCount;
}
public void setAdjacentMineCount(int adjacentMineCount) {
this.adjacentMineCount = adjacentMineCount;
}
}
private boolean started = false;
private int mineCount;
private int width, height;
private MineInfo[][] field;
mineCount
variableMineField(int width, int height) {
field = new MineInfo[height][width];
this.width = width;
this.height = height;
mineCount = (width * height)/5;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
field[y][x] = new MineInfo();
}
}
}
You may wonder why at this stage none of the instances of MineInfo have any mines - this is because we want to avoid the possibility of a player starting on a mine - so we will populate the mine field once they have chosen their starting location (and avoid placing a mine in that location)private Point getRandomPoint() {
Random rand = new Random();
int x = rand.nextInt(width);
int y = rand.nextInt(height);
return new Point(x, y);
}
private void populateMinesAvoidingPoint(int count, Point startPoint) {
ArrayList<Point> mineLocations = new ArrayList<Point>();
do {
final Point randPoint = getRandomPoint();
if (!startPoint.equals(randPoint) && !mineLocations.contains(randPoint)) {
mineLocations.add(randPoint);
}
} while (mineLocations.size() < count);
for (Point point : mineLocations) {
field[point.getY()][point.getX()].setMine(true);
}
}
private Set<Point> validNeighboursForPoint(Point point) {
Set<Point> neighbours = new HashSet<Point>();
final int minX = Math.max(0, point.getX() - 1);
final int maxX = Math.min(width - 1, point.getX() + 1);
final int minY = Math.max(0, point.getY() - 1);
final int maxY = Math.min(height - 1, point.getY() + 1);
for (int y = minY; y <= maxY; y++) {
for (int x = minX; x <= maxX; x++) {
neighbours.add(new Point(x, y));
}
}
return neighbours;
}
private int countMinesAtPoints(Set<Point> points) {
int count = 0;
for (Point point : points) {
if (field[point.getY()][point.getX()].isMine()) {
count++;
}
}
return count;
}
private void populateAdjacentMineCount() {
for (int y = 0; y < field.length; y++) {
for (int x = 0; x < field[y].length; x++) {
final Set<Point> neighbours = validNeighboursForPoint(new Point(x, y));
field[y][x].setAdjacentMineCount(countMinesAtPoints(neighbours));
}
}
}
MineInfo checkMine(Point point) {
if (!started) {
started = true;
populateMinesAvoidingPoint(mineCount, point);
populateAdjacentMineCount();
}
MineInfo location = field[point.getY()][point.getX()];
MineInfo mineInfo = new MineInfo();
mineInfo.setAdjacentMineCount(location.getAdjacentMineCount());
mineInfo.setMine(location.isMine());
return mineInfo;
}
private Point pointForId(int mineId)
{
int x = mineId % width;
int y = mineId / width;
return new Point(x, y);
}
void explodeAllMines(){
for (Component component:minePanel.getComponents()){
if (component instanceof JButton) {
JButton button = (JButton) component;
int mineId = Integer.parseInt(button.getActionCommand());
Point point = pointForId(mineId);
MineInfo info = mineField.checkMine(point);
if (info.isMine()) {
button.setText("*");
}
}
}
}
TODO: determine position of mine in grid and handle
statement in the setupMineButtonListener()
method and add the following code:int mineId = Integer.parseInt(e.getActionCommand());
Point point = pointForId(mineId);
MineInfo info = mineField.checkMine(point);
JButton button = (JButton)e.getSource();
if (info.isMine()){
explodeAllMines();
}else {
button.setText(String.valueOf(info.getAdjacentMineCount()));
}
void resetButtons(){
for (Component possibleButton:minePanel.getComponents()) {
if (possibleButton instanceof JButton){
JButton actualButton = (JButton)possibleButton;
actualButton.setText(" ");
}
}
}
mineField
variable to a new instance of MineField. This can be done in the constructorvalidNeighboursForPoint()
method public to do thisYou may have noticed that the populateMinesAvoidingPoint
method was not terribly efficient - everytime a random Point was generated that was identical to one already generated would require an additional iteration of the do-while loop.
The method could instead be replaced with the code below, which prevents that from happening. It also allows the getRandomPoint()
method to be removed. The code has some comments, but read through it carefully and try to fully understand what is going on:
private void populateMinesAvoidingPoint(int noOfMines, Point startPoint) {
ArrayList<Integer> minesIndexList = new ArrayList<Integer>();
//populate list of integers corresponding to all positions in 2D array except start point
for (int i = 0; i < (width * height); i++) {
int startMineIndex = startPoint.getX() + (startPoint.getY() * width);
if (i != startMineIndex) {
minesIndexList.add(i);
}
}
Random rand = new Random();
while (noOfMines > 0){
int randomPosition = rand.nextInt(minesIndexList.size());
int mineIndex = minesIndexList.get(randomPosition);
//get position in minefield corresponding to index
int x = mineIndex % width;
int y = mineIndex / width;
field[y][x].setMine(true);
//remove position from list so it can't be selected again
minesIndexList.remove(randomPosition);
noOfMines--;
}
}