Итак, yaminesweeper.appspot.com. Сделал на выходных, так что не бейте за простой вид и некоторые баги, о который напишу ниже. Исходники вы можете найти здесь: http://github.com/wargoth/yaminesweeper.
Основные возможности:
- возможность отмечать флажками мины (правой кнопкой мыши)
- возможность быстро открывать поля (средняя кнопка мыши)
- изменять параметры поля (ширина, высота, кол-во мин)
- сохранять время решения поля и смотреть общий рейтинг пользователей (необходимо залогиниться через аккаунт гугла).
Из багов отмечу:
- общая кривость в ИЕ (решается)
- кривость в опере (проблемы с переопределением поведения при нажатии средней и правой клавиш мыши)
Планируется сделать:
- быстрое открывание полей через одновременное нажатие левой и правой клавиш мыши (сейчас только средней клавишей)
- оптимизировать алгоритмы (сейчас все-таки не так быстро работает, как хотелось бы)
- улучшить внешний вид
Клиентская часть написана на GWT, серверная — для подсчета статистки, — на AppEngine. Меня очень вдохновили эти две технологии, как, в прочем, и скорость разработки на них. Ниже я остановлюсь на основных моментах в планировании архитектуры и реализации. С удовольствием приму критику по части кода. Я не пытался создать супер-пупер красивый интерфейс, да и не в этом суть. По части кода мне интересно мнение опытных программистов, т.к. я, повторюсь, совсем недавно начал программировать на яве.
О преимуществах GWT писалось много раз, но я отмечу еще раз то, что для себя открыл:
- контроль типов. Можно забыть о проблемах с типами в яваскрипте, которые часто возникают при разработке на яваскрипте. И вообще, после динамически типизированных языков (я PHP программист), я в восторге от строгой типизации явы и возможностей, которые она предоставляет
- кросбраузерность. Конечно, и тут есть некоторые нестыковки, с которыми я столкнулся, но о многих вещах можно не задумываться. GWT скомпилирует 6 скриптов, который будет загружаться только для своего типа браузера — оптимизированные и включающий только тот код, который будет исполнен
- отладка кода — в любимом IDE
Ну ладно, хватит воды, перейду к самому главному…
Алгоритмы
Чтобы реализовать игру, нужно выписать для себя правила игры, а так же представить как будет все работать.
Минное поле строим случайным расположением мин. Что же должно происходить когда игрок кликает на поле?
- Если это поле — мина, то подрываемся :)
- Если нет, то считаем сколько мин вокруг
- Если вокруг имеются мины, нужно отобразить их количество
- Если вокруг нет мин, то это поле считается пустым и надо открыть все поля, которые находятся вокруг (рекурсивно проходим по пунктам 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, то и о нем. С удовольствием выслушаю замечания и предложения.
Спасибо за внимание.

