В минувшие выходные Clojure-сообщество испытывало… Ой, так начинался прошлогодний пост с отчётом о прошедшем ClojureCup 2013.
ClojureCup — это 48-часовой онлайн-хакатон, обязательным условием которого является написание приложений на языках Clojure/ClojureScript. В этом году в хакатоне участвовало около 50 команд (в каждой по 1-4 человека), которые сделали множество классных приложений.
Приложения оценивают как жюри (известные в Clojure-сообществе люди), так и обычные пользователи: зайти с помощью twitter-а и проголосовать за понравившиеся вам приложения можно на странице приложений.
А попробовать, что у нас получилось, можно вот тут.
Осторожно: под катом много картинок.
Идея
Идея поучаствовать в ClojureCup появилась у меня достаточно давно, но лень сделала свое дело, и спохватился я о сборе команды лишь за три дня до начала хакатона (как оказалось, за два дня до хакатона нужно было сформировать команды). Однако этот мир оказался не без программистов, желающих потерять свои выходные, для того чтобы написать что-нибудь™ на странном языке со скобочками™. Тут нужно дополнительно упомянуть, что никто из нас сколько-нибудь серьёзно на Сlojure не писал, а двое и вовсе не писали. И да, все четверо были программистами.
Изначально в качестве проекта было желание делать странную-штуку-для-статических-сайтов (одна из команд сделала что-то немного похожее в итоге), но возникли большие вопросы в нужности результата.
В итоге мы сформировали критерии выбора идеи: что-нибудь понятное сразу и для простого юзера.
От таких критериев мы быстро пришли к идее сделать какую-нибудь игрушку. И тут вспомнилась прекрасная Puzzle Bobble:
Это пазл с очень озорным режимом P2P: чем больший кусок шариков ты собьёшь группой, тем больше этих самых шариков придёт ко врагу. Так мы сосредоточились на поиске игры, к которой можно было бы прикрутить механику “сделать противнику неприятно”.
Думали и о тетрисе, и о 2048, но потом вспомнили Rocket Mania!
Так, ночью, за два дня до начала, родилась идея приложения: двое игроков будут играть в упрощённый вариант Rocket Mania!, но количество ракет будет ограничено и они будут улетать к врагу, а чтобы игра точно когда-нибудь закончилась, у них будет топливо, которое при каждом перелёте уменьшается, а в конце ракета будет улетать в вечность космос.
На тот момент игра должна была быть с классной графикой и возможностью играть по сети, но когда ночью в пятницу мы начали прорабатывать идею, внеслось два изменения: решили выкинуть игру по сети и поддержать только режим hotseat (помните, был такой в героях, когда два игрока за одним компьютером), а также использовать flat design, потому что так проще, а среди нас одни программисты =)
Это были очень правильные решения: нужно выбирать целями то, чего реально можно достичь. Упрощать-упрощать-упрощать.
Как и опыта в Clojure, опыта в функциональном стиле программирования у нас не было. Но тут мы решили брать функциональную парадигму по полной: каждое наблюдение мира — неизменяемо, функции чистые, и т.д. и т.п. В итоге в нашей архитектуре был один “атом мира”, состояние которого однозначно задавало то, что на экране, и изменением которого мир менялся. Также мы решили использовать react.js (это было очень правильное решение) и обертку над ним quiescent (а это неправильное, но терпимое). Сейчас мне кажется, что нужно было глядеть на phaser, но тогда такой мысли не возникло. Кстати, на phaser-е другая команда сделала весёлую игру с котами!
Тем временем время подошло к 4 утра, начался ClojureCup, а мы пошли спать. Один из членов команды (живущий в другом часовом поясе) в это время поднял сервер и запустил туда заглушку с ракетой Элона Маска:
Реализация
Приложение очень хорошо разделилось на части: кто-то пилил чисто игровую логику, кто-то отображение игрового поля, кто-то занимался обработкой клавиатуры. Почему-то активного использования терминального REPL-а у нас не случилось, а вот figwheel — решение для автоматической перекомпиляции ClojureScript кода с hot reload-ом кода в браузере — был очень кстати и очень-очень упрощал нам жизнь. Тут надо заметить еще один интересный момент: состояние мира хранилось в неперезагружаемом атоме, поэтому даже после перекомпиляции приложение оставалось в том же состоянии, что и было до. Из минусов можно отметить то, что у нас время от времени слетала инкрементальная компиляция, приходилось делать полный ребилд.
Вначале мы хранили сгенеренный код в репозитории, поэтому у нас получились волшебные числа в десятки тысяч изменённых строк кода, при итоговом размере порядка 1К строк кода.
Из неприятных запар у нас случилось две:
- первая была связана с тем, что quiescent очень странно решал, какие части страницы нужно обновлять, а какие нет; в тот момент я научился вставлять отладочный вывод в сгенеренный JS-код и даже понимать что там нагенерено,
- вторая была по всей видимости багой ClojureScript компилятора: правильный код компилировался, но потом не работал.
Во время реализации игровой логики перед нами встала задача масштабного глубокого изменения immutable состояния мира. В итоге было придумано решение, как потом оказалось, с точностью до сигнатуры совпадающее с решением из стандартной библиотеки =) Мораль: изучайте хоть немного инструменты, которые будете использовать. И перед тем, как делать что-то своё, посмотрите: не сделал ли это кто-нибудь другой?
А ещё мы не знали, как сделать изменяемое состояние в Clojure (функциональщики поневоле), это родило такой монолог:
жутко бесит меня кложур, чтоб я ещё раз на нём кодил; зачем нужен этот язык, если его невозможно читать, а чтобы что-то нормальное писать, нужно всё равно выходить из концепций языка.
В итоге изменяемым состоянием мы пользоваться так и не научились, но заиспользовали loop.
В конце первого дня у нас уже была что-то показывающая версия приложения, а за 3 часа до конца хакатона — уже более-менее полноценная работающая версия игры. В последние 3 часа мы добавили в игру звуки (тут от функциональной парадигмы пришлось отказаться и появились методы вида play!) и очень сильно отполировали UI.
За часа 2 до конца у нас упал сервер, и оказалось, что игра раздавалась встроенным python-сервером из коробки. Быстро подняли nginx и были рады.
За полчаса до конца я открыл приложение в Сафари, и оказалось, что оно там вообще неработоспособно (поворот блоков не работал).
За 15 минут до конца у нас никак не хотел работать звук для победы (видимо, снова была проблема с компилятором ClojureScript-а), это всё заметно добавило адреналина.
Так что даже имея за 3 часа более-менее работающую версию, последние минуты мы провели в запаре.
А вот и этот самый момент, когда мы затегали релизную версию и получили три зелёных галочки, означающие, что всё необходимое мы сделали =)
Вместо заключения
У нас получилась, хотелось бы верить, весёлая игра на двоих. А даже если и не получилась — мы получили огромное количество веселья и не меньшее количество опыта. Вот тут можно смотреть исходники проекта, а тут при желании проголосовать.
Список проектов, упорядоченных по текущему положению в публичном голосовании, можно посмотреть здесь и здесь. Советую посмотреть на эти списки: среди проектов есть очень и очень приличные, весёлые и полезные.
За этот год уже можно почитать отчёты Александра Соловьёва (вы его можете знать по этому замечательному выступлению про FRP и Clojure), команды проекта Funstructor, а также Milestones.
Отдельно хочется сказать спасибо Александру Соловьёву ingspree и Никите Прокопову tonsky за поддержание интереса к Clojure на высоком уровне, а также Дмитрию Грошеву si14, Николаю Рыжикову и Михаилу Лапшину за проведенный workshop по clojure в компании JetBrains.
Также огромное спасибо Юле Беляевой juliette, Серёже Серебрякову megaserg и Андрею Сиунову fandes за прекрасную компанию на этом хакатоне. Хакатонить в прекрасной компании — прекрасно!
Автор: ttim