суббота, 22 декабря 2012 г.

IKS + SKI: Infognition December Functional Programming Contest

В этом году thedeemon (также известный как DeeMon - "drop the "the", it's cleaner" <c>) решил устроить всем отличный подарок на Новый Год.

Конечно же речь о соревновании IDFPC-2012, которое можно охарактеризовать как квест в духе всенародно любимого Save Endo. Ну разве что попроще. Но именно квест. С несколькими этапами, спрятанными тут и там кодовыми словами и таблицей результатов online.

И нет, я совершенно не собирался участвовать. У меня жуткий цейтнот по двум другим проектам, не говоря уже о приближающихся праздниках. Даже на тусовку хаскеллистов, которую организовал thesz на прошлой неделе, выбраться не удалось. Хотя было бы неплохо уже с народом познакомиться. Короче говоря, никакой возможности участвовать в IDFPC не было. Ссылку я, конечно, Лёхе кинул. Можно даже сказать выкинул. Выкинул из головы в скайп, чтобы поскорее о ней забыть. И благополучно забыл. До пятницы.

В пятницу в районе двух в скайпе появился Лёха и с порога спросил

- 11 UTC - это 15 MSK?

И пошло-поехало. Я ответил, что да. Лёха полез на Wolfram Alpha проверять. Вольфрам показывал явную ерунду. Мы зарепортили им баг, который они не исправили до сих пор... В таком режиме и пролетел оставшийся час.

Задание было опубликовано в 15:00 и отличалось крайней лаконичностью. Всем организаторам подобных контестов надо поучиться у DeeMon'а.

Дан файл - картинка в формате bmp, - в нём зашифрован кадр из фильма. Нужно сказать, что за фильм. Еще был обещан scoreboard, куда надо будет вбивать встреченные по ходу пьесы кодовые слова. Но поначалу он не работал.

Картинку из любопытства я глянул, однако примерно минут 40 после публикации еще отбивался от Лёхиных атак и пытался заниматься делом. Но потом всё равно сдался. И здесь начинается короткая, но полная драматизма, история двух антивселенных: IKS и SKI.

Ну что ж, давайте поиграем.

CODEWORD: PLAY

Итак, джентльмены. Есть файл и с ним надо что-то делать. Поехали!
Ну, с файлом всё ясно. Говорим ему less, узнаём, что grep - это наш друг, и вот:
cat pic.bmp | grep -a "###"
### These three # signs marked the beginning of data segment as well as first comment. Grep is your friend now.
### RGB triplets in this file do not really store colors.
### Each triplet of bytes correspoding to a pixel contains values A,B,C (in this order), each a signed 8-bit integer.
### Remember that BMP stores image upside-down, so first bytes of data segment are first triplets of last row of image.
### Let X=70 and Y=79 be coordinates of current pixel. Let VX=18 and VY=26 (signed 8-bit integers) be current vector. Let CLR=0.
### Loop: take values A,B,C from triplet corresponding to current pixel in this BMP.
### Xor VX with A, VY with B and CLR with C.
### If CLR is not 0 draw a line from (X,Y) to (X+VX,Y+VY).
### Add VX to X and VY to Y, repeat the loop until A=B=C=0.
### So first visited points should be (70,79), (70,87), (64,79), (64,87)...
### Codeword: play
Когда grep на вашей стороне, вам по плечу любые проблемы. А теперь настало время заняться векторной графикой.

CODEWORD: SVG

Итак, джентльмены, нам предстоит первое содержательное задание. Оно заключается в том, чтобы реализовать интерпретатор для своеобразной разновидности Черепашки Logo. Правда вряд ли эта версия подойдёт для обучения детей программированию. Но ведь и мы уже давно не дети, верно? Приступаем!

Emacs открыт, ghci запущен, будущее предельно понятно. Самый сложный вопрос - это в каком виде рендерить картинку. И вроде бы логично бы делать bmp такого же формата и размера, как исходная картинка. Но поскольку недавно прочитан "Dive Into HTML5", то выбор падает в пользу рисования на canvas'е. Это будет иметь свои последствия: и положительные, и отрицательные.

В параллельной антивселенной Лёха еще час назад заявил, что эта задача элементарно решается на MATLAB'е. Но у него до сих пор "блин, чего-то не бьётся" и никак не получаются контрольные точки (70,79), (70,87), (64,79), (64,87). Подкидываю ему пару идей и даже RGB - или, точнее, ABC - исходной точки (18,18,42), но он такой точки в MATLAB'е в упор не видит.

Через полчаса, в половину пятого, Лёха сваливает домой и просит не рассказывать ему подробности, потому что собирается еще порешать задачку... в понедельник. Смешно, ага.

Еще через 8 минут Черепашка рисует первую страницу Векторного Гипертекстового Фидонета с адресом 70:79/18.26:

А еще через минуту выясняется, что предстоит разработка интерпретатора Весьма Фон-Неймановской Виртуальной Машины (50:22/24.34):
Забавно, что программой для VVVM будет являться всё тот же многострадальный bmp.
Уже запущен скоринговый сервер, я регистрирую команду IKS и получаю первые 15 очков.

Чтобы не сойти с ума в мире декартовых черепах, вам нужны чёткие, ясные идеи.

CODEWORD: CARTESIAN

Итак, джентльмены, нам требуется найти потерянную страничку Векторного Гипертекстового Фидонета, на которую нет ссылки. Это не иголка в стоге сена, но всё же. Какие будут идеи?

