ColorFighter - C ++ - ест пару глотателей на завтрак
РЕДАКТИРОВАТЬ
- убрал код
- добавлена простая, но эффективная оптимизация
- добавлены некоторые GIF-анимации
Боже, я ненавижу змей (просто притворяйся, что они пауки, Инди)
На самом деле я люблю Python. Хотелось бы, чтобы я был менее ленивым мальчиком и начал учить его правильно, вот и все.
После всего этого, мне пришлось бороться с 64-битной версией этой змеи, чтобы заставить судью работать. Чтобы PIL работал с 64-битной версией Python под Win7, требуется больше терпения, чем я готов был посвятить этому испытанию, поэтому в итоге я (болезненно) переключился на версию Win32.
Кроме того, судья имеет тенденцию сильно падать, когда бот слишком медленно реагирует.
Будучи не разбирающимся в Python, я не исправил это, но это связано с чтением пустого ответа после тайм-аута на stdin.
Незначительным улучшением было бы поместить вывод stderr в файл для каждого бота. Это облегчило бы отслеживание для посмертной отладки.
За исключением этих мелких проблем, я нашел Судью очень простым и приятным в использовании.
Престижность для еще одного изобретательного и веселого испытания.
Код
#define _CRT_SECURE_NO_WARNINGS // prevents Microsoft from croaking about the safety of scanf. Since every rabid Russian hacker and his dog are welcome to try and overflow my buffers, I could not care less.
#include "lodepng.h"
#include <vector>
#include <deque>
#include <iostream>
#include <sstream>
#include <cassert> // paranoid android
#include <cstdint> // fixed size types
#include <algorithm> // min max
using namespace std;
// ============================================================================
// The less painful way I found to teach C++ how to handle png images
// ============================================================================
typedef unsigned tRGB;
#define RGB(r,g,b) (((r) << 16) | ((g) << 8) | (b))
class tRawImage {
public:
unsigned w, h;
tRawImage(unsigned w=0, unsigned h=0) : w(w), h(h), data(w*h * 4, 0) {}
void read(const char* filename) { unsigned res = lodepng::decode(data, w, h, filename); assert(!res); }
void write(const char * filename)
{
std::vector<unsigned char> png;
unsigned res = lodepng::encode(png, data, w, h, LCT_RGBA); assert(!res);
lodepng::save_file(png, filename);
}
tRGB get_pixel(int x, int y) const
{
size_t base = raw_index(x,y);
return RGB(data[base], data[base + 1], data[base + 2]);
}
void set_pixel(int x, int y, tRGB color)
{
size_t base = raw_index(x, y);
data[base+0] = (color >> 16) & 0xFF;
data[base+1] = (color >> 8) & 0xFF;
data[base+2] = (color >> 0) & 0xFF;
data[base+3] = 0xFF; // alpha
}
private:
vector<unsigned char> data;
void bound_check(unsigned x, unsigned y) const { assert(x < w && y < h); }
size_t raw_index(unsigned x, unsigned y) const { bound_check(x, y); return 4 * (y * w + x); }
};
// ============================================================================
// coordinates
// ============================================================================
typedef int16_t tCoord;
struct tPoint {
tCoord x, y;
tPoint operator+ (const tPoint & p) const { return { x + p.x, y + p.y }; }
};
typedef deque<tPoint> tPointList;
// ============================================================================
// command line and input parsing
// (in a nice airtight bag to contain the stench of C++ string handling)
// ============================================================================
enum tCommand {
c_quit,
c_update,
c_play,
};
class tParser {
public:
tRGB color;
tPointList points;
tRGB read_color(const char * s)
{
int r, g, b;
sscanf(s, "(%d,%d,%d)", &r, &g, &b);
return RGB(r, g, b);
}
tCommand command(void)
{
string line;
getline(cin, line);
string cmd = get_token(line);
points.clear();
if (cmd == "exit") return c_quit;
if (cmd == "pick") return c_play;
// even more convoluted and ugly than the LEFT$s and RIGHT$s of Apple ][ basic...
if (cmd != "colour")
{
cerr << "unknown command '" << cmd << "'\n";
exit(0);
}
assert(cmd == "colour");
color = read_color(get_token(line).c_str());
get_token(line); // skip "chose"
while (line != "")
{
string coords = get_token(line);
int x = atoi(get_token(coords, ',').c_str());
int y = atoi(coords.c_str());
points.push_back({ x, y });
}
return c_update;
}
private:
// even more verbose and inefficient than setting up an ADA rendezvous...
string get_token(string& s, char delimiter = ' ')
{
size_t pos = 0;
string token;
if ((pos = s.find(delimiter)) != string::npos)
{
token = s.substr(0, pos);
s.erase(0, pos + 1);
return token;
}
token = s; s.clear(); return token;
}
};
// ============================================================================
// pathing
// ============================================================================
class tPather {
public:
tPather(tRawImage image, tRGB own_color)
: arena(image)
, w(image.w)
, h(image.h)
, own_color(own_color)
, enemy_threat(false)
{
// extract colored pixels and own color areas
tPointList own_pixels;
color_plane[neutral].resize(w*h, false);
color_plane[enemies].resize(w*h, false);
for (size_t x = 0; x != w; x++)
for (size_t y = 0; y != h; y++)
{
tRGB color = image.get_pixel(x, y);
if (color == col_white) continue;
plane_set(neutral, x, y);
if (color == own_color) own_pixels.push_back({ x, y }); // fill the frontier with all points of our color
}
// compute initial frontier
for (tPoint pixel : own_pixels)
for (tPoint n : neighbour)
{
tPoint pos = pixel + n;
if (!in_picture(pos)) continue;
if (image.get_pixel(pos.x, pos.y) == col_white)
{
frontier.push_back(pixel);
break;
}
}
}
tPointList search(size_t pixels_required)
{
// flood fill the arena, starting from our current frontier
tPointList result;
tPlane closed;
static tCandidate pool[max_size*max_size]; // fastest possible garbage collection
size_t alloc;
static tCandidate* border[max_size*max_size]; // a FIFO that beats a deque anytime
size_t head, tail;
static vector<tDistance>distance(w*h); // distance map to be flooded
size_t filling_pixels = 0; // end of game optimization
get_more_results:
// ready the distance map for filling
distance.assign(w*h, distance_max);
// seed our flood fill with the frontier
alloc = head = tail = 0;
for (tPoint pos : frontier)
{
border[tail++] = new (&pool[alloc++]) tCandidate (pos);
}
// set already explored points
closed = color_plane[neutral]; // that's one huge copy
// add current result
for (tPoint pos : result)
{
border[tail++] = new (&pool[alloc++]) tCandidate(pos);
closed[raw_index(pos)] = true;
}
// let's floooooood!!!!
while (tail > head && pixels_required > filling_pixels)
{
tCandidate& candidate = *border[head++];
tDistance dist = candidate.distance;
distance[raw_index(candidate.pos)] = dist++;
for (tPoint n : neighbour)
{
tPoint pos = candidate.pos + n;
if (!in_picture (pos)) continue;
size_t index = raw_index(pos);
if (closed[index]) continue;
if (color_plane[enemies][index])
{
if (dist == (distance_initial + 1)) continue; // already near an enemy pixel
// reached the nearest enemy pixel
static tPoint trail[max_size * max_size / 2]; // dimensioned as a 1 pixel wide spiral across the whole map
size_t trail_size = 0;
// walk back toward the frontier
tPoint walker = candidate.pos;
tDistance cur_d = dist;
while (cur_d > distance_initial)
{
trail[trail_size++] = walker;
tPoint next_n;
for (tPoint n : neighbour)
{
tPoint next = walker + n;
if (!in_picture(next)) continue;
tDistance prev_d = distance[raw_index(next)];
if (prev_d < cur_d)
{
cur_d = prev_d;
next_n = n;
}
}
walker = walker + next_n;
}
// collect our precious new pixels
if (trail_size > 0)
{
while (trail_size > 0)
{
if (pixels_required-- == 0) return result; // ;!; <-- BRUTAL EXIT
tPoint pos = trail[--trail_size];
result.push_back (pos);
}
goto get_more_results; // I could have done a loop, but I did not bother to. Booooh!!!
}
continue;
}
// on to the next neighbour
closed[index] = true;
border[tail++] = new (&pool[alloc++]) tCandidate(pos, dist);
if (!enemy_threat) filling_pixels++;
}
}
// if all enemies have been surrounded, top up result with the first points of our flood fill
if (enemy_threat) enemy_threat = pixels_required == 0;
tPathIndex i = frontier.size() + result.size();
while (pixels_required--) result.push_back(pool[i++].pos);
return result;
}
// tidy up our map and frontier while other bots are thinking
void validate(tPointList moves)
{
// report new points
for (tPoint pos : moves)
{
frontier.push_back(pos);
color_plane[neutral][raw_index(pos)] = true;
}
// remove surrounded points from frontier
for (auto it = frontier.begin(); it != frontier.end();)
{
bool in_frontier = false;
for (tPoint n : neighbour)
{
tPoint pos = *it + n;
if (!in_picture(pos)) continue;
if (!(color_plane[neutral][raw_index(pos)] || color_plane[enemies][raw_index(pos)]))
{
in_frontier = true;
break;
}
}
if (!in_frontier) it = frontier.erase(it); else ++it; // the magic way of deleting an element without wrecking your iterator
}
}
// handle enemy move notifications
void update(tRGB color, tPointList points)
{
assert(color != own_color);
// plot enemy moves
enemy_threat = true;
for (tPoint p : points) plane_set(enemies, p);
// important optimization here :
/*
* Stop 1 pixel away from the enemy to avoid wasting moves in dogfights.
* Better let the enemy gain a few more pixels inside the surrounded region
* and use our precious moves to get closer to the next threat.
*/
for (tPoint p : points) for (tPoint n : neighbour) plane_set(enemies, p+n);
// if a new enemy is detected, gather its initial pixels
for (tRGB enemy : known_enemies) if (color == enemy) return;
known_enemies.push_back(color);
tPointList start_areas = scan_color(color);
for (tPoint p : start_areas) plane_set(enemies, p);
}
private:
typedef uint16_t tPathIndex;
typedef uint16_t tDistance;
static const tDistance distance_max = 0xFFFF;
static const tDistance distance_initial = 0;
struct tCandidate {
tPoint pos;
tDistance distance;
tCandidate(){} // must avoid doing anything in this constructor, or pathing will slow to a crawl
tCandidate(tPoint pos, tDistance distance = distance_initial) : pos(pos), distance(distance) {}
};
// neighbourhood of a pixel
static const tPoint neighbour[4];
// dimensions
tCoord w, h;
static const size_t max_size = 1000;
// colors lookup
const tRGB col_white = RGB(0xFF, 0xFF, 0xFF);
const tRGB col_black = RGB(0x00, 0x00, 0x00);
tRGB own_color;
const tRawImage arena;
tPointList scan_color(tRGB color)
{
tPointList res;
for (size_t x = 0; x != w; x++)
for (size_t y = 0; y != h; y++)
{
if (arena.get_pixel(x, y) == color) res.push_back({ x, y });
}
return res;
}
// color planes
typedef vector<bool> tPlane;
tPlane color_plane[2];
const size_t neutral = 0;
const size_t enemies = 1;
bool plane_get(size_t player, tPoint p) { return plane_get(player, p.x, p.y); }
bool plane_get(size_t player, size_t x, size_t y) { return in_picture(x, y) ? color_plane[player][raw_index(x, y)] : false; }
void plane_set(size_t player, tPoint p) { plane_set(player, p.x, p.y); }
void plane_set(size_t player, size_t x, size_t y) { if (in_picture(x, y)) color_plane[player][raw_index(x, y)] = true; }
bool in_picture(tPoint p) { return in_picture(p.x, p.y); }
bool in_picture(int x, int y) { return x >= 0 && x < w && y >= 0 && y < h; }
size_t raw_index(tPoint p) { return raw_index(p.x, p.y); }
size_t raw_index(size_t x, size_t y) { return y*w + x; }
// frontier
tPointList frontier;
// register enemies when they show up
vector<tRGB>known_enemies;
// end of game optimization
bool enemy_threat;
};
// small neighbourhood
const tPoint tPather::neighbour[4] = { { -1, 0 }, { 1, 0 }, { 0, -1 }, { 0, 1 } };
// ============================================================================
// main class
// ============================================================================
class tGame {
public:
tGame(tRawImage image, tRGB color, size_t num_pixels)
: own_color(color)
, response_len(num_pixels)
, pather(image, color)
{}
void main_loop(void)
{
// grab an initial answer in case we're playing first
tPointList moves = pather.search(response_len);
for (;;)
{
ostringstream answer;
size_t num_points;
tPointList played;
switch (parser.command())
{
case c_quit:
return;
case c_play:
// play as many pixels as possible
if (moves.size() < response_len) moves = pather.search(response_len);
num_points = min(moves.size(), response_len);
for (size_t i = 0; i != num_points; i++)
{
answer << moves[0].x << ',' << moves[0].y;
if (i != num_points - 1) answer << ' '; // STL had more important things to do these last 30 years than implement an implode/explode feature, but you can write your own custom version with exception safety and in-place construction. It's a bit of work, but thanks to C++ inherent genericity you will be able to extend it to giraffes and hippos with a very manageable amount of code refactoring. It's not anyone's language, your C++, eh. Just try to implode hippos in Python. Hah!
played.push_back(moves[0]);
moves.pop_front();
}
cout << answer.str() << '\n';
// now that we managed to print a list of points to stdout, we just need to cleanup the mess
pather.validate(played);
break;
case c_update:
if (parser.color == own_color) continue; // hopefully we kept track of these already
pather.update(parser.color, parser.points);
moves = pather.search(response_len); // get cracking
break;
}
}
}
private:
tParser parser;
tRGB own_color;
size_t response_len;
tPather pather;
};
void main(int argc, char * argv[])
{
// process command line
tRawImage raw_image; raw_image.read (argv[1]);
tRGB my_color = tParser().read_color(argv[2]);
int num_pixels = atoi (argv[3]);
// init and run
tGame game (raw_image, my_color, num_pixels);
game.main_loop();
}
Сборка исполняемого файла
Я использовал LODEpng.cpp и LODEpng.h для чтения изображений PNG.
О самом простом способе, которым я научил этот отсталый язык C ++, как читать картинку, не создавая полдюжины библиотек.
Просто скомпилируйте и свяжите LODEpng.cpp вместе с главной и Бобом, вашим дядей.
Я скомпилировал с MSVC2013, но так как я использовал только несколько базовых контейнеров STL (deque и векторы), он может работать с gcc (если вам повезет).
Если это не так, я мог бы попробовать сборку MinGW, но, честно говоря, я устал от проблем с переносимостью C ++.
В свое время я много занимался переносом C / C ++ (на экзотических компиляторах для различных 8–32-битных процессоров, а также на SunOS, Windows от 3.11 до Vista и Linux от младенчества до Ubuntu Cooing Zebra или чего-то еще, так что я думаю, У меня есть довольно хорошее представление о том, что означает переносимость), но в то время не требовалось запоминать (или обнаруживать) бесчисленные расхождения между интерпретациями GNU и Microsoft загадочных и раздутых спецификаций монстра STL.
Результаты против Swallower
Как это работает
По сути, это простой путь заливки.
Граница цвета игрока (то есть пикселей, которые имеют по крайней мере одного белого соседа) используется в качестве начального числа для выполнения классического алгоритма затопления расстояний.
Когда точка достигает окрестности вражеского цвета, вычисляется обратный путь, чтобы создать ряд пикселей, движущихся в направлении ближайшего вражеского пятна.
Процесс повторяется до тех пор, пока не будет набрано достаточное количество точек для ответа желаемой длины.
Это повторение непристойно дорого, особенно когда сражаешься рядом с врагом.
Каждый раз, когда последовательность пикселей, ведущая от границы к вражескому пикселю, была найдена (и нам нужно больше точек, чтобы завершить ответ), заливка затопляется заново с начала, с новым путем, добавленным к границе. Это означает, что вы могли бы сделать 5 заливок или больше, чтобы получить ответ в 10 пикселей.
Если вражеские пиксели больше недоступны, выбираются произвольные соседи пограничных пикселей.
Алгоритм сводится к довольно неэффективному заполнению флудом, но это происходит только после того, как будет решен исход игры (то есть больше нет нейтральной территории для борьбы).
Я оптимизировал его так, чтобы судья не тратил целую вечность, заполняя карту после того, как соревнование было рассмотрено. В своем текущем состоянии время исполнения пренебрежимо мало по сравнению с самим судьей.
Поскольку вражеские цвета не известны в начале, исходное изображение арены сохраняется в хранилище, чтобы скопировать начальные области врага, когда он делает свой первый ход.
Если код воспроизводится первым, он просто заполняет несколько произвольных пикселей.
Это делает алгоритм способным бороться с произвольным числом противников и даже, возможно, с новыми противниками, прибывающими в произвольный момент времени, или с цветами, появляющимися без начальной области (хотя это абсолютно не имеет практического применения).
Обработка врага на основе цвета за цвет также позволила бы двум экземплярам взаимодействовать с ботом (используя пиксельные координаты для передачи секретного знака распознавания).
Звучит весело, я, наверное, попробую :).
Интенсивный вычислительный путь выполняется, как только появляются новые данные (после уведомления о перемещении), и некоторые оптимизации (обновление границы) выполняются сразу после получения ответа (чтобы сделать как можно больше вычислений во время поворотов других ботов) ).
Здесь снова могут быть способы сделать более тонкие вещи, если бы было более одного противника (например, прервать вычисление, если новые данные стали доступны), но, во всяком случае, я не вижу, где необходима многозадачность, пока алгоритм способен работать на полную нагрузку.
Проблемы с производительностью
Все это не может работать без быстрого доступа к данным (и большей вычислительной мощности, чем у всей программы Appolo, т. Е. Вашего обычного ПК, когда вы используете больше, чем публикуете несколько твитов).
Скорость сильно зависит от компилятора. Обычно GNU опережает Microsoft на 30% (это магическое число, которое я заметил при трех других проблемах с кодом, связанным с маршрутизацией), но этот пробег может меняться, конечно.
Код в его текущем состоянии с трудом справляется с трудностями на арене 4. Перфметр Windows сообщает о 4–7% -ной загрузке ЦП, поэтому он должен быть в состоянии справиться с картой 1000x1000 в пределах времени отклика 100 мс.
В основе каждого алгоритма маршрутизации лежит FIFO (возможно, проритизированный, но не в этом случае), который, в свою очередь, требует быстрого выделения элементов.
Так как ОП обязательно установил ограничение на размер арены, я немного посчитал и увидел, что фиксированные структуры данных размером до максимума (т. Е. 1.000.000 пикселей) не будут потреблять более пары десятков мегабайт, которые ваш средний компьютер ест на завтрак.
Действительно, под Win7 и скомпилированный с MSVC 2013, код потребляет около 14 МБ на арене 4, в то время как два потока Swallower используют более 20 МБ.
Я начал с контейнеров STL для упрощения создания прототипов, но STL сделал код еще менее читаемым, поскольку у меня не было желания создавать класс для инкапсуляции каждого бита данных, чтобы скрыть запутывание (связано ли это с моими собственными возможностями справиться с STL оставлено на усмотрение читателя).
Несмотря на это, результат был настолько ужасно медленным, что сначала я думал, что по ошибке строю отладочную версию.
Я считаю, что это отчасти связано с невероятно плохой реализацией Microsoft STL (где, например, векторы и наборы битов выполняют связанные проверки или другие криптические операции над оператором [], в прямом нарушении спецификации), а также частично из-за конструкции STL сам.
Я мог бы справиться с жестокими проблемами синтаксиса и переносимости (то есть Microsoft против GNU), если бы производительность была там, но это, конечно, не так.
Например, deque
изначально медленный, потому что он тасует много бухгалтерских данных в ожидании случая, чтобы сделать его супер-умное изменение размера, о котором мне наплевать.
Конечно, я мог бы реализовать собственный распределитель и другие биты пользовательских шаблонов, но один только собственный распределитель стоит несколько сотен строк кода и лучшую часть дня, чтобы проверить, что с дюжиной интерфейсов, которые он должен реализовать, в то время как Эквивалентная структура, созданная вручную, состоит из нуля строк кода (хотя и более опасно, но алгоритм не сработал бы, если бы я не знал - или, думаю, знал - что я делал в любом случае).
Таким образом, в конце концов я сохранил контейнеры STL в некритических частях кода и создал свой собственный брутальный распределитель и FIFO с двумя массивами 1970 года и тремя шортами без знака.
Глотать глотатель
Как подтвердил его автор, неустойчивые паттерны «ласточки» вызваны задержкой между уведомлениями о перемещениях противника и обновлениями из потока пути.
Системный перфметр четко показывает траекторию потока, потребляющую 100% ЦП все время, и неровные узоры имеют тенденцию появляться, когда фокус сражения смещается в новую область. Это также вполне очевидно с анимацией.
Простая, но эффективная оптимизация
Посмотрев на грандиозные воздушные бои между Ласточкой и моим бойцом, я вспомнил старую поговорку из игры в Го: защищайся крупным планом, но атакуй издалека.
В этом есть мудрость. Если вы слишком стараетесь придерживаться своего противника, вы будете тратить драгоценные ходы, пытаясь заблокировать каждый возможный путь. Напротив, если вы останетесь на расстоянии всего одного пикселя, вы, скорее всего, избежите заполнения небольших пробелов, которые могут принести очень мало, и будете использовать свои действия для противодействия более важным угрозам.
Чтобы реализовать эту идею, я просто расширил ходы врага (отметив 4 соседа каждого хода как вражеский пиксель).
Это останавливает алгоритм прохождения пути на расстоянии одного пикселя от границы врага, позволяя моему бойцу обходить противника, не оказываясь втянутым в слишком много воздушных боев.
Вы можете увидеть улучшение
(хотя все прогоны не так успешны, вы можете заметить более плавные контуры):