Привет, друзья.
Меня зовут Алексей, я работаю фронтенд-разработчиком в Санкт-Петербургском офисе компании Wrike, и сегодня я хочу рассказать про то, как я поучаствовал в хакатоне AngularAttack, где моя работа Sherlock в итоговом протоколе заняла первое место.
О том, что это и как начиналось
Вдохновением послужил прошлогодний пример Андрея, с которым мы работаем в одной команде.
Тем более, для того, чтобы вписаться в участие, не нужно ничего, кроме настроенного рабочего окружения… и готовности писать код в течение 48 часов на выходных без сна и отдыха.
Как правило, и с первым, и со вторым у хороших разработчиков проблем не бывает.
Условия хакатона элементарны. Все происходит удаленно. Старт – в полночь по Гринвичу в субботу, финиш – в полночь в понедельник, в команде от 1 до 4 человек, до старта эвента никакого электронного контента создавать нельзя (ни кода, ни ассетов), хотя рисовать на бумажке блок-схемы и придумывать алгоритмы – сколько угодно. Каждому участнику выдается приватный репозиторий на GitHub, в который он кладет код в процессе написания. Обязательное условие – на выходе должно быть web-приложение с использованием фреймворка Angular. Свободно распространяемый контент и библиотеки в своем приложении использовать можно с соблюдением условий лицензии (например, ссылка на автора). Все.
А вот чего бы этакого написать?
Где-то за пару недель до начала я начал задумываться над идеей. Мысли про будущее приложение возникали следующие:
- Оно должно быть простым в использовании – работ будет много, судьи – обычные люди, и, если человек за пару десятков секунд не поймет, как пользоваться приложением, он скорее всего не будет разбираться дальше.
- Оно должно быть несложным в разработке. Я собирался участвовать в одиночку, и идея должна быть такова, чтобы банально успеть ее реализовать за эти 48 часов. Поэтому всякие мысли про бэкенд и прочее отпали сразу – только single page app, только фронт, только чистый Angular.
- Оно должно быть не очень банальным – нужно, чтобы идея цепляла. Опять же – судьи оценивают не крутость кода, а работу и внешний вид. Поэтому можно написать игру “виселица” (как кстати, многие и сделали), но человек, сыграв в нее раз, больше никогда в игру не вернется (а одним из критериев оценки является так называемый Utility/Fun – насколько работа полезна, прикольна, и, в общем, вызывает желание пользоваться ей вновь и вновь).
Вообще, если придумать совершенно новую идею, отвечающую всем трем пунктам, то можно запросто стать миллионером. У меня в итоге не получилось – я решил остановиться на собственной реализации уже известной, старой-старой логической головоломки, которая работала еще под DOS, и в которую мы в студенчестве заигрывались с головой.
Суть ее относительно проста: есть поле из шести рядов по шесть клеток, в каждом ряду могут находиться картинки-элементы, объединенные каким-либо признаком, например, в первом ряду сверху – лица, во втором – знаки зодиака, в третьем — цифры – и так далее. В начале игры на доске загадана некоторая правильная комбинация положения всех элементов, неизвестная играющему, и цель игры – используя предложенный набор подсказок, определить правильную позицию для каждого элемента на доске.
При старте игры некоторые элементы уже могут быть открыты (например, буква L в нижнем ряду доски – это означает, что она точно находится именно здесь). В клетках же, где правильный элемент еще точно не определен, показываются оставшиеся возможные варианты. К примеру, в каждой из остальных клеток нижнего ряда, исходя из текущей позиции, все еще возможны варианты из букв H, O, M, E и S.
Далее, смотрим на подсказки. В самой игре можно навести на подсказку курсор мыши, и появится тултип с ее описанием (правда, на английском, но я верю, что для хабровцев это не проблема), здесь же просто упомяну, что каждая подсказка в нижнем ряду под игровой доской означает, что оба элемента, указанные на ней, должны находиться в одном столбце.
Если мы глянем, например, на седьмую подсказку слева, то увидим, что буква L должна быть в том же столбце, что и цифра 5. L у нас уже открыта – значит, уверенно открываем 5 над ней (левый клик). Теперь посмотрим на пятую слева подсказку (знак зодиака Овен в одном столбце с Е). Ни Овен, ни Е у нас пока не открыты, но открыта L, а это означает, что над ней Овна точно нет. Следовательно, мы можем убрать знак Овен из клетки над буквой L (правый клик).
Таким образом, применяя подсказки для открывания картинок, положение которых мы можем определить однозначно, и убирая элементы, которые точно не могут быть в соответствующих клетках, мы постепенно уменьшаем неопределенность, и в конце концов, приходим к единственно верному решению головоломки. Ну, или не приходим – пока навыки не отточены, с решением можно запросто зайти не туда, но в этом случае поможет хитрая кнопка Undo to Last Correct, которая откатывает позицию на последнюю верную, до ошибки, и решение головоломки можно продолжить с правильного места.
Понеслась!, или немного про алгоритмы
В пятницу вечером, перед началом эвента настроил окружение. В качестве языка выбрал Google Dart – тут сомнений не было: несмотря на то, что основным трендом в разработке под Angular является TypeScript, практически весь фронтенд в нашей компании сейчас пишется именно на Дарте, и я решил не изобретать велосипед. Также выполнил настройки git-а и
В субботу, к старту хакатона в три ночи решил не вставать, проснулся где-то в шесть утра. Решил – сегодня пишу логику, завтра – UI. Если с логикой в день не укладываюсь – то завтра утром решаю, имеет ли смысл вообще продолжать.
А вот с этой самой логической частью, кстати, был довольно интересный челлендж.
Для головоломки надо было сгенерировать: 1) правильную позицию на доске, 2) позицию, которая изначально открыта, и, самое главное, 3) набор подсказок, которые позволяют перевести открытую позицию в правильную. Причем, набор подсказок должен быть в идеале необходимым и достаточным – задача должна решаться, и при этом, единственным способом.
С генерацией случайной правильной позиции и изначально рандомно открытых нескольких картинок проблем, естественно, не возникло, а вот как создать набор подсказок для конкретной позиции, придумалось не сразу. Пытаясь решить задачу в лоб, я попробовал добавлять случайные подсказки в набор до тех пор, пока они не позволяли открыть доску полностью (для этого потребовался метод, который с данным набором подсказок пытается решить позицию – и либо решает ее, либо рапортует, что этого сделать нельзя), но столкнулся с тем, что набор часто получался избыточным, и дело было даже не в дубликатах (их как раз можно было отследить) – случалась ситуация, когда некий поднабор подсказок полностью дублировался другим поднабором, при этом одинаковых элементов в них не было, а итогового сета все еще не хватало для полного открытия доски.
В итоге я решил поступить следующим образом: изначально создать заведомо избыточный набор подсказок, и попытаться решить им позицию. Если не вышло – пересоздаем набор, если же получилось – рекурсивно выкидываем по одной подсказке и снова пытаемся найти решение. В какой-то момент задача решаться перестанет, тогда мы берем минимальный сет подсказок, с которым решение было найдено, и объявляем его итоговым. Это сработало.
Были опасения, что все эти рекурсивные построения будут зверски тормозить (все вычисления происходят в браузере на клиенте, во время работы алгоритма мы пытаемся программно решить головоломку несколько сотен раз, а у процедуры решения своя рекурсия… и так далее), но, на удивление, после небольшой оптимизации, все заработало довольно шустро. На современных машинах позиция с подсказками строится практически мгновенно, на медленных и древних – около секунды).
Естественно, все заработало не с первого раза. Какие-то вещи я не сразу учел, что-то было настолько сложно проверяемо без UI, что пришлось написать пару юнит-тестов, которые, в свою очередь, тоже нашли пару ошибок, но факт – в субботу ближе к вечеру логическая часть игры у меня была, и можно было приступить к реализации графической части и взаимодействия с пользователем.
Графика? Все уже нарисовано до нас!
Для игровой доски требовались картинки, отвечающие условию: 6 наборов по 6 штук, каждый набор объединен определенным признаком. Сам бы я замучался их делать – к сожалению, именно в рисовании медведь оттоптал мне оба уха, руки, и все пальцы в придачу, включая те, которые на ногах. Но здесь выручил набор смайлов Emojione, которые с недавнего времени свободно входят в поставку Photoshop CC, на который у меня есть совершенно официальная подписка как часть плана Adobe для фотографов, так что условия лицензии соблюдены. Впрочем, без подписки, думаю, тоже все было бы в порядке: Emojione – свободно распространяемая библиотека.
Кстати, изображения, естественно, упаковал в спрайт использованием responsive backround sprites css — это позволило отображать картинки просто заданием класса для элементов, более того — при необходимости легко менять их размер. Про саму технологию написано уже немало — можно, например, почитать тут.
И да – несколько картинок все-таки пришлось нарисовать самому, включая стрелочки на подсказках, пару круглых кнопочек для help-страницы, бэкграунд для игрового поля, имитирующий лист бумаги в клеточку, и лого, которое показывается при загрузке игры. Впрочем, тут использовались стандартные текстуры и фигуры Photoshop, и с этим неразрешимых проблем не возникло.
Но если у вас в команде есть нормальный дизайнер – знайте, что вам уже невероятно повезло.
Штирлиц на грани провала
Воскресенье ушло на создание и вылизывание UI. Простейшая играбельная версия была готова где-то после полудня, но до комфортной игры было еще далеко – не было ни функциональности Undo, ни уведомлений о том, что позиция решена правильно/неправильно, ни уровней сложности. Первые две фичи были чисто техническими, а вот с последней, кстати, я поступил просто: сделал три уровня – легкий, средний и тяжелый, которые отличаются только количеством изначально открытых клеток. Для Easy это всегда 10, для Medium – 5, а вот на Hard – от нуля до двух. Да, в некоторых раскладах на харде изначально не открыто ни одной клетки. И более того – задача в этом случае тоже решается!
В какой-то момент я понял, что каждой подсказке обязательно нужен тултип, в котором будет подробно описано, что она из себя представляет, иначе судьям, незнакомым с игрой, будет крайне сложно понять, что означает весь этот пестрый набор картинок. Прошерстив в интернете имеющиеся библиотеки с компонентами для Angular Dart, остановился на Angular Material, в котором этот тултип имелся. Одна беда – библиотека была в бета-версии, которой в свою очередь, требовалась последняя бета Ангулара. Ну, где наша не пропадала – проапдейтил сборку со стабильного Angular 2.2.0 до беты 3.0.0, посмотрел, что вроде ничего не поломалось, и все работает как надо, обрадовался, и начал приделывать красивый тултип к подсказкам.
Заняло это примерно часа два, и вот что в итоге получилось:
Теперь небольшое отступление, для тех, кто не знаком с языком Dart.
Код, написанный на нем, не может исполняться в обычном браузере напрямую, его обязательно нужно скомпилировать в Javascript специальной командой. Но для разработки Google выпускает специальную версию браузера Chromium – Dartium, который содержит встроенную виртуальную машину, способную выполнять чистый дартовский код. Понятное дело, что вся разработка идет в этом специальном браузере – так значительно быстрее, а компилируется итоговая версия только тогда, когда ее нужно куда-то выложить.
Так вот – тултипы заработали, подумалось – самое время выложить промежуточную версию на внешний
И вот тут-то и стало ясно – переход на бету Angular, наверное, не такая хорошая идея, как показалось вначале – работоспособность-то ее никто не гарантирует. Внезапно осозналось, что и время как-то довольно быстро уже движется к вечеру второго дня, а тултипы придется переделывать с нуля, и потраченного впустую времени безумно жалко.
В качестве последней надежды около получаса читал профильные форумы – и на каком-то по счету внезапно обнаружил вопрос от участника с такой же, как у меня, проблемой. Более того, ему ответили – двадцать минут назад (!), посоветовав обновить сам Dart SDK на последнюю версию, вышедшую за несколько часов до этого.
Лихорадочно обновляю Dart на локальной машине, собираю пакет, выкладываю на сервер, проверяю – работает.
Выдыхаю – в этот раз просвистело мимо.
И серьезный урок – наверное, все учесть все-таки невозможно, но до старта хакатона надо по максимуму продумать, что, и как будет использоваться, какие библиотеки, совместимы ли они друг с другом, какие могут быть у всего этого подводные камни. Если бы я сразу понял, что тултипы есть только в бета-версиях Angular material, возможно, не стал бы рисковать и сразу знал, что нужно писать свой. Или поискал бы другую библиотеку.
На финишной прямой…
Остаток вечера ушел на создание help-странички. К тому времени глаза уже собрались в кучу, количество выпитого кофе за день измерялось десятками чашек, но очень хотелось, чтобы так называемый onboarding – процесс ознакомления пользователя с приложением – был настолько эффективным, насколько это возможно. С учетом ограниченного времени, затраченного на реализацию, получилось, наверное, не очень, но тем не менее, лучше, чем ничего – по крайней мере, пользователь видит, что о нем заботятся, и, прежде чем показать ему приложение, которое не всегда интуитивно понятно, пытаются объяснить, что его ожидает.
Уже ближе к ночи потратил пару часов на отлов орфографических и стилистических ошибок, после чего решил, что пытаться что-то улучшать дальше бессмысленно – голова к тому времени соображала очень туго, и вместо исправления косяков можно было запросто наплодить новые, собрал итоговую версию, выложил ее на
В сумме весь процесс написания кода занял чуть больше тридцати часов (да, я все-таки иногда спал), плюс пара часов на графику. И в общем, вся разработка прошла довольно гладко, за исключением истории с бетой Angular, но все хорошо, что хорошо кончается – в следующий раз буду опытнее.
…и неожиданный результат
Процесс оценки и обсуждения работ длился неделю. Игра собрала не очень много фидбэка (свои оценки и мнения могли высказывать не только судьи, но и другие участники эвента), хотя этот самый фидбэк был в основном положительным, а оценки от именно судей – практически неизменно высокими. Но, если честно, сложно было представить, что в день объявления результатов я увижу свою игру на первом месте из сотни с лишним работ участников. Для меня это был, в первую очередь, именно интересный опыт программирования в таком формате – двое суток, идея, реализация, готовый конкурентоспособный продукт на выходе, хотя итоговая победа безусловно, была приятным бонусом.
И только моя супруга, два дня таскавшая мне кофе и еду к компьютеру, в этой победе изначально не сомневалась. Спасибо ей.
Рабочая версия игры находится здесь: http://sherlock.netmafia.ru/ (крайне рекомендуется разрешение монитора Full HD).
Репозиторий с исходным кодом — тут: https://github.com/izolenta/sherlock, возможно, кому-то будет интересно познакомиться с языком Google Dart.
Спасибо за прочтение – и удачи!
Автор: stickytape