Самая первая идея - это тупой перебор всех 400*200*256*256 траекторий с проверкой, что они не покидают пределов картинки и заканчиваются в "чёрных" точках (0,0,0). Но это как-то не комильфо.

Вторая идея основывается на том наблюдении, что мир Черепашки T-симметричен. Можно выпустить траектории в обратном времени из всех "чёрных" точек, которых всего несколько тысяч. Это неплохая идея, но в итоге решено всё-таки воспользоваться намёком и плясать от шрифта.

Для начала нужно понять, как именно рисуются буквы. Открываем сгенерённую html'ку в emacs'е и комментируя куски javascript-кода ищем какую-нибудь одну букву.

Первое открытие слегка обескураживает. Используемый в Векторном Гипертекстовом Фидонете шрифт хоть и единственный, но довольно хитрый. По крайней мере часть букв, например "A", не являются элементами шрифта, а состоят из нескольких различных глифов. Да и сама страничка рисуется не последовательно, буква за буквой, а в довольно хаотическом порядке. Там прорисовалась палочка, здесь появилась закорючечка, тут вылез штришок. Ну DeeMon, ну злодей...

Второе открытие более обнадёживающее. Буква "C" рисуется без отрыва пишушей части Черепашки от canvas'а. Именно паттерн буквы "C" я использую для поиска следующих страниц.

Первой такой обнаруженной страницей становится 106:15/18.0:

С буквой "C" мне повезло, конечно. Мало того, что она есть на секретной странице, так еще и является самым первым символом, с которого начинается её прорисовка. IKS героически набирает 29 очков. Кайф.

Обнаруживается и страница 173:80/88.-75 с недостающим куском спецификации VVVM. Выглядит она, правда, в лучших традициях Векторного Гипертекстового Фидонета:

Неплохо, в принципе, но надо бы что-то получше.

После непродолжительного ковыряния с буквами выясняется, что "P" и "M" тоже рисуются по-человечески. Это позволяет быстро получить 23:71/-60.-15:

Но на этом сказки кончились и начался Феерический Тупняк №1, который надолго сбил меня с истинного пути.

Как известно, с canvas'ом есть такой нюанс, что целые значения координат соответствуют не середине пиксела, а линии между соседними пикселами. Мои Черепашки рисовали по целочисленным координатами, отчего все странички слегка замылены. В этом даже есть своя эстетика, но...

Как можно видеть на картинке, символ "+" выглядит практически неотличимо от "." (или \cdot в TeX). Я даже поначалу решил, что это так и задумано. Что сложения в наборе инструкций VVVM нет, зато есть некоторый специальный оператор ".", описание которого нужно искать на неизвестных науке Тёмных Страницах Векторного Гипертекстового Фидонета...

Поэтому вместо того, чтобы заниматься интерпретатором VVVM, я решаю вычислить все глифы используемого шрифта и с их помощью найти все недостающие страницы. Фундаментальное исследование.

К сожалению, участие в контесте было спонтанным и неподготовленным технически. Поэтому приходилось сильно отвлекаться на всякие домашние дела. И судя по git log, шрифт был известен мне только к 11 вечера. За эти три часа IKS скатился со 2-го на 6-ое, а потом и на 8-ое место.

Выглядит шрифт вот так:

Комбинации этих чудесных закорючек образуют буквы и цифры на страничках Векторного Гипертекстового Фидонета. Круто.

Не могу гарантировать, что это полный список глифов шрифта, но скорее всего это так. Можно заметить, что некоторые закорючки дублируются. Это означает, что на страницах Векторного Гипертекстового Фидонета они были начертаны разными способами. Не 20 способов написания слова "меч", конечно, но тоже ничего так.

Когда у вас есть алфавит, получение всех страниц Векторного Гипертекстового Фидонета - это дело техники. Для каждой точки исходной картинки (x,y) и каждого символа алфавита вычисляем все варианты (vx,vy), перебирая пары точек символа. Например для глифа "C" это даёт 400*200*56 вариантов, что очень быстро. Отбрасываем убежавшие за границу картинки траектории, отбрасываем траектории со слишком длинными линиями (скажем длиннее 15 пикселов), отбрасываем траектории, являющиеся подтраекториями более длинных и ву-ля - ни одной новой страницы. Увы.

Но унывать времени нет, настала пора поманьячиться с VVVM.

CODEWORD: MANIAC

Итак, джентльмены, на этот раз жизнь, похоже, не загадывает никаких загадок. От нас требуется взять и тупо реализовать интерпретатор VVVM по спецификации, изложенной на 4 страничках Векторного Гипертекстового Фидонета. Поехали.

Но поехать сразу не удаётся. Мои домашние злоключения еще не кончились и приходится отвлечься еще на час-полтора. За комп я возвращаюсь уже в первом часу ночи и принимаюсь лихорадочно клепать интерпретатор.

Интепретатор клепается быстро и начинает работать тоже сразу. Если не считать одного маленького, но забавного косяка. Хотя я, естественно, уже сообразил, что "." - это именно "+", но это нисколько не мешает мне считать, что ".=" - это ":=", а не "+=". В итоге как минимум полчаса на отладку инструкции 10 всё-таки убито.

Но в целом реализация интерпретатора проходит гладко и в половину третьего ночи перед моим изумлённым взором предстаёт

$ ./Main | head
Codeword: MANIAC
и еще примерно 240Мб специфического текста... Это впечатляет.

