ColorFighter - C ++ - з'їдає пару ковтальників на сніданок
EDIT
- очистили код
- додали просту, але ефективну оптимізацію
- додано кілька анімацій GIF
Боже, я ненавиджу змій (просто роблю вигляд, що вони павуки, Інді)
Насправді я люблю Python. Мені б хотілося, щоб я був менш ледачим хлопчиком і почав це правильно навчати, ось і все.
Враховуючи це, мені довелося боротися з 64-бітовою версією цієї змії, щоб суддя працював. Здійснення роботи PIL з 64-бітної версією Python під Win7 вимагає більше терпіння, ніж я був готовий присвятити цьому виклику, тому врешті-решт я перейшов (болісно) на версію Win32.
Крім того, Суддя схильний до краху, коли бот занадто повільний, щоб реагувати.
Не будучи кмітливим Python, я цього не виправив, але це стосується читання порожньої відповіді після тайм-ауту на stdin.
Невеликим поліпшенням буде поставити більш жорсткий вихід у файл для кожного бота. Це полегшило б відстежувати налагодження після смерті.
За винятком цих незначних проблем, я вважав Суддя дуже простим і приємним у використанні.
Кудо ще один винахідливий і веселий виклик.
Кодекс
#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 та vectors), він може працювати з gcc (якщо пощастить).
Якщо це не так, я можу спробувати збільшити MinGW, але, чесно кажучи, я втомився від проблем з портативністю на C ++.
Я робив досить багато портативних C / C ++ у свої дні (на екзотичних компіляторах для різних 8 - 32 бітових процесорів, а також SunOS, Windows від 3.11 до Vista та Linux, починаючи з дитинства до кубічної зебри Ubuntu чи будь-чого іншого, тому я думаю Я маю досить гарне уявлення про те, що означає портативність), але в той час воно не вимагало запам’ятовування (або виявлення) незліченних розбіжностей між інтерпретаціями GNU та Microsoft щодо криптованих та роздутих специфік монстра STL.
Результати проти Ластівки
Як це працює
По суті, це простий шлях, спрямований на заповнення грубої сили.
Межа кольору гравця (тобто пікселі, що мають принаймні одного білого сусіда) використовується як насіння для виконання класичного алгоритму затоплення відстані.
Коли точка досягає винності ворожих кольорів, обчислюється відсталий шлях для отримання рядка пікселів, що рухається до найближчого ворога.
Процес повторюється, поки не буде зібрано достатньо балів для відповіді потрібної тривалості.
Це повторення нецензурно дорого, особливо при боях поблизу ворога.
Щоразу, коли рядок пікселів, що ведуть від кордону до ворожих пікселів, знайдений (і нам потрібно більше очок для завершення відповіді), заливка потопу переробляється з початку, при цьому новий шлях додається до кордону. Це означає, що вам доведеться виконати 5 заливних чи більше, щоб отримати відповідь 10 пікселів.
Якщо більше ворожих пікселів недоступні, вибираються арбітражні сусіди прикордонних пікселів.
Алгоритм переходить до досить неефективного заповнення, але це відбувається лише після того, як буде вирішено результат гри (тобто більше немає нейтральної території, за яку потрібно боротися).
Я оптимізував це, щоб суддя не витрачав віки на заповнення карти після розгляду змагань. У своєму нинішньому стані час виконання страт є незначним порівняно із самим суддею.
Оскільки кольори противника невідомі на початку, початкове зображення арени зберігається в магазині, щоб скопіювати стартові зони противника під час першого кроку.
Якщо код відтвориться спочатку, він просто заповнить кілька довільних пікселів.
Це робить алгоритм здатним боротися з довільною кількістю супротивників і навіть, можливо, з новими супротивниками, що приходять у випадковий момент часу, або кольорами, що з’являються без початкової області (хоча це практично не має практичного використання).
Поводження з ворогом на основі кольору за кольором також дозволить співпрацювати два екземпляри бота (за допомогою піксельних координат для передачі секретного знаку розпізнавання).
Здається, це весело, я, мабуть, спробую це :).
Обчислювальний шлях для обчислень проводиться, як тільки з’являються нові дані (після повідомлення про переміщення), а деякі оптимізації (пограничне оновлення) проводяться одразу після надання відповіді (зробити якомога більше обчислень під час інших ботів. ).
Знову ж таки, можуть бути способи робити більш тонкі речі, якби було більше 1 супротивника (наприклад, перервати обчислення, якщо з’являться нові дані), але в будь-якому випадку я не бачу, де потрібна багатозадачність, доки алгоритм є вміє працювати на повне навантаження.
Питання продуктивності
Все це не може працювати без швидкого доступу до даних (і більше обчислювальної потужності, ніж уся програма Appolo, тобто ваш середній ПК, коли використовується більше, ніж опублікувати кілька твітів).
Швидкість сильно залежить від компілятора. Зазвичай GNU долає Майкрософт з 30% запасом (це магічне число, яке я помітив у 3 інших проблемах, пов'язаних з кодом), але цей пробіг може змінюватися, звичайно.
Код у своєму нинішньому стані ледве розбиває потужність на арені 4. Перфметр Windows повідомляє про 4–7% використання процесора, тому він повинен мати можливість справлятися з картою 1000х1000 у межах 100 мс часу відгуку.
В основі кожного алгоритму трасування лежить FIFO (можливо, проритизований, хоча і не в цьому випадку), що, в свою чергу, вимагає швидкого розподілу елементів.
Оскільки ОП обов'язково встановив обмеження розміру арени, я зробив декілька математик і побачив, що фіксовані структури даних розміром до максимуму (тобто 1.000.000 пікселів) не будуть споживати більше ніж пару десятків мегабайт, які ваш середній ПК їсть на сніданок.
Дійсно, під Win7 та складений з MSVC 2013, на арені 4 код споживає близько 14 Мб, тоді як два потоки Ластівки використовують більше 20 Мб.
Я почав з контейнерів STL для більш легкого прототипування, але STL зробив код ще менш читабельним, оскільки я не мав бажання створювати клас, щоб інкапсулювати кожний шматочок даних, щоб приховати заплутаність подалі (чи це через мої власні нездатності впоратися зі СТЛ залишається на оцінку читача).
Незважаючи на це, результат був настільки жорстоко повільним, що спочатку я подумав, що будую версію налагодження помилково.
Я вважаю, що це частково пов’язано з надзвичайно поганою реалізацією STL від Microsoft (де, наприклад, вектори та бітсети виконують зв'язані перевірки або інші криптовалютні операції з оператором [], в прямому порушенні специфікації), а частково і з дизайном STL себе.
Я міг би впоратися із жорстокими проблемами синтаксису та портативності (тобто Microsoft проти GNU), якби виступи там були, але це, звичайно, не так.
Наприклад, deque
вона по своїй суті є повільною, тому що вона перетасовує безліч даних бухгалтерського обліку навколо, чекаючи нагоди, щоб зробити її супер розумний розмір, про який я не міг би піклуватися менше.
Звичайно, я міг реалізувати спеціальний розподільник та інший користувальницький біт шаблону, але сам призначений розподільник коштує кілька сотень рядків коду та більшу частину дня, щоб перевірити, що з десяток інтерфейсів він має реалізувати, а еквівалентна структура ручної роботи - це приблизно нульові рядки коду (хоч і небезпечніше, але алгоритм не працював би, якби я не знав - або думав, що знаю - що я все-таки робив).
Тож зрештою я зберігав контейнери STL у некритичних частинах коду, і створив власний жорстокий розподільник та FIFO з двома масивами по близько 1970 та трьома неподписаними шортами.
Проковтування ковтальника
Як підтвердив її автор, помилкові зразки Ластівки спричинені відставанням між сповіщеннями про переміщення ворога та оновленнями від потокової лінії.
Системний перфметр чітко показує, що потоковий контур витрачає 100% процесора весь час, і нерівні візерунки, як правило, з'являються, коли фокус боротьби зміщується на нову область. Це також досить очевидно з анімаціями.
Проста, але ефективна оптимізація
Подивившись епічні собачі бої між Ластівчиною та моїм бійцем, я згадав стару приказку з гри Go: захищай близько, але атакуй здалеку.
У цьому є мудрість. Якщо ви будете намагатися дотримуватися свого противника занадто багато, ви витратите дорогоцінні кроки, намагаючись перекрити кожен можливий шлях. Навпаки, якщо ви залишаєтесь лише на один піксель, ви, швидше за все, уникнете заповнення невеликих прогалин, які набрали б дуже мало, і використовуйте свої кроки для протидії більш важливим загрозам.
Для втілення цієї ідеї я просто розширив ходи ворога (позначаючи 4 сусіди кожного ходу як піксель ворога).
Це зупиняє алгоритм проходження маршруту на один піксель від межі ворога, дозволяючи моєму винищувачеві обходити супротивника, не потрапляючи в занадто багато собачих боїв.
Ви можете побачити поліпшення
(хоча всі запуски не такі успішні, ви можете помітити набагато більш плавні контури):