Сапер на GWT

    Недавно прочитал топик пользователя 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, то и о нем. С удовольствием выслушаю замечания и предложения.

    Спасибо за внимание.
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 43

      +1
      Флажки бы чуть более выразительные, а то красная палочка… ) Но это все мелкие придирки.

      >> Так что когда будет время, я могу продолжить про кодинг в GWT

      Обязательно продождайте. Лучше всего учиться на примерах таких вот не сложных игрушек, ведь по ним всегда интереснее изучать новые технологии, я это гарантирую. (-:
        0
        Именно поэтому и написал. Практика — самое лучший способ изучить новую технологию.
        0
        большое поле (100х100 клеток) на достаточно мощном компе рисуется около 4 минут, при клике на любую клетку всплывает красивое окошечко с ошибками, которое можно передвигать. (FF 3.5.5)
          0
          Может все же 4 секнуды? У меня так вообще 2 на довольно среднем компе. Браузер Chromium.

          Про ошибку подтверждаю:
          Maximum call stack size excedded
          Type: stack_overflow"

          Еще надо бы сделать так, что при щелчке на поля из «Custom:» радиобокс сложности автоматом переключался на «Custom»
            0
            специально проверил с секундомером. FF — 3 минуты, 23 секунды (во время отрисовки кушает 600Мб оперативной памяти) хромимум — рисует всё практически мгновенно.
            0
            Ого. Не проверял в FF столь большие поля. Оптимизацию начну именно с алгоритмов, которые кушают так много. В хроме вообще все прекрасно — не стоило в нем отлаживать :)
            +6
            Есть небольшая недоработка — у вас можно попасть на мину первым же кликом.

            В оригинальном же сапере положение мин рассчитываются после первого клика и таким образом, что бы на нажатой клетке мин не было.
              0
              Да, от этого я не пытался пока избавлятся. При первом клике был бы не такой хороший отклик, т.к. после него генерировалось бы все поле. Т.к. это происходит не быстро, время на начальные издержки решил потратить на время, пока вы тянетесь от кнопки до поля :) На самом деле, в текущем коде добиться такого поведения не сложно — нужно лишь инициализацию запускать после первого клика. Как это делается с запуском таймера.
                0
                Вы можете оставить есть, но если первый клик попадает на бомбу, то перегенерировать поле. В таком случае долгий отклик будет лишь в 1 из 10 случаев, что не критично.

                Либо генерировать перед началом 2 поля, в таком случае вероятность того что вы тыкните в клетку, которая будет в обеих полях бомбой — 0,1*0,1 = 0,01, что уже совсем ничтожно мало.
                  +1
                  А как насчет того чтобы просто убрать мину если пользователь попал в нее и поставить в любом другом месте?
                    0
                    Дык а циферки-то, говорящие сколько мин рядом, пересчитать тоже надо.
                      0
                      Все правильно. В статье я написал, что циферки считаются после того, как игрок кликнул на поле. Так что этот вариант очень даже хороший.
                        0
                        Тогда да, извиняюсь что не заметил.
              +1
              возможность быстро открывать поля должна быть по нажатию двух кнопок, средняя — колесо. неудобно )
                0

                Либо я еще не проснулся, либо вы меня обманываете.
                  +2
                  Не приложилось
                    0
                    Только щас понял, что красная мина — не самый лучший способ показывать отсутствие этой мины. Я вас не обманываю :)
                      0
                      /me долго смотрел на картинку и так и не понял в чем проблема :)
                        0
                        В том, что если там где красный квадратик есть мина то сапер неправильно считает цифры — т.е. количество мин, соседних с ячейкой с цифрой.
                          0
                          В том-то и дело, что красным отмечается место, где _нет_ мины, т.е. где флажок был поставлен ошибочно.
                            0
                            Да… неоднозначно.
                            Может просто зачеркивать его <s>?
                            0
                            а. просто я сначала попробовал и когда уже смотрел на картинку, понимал, что мины там нет на самом деле :)
                      0
                      Ещё одно дополнение. в классическом сапёре между клетками нет границ. т.е. при нажатии в случайную область поля откроется клетка. а у вас при попадании на границу между клетками ничего не происходит. Это очень важно при установке рекордов.
                        0
                        Согласен, над этим работаю. Почему-то сразу не получилось уничтожить границы через CSS свойство border-collapse: collapse. Надо будет разобраться.
                        0


                        Извините, но играть довольно тяжело и необычно…
                          +3
                          Могу предложить генерировать поле, после первого нажатия. Мой самый первый клик попал на мину, да я неудачник, но всё же =)
                            0
                            Во время тестирования я себя ощущал полнейшим неудачником — на поле из 100 клеток с 3 минами в первый же клик я попадался на мину :)
                              0
                              Мне кажется это удача)) у меня так никогда не получалось…
                              +1
                              А сможете сделать многопользовательского сапера? :) Я пробовал несколько лет назад, но из-за кривизны рук забросил.

                              Идея была в том, чтобы несколько человек в онлайне разминировали одно поле. Команда проигрывает, когда подрывается последний участник. Можно прикрутить рейтинги команд, давать одно и то же поле на прохождение разным командам и т.д.
                                0
                                Ну это лишь способ ведения рейтинга, группирования пользователей. Не вижу в этой задачи чего-либо сложного или интересного. И духа соревнования тут нет. Выигрыш в эту игру во многом зависит не только от внимания, но и от удачи.

                                Думал сделать виджет для вейва, но одновременная игра двух игроков — тоже сомнительное мероприятие на минном-то поле :)

                                А вообще, многопользовательские игры — тема для меня очень интересная. Так что если есть какие-либо предложения, с удовольствием выслушаю.
                                  +3
                                  «Дабл-сапер»:
                                  Играют два игрока на большом поле. Каждый игрок может не только разминировать клетки, но и ставить свои мины(!). При этом он видит их положение, аналогично тому как помеченные клетки с минами. Мины можно ставить только на закрытые клетки. Если противник поставил мину, то для соседних отрытых пересчитывается кол-во мин. Это нужно для того, чтобы соперник видел, что картина как-то изменилась и ему нужно подумать над ней еще разок.
                                  Игрок побеждает, если:
                                  — его противник подорвался
                                  — если никто не взорвался, и он открыл больше клеток.

                                  В чем смак игры: с одной стороны идет жесктое соревнование на время — вы на своем поле видите, как ваш противник открывает клетки (дополнительно в углу можно показывать сколько клеток вы отркыли, или же процент). В тоже время спешить нелья — одно неловкое движение и вы проиграли. Нужно очень четко следить за действиями противника — может оказаться так, что он откроет какую-то клетку, рядом с которой лежит «клондайк» — то есть свободная зона, которую вы можете у него перехватить и тем самым урвать у него клетки.
                                  Но помимо этого можно делать «пакости» — поймать противника на невнимательности. Пока не продумал этот момент, но возможно надо ограничить кол-во мин, которые может поставить игрок. Чтобы это не порождало тупиков в игре.

                                  Кстати, не обязательно играть в двоем — можно и большей компанией.
                                    0
                                    Очень интересные идеи. Спасибо. Обязательно подумаю над этим. Вы аж зажгли во мне интерес в развитии идеи :)
                                      +1
                                      У меня много разных идей :). Если интересно, можно подумать совместно.
                                      У меня как раз было желание сделать что-нибудь на GAE для саморазвития. Правда, я python-ист, с java вообще не работал. Поэтому хотел взять Django для разработки.
                                        +1
                                        Вы можете попробовать GWT-подобные решения или порты типа pyjamas.
                                  +1
                                  Вариант — две команды по несколько игроков (минимум по одному, максимум, например, по 5 в каждой команде) разминируют одно и то же поле. Разминированная часть и отмеченные мины засчитываются в пользу той или другой команды, так можно видеть, кто побеждает, например «23% — 34%, 12мин — 20мин». При этом начальное поле имеет слева и справа пустую область, то есть, одна команда начинает открывать поле с левой стороны, другая с правой, и обе видят в самом начале открытую границу с указанным количеством мин на своей границе.

                                  Действительно, многое зависит от удачи, но есть место и для стратегии: например, слабых игроков в каждой команде можно использовать для проверки спорных мест — «Ваня, проверь вот здесь». Ваня подорвался, значит мина была, но остальная команда продолжает разминирование. :)
                                    +1
                                    Командная стратегия: «Мы закидаем их трупами» :)
                                0
                                Классная статья! Вроде для того, чтобы выиграть нужно открыть все пустые поля, а не только пометить все мины.
                                  +1
                                  Не знаю, как сейчас обстоят дела с оперой и правым кликом, но раньше, с аналогичной задачей написания кроссбраузного сапера, для оперы (7 или 8 была тогда, не помню) я справился так:

                                  Ессно обработка правых кликов должна быть включена:
                                  Открываем маленькое окно (windows.open, width,height=«маленькая цифирь») за пределом видимости экрана (top,left=«большая цифирь»); и тут же скриптом, внутри этого окна, закрываем его. Тогда стандартная менюшка, которая вызывается в опере на правый клик, перебивается этим окном, и не показывается.

                                  К сожалению исходники, за давностью лет, утеряны. Однако, насколько я вспоминаю, решение было не быстрым, но работало.
                                    0
                                    Спасибо, буду думать. Покликал оперой сайты типа гугладоков, ассемблу и прочее… Нигде правый клик не работает
                                      0
                                      Ага, с оперой никто не заморачивается. Это мое личное решение проблемы (ему года 4 точно, и действительно долго кумекал, как перебить эту менюшку на правом клике), которое сейчас, как я думаю, может и не сработать из-за блокера всплывающих окон, однако надо проверять.
                                    0
                                    Я тут, конечно, мимоходом. Для каждого поля у вас свой хэндлер создается?

                                  Only users with full accounts can post comments. Log in, please.