Таблица скоринга уверенно показывает 48 очков у IKS. План на завтра понятен. Всё - спать.

Когда у вас всё получается слишком легко, то вы двигаетесь вперёд с такой скоростью, что можете даже не заметить что-нибудь очень важное. Настолько важное, что это может вам стоить места в top5 и даже в top10. И тут будут бессильны даже Агенты K и J.

CODEWORD: MEN IN BLACK

Итак, джентльмены, программа для VVVM сгенерила нам впечатляющие 240Мб текстовых данных, которые из себя представляют описание задания и программы для 1048576 параллельных процессов, каждый из которых должен выполнить 7 итераций, и тогда Элли с Тотошкой возвратятся в Канзас. А для того, чтобы всё это случилось, нам предстоит плотно поработать. Приступим.

В субботу я проснулся в райне полудня и практически сразу занялся делом. Но сначала всё-таки состоялись скайп-перегоры с параллельной антивселенной, в результате которых мне удалось заинтриговать Лёху и включить его в процесс прохождения квеста. Это было совсем не сложно: "Жены нет дома, ребёнок спит..." Большим демотиватором было то, что лидеры уже завершили выступление, набрав свою сотню очков. Но за утешительные призы еще можно было побороться, да и вообще - с такой задачкой повозиться интересно.

Исходный bmp-файлик оказался ох как не прост... Вот какое описание квеста сгенерила программа для VVVM:

Imagine a system with a lot of parallel processes, each with its own program and variables. On each iteration each process runs its program once. After all of them ran their programs follows next iteration. Variables of each process keep their values between iterations. All variables are integer. Initially they are filled with arbitrary values between 0 and 255. Each process has a mailbox, can send values to other processes (put in their mailbox) and receive values by reading from its own mailbox.
Here are the programs:
Process 327438:
  send Value to process 233125,
  send Value to process 114668,
  send Value to process 113287,
  [Q,J,P,W] <- receive(4),
  T <- (Q + J + P + W + 2) / 4,
  Value <- -40 * T / 64 + 54.
...
Run 7 iterations. Now Value variables of processes are intensities of pixels of a 1024x1024 bitmap filled rowwise.
Нет, я понимаю, конечно, что DeeMon профессионально занимается алгоритмами обработки изображений, но всё равно - чертовски круто.

Ладно, что тут у нас? Насколько хватает глаз, все программы выглядят типовым образом. Но насколько типовым? И как тут правильно действовать? Может преобразовать все программы в хаскель? Синтаксис подходящий, потом останется только в правильной монаде всё это запустить параллельно. Можно, конечно, попытаться реализовать честный парсер и интерпретатор этого Эрланга. Вроде 7 итераций для миллиона с небольшим процессов - это вполне обозримо. Еще можно попытаться понять, как именно устроена этот итерационная конструкция, раз она сходится при любых начальных данных.

Поразмыслив, останавливаюсь на варианте с честным интерпретатором Эрланга, поскольку он выглядит наиболее предсказуемым.

Вооружившись Parsec'ом, уже через час я получаю приемлемый парсер для описания одного процесса и пытаюсь его натравить на весь файл. И тут меня ждёт засада..

Я ожидал чего угодно: того, что можно send не только Value, но и другие переменные, что не будут парситься выражения, что... но случилось гораздо более неприятное. Ко мне пришёл cabal hell. С Parcec'ом ко мне приехали новые bytestring'и, отчего перестал работать ghci, а значит прощай REPL и здравствуй "Edit-Compile-Run". С учётом того, что 240Мб парсились у меня порядка 2 минут, всё это было очень печально. Когда уже выпустят новую платформу?

Второй более прозаической проблемой стала память. Выделенных 32-битному процессу 4Gb памяти не хватало и он валился. Пришлось перейти на ленивые ByteString и основательно поработать над структурами данных. Основные инструменты: Ctrl-W и Ctrl-K, BangPatterns, "!" и прагма {-# UNPACK #-}. В финальной версии процессу стало хватать 500-600Mb для парсинга и исполнения программ. Правда мне еще пришлось увеличивать размер стэка до 16 мегов, что как бы намекает, что какой-то space leak где-то всё-таки остался. Ну, не суть.

Как только проблема с ограничением по памяти была снята, парсер сразу замечательно заработал и можно было приступать к интерпретатору.

Поначалу мне кажется, что тут не может быть никаких трудностей - запускай себе процессы один за другим и будет тебе счастье. Но счастья не происходит. Первый же запущенный процесс ломается на пустом mailbox'е. Ну конечно, по спецификации они же работают параллельно, значит функция receive просто должна подождать, пока в mailbox'е будет требуемое количество данных. Мне такой Эрланг реализовывать совершенно не улыбается. Поэтому решено сжульничать. Я делаю предположение, что процессы образуют ациклический граф и достаточно этот граф отсортировать топологически, чтобы узнать правильный порядок запуска. Но быстро выясняется, что в результате топологической сортировки наверх выплывает Process 0, у которого всё-таки есть [Q,T,A,J] <- receive(4). Хм... Что же делать?

Спасение находится в классическом методе преодоления пропасти в два прыжка. На каждой из 7 итераций все процессы сначала выполняют все свои инструкции send, а уж потом - все остальные свои инструкции. Конечно, мне приходится удостовериться, что это корректно. Заодно я проверяю и узнаю много других замечательных фактов: что все формулы типовые и определяются парой коэффициентов, что процессы получают ровно по 4 значения в mailbox, что порядок значений в mailbox'е не важен, как и имена переменных, и что сохранять между итерациями можно только значение Value.

И вот наступает счастливый момент первого запуска и на диске образуется файл с названием result.bmp, в котором зелёная компонента имеет значение искомых переменных Value. С замиранием сердца я его открываю и вижу мешанину из зелёных пикселов всех мастей и оттенков. Мда, беда.

Ну что тут делать? Надо искать ошибку. И чего я только не предпринимал. И менял типы данных с Int32 на Word8, и делал отладочную печать для отдельных пикселов, и выводил их цвета в csv'шку, и сравнивал эти csv'шки для разных начальных значений переменных от 0 до 255... И каждый раз это минимум 2 минуты на запуск.

Постепенно у меня растёт уверенность, что интерпетатор работает правильно, а ошибка в чём-то другом. Потеряв на эту долбанную отладку уже 4 часа времени, я в сотый раз перечитываю задание квеста и до меня доходит, в чём дело. А дело в было в Феерическом Тупняке №2.

Я выводил пикселы в файл не в порядке нумерации процессов, а в порядке их следования в файле. Мне почему-то казалось, что порядок следования в файле имеет какое-то значение.

В итоге, в 20:30 уже основательно испохабленный (в основном переводом с Int32 на Word8) интерпретатор генерит мне вот такое нечто:

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

Честно говоря, когда я смотрел недавно третью часть MiB, то плевался и зевал весь фильм. Однако ж, пригодилось. Так бы ни за что не догадался...

А тем временем в параллельной антивселенной жизнь в субботу развивалась своим чередом. Отчаявшись найти байты (18,18,42) в MATLAB'е, Лёха решил попытать счастья с C#. Однако в итоге наступил на те же грабли. Искомые значения RGB не встречалась нигде в загруженной картинке. Тогда Лёха сменил тактику и стал работать с файлом как с BLOB'ом, а не как с bmp-изображением. Это принесло свои плоды и команда SKI стала стремительно подниматься по турнирной таблице.

Поиск неизвестных страниц Векторного Гипертекстового Фидонета Лёха осуществлял через символ "N", т.к. в нём было всего три линии. Отсечение траекторий выходящих за границы картинки, отсечение траекторий со слишком длинными линиями - всё как в лучших домах. Впрочем, одно know-how всё же было. Для навигации между сгенерированными изображениями Лёха прикрутил графический интерфейс с горизонтальным слайдером. Я видел скриншоты - это сурово :-)

Таская туда-сюда слайдер, Лёха обнаружил недостающую страницу спецификации, но проворонил cartesian.

А настящая засада ждала его на квесте с VVVM. Он быстро реализовал интерпретатор и тот даже стал что-то печатать, но явно не целиком. Процесс доходил до фразы про "7 итераций" и дальше происходило деление на ноль и на этом всё заканчивалось.

С интерпретатором VVVM Лёха прошёл примерно те же круги ада, какие проходил я в то же самое время с Эрлангом: пробовал знаковые и беззнаковые типы, дебажил так и эдак, запускал разными способами... Даже научился "вышибать её из цикла", что бы это ни значило.

В итоге, всеми правдами и неправдами он получил-таки все куски спецификации и даже набрал 58 очков. Но работающего интерпретатора в субботу так и не получил, что почему-то помешало ему двигаться дальше. Странно. Не похоже на Лёху. Подумаешь, неполная спецификация и деление на ноль.

Если всё, что вам мешает, это Бороды Уодлера - применяйте бритву Хаттона. Народная мудрость.

CODEWORD: WADLER

Итак, джентльмены, квест пройден, но мы не досчитались 24 очков. Мы где-то что-то упустили и нам предстоит это исправить. За работу.

После получения названия фильма у меня образовалось 76 очков и я стал изучать скоринговую таблицу. С лидерами давно было всё понятно, но пролезть в top5 было еще совершенно реально. Я решил немного проветрить мозг после Эрланга и попробовать провернуть это дело.

И тут начинается история Феерического Тупняка №3, который по праву можно считать Великим Феерическим Тупняком.

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

Нехитрые наблюдения над таблицей скоринга подсказывают, что мне не известно, скорее всего, одно-единственное кодовое слово стоимостью 24 очка. Чуть более хитрые наблюдения говорят, что это слово становится известно некоторым участникам, которые еще не нашли maniac'а. Мою уверенность, что слово спрятано в первом квесте усиливает тот факт, что Лёха всё еще борется с интерпретатором VVVM, в котором он явно где-то жёстко накосячил, но имеет 58 очков. Это значит, что он идёт другим путём. Он не знает cartesian, но знает что-то другое.

В голове звенит тревожный звоночек, что вряд ли два дополнительных кодовых слова будут спрятаны в одном квесте, но я отключаю этот аларм и возвращаюсь к исходной картинке. Какие с ней варианты?

Есть вариант, что в Векторном Гипертекстовом Фидонете есть еще какая-то неизвестная страница. Построенная другим шрифтом, скажем. Есть вариант, что кодовое слово написано прямо в файле, как play, но каким-нибудь более хитрым образом, например по вертикали. Есть вариант... да море вариантов. И я начинаю их по очереди один за другим проверять, постепенно подбираясь к своей основной гипотезе.

Дело в том, что еще при первом поверхностном знакомстве с картинкой, мне показалось, что на ней есть Watermark или какое-то другое скрытое изображение. От DeeMon этого можно было ожидать. И исключив несколько более простых схем, к ночи я плотно приступаю к Стеганографии. Чего я только не пробую, какие только алгоритмы не применяю... Читаю википедию и тематические статьи. Никак.

Но самое интересное заключается в том, что мне кажется, будто я просто вижу это слово на картинке. Я даже поинтересовался у Лёхи в скайпе, не 6 ли букв в этом слове и не заканчивается ли оно на "ER". Оказывается, что всё так и есть. Ну вот как тут было поверить в случайное совпадение? Я не поверил.

Но разглядеть первые 4 буквы никак не удавалось. От отчаяния я даже попробовал повбивать просто рандомные слова, типа "cipher" или "header". Но скоринговый сервер уверенно говорил, что эти слова никуда не годятся. В середине первого ночи я понял, что моё слово заканчивается то ли на "DER", то ли на "LER". Чёрт возьми, я до сих пор не знаю, глюки ли это были или?..

Upd. DeeMon развеял все сомнения. Глюки, это были глюки...

Короче говоря, наутро я проснулся совершенно разбитый, демотивированный и с больной головой. И ни единой идеи. Даже хотел забросить это дело. Но как тут забросишь, когда первое же сообщение, которое приходит с утра в скайп выглядит так: "Скажи, у тебя в машине знаковые или беззнаковые слова?" И я опять возвращаюсь к Стеганографии...

Я опять пробую по кругу все вчерашние идеи и проверяю пару новых, включая то, не является ли эта картинка стереограммой :-) Никакого толку. К трём часам дня я уже в полном отчаянии и ужасно уставший. Да еще Лёха троллит по скайпу: "А как только ты узнаешь, где твои 24 очка, немедленно напьешься с горя :)))" На себя бы посмотрел! Если бы я не подсказал про буферизацию, то до сих пор бы небось с 58 очками сидел :-)

В параллельной антивселенной тем временем действительно продолжается неравный бой с интерпретатором VVVM. Чтобы исключить любые возможные глюки .NET'а Лёха переписывает реализацию интерпретатора на C. Но это ему не помогает. Программа печатает код для миллиона с копейками процессов и валится с делением на ноль либо зацикливается - в зависимости от выбранных типов данных.

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

К часу дня Лёха в отчаянии просит прислать ему исходник моего интерпретатора и сверяет побайтово со своим. Но даже это ему не помогает... Наконец в половину второго я предположил

- может у тебя какая-нибудь буферизация вывода
и ты просто не видишь последний кусок напечатанный?
И получил долгожданный ответ
- $@$@%#$^@%@%$$@^@$&@$^@%$@#$%@$%@$%~!!!!!!!!!!!!!!!!
- Я ДЯТЕЛ!!!!!!
- ДЯТЕЛДЯТЕЛДЯТЕЛ!!!!
Ну, слава богу, я уж боялся, что не дождусь. Восемь часов отладки прекрасно работающего с первого раза кода. Почти так же круто, как мой Феерический Тупняк №3.

А выяснилось вот что. В случае C#, несмотря на то, что выходной файл был обёрнут в using, Лёха, видя исключение на тему "division by zero", воспринимал это как ошибку и срубал процесс средствами дебаггера. В случае с классическим C без диезов всё было еще проще - деление на ноль сразу приводило к завершению процесса. В обоих случаях никакого flush не происходило...

А вот с Эрлангом Лёха расправился невероятно быстро. Предположив, что все формулы стандартного вида и применив тот же хак с преодолением пропасти в два прыжка, уже через полтора часа он получил 86 очков и обошёл меня в таблице результатов. Круто.

У меня же Феерический Тупняк №3 был в самом разгаре. В половине четвёртого я решаю "сверить часы" с Лёхой - проговорить явно те вещи, которые мы с ним оба знаем. И начинаю перечислять всякие очевидные факты: известные обоим кодовые слова, их стоимость и тому подобные вещи. И когда в ответ на какой-то простой и "очевидный" мне вопрос Лёха уклончиво заявляет, что "пойдёт с ребёнком гулять", в голове звенит уже не звоночек, а колокол. Какого чёрта я вожусь с первым квестом?! Что там со вторым, который я проскочил на всех парах? О боже! Не может ли там быть еще одной программы для VVVM?! Omg!

По-моему, Лёха еще даже собраться не успел, когда я прислал ему следующее:

for i in {33..240000} ; do echo $i; ./Main $i 2>/dev/null | head ; done;
33
34
Codeword: 35
36
Codeword: Wadler
О, Великие Боги Самуи! Вы даже не можете себе представить, в каком расстройстве я был. Сгоряча даже заявил
- блин, и на это я убил столько времени?
- это кодовое слово для криворуких программеров,
которые сразу не могут нормальный интерпретатор
реализовать и поэтому пробуют его с разных точек запускать
Но, если честно, то своя логика есть. В каждом квесте повторялся паттерн "сделай интерпретатор и запусти его на всех входах". Можно было и сообразить... А так - 11-ое место.

К этому моменту самые шустрые команды уже успели опубликовать свои отчёты и я углубился в их чтение. У меня, вот, на написание этого поста практически неделя ушла, а они по горячим следам успевают. Молодцы. Выяснилось, хотя я это и сам подозревал, что картинку можно построить и более качественную. На разные варианты художеств, включая правильные - без артефактов, - можно полюбоваться в отчёте _winnie и комментах к нему.

Вернувшись через два часа с прогулки, Лёха фактически мгновенно получил свои недостающие 14 очков. По состоянию scoreboard'а он без труда выяснил, что кодовое слово спрятано в первом квесте. Естественно, сразу сообразил, что есть неизвестная ему страница, и наверное дождаться не мог, когда можно будет вернуться за комп :-) Поиск недостающей страницы он осуществлял через "скобочку от E", что дало ему около 400 картинок на слайдере. Среди них попалась и искомая - не до конца отрисованная, но вполне читаемая.

Всё, у SKI и IKS по 100 очков. Пора закругляться.

Annihilation

Итак, джентьмены, время подводить итоги.

После того, как мы оба завершили выступление, мы почти час трепались по скайпу: делились впечатлениями, восстанавливали хронологию - в общем синхронизировали состояние антивселенных SKI и IKS и сколлапсировали их обратно как смогли.

Основной вывод: если участвовать, то всё-таки командой. Думаю, что дополнительных аргументов тут не требуется. И касается это не только соревнований и программирования.

DeeMon'у респектище.

Результаты.

Кухня

Отчёты других участников в комментариях к заданию.

Всё. Всех с наступающим!

среда, 8 августа 2012 г.

ICFPC-2012: Время собирать лямбды и разбрасывать камни

Предыстория

Начать это повествование стоит с того, что после феерического участия в ICFPC-2010 мы с Лёхой на ровном месте продолбали ICFPC-2011 (а какие отчёты народ делает...).
Нет, мы не были ни в разъездах, ни в отпусках, и вообще - не имели никаких уважительных причин. Просто прочитали задание и решили, что... ну скучно же. Нет online-скоринга?! Нет необходимости разгадывать какие-нибудь адские ребусы с адскими кодировками?! А в чём прикол тогда? Где драйв? Что будет поддерживать в нас высокий боевой дух в течение трёх суток?
В общем, пожали плечами и занялись своими делами. И только к вечеру субботы, вчитавшись в интернет и обнаружив дуэльный сервер, я стал проникаться идеей Lambda Gathering и спамить Лёху смс'ками. Утром в воскресенье проснулся с чётким пониманием, что нужно было участвовать. В панике написал на коленке интерпретатор, но в итоге никаких содержательных стратегий мы реализовать не успели и ничего не сабмитили...
Главным результатом этого фейла стало то, что в этот раз мы договорись всё-таки начать решать задачу, а там видно будет.
Организаторы слегка подогревали интерес, периодически публикуя что-нибудь наподобие видео про батут. Отчего я заставил Лёху прочитать про Trampolining a monad computation зачем-то...
Для тренировки порешали задачки с ICPC-2012. Правда Лёха их больше сводил к задачам матпрога и решал с помощью промышленных солверов за какие-то микросекунды. Но я честно повозился с переборными алгоритмами на хаскеле и руку поднабил. Отчасти это сыграло свою злую роль в будущем. Но не будем забегать вперёд.
Еще я поставил правильный Debian на два сервака, которые символизировали собой наш "вычислительный кластер". Деятельность эта закончилась в аккурат в момент публикации задания, так что даже дух перевести было некогда.

Пятница, 13, 16:00

По плану мы должны были осмыслить задание и через два часа встретиться дома у Лёхи в Ближнем Замкадье, где и провести время до утра понедельника. В процессе осмысления я даже проехал нужную станцию метро.
Ну ладно, встретились, стали разбираться, что к чему... Итак, как мы все знаем, в связи с огромным ростом популярности функционального программирования, в мире стало жёстко не хватать лямбд. Однако оказалось, что нехилый запас имеется в шахтах Шотландии (нет ли тут, кстати, какого-нибудь коррупционного совпадения, что в этом году организаторы как раз из этих мест?) и вопрос только в том, как их добыть. Оказалось также, что добывающий робот имеется в наличии, карты шахт тоже есть, нужно лишь составить последовательность команд для робота.
По сути, задача сводилась к написанию программы, которая получает на вход описание лабиринта, а на выходе формирует последовательность команд для его прохождения роботом. Команды могут быть L (влево), R (вправо), D (вниз), U (вверх), W (пропустить ход).
Самый простой лабиринт, из предложенных организаторами выглядел так:
######
#. *R#
#  \.#
#\ * #
L  .\#
######
R - робот, которым мы управляем
# - стенки, через них пройти нельзя
. - земля, её можно сгрызть, тогда она превращается пустую клетку
\ - лямбда... её надо собирать
L - лифт, выход из лабиринта, который откроется после сбора всех лямбд
* - камень...
Камни - это самое интересное. Камни можно двигать, камни могут упасть на голову и убить, они могут завалить проход или наоборот - открыть его. Камни скатываются с других камней и с лямбд направо и налево по определённым правилам. И даже могут сколлапситься друг в друга (rocks crash, yeah...).
Всё это должно звучать более или менее знакомо для любого, кто играл в digger, boulderdash, supaplex или любой из их клонов.
Кому хочется формальных подробностей - вот задание. Есть официальный validator, где можно попробовать свои силы на предложенных организаторами простых картах. Или вот тут вариант повеселее, тем более, что там почти 2000 "народных" карт. По слухам, там на ваших решениях люди какую-то нейросеть пытаются натаскивать... Ну не знаю...
Кстати, на упомянутой простейшей карте максимум можно набрать 212 очков и возможная программа прохождения такая: LDRDDUULLLDDL.
Организаторы обещали еще до 4 дополнений к заданию, поэтому было понятно, что наши "алгоритмы и структуры данных" должны быть "достаточно общими".
Всё это мы довольно быстро осмыслили, повозмущались опять отсутствию online-скоринга и взялись за дело. Я вооружился спецификацией и стал писать интерпретатор, который по данной карте и последовательности шагов робота вычисляет новое состояние карты и положение робота, а также количество набранных им очков. Лёха стал ковырять официальный валидатор. Периодически у меня возникали вопросы по спецификации и Лёха выяснял ответы путём эксперимента.
Интерпретатор был готов уже где-то к 11 вечера в пятницу. Он работал чертовски медленно, потому что представлял всю карту как один большой немутабельный двумерный массив, который целиком копировался на каждый чих. И он не был покрыт тестами совершенно. Но работал.
Примерно в то же время, как у нас появился интерпретатор, организаторы опубликовали первое дополнение к заданию. Им стали наводнения. Ну понятное дело, погода в Шотландии плохая, шахты подтапливает. В принципе робот обладает некоторым запасом водостойкости, но увлекаться не следует - периодически надо выныривать из воды, глотнуть свежего воздуха лямбда-шахт.
На дополнения мы сразу забили, решив в субботу сосредоточиться на главном: на алгоритме поиска решения. При этом еще зачем-то часа два ночью проспорили о том, будет ли на этой задаче работать метод динамического программирования или нет. Я считал, что будет, Лёха считал, что нет. В конечном итоге он оказался прав, но меня в тот момент не убедил.
Главным Лёхиным аргументом была мощность фазового пространства, неподъёмная даже на маленьких картах. Я же считал, что поскольку все карты подобраны вручную организаторами, то там будет очень мало содержательных траекторий робота, т.е. что нас будет интересовать ничтожное подмножество этого фазового пространства. Кроме того, была идея, что многие формально различные состояния игры легко отождествляются. Например если некоторый камень уже пролетел мимо робота, то можно смело считать, что он уже упал до самого низа. Догнать его роботом и изменить его траекторию уже невозможно. Из этой идеи должен был появиться "интерпретатор с большим шагом", но так и не появился...

Суббота

Если проигнорировать некоторое количество коммитов с комментариями, содержащими словосочетание "evil eclipse" (да, Лёха использует EclipseFP, а я - emacs), то первое, что было сделано в субботу, - это генератор случайных карт. Он быстро позволил нам выяснить, что интерпретатор никуда не годится. Точные цифры не помню, но большие карты он интерпретировал просто неприлично медленно. С одной стороны, мы рассчитывали переписать конечное решение на C/C++, но с другой стороны даже для исследовательской работы интерпретатор был признан малопригодным (и наверное зря...). Возникла задача его оптимизации. С ней я справился только к трём часам дня. Очень подвело отсутствие тестов. Параллельно Лёха написал вариант интерпретатора на C#. Он работал еще раз в 10 быстрее, чем моя оптимизированная реализация, но оказался нежизнеспособен: баги, мутабельные структуры данных итп. Даже Лёха признал, что C# не рулит.
Но главной моей ошибкой было то, что новый интерпретатор я делал, имея в виду конкретный алгоритм поиска решения, а именно упомянутый выше метод динамического программирования, и все структуры данных интерпретатора были неудачно заточены под него. В частности, меня очень интересовала скорость сравнения двух "состояний игры". Эх, Лёха, что же ты меня за руку в этот момент не поймал...
Наверное правильнее было бы оставить медленный "референсный" интерпретатор и сделать к нему набор тестов. А уже потом, определившись с алгоритмом поиска пути, переписать всё еще разок. Может быть даже и на C/C++.
Пока я возился с интерпретатором, Лёха не терял времени и исследовал содержательную часть задачи. Видимо он хорошо понимал, что несмотря на моё сопротивление, нам нужны будут приближённые или вероятностные методы поиска решения. Периодически на его мониторе я мельком видел страницы википедии типа Ant colony optimization algorithms и еще что-то на ту же тему с wolfram'а итп. К вечеру я уже и сам осознал, что динпрог в чистом виде здесь не сработает. Мы просто не сможем придумать и реализовать достаточное количество "тонких" оптимизаций, чтобы не перебирать всё фазовое пространство. Именно так к 11 вечера появилась первая версия алгоритма с муравьиной колонией. И - чёрт возьми! - некоторые маленькие карты муравьи проходили вообще оптимально, а на больших картах жрали хоть какие-то лямбды. Еще несколько часов ушло на допиливание и изучение алгоритма. Последний коммит в субботу был сделан в районе 3 часов ночи. Кстати, это был очень содержательный коммит. В нём был алгоритм "естественного отбора" (т.е. поддержания размера колонии муравьёв в заданных пределах), алгоритм присваивания цели муравьям ("иди к той лямбде"), куча исправлений и оптимизаций. Короче, вечер пропал не зря.

Воскресенье

