Pull to refresh

Сапер на GWT

Reading time 8 min
Views 7.2K
Недавно прочитал топик пользователя nsinreal, который предложил реализацию сапера на батниках. Так как я совсем недавно начал знакомство с GWT и вообще с явой, решил написать своего сапера с блэкджеком и прочим :) Попутно, расскажу про реализацию и проблемы, с которыми столкнулся.

Итак, yaminesweeper.appspot.com. Сделал на выходных, так что не бейте за простой вид и некоторые баги, о который напишу ниже. Исходники вы можете найти здесь: http://github.com/wargoth/yaminesweeper.

Основные возможности:
  • возможность отмечать флажками мины (правой кнопкой мыши)
  • возможность быстро открывать поля (средняя кнопка мыши)
  • изменять параметры поля (ширина, высота, кол-во мин)
  • сохранять время решения поля и смотреть общий рейтинг пользователей (необходимо залогиниться через аккаунт гугла).

Из багов отмечу:
  • общая кривость в ИЕ (решается)
  • кривость в опере (проблемы с переопределением поведения при нажатии средней и правой клавиш мыши)

Планируется сделать:
  • быстрое открывание полей через одновременное нажатие левой и правой клавиш мыши (сейчас только средней клавишей)
  • оптимизировать алгоритмы (сейчас все-таки не так быстро работает, как хотелось бы)
  • улучшить внешний вид


Клиентская часть написана на GWT, серверная — для подсчета статистки, — на AppEngine. Меня очень вдохновили эти две технологии, как, в прочем, и скорость разработки на них. Ниже я остановлюсь на основных моментах в планировании архитектуры и реализации. С удовольствием приму критику по части кода. Я не пытался создать супер-пупер красивый интерфейс, да и не в этом суть. По части кода мне интересно мнение опытных программистов, т.к. я, повторюсь, совсем недавно начал программировать на яве.

О преимуществах GWT писалось много раз, но я отмечу еще раз то, что для себя открыл:
  • контроль типов. Можно забыть о проблемах с типами в яваскрипте, которые часто возникают при разработке на яваскрипте. И вообще, после динамически типизированных языков (я PHP программист), я в восторге от строгой типизации явы и возможностей, которые она предоставляет
  • кросбраузерность. Конечно, и тут есть некоторые нестыковки, с которыми я столкнулся, но о многих вещах можно не задумываться. GWT скомпилирует 6 скриптов, который будет загружаться только для своего типа браузера — оптимизированные и включающий только тот код, который будет исполнен
  • отладка кода — в любимом IDE

Ну ладно, хватит воды, перейду к самому главному…

Алгоритмы


Чтобы реализовать игру, нужно выписать для себя правила игры, а так же представить как будет все работать.

Минное поле строим случайным расположением мин. Что же должно происходить когда игрок кликает на поле?
  1. Если это поле — мина, то подрываемся :)
  2. Если нет, то считаем сколько мин вокруг
  3. Если вокруг имеются мины, нужно отобразить их количество
  4. Если вокруг нет мин, то это поле считается пустым и надо открыть все поля, которые находятся вокруг (рекурсивно проходим по пунктам 1-4)

Игрок открывает несколько полей, расставляет флажки, обозначающие места, куда лучше не наступать. Нужно предоставить ему возможность быстро раскрыть поля вокруг поля, которое правильно отображает количество флажков вокруг. Это опять-таки реализуется круговым обходом полей и открытием их, если они не помечены флажком.

Если количество открытых полей равно общему количество полей минус количество мин, игрок считается счастливчиком и выигрывает.

Архитектура



Приложение состоит из 4-х основных частей:
  • игровое поле Minefield
  • включающее коллекцию Collection
  • заминированных полей Field
  • а так же алгоритмы обхода коллекции — CollectionIterator (обход всего игрового поля) и AroundFieldIterator (обход вокруг заминированного поля)


Остальные классы вспомогательные — таймер, диалог опций и прочее.

Т.к. с именованием на русском у меня проблема (легко запутаться в «игровых полях» и «заминированных полях»), то лучше буду использовать сразу имена классов — Minefield и Field.

Minefield предоставляет виджет Grid, в котором будут располагаться все поля Field. Он инкапсулирует логику, связанную с инициализацией игры.

Collection — содержит коллекцию объектов. Логика обхода коллекции инкапсулирована в итераторы CollectionIterator и AroundFieldIterator.

Одним из требований к приложению было легкость изменения алгоритмов, чтобы их можно было независимо от остальной части приложения заменить, оптимизировать и переработать. Для этого и были созданы итераторы CollectionIterator и AroundFieldIterator. Первый проходит игровое поле от начала до конца, возвращая соответствующий позиции Field, а второй обходит вокруг этого Field и возвращает все соседние поля.

Field инкапсулирует в себе логику поведения заминированного поля. Оно имеет состояния, такие как «отмеченный флагом» «открытый», реагирует на события пользователя — открывается, взрывается или открывает соседние поля. Так же оно предоставляет соответствующие состоянию виджеты. Для закрытого — кнопка, для открытого — лейбл с количеством мин и прочее.

Реализация



Пробегусь по основным моментам в реализации игры для того, чтобы продемонстрировать, на сколько просто программировать в GWT.

