swizard (swizard) wrote,
swizard
swizard

Category:

ICFPC 2017: Ticket to Ride на Rust (первая часть)

ICFPC 2017: Ticket to Ride на Rust.

Сетап.

В прошлом году я как-то так и не собрался написать отчёт про ежегодный конкурс, который проходит под эгидой ICFP. Хотя там тоже было интересно: наша команда впервые выступила не на традиционном Common Lisp, а вовсе даже на Haskell. Опыт был достаточно любопытным. Мне скорее понравилось, ребятам скорее нет -- но положенную долю драйва все получили (хотя выступили в тот раз так себе, конечно).

В этот раз решили продолжить с экспериментами, и попробовать в деле Rust. Так как этот язык кроме меня тоже никто не знал, было принято решение основательно подготовится в течении года: совместно порешать предыдущие задания ICFPC, поревьювить разнообразного коду на расте, и тд. Надо же ответственно подходить к делу, сколько можно проигрывать-то всяким бомжам, верно? Итак, внимание вопрос: сколько же раз мы с командой успели таким образом попрактиковаться? Правильно, ноль.

Соответственно, исходный расклад перед конкурсом выглядит следующим образом:

  • Пять человек в команде.
  • Из них "на полной ставке" четверо.
  • Программировать на Rust-е умеет один, остальные листали книжку.
Казалось бы, это же верная заявка на победу? Вот и я так думал, поэтому мирозданию пришлось ещё немного подкорректировать расклад:
  • Фёдор по семейным обстоятельствам отказывается от участия.
  • Я ломаю себе правую руку.
Вот теперь ситуация выглядит справедливо, следовательно, можно начинать играть :)

Подготовка.

Надо сказать, что немного потренироваться всё же как-то получилось: за сутки до конкурса мы с Turtle быстренько спрограммировали абстрактный sokoban: всего по-минимуму: какие-то там карты, какой-то там солвер и какой-то там визуализатор. Чисто чтобы освоить инструменты, которые могут пригодиться на конкурсе. Мне, в основном, было интересно изобразить что-то похожее на визуализатор, так как все предыдущие разы у нас до него не доходили руки. А ведь это зачастую полезная вещь для понимания того, как именно у тебя работает программа.

Вот что мне показались полезным по итогам экспериментов:

  • nom -- крутая библиотека zero-cost парсер-комбинаторов.
  • piston -- вообще, это игровой движок, но он сильно модульный, поэтому оттуда легко взять только самоы необходимый минимум для того же визуализатора, без необходимости разбираться в огромном навороченном апи.

Надо сказать, что пистон меня прямо впечатлил: я что-то даже не ожидал, как там легко всё делается: графика (и 2d, и opengl), рисовалки всякие, картинки, шрифты, мышь/клавиатура. Прям даже захотелось какую-нибудь игру напрограммировать =) По-факту визуализатор базового уровня (а что-то большее редко требуется на icfpc) пишется неспешно за 30 минут, а если приложить чуть больше усилий, можно вполне сделать игровой симулятор. Пример такого простенького графического интерфейса для сокобана можно видеть на картинке выше.

Начало конкурса.

Буквально за полтора часа до старта контеста я ещё раз обдумал наши шансы, и решил пригласить ещё одного человека в нашу доблестную паралимпийскую команду -- моего коллегу Игоря, который до этого изъявлял желание поучаствовать, но я про это совсем забыл. К счастью, это был второй человек, который хорошо знает Rust, но, к сожалению, он смог помочь нам только в пятницу, а на выходные ему надо было уехать на дачу.

В этот раз ребята, организовавшие конкурс (University of Edinburgh) осмелели настолько, что по какой-то причине решили захостить сайт с заданием на своей площадке (а не на гитхабе или гугл-доках, как обычно это делалось в последнее время). Разумеется, ровно в 15:00 сайт обвалили, и работягам пришлось ждать минут тридцать, пока орги всё-таки перевезут своё барахло на гитхаб. Но, так или иначе, задание всё же было получено в виде PDF-ки.

Участникам предлагалось сыграть в онлайн-версию "Паровозов": весьма популярной настолки Ticket To Ride. На старте выдаётся карта с набором объектов, малая часть из которых является шахтами, где добывают лямбды, а большая часть является потребителями этих лямбд. Объекты как-то соединены между собой реками, по которым лодочники могут доставлять добытые лямбды из шахт к потребителям.

