ColorFighter - C ++ - come un par de golondrinas para el desayuno
EDITAR
- limpiado el código
- agregó una optimización simple pero efectiva
- agregó algunas animaciones GIF
Dios, odio las serpientes (solo finge que son arañas, Indy)
En realidad me encanta Python. Desearía ser menos flojo y comenzar a aprenderlo correctamente, eso es todo.
Dicho todo esto, tuve que luchar con la versión de 64 bits de esta serpiente para que el juez funcionara. Hacer que PIL funcione con la versión de 64 bits de Python en Win7 requiere más paciencia de la que estaba listo para dedicar a este desafío, así que al final cambié (dolorosamente) a la versión Win32.
Además, el juez tiende a fallar gravemente cuando un bot es demasiado lento para responder.
Al no ser un experto en Python, no lo solucioné, pero tiene que ver con leer una respuesta vacía después de un tiempo de espera en stdin.
Una mejora menor sería colocar la salida stderr en un archivo para cada bot. Eso facilitaría el rastreo para la depuración post mortem.
Excepto por estos problemas menores, el juez me pareció muy simple y agradable de usar.
Felicitaciones por otro desafío inventivo y divertido.
El código
#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();
}
Construyendo el ejecutable
Solía LODEpng.cpp y LODEpng.h para leer imágenes PNG.
Sobre la forma más fácil que encontré para enseñarle a este lenguaje C ++ retrasado cómo leer una imagen sin tener que construir media docena de bibliotecas.
Simplemente compile y vincule LODEpng.cpp junto con el principal y Bob es su tío.
Compilé con MSVC2013, pero como solo utilicé unos pocos contenedores básicos STL (deque y vectores), podría funcionar con gcc (si tienes suerte).
Si no es así, podría intentar una compilación MinGW, pero, francamente, me estoy cansando de los problemas de portabilidad de C ++.
Hice bastante C / C ++ portátil en mis días (en compiladores exóticos para varios procesadores de 8 a 32 bits, así como SunOS, Windows desde 3.11 hasta Vista y Linux desde su infancia hasta Ubuntu Cooing Zebra o lo que sea, así que creo Tengo una idea bastante clara de lo que significa la portabilidad), pero en ese momento no requería memorizar (o descubrir) las innumerables discrepancias entre las interpretaciones de GNU y Microsoft de las especificaciones crípticas e hinchadas del monstruo STL.
Resultados contra Swallower
Cómo funciona
En el fondo, esta es una simple ruta de relleno de fuerza bruta.
La frontera del color del jugador (es decir, los píxeles que tienen al menos un vecino blanco) se utiliza como semilla para realizar el clásico algoritmo de inundación a distancia.
Cuando un punto alcanza la vinculación de un color enemigo, se calcula un camino hacia atrás para producir una cadena de píxeles que se mueven hacia el punto enemigo más cercano.
El proceso se repite hasta que se hayan reunido suficientes puntos para una respuesta de la longitud deseada.
Esta repetición es obscenamente costosa, especialmente cuando se lucha cerca del enemigo.
Cada vez que se encuentra una cadena de píxeles que va de la frontera a un píxel enemigo (y necesitamos más puntos para completar la respuesta), el relleno de inundación se vuelve a hacer desde el principio, con el nuevo camino agregado a la frontera. Significa que podría tener que hacer 5 rellenos de inundación o más para obtener una respuesta de 10 píxeles.
Si no se puede acceder a más píxeles enemigos, se seleccionan vecinos arbitrarios de los píxeles fronterizos.
El algoritmo se convierte en un relleno de inundación bastante ineficiente, pero esto solo sucede después de que se ha decidido el resultado del juego (es decir, no hay más territorio neutral por el que luchar).
Lo optimicé para que el Juez no pase años llenando el mapa una vez que se haya tratado la competencia. En su estado actual, el tiempo de ejecución es despreciable en comparación con el propio juez.
Dado que los colores enemigos no se conocen al principio, la imagen inicial de la arena se mantiene almacenada para copiar las áreas iniciales del enemigo cuando realiza su primer movimiento.
Si el código se reproduce primero, simplemente inundará unos pocos píxeles arbitrarios.
Esto hace que el algoritmo sea capaz de luchar contra un número arbitrario de adversarios, e incluso posiblemente nuevos adversarios que lleguen a un punto aleatorio en el tiempo, o que aparezcan colores sin un área de inicio (aunque esto no tiene ningún uso práctico).
El manejo del enemigo en una base de color por color también permitiría que dos instancias del bot cooperen (usando coordenadas de píxeles para pasar un signo de reconocimiento secreto).
Suena divertido, probablemente lo intentaré :).
La ruta de cálculo pesado se realiza tan pronto como hay nuevos datos disponibles (después de una notificación de movimiento), y algunas optimizaciones (la actualización de la frontera) se realizan justo después de que se haya dado una respuesta (para hacer la mayor cantidad de cómputo posible durante los turnos de otros bots) )
Una vez más, podría haber formas de hacer cosas más sutiles si hubiera más de 1 adversario (como abortar un cálculo si hay nuevos datos disponibles), pero de todos modos no veo dónde se necesita la multitarea, siempre que el algoritmo sea capaz de trabajar a plena carga.
Problemas de desempeño
Todo esto no puede funcionar sin un acceso rápido a los datos (y más potencia de cómputo que todo el programa Appolo, es decir, su PC promedio cuando solía hacer más que publicar algunos tweets).
La velocidad depende en gran medida del compilador. Por lo general, GNU supera a Microsoft por un margen del 30% (ese es el número mágico que noté en otros 3 desafíos de código relacionados con la ruta), pero este kilometraje puede variar, por supuesto.
El código en su estado actual apenas comienza a sudar en la arena 4. El perfímetro de Windows informa entre un 4 y un 7% de uso de CPU, por lo que debería ser capaz de hacer frente a un mapa de 1000x1000 dentro del límite de tiempo de respuesta de 100 ms.
En el corazón de casi todos los algoritmos de ruta se encuentra un FIFO (posiblemente proritizado, aunque no en ese caso), que a su vez requiere una rápida asignación de elementos.
Dado que el OP obligó obligatoriamente un límite al tamaño de la arena, hice algunos cálculos y vi que las estructuras de datos fijas dimensionadas al máximo (es decir, 1,000,000 de píxeles) no consumirían más de un par de docenas de megabytes, que su PC promedio come para el desayuno.
De hecho, bajo Win7 y compilado con MSVC 2013, el código consume alrededor de 14Mb en arena 4, mientras que los dos hilos de Swallower están usando más de 20Mb.
Comencé con contenedores STL para crear prototipos más fácilmente, pero STL hizo que el código fuera aún menos legible, ya que no deseaba crear una clase para encapsular todos y cada uno de los datos para ocultar la ofuscación (ya sea debido a mis propias incapacidades para hacer frente a la STL se deja a la apreciación del lector).
De todos modos, el resultado fue tan atrozmente lento que al principio pensé que estaba creando una versión de depuración por error.
Creo que esto se debe en parte a la increíblemente pobre implementación de Microsoft del STL (donde, por ejemplo, los vectores y los conjuntos de bits realizan comprobaciones encuadernadas u otras operaciones crípticas en el operador [], en violación directa de la especificación), y en parte al diseño del STL sí mismo.
Podría hacer frente a los atroces problemas de sintaxis y portabilidad (es decir, Microsoft vs GNU) si las actuaciones estuvieran allí, pero ciertamente este no es el caso.
Por ejemplo, deque
es inherentemente lento, ya que baraja muchos datos de contabilidad esperando que la ocasión haga su cambio de tamaño súper inteligente, por lo que no podría importarme menos.
Claro que podría haber implementado un asignador personalizado y cualquier otro bit de plantilla personalizada, pero un asignador personalizado solo cuesta unos cientos de líneas de código y la mejor parte del día para probar, con la docena de interfaces que tiene que implementar, mientras que un La estructura equivalente hecha a mano tiene aproximadamente cero líneas de código (aunque es más peligroso, pero el algoritmo no habría funcionado si no supiera, o creo que sabía, lo que estaba haciendo de todos modos).
Así que eventualmente mantuve los contenedores STL en partes no críticas del código, y construí mi propio asignador brutal y FIFO con dos arreglos de alrededor de 1970 y tres cortos sin firmar.
Tragar la golondrina
Como confirmó su autor, los patrones erráticos de Swallower son causados por el retraso entre las notificaciones de movimientos enemigos y las actualizaciones del hilo de ruta.
El perfímetro del sistema muestra claramente el hilo de ruta que consume 100% de CPU todo el tiempo, y los patrones irregulares tienden a aparecer cuando el foco de la lucha se desplaza a una nueva área. Esto también es bastante evidente con las animaciones.
Una optimización simple pero efectiva
Después de mirar las épicas peleas de perros entre Swallower y mi luchador, recordé un viejo dicho del juego de Go: defender de cerca, pero atacar desde lejos.
Hay sabiduría en eso. Si intentas apegarte demasiado a tu adversario, desperdiciarás movimientos preciosos tratando de bloquear cada posible camino. Por el contrario, si te mantienes a solo un píxel de distancia, es probable que evites llenar pequeños huecos que ganarían muy poco y usar tus movimientos para contrarrestar las amenazas más importantes.
Para implementar esta idea, simplemente extendí los movimientos de un enemigo (marcando los 4 vecinos de cada movimiento como un píxel enemigo).
Esto detiene el algoritmo de ruta a un píxel de la frontera del enemigo, lo que permite a mi luchador sortear a un adversario sin quedar atrapado en demasiadas peleas de perros.
Puede ver la mejora
(aunque todas las ejecuciones no son tan exitosas, puede notar los contornos mucho más suaves):