Инициализация Minefield начинается со случайного распределения на нем мин:

  private void populateMines() {
    for (int i = 0; i < minesNum; i++) {
      int col, row;
      do {
        col = (int) Math.round(Math.random() * (double) (cols - 1));
        row = (int) Math.round(Math.random() * (double) (rows - 1));
      } while (collection.get(col, row) != null);
      collection.set(col, row, new Field(this, col, row, Field.MINE));
    }
  }


* This source code was highlighted with Source Code Highlighter.


Затем строим само поле, заполняя его виджетами, которые предоставляют поля Field:

  private void initWidget() {
    grid = getWidget();
    grid.clear();
    grid.resize(rows, cols);

    for (CollectionIterator iterator = collection.iterator(); iterator
        .hasNext();) {
      Field field = iterator.next();
      grid.setWidget(iterator.getRow(), iterator.getCol(), field
          .getWidget());
    }
  }


* This source code was highlighted with Source Code Highlighter.


Когда пользователь нажимает на кнопку Field, вызывается событие Field.open(). Если это поле — мина, то «взрываемся». Для этого делегируем это событие родительскому Minefield, чтобы он прошелся по всем минам и их «взорвал». Если же поле — не мина, то рассчитываем количество мин вокруг:

  private void calculateMinesNum() {
    for (AroundFieldIterator iterator = parent.getCollection()
        .aroundFieldIterator(col, row); iterator.hasNext();) {
      Field field = iterator.next();

      if (field.isMine()) {
        incrementMinesNum();
      }
    }
  }


* This source code was highlighted with Source Code Highlighter.


Таким образом, пользуясь одними и теми же итераторами коллекции, можно легко реализовать всю логику игры.

Грабли



Хотелось бы рассказать о тех граблях, на которые я наступил. Может, это кому-нибудь поможет. Некоторые проблемы я до сих пор не решил. Так что я буду очень благодарен, если кто-нибудь подскажет как они решаются.

Переопределение правого клика мышью



Не во всех браузерах работает одинаково. Причем, поведения в режиме отладки (т.н. hosted mode ) и в обычном браузере (web mode), тоже различаются. Следующий код навешивает событие на нажатие правой кнопкой на кнопку:

  private Widget getButtonWidget() {
    button = new Button();
    button.addMouseDownHandler(new MouseDownHandler() {
      @Override
      public void onMouseDown(MouseDownEvent event) {
        switch (event.getNativeButton()) {
        case NativeEvent.BUTTON_RIGHT:
          toggleFlag();
          break;
        }
      }
    });

    return button;
  }


* This source code was highlighted with Source Code Highlighter.


В режиме отладки все работает замечательно, но в обычных браузерах выпадает контекстное меню, от которого нужно избавиться, переопределив метод onBrowserEvent либо у самой кнопки, либо у родительского виджета, что я и сделал в классе Minefield:

  public Grid getWidget() {
    if (grid == null) {
      grid = new Grid() {
        @Override
        public void onBrowserEvent(Event event) {
          event.stopPropagation();
          event.preventDefault();
        }
      };
      grid.sinkEvents(Event.ONCONTEXTMENU | Event.ONMOUSEDOWN | Event.ONDBLCLICK);
      grid.addStyleName("grid");
    }
    return grid;
  }


* This source code was highlighted with Source Code Highlighter.


Метод sinkEvents говорит, какие события надо перехватывать, а event.stopPropagation() и event.preventDefault() — запрещают им дальнейшее распространение и исполнение. В теории.

На практике, это хорошо работает в Chrome, FF, а в опере — нет. Мало того, в опере по-умолчанию вообще выключена возможность контроля над правым щелчком мыши, а поведение при нажатие средней клавиши над текстом — вообще для меня загадочно. Над этим я еще буду работать.

Перехват события средней клавишей реализуется аналогично правой.

Повторная инициализация виджетов



Может быть это было только для меня открытием, но когда я реализовывал повторную инициализацию приложения для того, чтобы реализовать «новую игру» или смену опций, для меня было сюрпризом, почему следующий код ведет себя не так, как я думал:

  private void initWidget() {
    grid = new Grid();
    grid.resize(rows, cols);
    ...
  }


* This source code was highlighted with Source Code Highlighter.


Я предполагал, что при повторном вызове метода initWidget, старое поле должно было быть уничтожено, а новое с новыми параметрами — создано. Для типичного приложения это было бы справедливо, но не надо забывать о том, что это всего лишь «отражение» DOM-объекта в яве (точнее, наоборот). Т.е. действительно был создан новый объект в яве, но старый объект из DOM'а не был удален или замещен. И на нем даже остаются прикрепленными старые события. Поэтому хорошей практикой является инициализация всех виджетов либо в конструкторе класса, либо отдельными методами, например так:

public class Minefield {
  private Grid grid = new Grid();
  ...
}


* This source code was highlighted with Source Code Highlighter.


Или так:

public class Minefield {
  private Grid grid;
  ...
  public Grid getWidget() {
    if (grid == null) {
      grid = new Grid();
      ....
    }
    return grid;
  }
}


* This source code was highlighted with Source Code Highlighter.


Теперь при переинициализации вы будете использовать одни и те же объекты, «отраженные» в объекты DOM'а.

Статья получилась достаточно обширная, а идей о том, что рассказать — много. Так что когда будет время, я могу продолжить про кодинг в GWT. А когда по-лучше разберусь с AppEngine, то и о нем. С удовольствием выслушаю замечания и предложения.

Спасибо за внимание.
Tags:
Hubs:
+33
Comments 43
Comments Comments 43

Articles