Недавно прочитал топик пользователя nsinreal, который предложил реализацию сапера на батниках. Так как я совсем недавно начал знакомство с GWT и вообще с явой, решил написать своего сапера с блэкджеком и прочим :) Попутно, расскажу про реализацию и проблемы, с которыми столкнулся.
Итак, yaminesweeper.appspot.com. Сделал на выходных, так что не бейте за простой вид и некоторые баги, о который напишу ниже. Исходники вы можете найти здесь: http://github.com/wargoth/yaminesweeper.
Основные возможности:
Из багов отмечу:
Планируется сделать:
Клиентская часть написана на GWT, серверная — для подсчета статистки, — на AppEngine. Меня очень вдохновили эти две технологии, как, в прочем, и скорость разработки на них. Ниже я остановлюсь на основных моментах в планировании архитектуры и реализации. С удовольствием приму критику по части кода. Я не пытался создать супер-пупер красивый интерфейс, да и не в этом суть. По части кода мне интересно мнение опытных программистов, т.к. я, повторюсь, совсем недавно начал программировать на яве.
О преимуществах GWT писалось много раз, но я отмечу еще раз то, что для себя открыл:
Ну ладно, хватит воды, перейду к самому главному…
Чтобы реализовать игру, нужно выписать для себя правила игры, а так же представить как будет все работать.
Минное поле строим случайным расположением мин. Что же должно происходить когда игрок кликает на поле?
Игрок открывает несколько полей, расставляет флажки, обозначающие места, куда лучше не наступать. Нужно предоставить ему возможность быстро раскрыть поля вокруг поля, которое правильно отображает количество флажков вокруг. Это опять-таки реализуется круговым обходом полей и открытием их, если они не помечены флажком.
Если количество открытых полей равно общему количество полей минус количество мин, игрок считается счастливчиком и выигрывает.
Приложение состоит из 4-х основных частей:
Остальные классы вспомогательные — таймер, диалог опций и прочее.
Т.к. с именованием на русском у меня проблема (легко запутаться в «игровых полях» и «заминированных полях»), то лучше буду использовать сразу имена классов — Minefield и Field.
Minefield предоставляет виджет Grid, в котором будут располагаться все поля Field. Он инкапсулирует логику, связанную с инициализацией игры.
Collection — содержит коллекцию объектов. Логика обхода коллекции инкапсулирована в итераторы CollectionIterator и AroundFieldIterator.
Одним из требований к приложению было легкость изменения алгоритмов, чтобы их можно было независимо от остальной части приложения заменить, оптимизировать и переработать. Для этого и были созданы итераторы CollectionIterator и AroundFieldIterator. Первый проходит игровое поле от начала до конца, возвращая соответствующий позиции Field, а второй обходит вокруг этого Field и возвращает все соседние поля.
Field инкапсулирует в себе логику поведения заминированного поля. Оно имеет состояния, такие как «отмеченный флагом» «открытый», реагирует на события пользователя — открывается, взрывается или открывает соседние поля. Так же оно предоставляет соответствующие состоянию виджеты. Для закрытого — кнопка, для открытого — лейбл с количеством мин и прочее.
Пробегусь по основным моментам в реализации игры для того, чтобы продемонстрировать, на сколько просто программировать в GWT.
Инициализация Minefield начинается со случайного распределения на нем мин:
Затем строим само поле, заполняя его виджетами, которые предоставляют поля Field:
Когда пользователь нажимает на кнопку Field, вызывается событие Field.open(). Если это поле — мина, то «взрываемся». Для этого делегируем это событие родительскому Minefield, чтобы он прошелся по всем минам и их «взорвал». Если же поле — не мина, то рассчитываем количество мин вокруг:
Таким образом, пользуясь одними и теми же итераторами коллекции, можно легко реализовать всю логику игры.
Хотелось бы рассказать о тех граблях, на которые я наступил. Может, это кому-нибудь поможет. Некоторые проблемы я до сих пор не решил. Так что я буду очень благодарен, если кто-нибудь подскажет как они решаются.
Не во всех браузерах работает одинаково. Причем, поведения в режиме отладки (т.н. hosted mode ) и в обычном браузере (web mode), тоже различаются. Следующий код навешивает событие на нажатие правой кнопкой на кнопку:
В режиме отладки все работает замечательно, но в обычных браузерах выпадает контекстное меню, от которого нужно избавиться, переопределив метод onBrowserEvent либо у самой кнопки, либо у родительского виджета, что я и сделал в классе Minefield:
Метод sinkEvents говорит, какие события надо перехватывать, а event.stopPropagation() и event.preventDefault() — запрещают им дальнейшее распространение и исполнение. В теории.
На практике, это хорошо работает в Chrome, FF, а в опере — нет. Мало того, в опере по-умолчанию вообще выключена возможность контроля над правым щелчком мыши, а поведение при нажатие средней клавиши над текстом — вообще для меня загадочно. Над этим я еще буду работать.
Перехват события средней клавишей реализуется аналогично правой.
Может быть это было только для меня открытием, но когда я реализовывал повторную инициализацию приложения для того, чтобы реализовать «новую игру» или смену опций, для меня было сюрпризом, почему следующий код ведет себя не так, как я думал:
Я предполагал, что при повторном вызове метода initWidget, старое поле должно было быть уничтожено, а новое с новыми параметрами — создано. Для типичного приложения это было бы справедливо, но не надо забывать о том, что это всего лишь «отражение» DOM-объекта в яве (точнее, наоборот). Т.е. действительно был создан новый объект в яве, но старый объект из DOM'а не был удален или замещен. И на нем даже остаются прикрепленными старые события. Поэтому хорошей практикой является инициализация всех виджетов либо в конструкторе класса, либо отдельными методами, например так:
Или так:
Теперь при переинициализации вы будете использовать одни и те же объекты, «отраженные» в объекты DOM'а.
Статья получилась достаточно обширная, а идей о том, что рассказать — много. Так что когда будет время, я могу продолжить про кодинг в GWT. А когда по-лучше разберусь с AppEngine, то и о нем. С удовольствием выслушаю замечания и предложения.
Спасибо за внимание.
Итак, 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, то и о нем. С удовольствием выслушаю замечания и предложения.
Спасибо за внимание.