Нам понадобятся:
- 15 минут свободного времени;
- Настроенная рабочая среда, т.е. JDK и IDE (например Eclipse);
- Библиотека LWJGL (версии 2.x.x) для работы с Open GL. Обратите внимание, что для LWJGL версий выше 3 потребуется написать код, отличающийся от того, что приведён в статье;
- Иконки для клеток, т.е. цифры, флаг, неверно поставленный флаг, мина, взорвавшаяся мина и закрытое поле. Можно чисто символически нарисовать самому, или скачать использовавшиеся при написании статьи.
Работа с графикой
Для работы с графикой создадим отдельный класс — GUI. От него нам потребуется хранение всех графических элементов управления (т.е. полей клеток), определение элемента, по которому пришёлся клик и передача ему управления, вывод графических элементов на экран и управление основными функциями OpenGL.
Благо класс GUI будет взаимодействовать с графическими элементами, нам нужно создать интерфейс (писать классы сетки, клеток и прочую механику пока рано), который определит, что это такое. Логика подсказывает, что у графического элемента должны быть:
- Внешний вид (текстура);
- Координаты;
- Размеры (ширина и высота);
- Метод, который по переданным координатам клика определит, попал ли клик по элементу;
- Метод, который обработает нажатие на элемент.
Таким образом, пишем:
public interface GUIElement {
int getWidth();
int getHeight();
int getY();
int getX();
Sprite getSprite(); ///Создадим enum с таким именем, заполним позже
int receiveClick(int x, int y, int button); /// Возвращаем результат клика
///Параметр button определяет кнопку мыши, которой был сделан щелчок.
/// Здесь используется фишка Java 8 --- default методы в интерфейсах.
/// Если у вас более ранняя версия, вы можете использовать абстрактный класс
/// вместо интерфейса.
default boolean isHit(int xclick, int yclick){
return ( (xclick > getX()) && (xclick < getX()+this.getWidth()) )
&&( (yclick > getY()) && (yclick < getY()+this.getHeight()) );
}
}
В GUI должны храниться ячейки поля. Создадим для этих целей двумерный массив:
///CELLS_COUNT_X и CELLS_COUNT_Y -- константы
//Cell -- класс, который реализует GUIElement; им займёмся немного позже
private static Cell[][] cells;
GUI должен передавать клики элементам, которые он содержит. Вычислить адрес клетки, по которой кликнули, нетрудно:
public static int receiveClick(int x, int y, int button){
int cell_x = x/CELL_SIZE;
int cell_y = y/CELL_SIZE;
return cells[cell_x][cell_y].receiveClick(x,y,button);
}
Теперь разберёмся с основными функциями OpenGL. Во-первых, нам нужна инициализация.
///Class GUI
private static void initializeOpenGL(){
try {
//Задаём размер будущего окна
Display.setDisplayMode(new DisplayMode(SCREEN_WIDTH, SCREEN_HEIGHT));
//Задаём имя будущего окна
Display.setTitle(NAME);
//Создаём окно
Display.create();
} catch (LWJGLException e) {
e.printStackTrace();
}
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glOrtho(0,SCREEN_WIDTH,0,SCREEN_HEIGHT,1,-1);
glMatrixMode(GL_MODELVIEW);
/*
* Для поддержки текстур
*/
glEnable(GL_TEXTURE_2D);
/*
* Для поддержки прозрачности
*/
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
/*
* Белый фоновый цвет
*/
glClearColor(1,1,1,1);
}
На этом мы подробно останавливаться не будем, т.к. изучение LWJGL не входит в наши сегодняшние планы. Во-вторых, нам нужно обновлять изображение на экране:
///Этот метод будет вызываться извне
public static void update() {
updateOpenGL();
}
///А этот метод будет использоваться только локально,
/// т.к. базовым другие классы должны работать на более высоком уровне
private static void updateOpenGL() {
Display.update();
Display.sync(60);
}
И, наконец, нам нужно это изображение вообще рисовать. Для этого пора закончить enum Sprite. Его элементы будут представлять из себя обёртку для текстуры с удобочитаемыми именами.
///enum Sprite
///Файлы со всеми этими именами должны лежать по адресу
/// *папка проекта*/res/*имя текстуры*.png
ZERO("0"), ONE("1"), TWO("2"), THREE("3"), FOUR("4"), FIVE("5"), SIX("6"),
SEVEN("7"), EIGHT("8"), HIDEN("space"), BOMB("bomb"), EXPLOSION("explosion"),
FLAG("flag"), BROKEN_FLAG("broken_flag");
private Texture texture;
private Sprite(String texturename){
try {
this.texture = TextureLoader.getTexture("PNG", new FileInputStream(new File("res/"+texturename+".png")));
} catch (IOException e) {
e.printStackTrace();
}
}
public Texture getTexture(){
return this.texture;
}
Теперь мы можем написать метод для GUI, который будет рисовать элементы:
///Рисует все клетки
public static void draw(){
///Очищает экран от старого изображения
glClear(GL_COLOR_BUFFER_BIT);
for(GUIElement[] line:cells){
for(GUIElement cell:line){
drawElement(cell);
}
}
}
///Рисует элемент, переданный в аргументе
private static void drawElement(GUIElement elem){
elem.getSprite().getTexture().bind();
glBegin(GL_QUADS);
glTexCoord2f(0,0);
glVertex2f(elem.getX(),elem.getY()+elem.getHeight());
glTexCoord2f(1,0);
glVertex2f(elem.getX()+elem.getWidth(),elem.getY()+elem.getHeight());
glTexCoord2f(1,1);
glVertex2f(elem.getX()+elem.getWidth(), elem.getY());
glTexCoord2f(0,1);
glVertex2f(elem.getX(), elem.getY());
glEnd();
}
Нам осталось только сделать единый метод для инициализации графики, и остальное мы будем писать в основном управляющем классе, возвращаясь сюда, только чтобы внести незначительные изменения.
public static void init(){
initializeOpenGL();
///Классом генератора мы займёмся чуть позже. Пока можно просто
///создать его, вместе с пустым методом generate
this.cells = Generator.generate();
}
Ячейки
Создадим класс Cell, реализующий интерфейс GUIElement. В методах getWidth()
и getHeight()
вернём константу, для координат придётся создать поля, которые будут инициализироваться конструктором. Так же конструктором будем передавать состояние клетки: «-1», если это мина, «-2», если это взорванная мина, число мин поблизости в остальных случаях. Для этой цели можно было бы использовать enum, но число мин удобнее передавать как integer, имхо. Итак, конструктор:
private int x;
private int y;
private int state;
public Cell (int x, int y, int state){
this.x=x;
this.y=y;
this.state=state;
}
Ещё два поля — boolean isMarked
и boolean isHidden
будут отвечать за то, отметили ли клетку флажком, и открыли ли её. По умолчанию оба флага выставлены на false
.
Разберёмся с методом getSprite()
.
public Sprite getSprite() {
if(this.isMarked){
if(!this.isHidden && this.state!=-1){
///Если эта клетка не скрыта, и на ней
///ошибочно стоит флажок...
return Sprite.BROKEN_FLAG;
}
///В другом случае --
return Sprite.FLAG;
}else if(this.isHidden){
///Если клетка не помечена, притом скрыта...
return Sprite.HIDEN;
}else{
///Если не помечена и не скрыта, выводим как есть:
switch (state){
case -2:
return Sprite.EXPLOSION;
case -1:
return Sprite.BOMB;
default:
assert (state>=0 && state<=8): "Some crap :c";
///Сделал массив для удобства. Можете, конечно,
///Писать 9 кейсов -- ваш выбор ;)
return skin_by_number[state];
}
}
}
В случае, если на кнопку нажали, нам снова необходимо рассмотреть несколько простейших случаев:
@Override
public int receiveClick(int x, int y, int button) {
if(isHidden){
///Нет смысла обрабатывать клики по уже открытым полям
if(button==0 && !this.isMarked){
///Здесь обработаем щелчки левой кнопкой
///Заметим, что щёлкать левой кнопкой по флагам
///абсолютно бессмысленно
///Открываем клетку
this.isHidden = false;
if(this.state==-1){
///Если это была мина, меняем состояние
///на взорванную и передаём сигнал назад
this.state=-2;
return -1;
}
if(this.state == 0){
///Если мы попали в нолик, нужно открыть
///Все соседние ячейки. Этим займётся GUI :)
return 1;
}
}else if(button==1){
///В любой ситуации, щелчок правой кнопкой
///либо снимает отметку, либо ставит её
this.isMarked = ! this.isMarked;
}
}
return 0;
}
Чтобы при поражении клетки можно было вскрыть, добавим метод:
public void show() {
this.isHidden=false;
}
Для более удобной реализации генератора добавьте ещё и этот метод:
public void incNearMines() {
if(state<0){
//ignore
}else{
state++;
}
}
Обработка ответов от клеток
Вернёмся к методу GUI.receiveClick()
. Теперь мы не можем просто вернуть результат назад, т.к. если результат выполнения — единица, то нам нужно открыть соседние ячейки, а в главный управляющий класс вернуть уже ноль, в знак того, что всё прошло корректно.
public static int receiveClick(int x, int y, int button){
int cell_x = x/CELL_SIZE;
int cell_y = y/CELL_SIZE;
int result = cells[cell_x][cell_y].receiveClick(x,y,button);
if(result==1){
///Делаем вид, что тыкнули в клетки
///Сверху, снизу, справа и слева
///Игнорируем выхождение за границы поля
try{
receiveClick(x+CELL_SIZE,y,button);
}catch(java.lang.ArrayIndexOutOfBoundsException e){
//ignore
}
try{
receiveClick(x-CELL_SIZE,y,button);
}catch(java.lang.ArrayIndexOutOfBoundsException e){
//ignore
}
try{
receiveClick(x,y+CELL_SIZE,button);
}catch(java.lang.ArrayIndexOutOfBoundsException e){
//ignore
}
try{
receiveClick(x,y-CELL_SIZE,button);
}catch(java.lang.ArrayIndexOutOfBoundsException e){
//ignore
}
return 0;
}
return result;
}
Пишем генератор
Задачка эта не сложнее, чем создать массив случайных boolean-величин. Идея следующая — для каждой ячейки матрицы мы генерируем случайное число от 0 до 100. Если это число меньше 15, то в этом месте записываем в матрицу мину (таким образом, шанс встретить мину — 15%). Записав мину, мы вызываем у всех клеток вокруг метод incNearMines()
, а для тех ячеек, где клетка ещё не создана храним значение в специальном массиве.
public static Cell[][] generate() {
{
Random rnd = new Random();
///Карта, которую мы вернём
Cell[][] map = new Cell[CELLS_COUNT_X][CELLS_COUNT_Y];
///Матрица с пометками, указывается кол-во мин рядом с каждой клеткой
int[][] counts = new int[CELLS_COUNT_X][CELLS_COUNT_Y];
for(int x=0; x<CELLS_COUNT_X; x++){
for(int y=0; y<CELLS_COUNT_Y; y++){
boolean isMine = rnd.nextInt(100)<15;
if(isMine){
map[x][y] = new Cell(x*CELL_SIZE, y*CELL_SIZE, -1);
for(int i=-1; i<2; i++){
for(int j=-1; j<2; j++){
try{
if(map[x+i][y+j]==null){
///Если клетки там ещё нет, записываем сведение
///о мине в матрицу
counts[x+i][y+j]+=1;
}else{
///Если есть, говорим ей о появлении мины
map[x+i][y+j].incNearMines();
}
}catch(java.lang.ArrayIndexOutOfBoundsException e){
//ignore
}
}
}
}else{
///Если была сгенерирована обычная клетка, создаём её, со
///state равным значению из матрицы
map[x][y] = new Cell(x*CELL_SIZE, y*CELL_SIZE, counts[x][y]);
}
}
}
return map;
}
}
Главный управляющий класс и ввод
Создадим класс Main, в нём входной метод — public static void main(String[] args)
. Этот метод должен будет делать всего две вещи: вызывать инициализацию GUI и циклически вызывать рабочие методы (input()
, GUI.draw()
и GUI.update()
), пока не получит сигнал закрытия.
private static boolean end_of_game=false;
public static void main(String[] args) {
GUI.init();
while(!end_of_game){
input();
GUI.draw();
GUI.update();
}
}
Здесь нам не хватает метода input()
, займёмся им.
///Если за последний такт произошли какие-то события с мышью,
///перебираем их по очереди
while(Mouse.next()){
///Если это было нажатие кнопки мыши, а не
///перемещение...
if(Mouse.getEventButton()>=0 && Mouse.getEventButtonState()){
int result;
///Отсылаем это на обработку в GUI
result = GUI.receiveClick(Mouse.getEventX(), Mouse.getEventY(), Mouse.getEventButton());
switch(result){
case 0:
//отлично!
break;
case -1:
//не очень :c
GUI.gameover();
break;
}
}
}
///То же самое с клавиатурой
while(Keyboard.next()){
if(Keyboard.getEventKeyState()){
if(Keyboard.getEventKey()==Keyboard.KEY_ESCAPE){
isExitRequested = true;
}
}
}
///Обрабатываем клик по кнопке "закрыть" окна
isExitRequested=isExitRequested || Display.isCloseRequested();
Метод GUI.gameover()
будет просто вызывать метод show()
у каждой клетки, показывая таким образом всё поле:
public static void gameover() {
for(Cell[] line:cells){
for(Cell cell:line){
cell.show();
}
}
}
Запускаем:
Готово!
UPD: исходники выложены на GitHub
Лет 10 назад в народе ходила примета: если офисный сотрудник чересчур сосредоточенно смотрит в экран и столь же сосредоточенно периодически кликает мышкой, скорее всего, там открыта игра «Сапер».
Напомним тем, кто уже забыл, и расскажем другим, кто в силу возраста эту эпоху пропустил: «Сапер» — одна из самых популярных офисных игр, которая поставлялась вместе с Microsoft Windows. И даже сегодня, если вы сидите за компьютером с операционной системой этого семейства, то «Сапер» либо уже там установлен (просто наберите в поиске ОС слово «Сапер» или «Minesweeper»), либо его можно загрузить бесплатно из Microsoft Store. Многие Linux-сборки, например KDE и GNOME, также обзавелись похожими играми.
В отличие от большинства других «убивалок времени», в этой игре отлично соблюдён баланс ходов логических и ходов наудачу, что делает «Сапера» одновременно очень увлекательной и… чуть-чуть менее бессмысленной, чем другие игры этого класса.
Итак, у нас есть поле, разделённое на квадраты. Какие-то из них «заминированы», но сколько таковых и где они, мы не в курсе. Наша цель — открыть все незаминированные квадраты и не подорваться. Вы открываете квадрат левой кнопкой мыши, и если под ней мины нет, то появится число, означающее количество мин, расположенных по соседству с открытой ячейкой. Теперь нужно немного посчитать и предположить, какие ячейки можно открывать, а какие следует пометить, как заминированные. И так до победы или подрыва.
В Windows разработчики встроили эту игрушку для того, чтобы люди учились управляться с мышкой (да, были времена, когда у компьютера её не было, приходилось довольствоваться только клавиатурой). Но на самом деле история «Сапера» ещё более древняя, чем история Microsoft, и её предки были доступны ещё на мейнфреймах в шестидесятых годах прошлого столетия…
Впрочем, речь сейчас не об этом, а о том, чтобы создать собственную версию «Сапера»! Правила у нашей «головоломки с риском» будут точь-в-точь такими, как описано выше. Мы на JavaRush уже разделили эту непростую задачу на подзадачи и будем подсказывать вам, что делать. Дерзайте.
I created a Java Swing Minesweeper, for which I would like some feedback.
Note : I’m just in high school so please excuse any ‘dumb’ ways I may have coded certain segments.
Any tips or books on how to improve the general quality of the code, or any information that will help me become better are deeply appreciated.
Apart from feedback, I would also like to know how to preserve colours in buttons which I have disabled. In the context that, to make an already clicked cell unclickable again, I just disabled it. Or, is there a better way to do the same task?
The blueprint for the code is :
-
Real Board(buttons[][] for the user to click).
-
MyBoard (back-end to configure number counts of each cell, etc.
-
Methods to handle each event of the game.
Please excuse any ambiguity in language.
package MinesweeperGame;
//Following is the implementation of Minesweeper.
import java.awt.*;
import javax.swing.*;
import java.awt.event.*;
import java.util.*;
public class Minesweeper extends JFrame implements ActionListener, MouseListener{
JFrame frame = new JFrame();
JButton reset = new JButton("Reset"); //Reset Button as a side.
JButton giveUp = new JButton("Give Up"); //Similarly, give up button.
JPanel ButtonPanel = new JPanel();
Container grid = new Container();
int[][] counts; //integer array to store counts of each cell. Used as a back-end for comparisons.
JButton[][] buttons; //Buttons array to use as a front end for the game.
int size,diff;
final int MINE = 10;
/**
@param size determines the size of the board
*/
public Minesweeper(int size){
super("Minesweeper");
this.size = size;
counts = new int[size][size];
buttons = new JButton[size][size];
frame.setSize(900,900);
frame.setLayout(new BorderLayout());
frame.add(ButtonPanel,BorderLayout.SOUTH);
reset.addActionListener(this);
giveUp.addActionListener(this);
grid.setLayout(new GridLayout(size,size));
for(int a = 0; a < buttons.length; a++)
{
for(int b = 0; b < buttons[0].length; b++)
{
buttons[a][b] = new JButton();
buttons[a][b].addActionListener(this);
grid.add(buttons[a][b]);
}
}
// above initializes each button in the minesweeper board and gives it functionality.
ButtonPanel.add(reset);
ButtonPanel.add(giveUp); // adding buttons to the panel.
frame.add(grid,BorderLayout.CENTER);
createMines(size); //calling function to start the game by filling mines.
frame.setLocationRelativeTo(null);
frame.setDefaultCloseOperation(EXIT_ON_CLOSE); //frame stuff
frame.setVisible(true);
}
/**
* Function to check whether user has lost the game ( i.e clicked a mine).
* @param m indicated whether the function has been called when user clicks a mine( m=1)
* or when he clicks the give up button.(m = any other integer).
* Shows a dialog box which tells the user that they have lost the game.
*/
public void takeTheL(int m){
for(int x = 0; x < size; x++)
{
for(int y = 0; y < size; y++)
{
if(buttons[x][y].isEnabled()) // when a button has been clicked, it is disabled.
{
if(counts[x][y] != MINE)
{
buttons[x][y].setText(""+ counts[x][y]);
}
else
{
buttons[x][y].setText("X");
}
buttons[x][y].setEnabled(false);
}
}
}
JOptionPane.showMessageDialog(null, m==1? "You clicked a mine!":"You Gave Up",
"Game Over", JOptionPane.ERROR_MESSAGE);
}
/**
* Function to check whether user has won or not
* It performs this by checking whether a cell that is NOT a mine
* remains to be clicked by the user.
* (Works because, when a user clicks a button, it is disabled to avoid further moves on the same cell).
* Function prints a pop-up message congratulating user on victory.
*/
public void takeTheW() {
boolean won = true;
for(int i = 0; i < size; i++)
{
for(int j = 0; j < size; j++)
{
if(counts[i][j] != MINE && buttons[i][j].isEnabled())
{
won = false;
}
}
}
if(won)
{
JOptionPane.showMessageDialog(null,"You have won!", "Congratulations!",
JOptionPane.INFORMATION_MESSAGE);
}
}
@Override
public void actionPerformed(ActionEvent ae) {
if(ae.getSource() == reset) //resets grid
{
for(int x = 0; x < size; x++)
{
for(int y = 0; y < size; y++)
{
buttons[x][y].setEnabled(true);
buttons[x][y].setText("");
}
}
createMines(30); //triggers a new game.
}
else if(ae.getSource() == giveUp) //user has given up. trigger takeTheL( m!= 1).
{
takeTheL(0); // anything not = 1
}
else // click was on a cell
{
for(int x = 0; x < size; x++)
{
for( int y = 0; y < size; y++)
{
if(ae.getSource() == buttons[x][y])
{
switch (counts[x][y]) {
case MINE:
buttons[x][y].setForeground(Color.RED);
buttons[x][y].setIcon(new ImageIcon("")); // add bomb image
takeTheL(1); //user clicked on a mine
break;
case 0:
buttons[x][y].setText(counts[x][y] +"");
buttons[x][y].setEnabled(false);
ArrayList<Integer> clear = new ArrayList<>();
clear.add(x*100+y);
dominoEffect(clear); // To recursively clear all surrounding '0' cells.
takeTheW(); //checks win every move
break;
default:
buttons[x][y].setText(""+counts[x][y]);
buttons[x][y].setEnabled(false);
takeTheW(); // its a number > 0 and not a mine, so just check for win
break;
}
}
}
}
}
}
/**
* Function creates mines at random positions.
* @param s the size of the board(row or column count)
*/
public void createMines(int s){
ArrayList<Integer> list = new ArrayList<>(); //Modifiable array to store pos. of mines.
for(int x = 0; x < s; x++)
{
for(int y = 0; y < s; y++)
{
list.add(x*100+y); // x & y shall be individually retrieved by dividing by 100 and modulo 100 respectively.
// refer to lines 284 and 285 for implementation
}
}
counts = new int[s][s]; //resetting back-end array
for(int a = 0; a < (int)(s * 1.5); a++)
{
int choice = (int)(Math.random() * list.size());
counts [list.get(choice) / 100] [list.get(choice) % 100] = MINE; //Using corollary of before-last comment to set mines as well.
list.remove(choice); // We don't want two mines in the same pos., so remove that pos. from list.
}
/*
Following segment initializes 'neighbor counts' for each cell. That is, the number of
mines that are present in the eight surrounding cells. IF the cell isn't a mine.
Note : It is done in the back-end array as that contains the numbers (MINE or 1 or 2 or 3 or 4 or 5 or 6 or 7 or 8)
*/
for(int x = 0; x < s; x++)
{
for(int y = 0; y < s; y++)
{
if(counts[x][y] != MINE)
{
int neighbor = 0;
if( x > 0 && y > 0 && counts[x-1][y-1] == MINE) //top left
{
neighbor++;
}
if( y > 0 && counts[x][y-1] == MINE) //left
{
neighbor++;
}
if( y < size - 1 && counts[x][y+1] == MINE) //right
{
neighbor++;
}
if( x < size - 1 && y > 0 && counts[x+1][y-1] == MINE) //bottom left
{
neighbor++;
}
if( x > 0 && counts[x-1][y] == MINE) //up
{
neighbor++;
}
if( x < size - 1 && counts[x+1][y] == MINE)//down
{
neighbor++;
}
if( x > 0 && y < size - 1 &&counts[x-1][y+1] == MINE) //top right
{
neighbor++;
}
if( x < size - 1 && y < size - 1 && counts[x+1][y+1] == MINE) //bottom right
{
neighbor++;
}
counts[x][y] = neighbor; //setting value
}
}
}
}
/**
* This function, called the domino effect, is an implementation of the idea that,
* when a cell with no surrounding mines is clicked, there's no point in user clicking
* all eight surrounding cells. Therefore, all surrounding
* cells' counts will be displayed in corresponding cells.
* The above is done recursively.
* @param toClear the ArrayList which is passed to the function with positions in array
* that are zero, and are subsequently clicked.
*/
public void dominoEffect(ArrayList<Integer> toClear){
if(toClear.isEmpty())
return; //base case
int x = toClear.get(0) / 100; //getting x pos.
int y = toClear.get(0) % 100; //getting y pos.
toClear.remove(0); //remove that element from array to prevent infinite recursion.
if(counts[x][y] == 0)
{ //similar to neighbor counts, each surrounding cell is filled
if( x > 0 && y > 0 && buttons[x-1][y-1].isEnabled()) //top left
{
buttons[x-1][y-1].setText(counts[x-1][y-1] + "");
buttons[x-1][y-1].setEnabled(false);
if(counts[x-1][y-1] == 0)
{
toClear.add((x-1)*100 + (y-1)); //to recursively implement, each surrounding cell is the new cell,
// the surrounding cells of which we shall check and so on.
}
}
if( y > 0 && buttons[x][y-1].isEnabled()) //left
{
buttons[x][y-1].setText(counts[x][y-1] + "");
buttons[x][y-1].setEnabled(false);
if(counts[x][y-1] == 0)
{
toClear.add(x*100 + (y-1));
}
}
if( y < size - 1 && buttons[x][y+1].isEnabled()) //right
{
buttons[x][y+1].setText(counts[x][y+1] + "");
buttons[x][y+1].setEnabled(false);
if(counts[x][y+1] == 0)
{
toClear.add(x*100 + (y+1));
}
}
if( x < size - 1 && y > 0 && buttons[x+1][y-1].isEnabled()) //bottom left
{
buttons[x+1][y-1].setText(counts[x+1][y-1] + "");
buttons[x+1][y-1].setEnabled(false);
if(counts[x+1][y-1] == 0)
{
toClear.add((x+1)*100 + (y-1));
}
}
if( x > 0 && buttons[x-1][y].isEnabled()) //up
{
buttons[x-1][y].setText(counts[x-1][y] + "");
buttons[x-1][y].setEnabled(false);
if(counts[x-1][y] == 0)
{
toClear.add((x-1)*100 + y);
}
}
if( x < size - 1 && buttons[x+1][y].isEnabled())//down
{
buttons[x+1][y].setText(counts[x+1][y] + "");
buttons[x+1][y].setEnabled(false);
if(counts[x+1][y] == 0)
{
toClear.add((x+1)*100 + y);
}
}
if( x > 0 && y < size - 1 && buttons[x-1][y+1].isEnabled()) //top right
{
buttons[x-1][y+1].setText(counts[x-1][y+1] + "");
buttons[x-1][y+1].setEnabled(false);
if(counts[x-1][y+1] == 0)
{
toClear.add((x-1)*100 + (y+1));
}
}
if( x < size - 1 && y < size - 1 && buttons[x+1][y+1].isEnabled()) //bottom right
{
buttons[x+1][y+1].setText(counts[x+1][y+1] + "");
buttons[x+1][y+1].setEnabled(false);
if(counts[x+1][y+1] == 0)
{
toClear.add((x+1)*100 + (y+1));
}
}
}
dominoEffect(toClear); //recursive call with list containing surrounding cells, for further check-and-clear of THEIR surr. cells.
}
//Main method.
public static void main(String[] args){
new Minesweeper(20); // Can be made of any size. (For now only squares)
}
@Override
public void mouseClicked(MouseEvent me) {
if (SwingUtilities.isRightMouseButton(me)){
// TODO : Handle flagging of mines.
}
}
@Override
public void mousePressed(MouseEvent me) {
// Do nothing
}
@Override
public void mouseReleased(MouseEvent me) {
// Do nothing
}
@Override
public void mouseEntered(MouseEvent me) {
// Do nothing
}
@Override
public void mouseExited(MouseEvent me) {
// Do nothing
}
}
I created a Java Swing Minesweeper, for which I would like some feedback.
Note : I’m just in high school so please excuse any ‘dumb’ ways I may have coded certain segments.
Any tips or books on how to improve the general quality of the code, or any information that will help me become better are deeply appreciated.
Apart from feedback, I would also like to know how to preserve colours in buttons which I have disabled. In the context that, to make an already clicked cell unclickable again, I just disabled it. Or, is there a better way to do the same task?
The blueprint for the code is :
-
Real Board(buttons[][] for the user to click).
-
MyBoard (back-end to configure number counts of each cell, etc.
-
Methods to handle each event of the game.
Please excuse any ambiguity in language.
package MinesweeperGame;
//Following is the implementation of Minesweeper.
import java.awt.*;
import javax.swing.*;
import java.awt.event.*;
import java.util.*;
public class Minesweeper extends JFrame implements ActionListener, MouseListener{
JFrame frame = new JFrame();
JButton reset = new JButton("Reset"); //Reset Button as a side.
JButton giveUp = new JButton("Give Up"); //Similarly, give up button.
JPanel ButtonPanel = new JPanel();
Container grid = new Container();
int[][] counts; //integer array to store counts of each cell. Used as a back-end for comparisons.
JButton[][] buttons; //Buttons array to use as a front end for the game.
int size,diff;
final int MINE = 10;
/**
@param size determines the size of the board
*/
public Minesweeper(int size){
super("Minesweeper");
this.size = size;
counts = new int[size][size];
buttons = new JButton[size][size];
frame.setSize(900,900);
frame.setLayout(new BorderLayout());
frame.add(ButtonPanel,BorderLayout.SOUTH);
reset.addActionListener(this);
giveUp.addActionListener(this);
grid.setLayout(new GridLayout(size,size));
for(int a = 0; a < buttons.length; a++)
{
for(int b = 0; b < buttons[0].length; b++)
{
buttons[a][b] = new JButton();
buttons[a][b].addActionListener(this);
grid.add(buttons[a][b]);
}
}
// above initializes each button in the minesweeper board and gives it functionality.
ButtonPanel.add(reset);
ButtonPanel.add(giveUp); // adding buttons to the panel.
frame.add(grid,BorderLayout.CENTER);
createMines(size); //calling function to start the game by filling mines.
frame.setLocationRelativeTo(null);
frame.setDefaultCloseOperation(EXIT_ON_CLOSE); //frame stuff
frame.setVisible(true);
}
/**
* Function to check whether user has lost the game ( i.e clicked a mine).
* @param m indicated whether the function has been called when user clicks a mine( m=1)
* or when he clicks the give up button.(m = any other integer).
* Shows a dialog box which tells the user that they have lost the game.
*/
public void takeTheL(int m){
for(int x = 0; x < size; x++)
{
for(int y = 0; y < size; y++)
{
if(buttons[x][y].isEnabled()) // when a button has been clicked, it is disabled.
{
if(counts[x][y] != MINE)
{
buttons[x][y].setText(""+ counts[x][y]);
}
else
{
buttons[x][y].setText("X");
}
buttons[x][y].setEnabled(false);
}
}
}
JOptionPane.showMessageDialog(null, m==1? "You clicked a mine!":"You Gave Up",
"Game Over", JOptionPane.ERROR_MESSAGE);
}
/**
* Function to check whether user has won or not
* It performs this by checking whether a cell that is NOT a mine
* remains to be clicked by the user.
* (Works because, when a user clicks a button, it is disabled to avoid further moves on the same cell).
* Function prints a pop-up message congratulating user on victory.
*/
public void takeTheW() {
boolean won = true;
for(int i = 0; i < size; i++)
{
for(int j = 0; j < size; j++)
{
if(counts[i][j] != MINE && buttons[i][j].isEnabled())
{
won = false;
}
}
}
if(won)
{
JOptionPane.showMessageDialog(null,"You have won!", "Congratulations!",
JOptionPane.INFORMATION_MESSAGE);
}
}
@Override
public void actionPerformed(ActionEvent ae) {
if(ae.getSource() == reset) //resets grid
{
for(int x = 0; x < size; x++)
{
for(int y = 0; y < size; y++)
{
buttons[x][y].setEnabled(true);
buttons[x][y].setText("");
}
}
createMines(30); //triggers a new game.
}
else if(ae.getSource() == giveUp) //user has given up. trigger takeTheL( m!= 1).
{
takeTheL(0); // anything not = 1
}
else // click was on a cell
{
for(int x = 0; x < size; x++)
{
for( int y = 0; y < size; y++)
{
if(ae.getSource() == buttons[x][y])
{
switch (counts[x][y]) {
case MINE:
buttons[x][y].setForeground(Color.RED);
buttons[x][y].setIcon(new ImageIcon("")); // add bomb image
takeTheL(1); //user clicked on a mine
break;
case 0:
buttons[x][y].setText(counts[x][y] +"");
buttons[x][y].setEnabled(false);
ArrayList<Integer> clear = new ArrayList<>();
clear.add(x*100+y);
dominoEffect(clear); // To recursively clear all surrounding '0' cells.
takeTheW(); //checks win every move
break;
default:
buttons[x][y].setText(""+counts[x][y]);
buttons[x][y].setEnabled(false);
takeTheW(); // its a number > 0 and not a mine, so just check for win
break;
}
}
}
}
}
}
/**
* Function creates mines at random positions.
* @param s the size of the board(row or column count)
*/
public void createMines(int s){
ArrayList<Integer> list = new ArrayList<>(); //Modifiable array to store pos. of mines.
for(int x = 0; x < s; x++)
{
for(int y = 0; y < s; y++)
{
list.add(x*100+y); // x & y shall be individually retrieved by dividing by 100 and modulo 100 respectively.
// refer to lines 284 and 285 for implementation
}
}
counts = new int[s][s]; //resetting back-end array
for(int a = 0; a < (int)(s * 1.5); a++)
{
int choice = (int)(Math.random() * list.size());
counts [list.get(choice) / 100] [list.get(choice) % 100] = MINE; //Using corollary of before-last comment to set mines as well.
list.remove(choice); // We don't want two mines in the same pos., so remove that pos. from list.
}
/*
Following segment initializes 'neighbor counts' for each cell. That is, the number of
mines that are present in the eight surrounding cells. IF the cell isn't a mine.
Note : It is done in the back-end array as that contains the numbers (MINE or 1 or 2 or 3 or 4 or 5 or 6 or 7 or 8)
*/
for(int x = 0; x < s; x++)
{
for(int y = 0; y < s; y++)
{
if(counts[x][y] != MINE)
{
int neighbor = 0;
if( x > 0 && y > 0 && counts[x-1][y-1] == MINE) //top left
{
neighbor++;
}
if( y > 0 && counts[x][y-1] == MINE) //left
{
neighbor++;
}
if( y < size - 1 && counts[x][y+1] == MINE) //right
{
neighbor++;
}
if( x < size - 1 && y > 0 && counts[x+1][y-1] == MINE) //bottom left
{
neighbor++;
}
if( x > 0 && counts[x-1][y] == MINE) //up
{
neighbor++;
}
if( x < size - 1 && counts[x+1][y] == MINE)//down
{
neighbor++;
}
if( x > 0 && y < size - 1 &&counts[x-1][y+1] == MINE) //top right
{
neighbor++;
}
if( x < size - 1 && y < size - 1 && counts[x+1][y+1] == MINE) //bottom right
{
neighbor++;
}
counts[x][y] = neighbor; //setting value
}
}
}
}
/**
* This function, called the domino effect, is an implementation of the idea that,
* when a cell with no surrounding mines is clicked, there's no point in user clicking
* all eight surrounding cells. Therefore, all surrounding
* cells' counts will be displayed in corresponding cells.
* The above is done recursively.
* @param toClear the ArrayList which is passed to the function with positions in array
* that are zero, and are subsequently clicked.
*/
public void dominoEffect(ArrayList<Integer> toClear){
if(toClear.isEmpty())
return; //base case
int x = toClear.get(0) / 100; //getting x pos.
int y = toClear.get(0) % 100; //getting y pos.
toClear.remove(0); //remove that element from array to prevent infinite recursion.
if(counts[x][y] == 0)
{ //similar to neighbor counts, each surrounding cell is filled
if( x > 0 && y > 0 && buttons[x-1][y-1].isEnabled()) //top left
{
buttons[x-1][y-1].setText(counts[x-1][y-1] + "");
buttons[x-1][y-1].setEnabled(false);
if(counts[x-1][y-1] == 0)
{
toClear.add((x-1)*100 + (y-1)); //to recursively implement, each surrounding cell is the new cell,
// the surrounding cells of which we shall check and so on.
}
}
if( y > 0 && buttons[x][y-1].isEnabled()) //left
{
buttons[x][y-1].setText(counts[x][y-1] + "");
buttons[x][y-1].setEnabled(false);
if(counts[x][y-1] == 0)
{
toClear.add(x*100 + (y-1));
}
}
if( y < size - 1 && buttons[x][y+1].isEnabled()) //right
{
buttons[x][y+1].setText(counts[x][y+1] + "");
buttons[x][y+1].setEnabled(false);
if(counts[x][y+1] == 0)
{
toClear.add(x*100 + (y+1));
}
}
if( x < size - 1 && y > 0 && buttons[x+1][y-1].isEnabled()) //bottom left
{
buttons[x+1][y-1].setText(counts[x+1][y-1] + "");
buttons[x+1][y-1].setEnabled(false);
if(counts[x+1][y-1] == 0)
{
toClear.add((x+1)*100 + (y-1));
}
}
if( x > 0 && buttons[x-1][y].isEnabled()) //up
{
buttons[x-1][y].setText(counts[x-1][y] + "");
buttons[x-1][y].setEnabled(false);
if(counts[x-1][y] == 0)
{
toClear.add((x-1)*100 + y);
}
}
if( x < size - 1 && buttons[x+1][y].isEnabled())//down
{
buttons[x+1][y].setText(counts[x+1][y] + "");
buttons[x+1][y].setEnabled(false);
if(counts[x+1][y] == 0)
{
toClear.add((x+1)*100 + y);
}
}
if( x > 0 && y < size - 1 && buttons[x-1][y+1].isEnabled()) //top right
{
buttons[x-1][y+1].setText(counts[x-1][y+1] + "");
buttons[x-1][y+1].setEnabled(false);
if(counts[x-1][y+1] == 0)
{
toClear.add((x-1)*100 + (y+1));
}
}
if( x < size - 1 && y < size - 1 && buttons[x+1][y+1].isEnabled()) //bottom right
{
buttons[x+1][y+1].setText(counts[x+1][y+1] + "");
buttons[x+1][y+1].setEnabled(false);
if(counts[x+1][y+1] == 0)
{
toClear.add((x+1)*100 + (y+1));
}
}
}
dominoEffect(toClear); //recursive call with list containing surrounding cells, for further check-and-clear of THEIR surr. cells.
}
//Main method.
public static void main(String[] args){
new Minesweeper(20); // Can be made of any size. (For now only squares)
}
@Override
public void mouseClicked(MouseEvent me) {
if (SwingUtilities.isRightMouseButton(me)){
// TODO : Handle flagging of mines.
}
}
@Override
public void mousePressed(MouseEvent me) {
// Do nothing
}
@Override
public void mouseReleased(MouseEvent me) {
// Do nothing
}
@Override
public void mouseEntered(MouseEvent me) {
// Do nothing
}
@Override
public void mouseExited(MouseEvent me) {
// Do nothing
}
}
Minesweeper is most popular game. I am going to present Minesweeper game for Desktop which is developed in java with the NetBeans IDE. It is same like as windows minesweeper game. Here i present one playing user interface:
In this User interface we can see it has a menu bar two black screen and one button with smile face.
- Left black screen shows the number of mine it has.
- Right black screen shows how much time spent by the player.
- Middle button restart the game. when player hit the mine means game over then the smile face become sad face.
This game has multiple level. Menu Bar contain the options levels are:
- Beginner
- Medium
- Expert
Player can also customize level by changing the Row column and how many mine game will have.
When the Game is Over it will notify the player. The count down timer will be stop and the smile face become sad face. Here is the Game Over interface.
Source Code Description:
This project name is Minesweeper. It contain two file Minesweeper.java and Main.java. Minesweeper.java have all gaming logic and graphical user interface. Main.java have main method in where Minesweeper Object is created. Source code is given bellow :
Minesweeper.java:
import java.awt.*; import java.awt.Dimension; import javax.swing.*; import java.awt.event.*; import java.util.*; public class Minesweeper extends JFrame implements ActionListener, ContainerListener { int fw, fh, blockr, blockc, var1, var2, num_of_mine, detectedmine = 0, savedlevel = 1, savedblockr, savedblockc, savednum_of_mine = 10; int[] r = {-1, -1, -1, 0, 1, 1, 1, 0}; int[] c = {-1, 0, 1, 1, 1, 0, -1, -1}; JButton[][] blocks; int[][] countmine; int[][] colour; ImageIcon[] ic = new ImageIcon[14]; JPanel panelb = new JPanel(); JPanel panelmt = new JPanel(); JTextField tf_mine, tf_time; JButton reset = new JButton(""); Random ranr = new Random(); Random ranc = new Random(); boolean check = true, starttime = false; Point framelocation; Stopwatch sw; MouseHendeler mh; Point p; Minesweeper() { super("Minesweeper"); setLocation(400, 300); setic(); setpanel(1, 0, 0, 0); setmanue(); sw = new Stopwatch(); reset.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent ae) { try { sw.stop(); setpanel(savedlevel, savedblockr, savedblockc, savednum_of_mine); } catch (Exception ex) { setpanel(savedlevel, savedblockr, savedblockc, savednum_of_mine); } reset(); } }); setDefaultCloseOperation(EXIT_ON_CLOSE); show(); } public void reset() { check = true; starttime = false; for (int i = 0; i < blockr; i++) { for (int j = 0; j < blockc; j++) { colour[i][j] = 'w'; } } } public void setpanel(int level, int setr, int setc, int setm) { if (level == 1) { fw = 200; fh = 300; blockr = 10; blockc = 10; num_of_mine = 10; } else if (level == 2) { fw = 320; fh = 416; blockr = 16; blockc = 16; num_of_mine = 70; } else if (level == 3) { fw = 400; fh = 520; blockr = 20; blockc = 20; num_of_mine = 150; } else if (level == 4) { fw = (20 * setc); fh = (24 * setr); blockr = setr; blockc = setc; num_of_mine = setm; } savedblockr = blockr; savedblockc = blockc; savednum_of_mine = num_of_mine; setSize(fw, fh); setResizable(false); detectedmine = num_of_mine; p = this.getLocation(); blocks = new JButton[blockr][blockc]; countmine = new int[blockr][blockc]; colour = new int[blockr][blockc]; mh = new MouseHendeler(); getContentPane().removeAll(); panelb.removeAll(); tf_mine = new JTextField("" + num_of_mine, 3); tf_mine.setEditable(false); tf_mine.setFont(new Font("DigtalFont.TTF", Font.BOLD, 25)); tf_mine.setBackground(Color.BLACK); tf_mine.setForeground(Color.RED); tf_mine.setBorder(BorderFactory.createLoweredBevelBorder()); tf_time = new JTextField("000", 3); tf_time.setEditable(false); tf_time.setFont(new Font("DigtalFont.TTF", Font.BOLD, 25)); tf_time.setBackground(Color.BLACK); tf_time.setForeground(Color.RED); tf_time.setBorder(BorderFactory.createLoweredBevelBorder()); reset.setIcon(ic[11]); reset.setBorder(BorderFactory.createLoweredBevelBorder()); panelmt.removeAll(); panelmt.setLayout(new BorderLayout()); panelmt.add(tf_mine, BorderLayout.WEST); panelmt.add(reset, BorderLayout.CENTER); panelmt.add(tf_time, BorderLayout.EAST); panelmt.setBorder(BorderFactory.createLoweredBevelBorder()); panelb.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10), BorderFactory.createLoweredBevelBorder())); panelb.setPreferredSize(new Dimension(fw, fh)); panelb.setLayout(new GridLayout(0, blockc)); panelb.addContainerListener(this); for (int i = 0; i < blockr; i++) { for (int j = 0; j < blockc; j++) { blocks[i][j] = new JButton(""); //blocks[i][j].addActionListener(this); blocks[i][j].addMouseListener(mh); panelb.add(blocks[i][j]); } } reset(); panelb.revalidate(); panelb.repaint(); //getcontentpane().setOpaque(true); getContentPane().setLayout(new BorderLayout()); getContentPane().addContainerListener(this); //getContentPane().revalidate(); getContentPane().repaint(); getContentPane().add(panelb, BorderLayout.CENTER); getContentPane().add(panelmt, BorderLayout.NORTH); setVisible(true); } public void setmanue() { JMenuBar bar = new JMenuBar(); JMenu game = new JMenu("GAME"); JMenuItem menuitem = new JMenuItem("new game"); final JCheckBoxMenuItem beginner = new JCheckBoxMenuItem("Begineer"); final JCheckBoxMenuItem intermediate = new JCheckBoxMenuItem("Intermediate"); final JCheckBoxMenuItem expart = new JCheckBoxMenuItem("Expart"); final JCheckBoxMenuItem custom = new JCheckBoxMenuItem("Custom"); final JMenuItem exit = new JMenuItem("Exit"); final JMenu help = new JMenu("Help"); final JMenuItem helpitem = new JMenuItem("Help"); ButtonGroup status = new ButtonGroup(); menuitem.addActionListener( new ActionListener() { public void actionPerformed(ActionEvent e) { //panelb.removeAll(); //reset(); setpanel(1, 0, 0, 0); //panelb.revalidate(); //panelb.repaint(); } }); beginner.addActionListener( new ActionListener() { public void actionPerformed(ActionEvent e) { panelb.removeAll(); reset(); setpanel(1, 0, 0, 0); panelb.revalidate(); panelb.repaint(); beginner.setSelected(true); savedlevel = 1; } }); intermediate.addActionListener( new ActionListener() { public void actionPerformed(ActionEvent e) { panelb.removeAll(); reset(); setpanel(2, 0, 0, 0); panelb.revalidate(); panelb.repaint(); intermediate.setSelected(true); savedlevel = 2; } }); expart.addActionListener( new ActionListener() { public void actionPerformed(ActionEvent e) { panelb.removeAll(); reset(); setpanel(3, 0, 0, 0); panelb.revalidate(); panelb.repaint(); expart.setSelected(true); savedlevel = 3; } }); custom.addActionListener( new ActionListener() { public void actionPerformed(ActionEvent e) { //panelb.removeAll(); Customizetion cus = new Customizetion(); reset(); panelb.revalidate(); panelb.repaint(); //Minesweeper ob=new Minesweeper(4); custom.setSelected(true); savedlevel = 4; } }); exit.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { System.exit(0); } }); helpitem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { JOptionPane.showMessageDialog(null, "instruction"); } }); setJMenuBar(bar); status.add(beginner); status.add(intermediate); status.add(expart); status.add(custom); game.add(menuitem); game.addSeparator(); game.add(beginner); game.add(intermediate); game.add(expart); game.add(custom); game.addSeparator(); game.add(exit); help.add(helpitem); bar.add(game); bar.add(help); } public void componentAdded(ContainerEvent ce) { } public void componentRemoved(ContainerEvent ce) { } public void actionPerformed(ActionEvent ae) { } class MouseHendeler extends MouseAdapter { public void mouseClicked(MouseEvent me) { if (check == true) { for (int i = 0; i < blockr; i++) { for (int j = 0; j < blockc; j++) { if (me.getSource() == blocks[i][j]) { var1 = i; var2 = j; i = blockr; break; } } } setmine(); calculation(); check = false; } showvalue(me); winner(); if (starttime == false) { sw.Start(); starttime = true; } } } public void winner() { int q = 0; for (int k = 0; k < blockr; k++) { for (int l = 0; l < blockc; l++) { if (colour[k][l] == 'w') { q = 1; } } } if (q == 0) { //panelb.hide(); for (int k = 0; k < blockr; k++) { for (int l = 0; l < blockc; l++) { blocks[k][l].removeMouseListener(mh); } } sw.stop(); JOptionPane.showMessageDialog(this, "u R a lover"); } } public void showvalue(MouseEvent e) { for (int i = 0; i < blockr; i++) { for (int j = 0; j < blockc; j++) { if (e.getSource() == blocks[i][j]) { if (e.isMetaDown() == false) { if (blocks[i][j].getIcon() == ic[10]) { if (detectedmine < num_of_mine) { detectedmine++; } tf_mine.setText("" + detectedmine); } if (countmine[i][j] == -1) { for (int k = 0; k < blockr; k++) { for (int l = 0; l < blockc; l++) { if (countmine[k][l] == -1) { //blocks[k][l].setText("X"); blocks[k][l].setIcon(ic[9]); //blocks[k][l].setBackground(Color.BLUE); //blocks[k][l].setFont(new Font("",Font.CENTER_BASELINE,8)); blocks[k][l].removeMouseListener(mh); } blocks[k][l].removeMouseListener(mh); } } sw.stop(); reset.setIcon(ic[12]); JOptionPane.showMessageDialog(null, "sorry u R loser"); } else if (countmine[i][j] == 0) { dfs(i, j); } else { blocks[i][j].setIcon(ic[countmine[i][j]]); //blocks[i][j].setText(""+countmine[i][j]); //blocks[i][j].setBackground(Color.pink); //blocks[i][j].setFont(new Font("",Font.PLAIN,8)); colour[i][j] = 'b'; //blocks[i][j].setBackground(Color.pink); break; } } else { if (detectedmine != 0) { if (blocks[i][j].getIcon() == null) { detectedmine--; blocks[i][j].setIcon(ic[10]); } tf_mine.setText("" + detectedmine); } } } } } } public void calculation() { int row, column; for (int i = 0; i < blockr; i++) { for (int j = 0; j < blockc; j++) { int value = 0; int R, C; row = i; column = j; if (countmine[row][column] != -1) { for (int k = 0; k < 8; k++) { R = row + r[k]; C = column + c[k]; if (R >= 0 && C >= 0 && R < blockr && C < blockc) { if (countmine[R][C] == -1) { value++; } } } countmine[row][column] = value; } } } } public void dfs(int row, int col) { int R, C; colour[row][col] = 'b'; blocks[row][col].setBackground(Color.GRAY); blocks[row][col].setIcon(ic[countmine[row][col]]); //blocks[row][col].setText(""); for (int i = 0; i < 8; i++) { R = row + r[i]; C = col + c[i]; if (R >= 0 && R < blockr && C >= 0 && C < blockc && colour[R][C] == 'w') { if (countmine[R][C] == 0) { dfs(R, C); } else { blocks[R][C].setIcon(ic[countmine[R][C]]); //blocks[R][C].setText(""+countmine[R][C]); //blocks[R][C].setBackground(Color.pink); //blocks[R][C].setFont(new Font("",Font.BOLD,)); colour[R][C] = 'b'; } } } } public void setmine() { int row = 0, col = 0; Boolean[][] flag = new Boolean[blockr][blockc]; for (int i = 0; i < blockr; i++) { for (int j = 0; j < blockc; j++) { flag[i][j] = true; countmine[i][j] = 0; } } flag[var1][var2] = false; colour[var1][var2] = 'b'; for (int i = 0; i < num_of_mine; i++) { row = ranr.nextInt(blockr); col = ranc.nextInt(blockc); if (flag[row][col] == true) { countmine[row][col] = -1; colour[row][col] = 'b'; flag[row][col] = false; } else { i--; } } } public void setic() { String name; for (int i = 0; i <= 8; i++) { name = i + ".gif"; ic[i] = new ImageIcon(name); } ic[9] = new ImageIcon("mine.gif"); ic[10] = new ImageIcon("flag.gif"); ic[11] = new ImageIcon("new game.gif"); ic[12] = new ImageIcon("crape.gif"); } public class Stopwatch extends JFrame implements Runnable { long startTime; //final static java.text.SimpleDateFormat timerFormat = new java.text.SimpleDateFormat("mm : ss :SSS"); //final JButton startStopButton= new JButton("Start/stop"); Thread updater; boolean isRunning = false; long a = 0; Runnable displayUpdater = new Runnable() { public void run() { displayElapsedTime(a); a++; } }; public void stop() { long elapsed = a; isRunning = false; try { updater.join(); } catch (InterruptedException ie) { } displayElapsedTime(elapsed); a = 0; } private void displayElapsedTime(long elapsedTime) { if (elapsedTime >= 0 && elapsedTime < 9) { tf_time.setText("00" + elapsedTime); } else if (elapsedTime > 9 && elapsedTime < 99) { tf_time.setText("0" + elapsedTime); } else if (elapsedTime > 99 && elapsedTime < 999) { tf_time.setText("" + elapsedTime); } } public void run() { try { while (isRunning) { SwingUtilities.invokeAndWait(displayUpdater); Thread.sleep(1000); } } catch (java.lang.reflect.InvocationTargetException ite) { ite.printStackTrace(System.err); } catch (InterruptedException ie) { } } public void Start() { startTime = System.currentTimeMillis(); isRunning = true; updater = new Thread(this); updater.start(); } } class Customizetion extends JFrame implements ActionListener { JTextField t1, t2, t3; JLabel lb1, lb2, lb3; JButton b1, b2; int cr, cc, cm, actionc = 0; Customizetion() { super("CUSTOMIZETION"); setSize(180, 200); setResizable(false); setLocation(p); t1 = new JTextField(); t2 = new JTextField(); t3 = new JTextField(); b1 = new JButton("OK"); b2 = new JButton("Cencel"); b1.addActionListener(this); b2.addActionListener(this); lb1 = new JLabel("Row"); lb2 = new JLabel("Column"); lb3 = new JLabel("mine"); getContentPane().setLayout(new GridLayout(0, 2)); getContentPane().add(lb1); getContentPane().add(t1); getContentPane().add(lb2); getContentPane().add(t2); getContentPane().add(lb3); getContentPane().add(t3); getContentPane().add(b1); getContentPane().add(b2); show(); } public void actionPerformed(ActionEvent e) { if (e.getSource() == b1) { try { cr = Integer.parseInt(t1.getText()); cc = Integer.parseInt(t2.getText()); cm = Integer.parseInt(t3.getText()); //Minesweeper ms=new Minesweeper(); setpanel(4, row(), column(), mine()); dispose(); } catch (Exception any) { JOptionPane.showMessageDialog(this, "Wrong"); t1.setText(""); t2.setText(""); t3.setText(""); } //Show_rcm(); } if (e.getSource() == b2) { dispose(); } } public int row() { if (cr > 30) { return 30; } else if (cr < 10) { return 10; } else { return cr; } } public int column() { if (cc > 30) { return 30; } else if (cc < 10) { return 10; } else { return cc; } } public int mine() { if (cm > ((row() - 1) * (column() - 1))) { return ((row() - 1) * (column() - 1)); } else if (cm < 10) { return 10; } else { return cm; } } } }
How to run:
Net-Beans IDE is required to run this project then open this project in Net-Beans IDE and run this project.
source code of this project is here
Now enjoy it.
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
2
branches
0
tags
Code
-
Use Git or checkout with SVN using the web URL.
-
Open with GitHub Desktop
-
Download ZIP
Latest commit
Files
Permalink
Failed to load latest commit information.
Type
Name
Latest commit message
Commit time
minesweeper
Была реализована следующая задача:
Игра “Сапер”
Написать аналог игры “Сапер” (“Minesweeper”) из состава стандартных программ для
Windows OS. Архитектура программы должна быть основана на паттерне MVC (ModeView-Controller).
Требования
Поведение должно быть такое же, как у оригинальной
игры:
Игровое поле состоит из клеток. Вначале все клетки
закрыты. В каких-то клетках спрятаны мины.
- При нажатии левой кнопкой мыши на закрытую
клетку она открывается.
a. Если там мина, конец игры (проигрыш).
b. Если мины нет, но вокруг клетки есть
мины, отображается цифра,
обозначающая количество мин вокруг
c. Если мин вокруг нет, отображается
пустая открытая клетка и открываются
все клетки вокруг нее. Если вокруг нее
есть другие пустые клетки, открываем
все соседние для каждой пустой и так
далее. - При нажатии правой кнопкой мыши на закрытую клетку, она помечается флажком. Если на
ней уже есть флажок, он убирается. - При нажатии левой кнопкой мыши на клетку с флажком ничего не происходит.
- Если открыты все клетки без мин, конец игры (выигрыш)
- В интерфейсе отображается количество оставшихся мин (рассчитывается как реальное
количество мин минус количество поставленных флажков) - При нажатии колесиком на открытую клетку с цифрой открываются все клетки вокруг нее,
при условии, что вокруг этой клетки стоит столько флажков, какая цифра на клетке.
Алгоритм открытия соседних клеток такой же, как в пункте 1. Вместо колесика такой же
эффект должно производить нажатие на левую и правую кнопку одновременно – в
качестве усложнения, если останется время. - Размер поля и количество мин можно изменить. По умолчанию поле размером 9×9
количество мин 10. - Игра должна поддерживать таблицу рекордов, которая сохраняется между запусками
программы. Для этого необходимо запускать секундомер при первом нажатии на поле. - Пользователю должно быть доступно меню с командами: Exit, About, New Game, High
Scores.
Реализация графического UI
- Для реализации пользовательского интерфейса использовать библиотеку Swing
(javax.swing.*). - Мины и флажки отображать с помощью картинок.
- Для расположения элементов на игровой панели рекомендуется использовать класс
GridBagLayout. - Для расположения ячеек поля рекомендуется использовать класс GridLayout.
Анализ реализации мини-игры по разминированию с помощью Java
Используйте анализ документов кода для разработки программ
Я изучаю Java в течение полумесяца и хочу использовать идеи ООП, которые я усвоил за этот период самопроверки, поэтому я написал исчерпывающий код, который больше подходит для разминирования.
Инженерная структура
класс сущности хранилища компонентов
основной класс игры
тестовый тестовый класс
Класс сетки, содержимое и тип атрибута хранятся в пакете bean-компонента.
package bean;
public class Grid {
char content;
boolean type;
public char getContent() {
return content;
}
public void setContent(char content) {
this.content = content;
}
public boolean isType() {
return type;
}
public void setType(boolean type) {
this.type = type;
}
}
Дизайн основного класса
1. Определение соответствующего метода создания объекта.
2. Показать определение метода минного поля
3. Определение метода Брея
инструмент для случайных чисел
Random r=new Random();
// Получаем целое случайное число
int x = r.nextInt (parameter); // параметр указывает диапазон случайного числа
4.8 определения объектов направления
Класс точки: класс координатного объекта xy, предоставленный в java.
5. Как установить количество мин
Получите добавление количества мин, пройдя все объекты сетки
6. Ступай на гром
Рекурсивный вызов: реализовать текущий метод для вызова собственной операции в методе класса.
package core;
import java.awt.Point;
import java.util.Random;
import bean.Grid;
public class Core {
// Создание минного поля
Grid grid[][] = new Grid[9][9];
// Определяем состояние игры
public boolean state = true;
public void creatMineGrid() {
for (int i = 0; i < grid.length; i++) {
for (int j = 0; j < grid[i].length; j++) {
grid[i][j] = new Grid();
grid[i][j].setContent(' ');
grid[i][j].setType(false);
}
}
}
// Создаем десять случайных мин
public void creatMine() {
Random r = new Random();
int num = 10;
do {
int x = r.nextInt(9);
int y = r.nextInt(9);
if (grid[x][y].getContent() != '*') {
grid[x][y].setContent('*');
num--;
}
} while (num > 0);
}
// Как получить координаты
public Point[] getPoint(int x, int y) {
Point point[] = new Point[8];
// вверх
point[0] = new Point(x, y - 1);
// вниз
point[1] = new Point(x, y + 1);
// осталось
point[2] = new Point(x - 1, y);
// право
point[3] = new Point(x + 1, y);
// левая нижняя
point[4] = new Point(x - 1, y + 1);
// Нижний правый
point[5] = new Point(x + 1, y + 1);
// верхний левый
point[6] = new Point(x - 1, y - 1);
// в правом верхнем углу
point[7] = new Point(x + 1, y - 1);
return point;
}
// Создаем числа вокруг минного поля
public void creatNum() {
for (int i = 0; i < grid.length; i++) {
for (int j = 0; j < grid[i].length; j++) {
int num = 0;
if (grid[i][j].getContent() != '*') {
Point[] p = this.getPoint(i, j);
for (int k = 0; k < p.length; k++) {
Point point = p[k];
if (point.getX() >= 0 && point.getX() < 4 && point.getY() >= 0 && point.getY() < 4) {
if (grid[point.x][point.y].getContent() == '*') {
num++;
}
}
}
}
if (num > 0) {
grid[i][j].setContent((char) (48 + num));
}
}
}
}
// Определяем способ наступления на гром
public void stampMine(int x, int y) {
// Включаем текущий статус сетки как открытый
grid[x][y].setType(true);
// Ступаем на гром и заканчиваем
if (grid[x][y].getContent() != '*') {
// Определяем, является ли текущий объект сетки числом, если это число, ничего не делаем
if (grid[x][y].getContent() == ' ') {
// Получаем координатный объект сетки в восьми направлениях вокруг текущей сетки
Point p[] = this.getPoint(x, y);
// Перебираем 8 направлений, чтобы получить соответствующий объект сетки
for (int i = 0; i < p.length; i++) {
// Получаем объект направления отдельно
Point point = p[i];
// Определяем, находится ли он за пределами
if (point.getX() >= 0 && point.getX() < 4 && point.getY() >= 0 && point.getY() < 4) {
/**
* Определить, является ли содержимое объекта сетки, соответствующего текущему объекту координат, пробелом. Если это пробел, реализовать рекурсивный вызов, если это число, включить текущую сетку
*/
if (grid[point.x][point.y].getContent() == ' ' && !grid[point.x][point.y].isType()) {
// Игры, в которых текущая сетка пуста
this.stampMine(point.x, point.y);
} else {
grid[point.x][point.y].setType(true);
}
}
}
}
} else {
System.out.println("игра закончена....");
// Модифицируем текущее состояние игры для завершения
state = false;
}
}
public int detection() {
int a = 0;
for (int i = 0; i < grid.length; i++) {
for (int j = 0; j < grid[i].length; j++) {
if (!grid[i][j].isType()) {
a++;
}
}
}
return a;
}
// Показать метод минного поля
public void show() {
for (int i = 0; i < grid.length; i++) {
for (int j = 0; j < grid[i].length; j++) {
if (grid[i][j].isType()) {
System.out.print(grid[i][j].getContent() + " ");
} else {
System.out.print("■ ");
}
}
System.out.println();
}
}
Тестовый класс
package test;
import java.util.Scanner;
import core.Core;
public class Test {
public static void main(String[] args) {
Core c = new Core();
c.creatMineGrid();
c.creatMine();
c.creatNum();
while (c.state) {
System.out.println(«Пожалуйста, введите количество строк»);
Scanner s1 = new Scanner(System.in);
int x = s1.nextInt() - 1;
System.out.println(«Пожалуйста, введите количество столбцов»);
Scanner s2 = new Scanner(System.in);
int y = s2.nextInt() - 1;
c.stampMine(x, y);
c.show();
if (c.detection() == 2) {
System.out.println(«Успех игры»);
break;
}
}
}
}