strokoff

Хакатон DATSART — обзор и опыт участия

Хакатон DatsTeam

Сегодня, буквально спустя пол часа после финала, пока еще по горячим следам, хочу поделиться с вами историей о моем участии в команде DreamTeam вместе со своей женой, в хакатоне от кампании DatsTeam. Постараюсь изложить события в хронологическом порядке и уже самому взглянуть на свою работу со стороны.

Итак в чем краткая суть задачи? Существует виртуальный холст на который можно наносить краску путем броска краски через абстрактную катапульту, в катапульту можно положить разную краску, чтобы получить желаемый оттенок, саму же краску необходимо добывать на складе, а управлять стреляя катапультой через примитивные ‘баллистические’ расчеты. Хакатон проходит в два этапа, в 17:00 старт и до 16:00 следующего дня у вас есть время научиться всем этим управлять и показать какой-то результат. После в 17:00 второго дня, начинается финальная часть на 3 часа. Поехали

Первый этап

Старт — 17:00 — 20:00

Хакатон начинается с публикации документации на сервис API в котором кратко изложены доступные функции разработчикам.
И первый же шквал критики на создателей и разработчика хакатона (автор разработки хакатона Вадим Коваленко — мой бывший коллега yii php разработчик) обрушивается за непонятную документацию. Большинство команд, не могли попасть в холст и не понимали правил игры. Документация хоть и была написана +- доступным языком, но организаторы не стеснялись дополнять ее примеры и фиксить ее на ходу, что конечно же вызывает волну негодования у участников, а организаторы пытаются отшутится. 

Сами участники представляют собой набор команд в большинстве своем со стеком — C, Python, JavaScript, PHP
Я выбрал путь работы через SPA приложение на vue3+ts и сразу же влип в историю, когда тестовые запросы в PostMan у меня работают отлично, а вот в браузере у меня уже всем фронтендерам известная чихарда с CORS, да еще и все запросы даже те, где не нужен request.body на сервер нужно отправлять через POST запрос (после таких нестыковок технических, RESTом называть апишку не хочется, а еще там все не в json а в multipart form data что накладывает еще свои ограничения и неудобства т.к. обычно FormData используется для отправки файлов, а не обмена json объектами ) В общем мой совет разработчикам хакатона — подружиться с OpenApi спецификацией, научиться прикручивать Swagger и на основе нормальной спецификации публиковать (генерировать) UI — ну или взять готовый swagger UI — и избежать критики за отсутствие подствеки синтаксиса и убогую (примитивную) верстку в своей документации. Но такие же претензии лично к Коваленко Вадиму у меня были и когда я увольнялся с позапрошлого места работы) в этом плане для меня ничего не изменилось. Я пытался помочь сначала с кокнфигурацией и правками для nginx лично Вадиму, но потом, ответил ему — не чини это же хакатон и пошел написал свой server.js аж на 96 строк кода) нативно, без библиотек и лишнего, костыли ради обхода заголовков Authorization с клиента, + все передается в textplain чтобы при CORS режиме браузер и не думал обрезать мне ответ от этого космического сервиса. 