На картинке объекты 1 и 5 являются рудниками, а все остальные узлы графа -- потребителями. Игроки (лодочники) делают ход по-очереди, либо "захватывая" себе одну какую-то ничейную реку, либо пасуя. Чем дальше по построенному маршруту удалось увезти от шахты ламбды, тем больше очков зарабатывает игрок. Цель игры, соответственно, набрать больше всех очков.

Уметь играть предлагалось в двух режимах:

  • онлайн: по сетевому соединению с сервером -- в этом режиме можно было играть с другими участниками во время конкурса
  • и оффлайн: когда твой предварительно скомпилированный бинарь на каждый ход отдельно запускает рантайм организаторов, а стейт игры между ходами сериализуется -- в этом режиме должен быть посчёт очков

Вообще, конечно, я уже не первый раз наблюдаю в ICFPC эти милые мелочи, типа сериализуемого стейта: видно, что организаторы всячески пытаются подогнать задание под функциональные языки, чтобы на них делать было удобно, а на императивных, напротив, муторно. И, что любопытно, даже несмотря на это побеждают преимущественно всякие c++ и java :)

А для маленьких любителей попрограммировать инфраструктуру была выдана спека на несколько страниц про протокол для онлайн и оффлайн режимов, спецификации для кодировки данных (json), алгоритм для подсчёта очков и обработки таймаутов. В общем, всё как полагается.

Также, в этот раз были обещаны апдейты задания прямо в процессе конкурса (до четырёх раз), и lightning division: отдельное объявление победителей по итогам 24-х часового марафона.

День первый.

Первые пару часов мы традиционно тупим в правила, и пытаемся играть друг с другом в предоставленном организаторами симуляторе, который работает через веб-интерфейс. К 16:23 я коммичу скелет проекта и какие-то базовые структуры данных, а Sectoid начинает настраивать какой-то наколенный CI.

Надо сказать, что это был вообще первый раз, когда я встретился в своей практике с CI, хотя мир вокруг давно в тренде. В целом, надо сказать, что мне понравилось. Всё так интерактивненько и хипстерско: коммиты сами уезжают на сборку, там сами прогоняются тесты, обо всём приходят нотификации прямо в телеграмм, всякие там сабмишны сами собираются, и тд. Сделал такой себе коммит, и сидишь, наблюдаешь за суматохой вокруг :)

Единственно, что всё это, конечно, хорошо для большой команды, в которой много активных разработчиков. Если ты единственный, кто коммитит код, то смысла во всей этой движухе особой нет, имхо.

Далее работы распределяются следующим образом: Игорь начинает программировать сериализацию структур данных в json, я отправиляюсь писать игровой цикл с сопутствующей обвязкой, Sectoid продолжает мучать CI, а Turtle куда-то уходит.

В 17:23 я внезапно делаю игровой цикл с игровым стейтом. Осмотревшись вокруг, я обнаруживаю, что никто ещё не освободился. Поэтому я берусь делать дальше протокол чата с сервером. Ещё через двадцать минут возвращается Turtle и берётся писать подсчёт очков -- как это обычно у нас принято, испортив мастер. Я традиционно прогоняю его в бранч :) Sectoid в это время дорабатывает CI.

Ещё через два часа я делаю реализацию игрового чата с сервером. Причём, в этом виде функции практически без изменений доживают до самого конца конкурса, хотя это изначально планировался как черновой набросок. Так как у меня на текущий момент есть только игровой стейт, а самих клиентов (сетевого и оффлайн) и сериализации пока нет, я делаю вот такую функцию:

pub fn run_online<S, FS, SR, FR, RR, GB>(
    name: &str,
    mut fn_state: S,
    mut send_fn: FS,
    mut recv_fn: FR,
    gs_builder: GB)
    -> Result<(Vec<Score>, GB::GameState), Error<SR, RR, <GB::GameState as GameState>::Error>>
    where FS: FnMut(&mut S, Req, Option<GB::GameState>) -> Result<(), SR>,
          FR: FnMut(&mut S) -> Result<(Rep, Option<GB::GameState>), RR>,
          GB: GameStateBuilder
и аналогичную для run_offline. Конечно, возникает вопрос, почему я не сделал FS и FR трейтами вместо замыканий со стейтом. Ну, так просто получилось меньше церемониального кода с вводом новых типов и описыванием имплементаций, ну и плюс стейта S там в изначальной реализации вообще не было. Соответственно, в реализации клиента нужно было просто вызвать нужный run, передав функции для отсылки и получания сообщений, и всё должно было заработать само.