Утром вставать было совсем тяжко. А впереди было море работы. Стало ясно, что переписать всё на C/C++ - это утопия. Поэтому пришлось улучшать и чистить то, что есть.
Лёха взялся за эвристику, позволяющую муравьям идти по градиенту потенциальной энергии к цели. Я же стал внедрять дополнения. Кроме наводнений к тому времени уже были известны те самые трамплины (чтобы роботу было проще перемещаться по карте, ага) и бороды Водлера. А в четыре дня стало известно, что лямбд гораздо больше, чем мы полагали раньше. Просто нужно уметь добывать их из камней высших порядков.
Все дополнения были реализованы только в первом часу ночи. Я сейчас даже не помню, почему это было так долго. Но ясно, что тесты на интерпретатор спасли бы отца русской демократии... Потому что значительную часть времени он ловил какие-то изумительные баги. То лямбды, вывалившиеся из камней, продолжали свободное падение. То камни с лямбдами не убивали робота. То бороды отказывались нормально расти. То лифт отказывался открываться после съедания всех лямбд. Весь день я как мантру повторял одну и ту же фразу: "Алгоритмы и структуры данных". Тяжелое наследие несостоявшегося метода динамического программирования сквозило в каждой строчке кода...
По моим ощущениям, Лёха тоже день провёл крайне "плодотворно". Он свёл задачу к какой-то задаче линейного целочисленного программирования и попытался заставить какой-то matlab'овский солвер её решать. Я не вникал, чем дело кончилось, но помню, что робот долго ходил по стенам, потому что штрафы от этого ему казались пренебрежимо малыми по сравнению с мучениями по честному сбору лямбд... Но расчёт потенциальной энергии Лёха всё-таки сделал к вечеру, и муравьёв вроде как стало притягивать к лямбдам "более лучше".
Короче, Лёха успел в этот день изрядно порефлексировать над тем, что мы на самом деле сделали, и озвучил интересный прогноз, что мы пройдём первый тур и завалимся во втором. Я же весь день прозанимался какими-то частностями, поэтому не сразу понял, откуда такая уверенность.
А потом понял и согласился. Первый раунд по замыслу организаторов должен проводиться на картах небольшого размера. На таких картах наши муравьи могут перебрать практически все варианты ходов, поэтому вопрос о прохождении во второй тур сводится к вопросу о корректности интерпретатора. Так что первая часть Лёхиного прогноза - это просто его оценка моих способностей кодировать по спецификации :-) А вторая часть прогноза - это оценка способностей нашей муравьиной колонии решать поставленные перед ней задачи...
Во втором часу ночи мы занялись подготовкой пакета для отправки организаторам, потому что возможности по продолжению банкета в понедельник были сильно ограничены.

Понедельник

В понедельник я проснулся полон сил и энергии улучшать мир. Хотя бы малую его часть. Хотя бы один муравейник. Лёха, наоборот, сказал, что ужасно не выспался и делать что-либо не в состоянии.
Но добравшись до работы он всё-таки сделал очень полезное исправление, научив потенциальную энергию лямбды распространяться через трамплины.
А я успел классифицировать ходы муравьёв на "интересные" (съел лямбду, залез в трамплин итп) и "неинтересные" (ничего не случилось), рандомизировал неинтересные ходы, исправил кучу багов итп.
В итоге финальная сборка готовилась за 15 минут до дедлайна, как оно обычно и бывает. И вот тут уже драйва было выше крыши...
...В ночь с понедельника на вторник я спал сном младенца. Часов 12, не меньше.

Результаты

Пока я неспешно готовил этот пост, организаторы столь же неспешно провели первый раунд. Как и говорил Лёха, этот барьер мы взяли, заняв со своими муравьями 80-ое место (в полном соответствии с результатами 2010-го года...). Всего во второй раунд прошло 117 команд. Немного напрягает то, что на третьей карте мы набрали -14 очков, что как бы намекает, что некоторые баги в интерпретаторе всё-таки остались.
Ждём результатов второго раунда... Upd. Ну вот, всё по плану...

Некая рефлексия aka Выводы

Хочется отметить некоторые особенности задания.
Во-первых, уже не первый год обязательной частью задачи для ICFPC становится программирование по спецификации. Не знаю, в чём тут великая идея, но похоже, что это играет роль порога вхождения. Хотя мне, если честно, больше нравится, как это было устроено в 2010-ом году с разгадыванием префикса. Причём в отличие от прошлого года, в этот раз еще были 4 дополнения к спеку, чтобы жизнь не казалась мёдом.
Во-вторых, в который раз отмечу отсутствие online-скоринга. Узнавать свои результаты через несколько недель - это очень уныло и демотивирует чертовски. В прошлом году хотя бы дуэльный сервер был... В общем, это тоже становится печальной традицией.
В-третьих, тенденцией становится использование "игровой" модели соревнования, а не "квестовой". Наверное одним из самых ярких квестов в истории ICFPC был про Endo. А в последние годы организаторы всё чаще предлагают самим участникам побороться друг с другом. В 2010-ом это была возможность сабмитить свои машинки. В 2011 - это вообще были дуэли программ, т.е. игра в чистом виде. В нынешнем году была возможность засабмитить набор своих карт. Организаторы намекали, что самые интересные карты могут включить в состав официальных.
Что касается непосредственно нашей команды IKS, то все выводы 2010-го года остаются в силе :-) Хотя готовы мы вроде были чуть лучше, но на результате это визуально не сказалось. Главным же достижением я считаю то, что мы всё от и до сделали на хаскеле - языке, с которым профессионально практически не работаем, но который нравится и взрывает мозги своей красотой и сложностью.

Отчёты

Классика жанра - отчёт _adept_'а
И еще: отчёт sergeif'a, отчёт undefined.org.ua, отчёт 7-12, мини-отчёт yantayga, атчод sorhed, отчёт xoposhiy.