Пример рализации server.js для хакатона, слабоневрным не смотреть
const http = require('http');
const headers = { 
    'Access-Control-Allow-Origin': 'http://127.0.0.1:5173',
    'Access-Control-Allow-Methods': 'OPTIONS, POST, GET',
    'Access-Control-Allow-Headers': 'Origin, Content-Type, Authorization',
    'Access-Control-Max-Age': 2592000,
    'Access-Control-Allow-Credentials': true,
    'Content-Type': 'application/json',
};
//Call dats api 
 function fetch(url, options = {}) {
    
    options = Object.assign(options, {
        headers: {
            'Authorization': 'Bearer 644016da92f7f644016da92f80',
            'Content-Type': 'multipart/form-data; boundary=xxxxxxxxxx'
        },
        method: 'POST'
    });
  return new Promise((resolve, reject) => {
    
    const request = http.request(`http://api.datsart.dats.team${url}`, options, (response) => {
      const body = [];
      response.on('data', (chunk) => body.push(chunk));
      response.on('end', () => {
        const responseBody = Buffer.concat(body).toString();
        resolve({
          statusCode: response.statusCode,
          headers: response.headers,
          body: responseBody
        });
      });
    });
     request.on('error', (err) => {
      reject(err);
    });
    

    if (options && options.body) {
      request.write(options.body);
    }
     request.end();
  });
}

 const handleApiRequest = (req, res) => {
    let body = '';
    req.on('data', function (data) {
        body += data;
        if (body.length > 1e6)
            req.connection.destroy();
    });

    req.on('end', function () {
        
        const json = JSON.parse(body);
        var boundary = "xxxxxxxxxx";
        var data = "";
        for(var i in json) {
            if ({}.hasOwnProperty.call(json, i)) {
                data += "--" + boundary + "\r\n";
                data += "Content-Disposition: form-data; name=\"" + i + "\"; \r\n\r\n" + json[i] + "\r\n";
            }
        };
        data += "--" + boundary + "\r\n";
        data += "Content-Type:application/octet-stream\r\n\r\n";
        var payload = Buffer.concat([
                Buffer.from(data, "utf8"),
                Buffer.from("\r\n--" + boundary + "--\r\n", "utf8"),
        ]);
        // console.log('form_data', payload);
        fetch(req.url, {
            body: payload,
        }).then(response => {
            // console.log('DATS RESP:', response.body);
    
            res.writeHead(200, headers);
            res.end(JSON.stringify(response.body));
        });
    });
};
 const server = http.createServer((req, res) => {
    console.log('Received request:', req.method, req.url);
    if (req.method === 'OPTIONS') {
        res.writeHead(204, headers);
        // console.log('skip options 204', req)
        res.end();
        return;
    }
    handleApiRequest(req, res);
});
 const port = 3001;
 server.listen(port, () => {
  console.log(`HTTP server is listening on port ${port}`);
});
Разминнка
Так выглядело окончание разминки и попытки генерации первого изображения. Первые пиксели принтера

Вот пример реализации идеи если мыслить, не как принтер)

Набираем оборооты 20:00 — 00:00

После потери времени на CORS, фиксов nginx и лирики про верстку документации, я приступил наконец-то к выполнению задачи. И это моя первая ошибка. Мне стоило провести декомпозицию задачи не в 20:00 а в 18:00, пропустив время на лирику и CORS, но кто же знал :) в общем суть и порядок задачи сводился к организации нескольких процессов, с которыми мне помогала жена. Я взял на себя доработки по nodejs серверной части, а жена пошла на склад генерировать цвета и добавлять их к нам в виртуальный профиль. Из первых подводных камней мы встретили баг с JSON.parse и bigint когда происходит округление больших чисел и часть значения отбрасывается. В общем multipart data аукнулась еще разок неприятным багом на полчаса времени. Второй проблемой было то, что опциональные параметры по документации по сути являлись обязательными и это тоже отняло время на понимание. Также ошибки API про то что у тебя уже есть похожее задание в очереди на выстрел составляли больше половины ответов на мои запросы. Удавалось это вылечить и стабильно стрелять только около раза в секунду, принудительно дергая апишку обновления остатка цветовв и дожидаясь конца ее ответа, иначе все сыпалось в 400.

Потери времени на общение с API
Потери времени на общение с API

Стреляем всю ночь 00:00 — 7:00

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

Первые пиксели принтера
Набираем обороты на втором уровне

Сон и второе дыхание 12:00-17:00

После весьма непродолжительного и нездорового сна, голова все-таки просветлела, а груз и печаль по казалось уже вчерашним проблемам — испарились. За время моего отсутствия появились явные лидеры, я сделали около 10 тысяч выстрелов и попадал более чем в 90% случаев, но несовпадение цвета давало мне всего 2 балла в таблице лидеров, в то время, когда ТОП лидеры уже имели десятки тысяч баллов, на уровне алгоритмов, уже было понятно, что чуда не будет и у нас есть явные победители на хакатоне, но я пришел на хакатон не за макбуком, а за участием и тусовкой в знакомой мне IT среде, а также для обмена опыта и заведения знакомства с другими участниками хакатона. Решено было продолжить наши начинания и идти до самого финала. Посмотрев на результаты и скриншоты работы других игроков, я понял, что моя главная ошибка это попиксельная стрельба, в то время когда большая клякса с нужным цветом за 1 выстрел давала сразу сотни баллов участникам хакатона. Я решил развивать далее свои наработки попиксельной стрельбы и загружать краски уже не по 1 а по 2-5-10-20 постепенно наращивая и корректируя формулу подсчета которую я отложил на потом и пользовался константами и соотношениями сил и углов для вычисления нужного пикселя на холсте. Смена тактики не заставила себя долго ждать, и уже за пару часов я настрелял +40 баллов, оставалось решить вопрос с попаданием в цвет и заменить эмпирическую формулу на полубаллистический аналог тем более что в чате уже были скинуты наработки по формулам. Также утром организаторы обьявили, что скорость игры возросла до 250мс на 1 тик, что кратно ускоряло мой пиксельный принтер который уже рисовал по 2-4 пикселя на холсте за выстрел. Оставалось развивать идею и догонять финалистов, но время бессердечная сука и не ждет никого, хакатон и первый этап подходили к концу, лидеры были явно определены, оставалось посмотреть развязку в финале и пообщаться за кулисами