Дальше я отправляюсь писать всякие тесты для игрового цикла и чата с сервером, в том числе, на основе демонстрационного чата Alice и Bob, который был приведён в конце спеки.

В этом месте мне следует сделать логическое отступление и поразмышлять о тестах в Rust. Дело в том, что на данный момент единственная объективная вещь, из-за которой раст может проигрывать в динамике разработки тому же Common Lisp, Haskell или Erlang -- это отсутствие REPL. Всем, кто уже успел привыкнуть к реплу, должно быть известно, насколько он ускоряет цикл создания нового кода. Особенно роскошен в этом плане CL + Slime, ни один другой репл даже близко к его возможностям даже близко не стоит. И настолько же мучительно потом возвращаться к абстрактному С++ или Java, где ты сначала несколько минут компилируешь программу, чтобы потом две секунды потратить на отлаживание printf-aми -- за это время ты бы успел провести десяток экспериментов в репле, чтобы проверить десяток идей.

Формально, Rust можно было бы причислить как раз к этой категории, если бы не его встроенная поддержка тестов. Эта поддержка на деле получилось насколько удобной, что, по-факту, цикл вида: "написал тест" + "вызвал cargo test" (C-C-t в емаксе) оказывается вполне такой себе заменой репла. Конечно, по интенсивности этот манёвр, всё же, реплу сильно уступает, но надо понимать, что тест-то остаётся "навсегда", в отличие от команды в консоли. Он будет прогоняться каждый раз во время cargo test (и в цикле CI), и, рано или поздно, он потенциально сможет вскрыть какой-нибудь новый недавно спрограммированный баг.

Тем временем Turtle начинает жаловаться в чатик на свой неравный бой с какой-то "арифметикой указателями", и я сразу подозреваю неладное :) Но, так как он упражняется в отдельной ветке, я не придаю его словам значения. Как потом выяснилось, зря.

В девять вечера, когда я возвращаюсь с ужина, обнаруживаю, что Sectoid делает автоматическую сборку сабмишнов, а Игорь доделал сериализацию и десериализацию наших структур данных в json. После небольшого совещания решили, что я попробую быстро спрограммировать сетевой клиент, а Игорь наваяет какой-нибудь базовый солвер, который что-то бы умел захватывать. Без какой-либо специальной стратегии пока, но любой солвер такого плана всяко лучше стратегии постоянного пасования каждый ход :) Игорь решает попробовать захватывать сначала все реки вокруг шахт, и потом все остальные рандомно. Я удаляюсь программировать клиента.

В 21:45 у меня готов клиент, и я обнаруживаю, что на текущий момент у нас уже есть сериализация, протокол, чат, игровой цикл, клиент, и почти готов какой-то солвер. В воздухе отчётливо запахло успешным сабмитом к lightning division :) Я бросаюсь программировать бинари lambda_punter_online и lambda_punter_offline, которые будут точками входа для онлайн и, соответственно, оффлайн реализаций протоколов. Через полчаса Sectoid куда-то убрёл, обещая вернуться в течении двух часов.

В 22:40 у Игоря готов солвер nearest, который столбит ближайшие к руднику реки. В 22:45 я заканчиваю lambda_punter_online который даже, внезапно, заработал с первой попытки! Вот она -- целебная сила написания тестов параллельно с разработкой :) Через веб-интерфейс оргов я даже умудряюсь сыграть со своим ботом. Это, конечно, всего лишь always_pass солвер, который каждый ход пасует, поэтому я разгромил его в пух и прах. В честь этой победы я ухожу дорабатывать lambda_punter_online (и, соответственно, -offline), чтобы можно было выбирать используемый солвер с помощью параметров командной строки.

В 23:17 мне это удаётся, и на карте sample.json я играю с нашим ботом уже вничью! Предлагаю в чате всем присоединиться к тестированию бота:

RUST_LOG=lambda_punter=debug,lambda_punter_online=debug cargo run --release -- --server-port 9002 nearest
В этот момент на связь снова выходит Turtle со своей "адресной арифметикой" в Rust. После краткого допроса выясняется, что он так и не смог освоить теорию про заимствования и владения в языке, и пытается программировать путём буквального следования подсказкам компилятора, который, в свою очередь, тщетно пытается угадать, что его хозяин имеет в виду :) Основная проблема заключается в том, что эти понятия в расте основополагающие, поэтому, судя по всему, особо много полезного кода Turtle'у породить не удастся.

В 23:33 из эфира пропадает Игорь, а я отправляюсь доделывать lambda_punter_offline, чтобы у нас был хотя бы один рабочий сабмит для лайнтнинга. Попутно, правда, пришлось оторваться на чат с Turtle'ом про то, как делать разное на Rust, а потом мне вообще нужно временно удалиться.

Вплоть до 01:30 мы с Turtle'ом усиленно учим Rust в ускоренном темпе, а в 1:40 я заканчиваю первую версию бинаря для оффлайна. Буквально через 10 минут организаторы апдейтят задание: появляются фьючерсы на маршруты. Тем, кто играл в паровозы должна быть понятна аналогия с карточками маршрутов, которые раздаются в начале игры (из которых можно выбрать какие хочешь). В ICFPC смысл точно такой же: на этапе инициализации игрок имеет право объявить несколько (от нуля до количества шахт) маршрутов, которые он обязуется выполнить. Получилось -- зарабатывает большой бонус, не получилось -- получает такой же большой штраф. В принципе, всё понятно :)

Тем временем Sectoid'а всё нет в эфире, и мы с Turtle'ом пытаемся понять, как собрать сабмишн. Формально-то lambda_punter_offline у меня готов, а сабмишна-то у нас нет.

В 02:46 всё-таки появляется Sectoid и обязуется сварить сабмишн. Все уже к этому довольно медленные и тупят (особенно я). Ещё через десять минут организаторы выкладывают программку-адаптер между оффлайн и онлайн режимом, с помощью которой оффлайновый клиент можно проверить прямо на игровых серверах. Программка на окамле, которую (кто бы сомневался) мне на макбуке собрать никак не удаётся.

В этом месте я ещё раз хочу сделать отступление, и отдать должное Rust-у. Я имел дело с очень большим количеством языков и их рантаймов, так вот, ответственно заявляю: почти у всего, что бывает в мире, гарантирован геморрой в плане кроссплатформенности. На чём бы вы не программировали большой проект, например, под линуксом -- будьте уверены, что отнести его мак и запустить его с без специальных плясок с первого раза не удастся (не говоря уж об венде, не к ночи будет упомянута). Неважно, хаскель это, лисп или си (особенно c++). Больше всего шансов в этом плане у джавы (хотя там есть свои приколы) и у эрланга (если не использовать порты). Так вот, проект на Rust соберётся в "чужеродной" среде с практически 100%-тной вероятностью. Особо отметим при этом, что раст -- низкоуровневый язык без рантайма.

Вплоть до трёх ночи я мучаюсь с окамловой сборкой, после чего позорно сдаюсь. Через некоторое время Turtle собирает этот адаптер у себя в виртуалке, а мне любезно даёт туда ssh.

Хоть все уже и тупят просто отчаянно, тем не менее, нам удаётся заметить, что lambda_punter_offline через орговский адаптер lamduct не работает: ошибка в клиенте "Resource temporarily unavailable". Хотя я же проверял руками через stdin/stdout, у меня всё пашет как ожидается. Что может быть увлекательней, чем ремонтировать такого рода баги в половину пятого ночи? :) Я уже было совсем собрался плюнуть и пойти спать.

04:30: пока пойти спать как-то не удалось, поэтому спрограммировал в протоколе поддержку фьючерсов. Вот теперь уже вроде все точно собрались идти спать.

04:40: я неожиданно нагугливаю информацию про "Resource temporarily unavailable" и решаю срочно ремонтировать lambda_punter_offline :) Разумеется, не получается. Хотя суматохи я навёл: Turtle'у надо пересобрать бинарь у себя в виртуалке, а Sectoid'у переварить заново сабмишн.

05:18: внезапно ремонтирую таки. Оказывается, это проблема возникает, когда на том конце пайп установлен в неблокирующий режим. Правда, вместо отремонтированного всплывает другой баг.

05:28: ремонтирую второй баг. Всплывает третий. Меня уже не остановить =)

05:36: ремонтирую третий баг. Всё, наконец заработало! Но, проблема: Sectoid'а нет, сабмишн собрать некому :) К счастью, через пять минут он появляется и сабмишн уходит. Тадам, у нас есть рабочий сабмишн к lightning division. Всё, я ухожу спать.

Продолжение здесь.
Tags: common lisp, icfp, icfpc, icfpc 2017, ratings, report, rust, skobochka
Subscribe
  • Post a new comment

    Error

    default userpic

    Your IP address will be recorded 

    When you submit the form an invisible reCAPTCHA check will be performed.
    You must follow the Privacy Policy and Google Terms of use.
  • 1 comment