Як написати свого сапера на Java за 15 хвилин?


Дізнайтесь більше про нові кар'єрні можливості в EchoUA. Цікаві проекти, ринкова оплата, гарний колектив. Надсилайте резюме та приєднуйтеся до нас.

Нам знадобляться:

  • 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

Київ, Харків, Одеса, Дніпро, Запоріжжя, Кривий Ріг, Вінниця, Херсон, Черкаси, Житомир, Хмельницький, Чернівці, Рівне, Івано-Франківськ, Кременчук, Тернопіль, Луцьк, Ужгород, Кам'янець-Подільський, Стрий - за статистикою саме з цих міст програмісти найбільше переїжджають працювати до Львова. А Ви розглядаєте relocate?


Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *