ICFP Contest. Команда EyesUpGuardian
Intro
Доброго дня, сьогодні з вами на зв’язку команда EyesUpgGardgian, яка приймала участь у ICFP Programming contest
Для тих, хто не знає, що то є: ICFP Programming Contest - це щорічне командне змагання зі спортивного 🤔 програмування, на 72 години. Зазвичай, можна використовувати будь яку мову програмування, і кількість людей в команді рідко буває обмежена.
Team Up
Зазвичай, у нас є деяка кількість людей, які час від часу приймають участь в цих змаганнях, і ми гуртуємося за декілька місяців, прикидуємо, хто буде, на чому будемо писати, і таке інше. Цього року, як ви всі розумієте, через те, що росія досі знаходиться на території нашої держави, і у людей зараз зовсім інші пріорітети ніж у звичайні роки. Тому активність людей в чаті була на мінімумі. Крім того, самі дати змагання цього року співпали з початком учбового року, що ще зменшило і так невелику потенційну кількість учасників. Це я ще мовчу про те, що за тиждень до початку змагань в мене народилася дитина ¯\_(ツ)\_/¯.
Власне це думка про те, що потенційно в цьому році, могло нічого і не вийти. Але якось вийшло.
Start?
У день початку змагань, я розумію, що потенційно, деякий час я все-таки зможу виділити на “трошки поICFPFшити” ( режим устаканився, всі мої більшість часу сплять, тому чом би й ні).
Я зконтактував з @nobidon (єдиною людиною, хто якось виявляв активність). І ми вирішили, що б хоча б в Lighting Round (перші 24 години), ми участь приймемо.
Language Selection
Цього року довго роздумувати не довелося, тому що, ніяких домовленостей не було, і ми просто використали Kotlin (знову). В цілому, через те що, у нас був тогорічний проект, можна втягувати Java libraries, і… і не знаю чому, але досі, якщо мене запитати, що я виберу між Swift і Kotlin, нажаль?, я виберу Kotlin. (Можливо, наступного року 😏)
В ультра швидкі терміни, я створюю новий репозиторій, вибираю все необхідне з попередньгоо року (куча helper-функцій та методів, які душе пришвидшують роботу в такого роду змаганнях). Затягнува візуалізатор з попереднього року, і зробив можливість писати тести.

+1 Player (Github copilot)
До цього часу, я бачив Github Copilot тільки на відосах, і використовувати його не доводилося, але чомусь, при підготовці, я натрапив на посилання про нього, і виявилося, що через те, що свого часу я багато комітив в Open Source, я можу його використовувати Free. (Ось вона, сила Open Source).
Тому, в нашій команді стало на одного учасника більше. (Github Copilot зіграв дуже значну роль цього року, але про це пізніше)
Day 1
15:00 - Команда EyesUpGuardian у складі Олексія (@nobidon), Павла (@TT_Kilew) і Github Copilot починає свою участь у ICFP Contest 2022
Завдання
Якщо дуже спростити, то завдання цього року було у трансформуванні одного зображення в інше за допомогою заданого набору команд.

Особливістю цього завдання, що з одного боку, треба було якнайточніше відобразити задане зображення, а з іншого боку, команди, якими мали оперувати команди, мали свою ціну, і чим з меншими блоками доводилося працювати, тим більша була ціна команди. (Наприклад, замалювати всю картинку 400х400 пікселів “коштувало” 1 одиницю, а замальовування блока 40х40 обійшлося б у 100 одиниць. Операції з меншими блоками - були ще дорожчими)
15:03 - ми розбираємо задачі, я беру на себе зробити базову структуру, яка дозволить використовувати команди, а Льоша пише клас, який дозволить серіалізувати нашу програму для подальшої відправки на сервер.
No solution is a solution
Завжди перевіряйте, чи можете ви відправити свої результати.
Просканувавши специфікацію завдання, я не побачив там ніякої інформації про API для завантаження рішень, і вирішив подивитися, що є на сайті. На сайті була форма, яка дозволяла відправити “програму” для кожного завдання, і показувала результат, який ми отримуємо. В формі не можна було відсилати “пусте” значення, відповідно що треба було надсилати правильні команди 😢. Але, звернувши увагу, що програма допускає пусті рядки, і коментарі, я швидко вирішив запостити пусту програму з коментарем. І…. і отримав результат!
<program> ::= <program-line> | <program-line> <newline> <program>
<program-line> ::= <newline> | <comment> | <move>
<comment> ::= "#" <unicode-string>
15:24 - Я швидко пробігся по всім задачам і відправив “пусті рішення”. Це дозволило нашій команді посісти 3-те місце! 🎉 (Я люблю казати, що в цьому змаганні - головне - це вчасно зробити скріншот)

15:51 - У нас є код, який дозволяє завантажувати проблеми у вигляді, з яким ми можемо нормально працювати. Поки базова ідея - це просто спрочатку якимось чином розбити картинку на декілька блоків, визначеного кольору, і намагатися після іх зафарбувати.
17:21 - Нарешті, ми можемо правильно обчислити, наскільки ми “промахнулися” зі згенерованою картинкою. Оновлюємо базове рішенння. Тепер замість пустої картинки ми просто надсилаємо квадрат, який замальований усередненим кольором того, що треба отримати :) Це дозволяє нам піднятися на 12 місце. Водночас, дивуємося, що іноді “усереднений колір” дає гірші результати, ніж пусте рішення🤔. Я, тим часом, відпадаю на декілька годин.
18:22 - У нас з’являється інтерпертатор, за допомогою якого ми можемо дуже зручно писати код, який потім трансформується в команди. В основному, це дозволяє легко оперувати блоками, які утворються після розрізання картинки
val builder = ISLBuilder()
builder.color(builder.root, Color(100, 100, 100, 255))
21:20 - Льоша дописує інтерптератор, намагаючись перемогти playground, який був предоставлений організаторами - той видає помилки, в залежності від браузера. Але тепер можемо рахувати наскільки важкими виходять наші програми.
21:30 - API для upload’а все ще немає, а заливати рішення руками, якось не дуже хочеться. Тому, підгледівши за допомогою Chrome Dev Tools, що саме, і куди надсилає сервер, ми реверс-інжініримо протокол обміну, і робимо собі можливість надсилати рішення в автоматичному режимі. Для тесту - надсилаємо всі рішення сірим кольором, і вони всі проходять. Треба сказати, що в умовах не було сказано, чи враховуються тільки кращі надіслані рішення, чи останні, і заливка нових рішень дозволила перевірити це питання.
22:07 - Бачимо, що порахована нами вартість дуже сильно розбігається з тим, що рахує сервер. В два підхода, міняємо всі Int на Double і отримуємо правильний результат*. Насправді, результат коливався десь в рамках декількох одиниць, але на фоні вартості у тисячі, це було не дуже принципово.
22:32 - Ми оформлюємо абстрактний “вирішувач” Solver, задача якого по заданій картинці повернути набір команд. Для тесту - розбиваємо основну картинку на дві частини і заливаємо їх усередненим кольором. Наче все працює. Одразу додаємо візуалізацію того, яку картинку ми надсилаємо, і зберігамо рішення, з його оцінкою.
2_23523.solution

22:35 - На разі - єдина ідея, куди рухатися далі - це писати вирішувач, який буде пропонувати розрізи блоку, фарбувати їх усередненим кольором, і дивитися, який розріз призведе до кращого результату.
23:45 - Пробуємо розріати картинку 4х4 і зафарбувати. Вигрібаємо на тому, що осі координат у нас направлені як попало в різних місцях. Через це, ми генеруємо перевернуту картинку, а місцями, наші команди фарбування, розфарбовують не там де треба. В цілому, не проблема - треба просто дивитися на згенеровані картинки - ось так:

Day 2
00:12 - дописуємо можливість робити так, що будь який “вирішувач”, який вирішить задачу з кращим результатом - зберігає свій результат, а всі інші ігноруються. Крім того, додали можливість в один запуск - відправити на сервер всі найкращі рішення, які на цей час згенерували. На майбутнє це дозволяло запустити вирішувачі задач з різними параметрами на деякий час і піти пити чай, а потім повернутися, і просто залити найкращі рішення.
01:18 - додаємо варіант вирішувача, який розбиває картинку на N рівнів і фарбує все що можна.
03:54 - я переписую логіку, як ми генеруємо рішення - ми йдемо в два кроки - спочатку генеруємо можливі варіанти, після цього вибираємо один з найкращих, і так до тих пір, поки у нас є можливість робити дії (розрізи, або фарбування), не збільшуючи вартість програми. Рішень стає забагато, і доводиться ннаписати скріпт, який підчищає не найркащі рішення.
05:40 - запускаємо наші алгоритми, всі що є, і йдемо спати, поки комп’ютери працюють.