Финал

В финал прошли все, что приятно, но с другой стороны моя тактика — набрать хоть сколько-то в начале, чтобы пройти в финал и там все пофиксить — оказалась тупой идеей и делать нормально надо было сразу. Также в финале не обязательно было играть в финальный уровень картинки, которая была ну просто космос и не имела белых (незакрашеных областей) что делало бесполезным мое потраченное время на попиксельную сверку холстов — теперь красить надо было каждый пиксель. Просто набивать очки ради очков на больших кляксах, чтобы потешить себя местом повыше — гнилой подход и полная соревновательная импотенция. Наша команда приняла решение играть в супер игру) и пробовать отрисовать целый космос цвет в цвет с четким совпадением пикселей. По сути к этому моменту пятно от выстрела было 4-6px в зависимости от количества краски и оставалось только скорректировать точность и разобраться с соотношением количества кидаемой краски к величине пикселя (если бы не мысли в стиле принтера, то это стоило бы сделать в самом начале) я начал увеличивать количеств смешиваемой краски и заполнять холст своим катапулеметом и на этом в целом и закончился мой финал. До полноценной реализации мне не хватило две функции:

  1. Управление размером пятна от выстрела
  2. Корректировки точности с поправкой на псевдобаллистику
    Уже после финала в чате за кулисами появилось больше информации от финалистов и больше информации от организаторов, стало понятно, что за выстрел нужно кидать на 10-20 красок а 100-200 и будет вполне хорошо и достаточно для победы. На этом стоит подвести итоги этого хакатона. Мои же изменения за последние 3 часа в финале дали рост с 40 до 182 баллов за совпадение и точность цвета, но уже слишком поздно было генерировать картинку выбранным мною подходом
Мой финальный результат на хакатоне
Так выглядел мой финал, спустя пару тысяч выстрелов

Итоги

Итого я занял 46 из 340 — по количеству очков. 11 место по количеству попаданий в холст. 51 место по точности попаданий. 33 место по количеству попаданий краски и 68 место по процентам точности подбора краски. Приобрел 18 новых знакомств с разработчиками и собрали команду на следующий более серьезный хакатон. 

Зачем вообще ходить на хакатоны?

  1. Опыт, бесценный опыт, только за ним стоит идти. Программирование на результат, а не на архитектуру, merge мыслей и функций с участниками команды в рамках одного файла без всяких MR\PR — на такое удовольствие просто нет времени
  2. Проверка на стрессоустойчивость и способность решать задачи при недостатке информации, инфрастрктуры, обеспечения, важен только конечный результат.
  3. Знакомства, хакатоны это шикарная возможность познакомиться с другими разработчиками и найти себе команду и просто единомышленников. Лично у меня появилось +19 новых контактов и я рад каждому) на следующий хакатон пойдем уже не просто командой а несколькими командами сразу! Чтобы занимать весь рейтинг.

Фидбек от участников

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

Если не брать в расчет недочеты с документацией и  API, то хакатон в целом удался на сто процентов и можно выразить респект Коваленко Вадиму, за организацию такого творческого и креативного хакатона и его техническую реализацию, выдержать DDOS от 340 команд в момент проведения хакатона — это достойный технический челендж.

Также для тех кто ищет комаду, или единомышленников, несколько участников после хакатона попросили оставить их контакты под информацией об этом событии, чтобы найти еще единомышленников

@dead_inside_but_still_hornyy — Лидер командды PyCat

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

Павел Зубков
для себя: разберись уже в школьном матане, делай сначала то что приносит больший импакт, научись работать с мотивацией, суточный хакатон начинай на старте а не спустя 8 часов после старта.
как-то так)

Максат
По началу было не понятно но потом углубился в суть и стало понятно , но я всё ещё не понял как работать с светам. Но в целом не жалею что участвовал было круто спасибо вам

Farel
Задания интересные, есть много моментов к которым можно найти довольно неожиданный подход. Порог входа не слишком низкий, и это радует — задолбали ивенты, рассчитанные на новичков-новичков. Было интересно участвовать даже несмотря на то, что наша команда практически ничего не смогла выполнить

LostDesu
Круто постреляли))


Последняя редакция 25 апреля, 2023 в 06:04