08:40 - після недового сну, бачимо, що ми нарішали на 13 місце, що в принципі, не так погано, зважаючи на обставини.
09:40 - оптимізуємо вирішувач, який дозволяє не фарбувати все підряд, а робити розріз картини на 9 частин;, і фарбувати лише одну. Часу ні в кого з нас немає, тому просто запускаємо рішатори, і йдемо по справах.
13:40 - залишається менше ніж дві години до кінця Ligting Roundу (окремий залік, коли враховуються рішення, які були дані в перші 24 години). Єдина оптимізація, до якої в мене доходить голова - це після того, як сформована програма, перевірити, а чи не можна, часом підтюнити кольори, якими ми фарбували картину. Логіка полягала в тому, що якщо ми фарбуємо блоки наприклад у колір Х, а потім частину цього блоку знову їх перефарбовуємо у новий колір Y, то нам вже не суть важливі ті пікселі, які перетерлися новим кольором. І можна перерахувати колір Х в Х’, так, щоб отримати краще значення, обраховуючи лише ті пікселі, які залишилися.

15:00 - Кінець Lighting Round’у. Ми підтягнули свої результати, і отримали зрізали пару тисяч балів, за допомогою різних оптимізацій. Набагто пізніше, ми дізнаємося, що зайняли 29 місце.
P.S. Вважаю, зайняте 29 місце, скоріше перемогою, ніж зрадою :). Часу було обмаль, нас було двоє + Github Copilot. Звичайно, приємно було б попасти хоча б в топ 10, але, маємо що маємо :)
Day N
New Tasks
Далі були ще два дні контеста, в яких ми приймали участь виключно епізодично, і просто підтримували код, щоб він міг працювати з новими типами завдань. Так, спочатку додалися завдання, які були розділені на багато частин. Тому спочатку треба було частинки об’єднати, і вже потім фарбувати.
В теорії, організатори розраховували на те, що учасники будуть використовувати початкові кольори і блоки, але, наскільки мені відомо, всі команди пішли шляхом - спочатку об’єднуємо всі блоки за найменшу вартість, а потім вирішуємо попередню задачу.

Moar optimizations
Для того, щоб наші лептопи не просто так використовували процесорний час, ми зробили декілька оптимізацій.
Так, багато часу займали операції з картинками, і роботою з пікселями, тому базові функції бібліотеки просто довелося заінлайнити
red += color shr 16 and 0xFF // red += color.red()
green += color shr 8 and 0xFF // green += color.green()
blue += color and 0xFF // blue += color.blue()
alpha += color shr 24 and 0xFF // alpha += color.alpha()
Дуже вчасно ми згадали, що порядок обходу у картинках дуже важливий, і просто помінявши порядок обходу пікселів, ми пришвидишили функцію в три з гаком рази
// Fast Version (3x speedup)
val imageData = (image.awt().raster.dataBuffer as DataBufferInt).data
for (y in block.minY.value until block.maxY.value) {
for (x in block.minX.value until block.maxX.value) {
// Slow Version
val imageData = (image.awt().raster.dataBuffer as DataBufferInt).data
for (x in block.minX.value until block.maxX.value) {
for (y in block.minY.value until block.maxY.value) {
Github Copilot
Окремо хочеться сказати про Copilot. Я був приємно вражений тим, наскільки швидко за допомогою нього можна писати код. Без нього, не факт, щоб нам вдалося наблизитися до тих результатів, які ми отримали. Часто я просто перестав перевіряти те, що він пропонує і просто давав змогу згенерувати код.
Ось такого коментаря було майже достатньо, щоб зробити те, що я хотів
// parse list of initial blocks from json file
А коли я зрозумів, що для того, щоб правильно розпарсити JSON, йому не вистачає контексту, то просто додавши приклад JSONу, моментально отримав код, який розпарсив його і розклав в мої модельки
// {"width": 400, "height": 400, "blocks": [{"blockId": "0", "bottomLeft": [0, 0], "topRight": [
val blockId = block.jsonObject["blockId"]!!.jsonPrimitive.int
val bottomLeft = block.jsonObject["bottomLeft"]!!.jsonArray.map { it.jsonPrimitive.int }
val topRight = block.jsonObject["topRight"]!!.jsonArray.map { it.jsonPrimitive.int }
val color = block.jsonObject["color"]?.jsonArray?.map { it.jsonPrimitive.int }
Магія ✨. Всім раджу хоча б спробувати.
Results
Ми, команда EyesUpGuardian, заняли десь
29 місцеза перші 24 години47 місцев результаті, за 72 години.
Чи ми задоволені? І так і ні :) Завжди хочеться бути вище, сильніше і краще. Але свою частину веселощів ми отримали, ну то й добре.
P.S.
Окремо хочеться подякувати ЗСУ і всім Українцям, які зараз дають можливість мирно робити свої справи під час війни.
Для того, щоб наступного разу ми, як команда виступили краще, треба задонатити сюди :)
Слава Україні! 🇺🇦