ColorFighter - C ++ - come dois engolidores no café da manhã
EDITAR
- limpou o código
- adicionou uma otimização simples, mas eficaz
- adicionou algumas animações GIF
Deus, eu odeio cobras (apenas finja que são aranhas, Indy)
Na verdade, eu amo Python. Eu gostaria de ser menos um garoto preguiçoso e comecei a aprender direito, só isso.
Tudo isso dito, eu tive que lutar com a versão de 64 bits desta cobra para fazer o juiz funcionar. Fazer o PIL funcionar com a versão de 64 bits do Python no Win7 requer mais paciência do que eu estava pronto para me dedicar a esse desafio; portanto, no final, mudei (dolorosamente) para a versão do Win32.
Além disso, o juiz tende a falhar muito quando um bot é muito lento para responder.
Como não sou experiente em Python, não o corrigi, mas tem a ver com a leitura de uma resposta vazia após um tempo limite no stdin.
Uma pequena melhoria seria colocar a saída stderr em um arquivo para cada bot. Isso facilitaria o rastreamento para depuração post-mortem.
Exceto por esses pequenos problemas, achei o juiz muito simples e agradável de usar.
Parabéns por mais um desafio inventivo e divertido.
O 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();
}
Construindo o executável
Eu usei LODEpng.cpp e LODEpng.h para ler imagens png.
Sobre a maneira mais fácil que encontrei para ensinar a essa linguagem C ++ retardada como ler uma imagem sem precisar criar meia dúzia de bibliotecas.
Basta compilar e vincular o LODEpng.cpp junto com o principal e Bob é seu tio.
Compilei com o MSVC2013, mas como usei apenas alguns contêineres básicos STL (deque e vetores), ele pode funcionar com o gcc (se você tiver sorte).
Se isso não acontecer, eu poderia tentar uma compilação MinGW, mas, sinceramente, eu estou ficando cansado de problemas de portabilidade C ++.
Eu fiz bastante C / C ++ portátil nos meus dias (em compiladores exóticos para vários processadores de 8 a 32 bits, bem como SunOS, Windows desde 3.11 até Vista e Linux desde a sua infância até o Ubuntu arremessando zebra ou qualquer outra coisa, então eu acho Eu tenho uma boa idéia do que significa portabilidade), mas na época não era necessário memorizar (ou descobrir) as inúmeras discrepâncias entre as interpretações GNU e Microsoft das especificações enigmáticas e inchadas do monstro STL.
Resultados contra Swallower
Como funciona
No fundo, trata-se de um simples caminho de aterro de força bruta.
A fronteira da cor do jogador (ou seja, os pixels que têm pelo menos um vizinho branco) é usada como uma semente para executar o algoritmo clássico de inundação à distância.
Quando um ponto atinge a proximidade de uma cor inimiga, um caminho para trás é calculado para produzir uma sequência de pixels se movendo em direção ao ponto inimigo mais próximo.
O processo é repetido até que pontos suficientes tenham sido reunidos para uma resposta do comprimento desejado.
Essa repetição é obscenamente cara, especialmente quando lutamos perto do inimigo.
Cada vez que uma seqüência de pixels que leva da fronteira para um pixel inimigo é encontrada (e precisamos de mais pontos para concluir a resposta), o preenchimento é refeito desde o início, com o novo caminho adicionado à fronteira. Isso significa que você pode ter que fazer 5 preenchimentos ou mais para obter uma resposta de 10 pixels.
Se não houver mais pixels inimigos disponíveis, vizinhos arbitrários dos pixels da fronteira serão selecionados.
O algoritmo se transforma em um preenchimento bastante ineficiente, mas isso só acontece depois que o resultado do jogo é decidido (ou seja, não há mais território neutro pelo qual lutar).
Eu o otimizei para que o juiz não passasse anos preenchendo o mapa depois que a competição fosse resolvida. Em seu estado atual, o tempo de execução é negligenciável em comparação com o próprio juiz.
Como as cores inimigas não são conhecidas no início, a imagem inicial da arena é mantida na loja para copiar as áreas iniciais do inimigo quando ele faz seu primeiro movimento.
Se o código for reproduzido primeiro, ele simplesmente preencherá alguns pixels arbitrários.
Isso torna o algoritmo capaz de combater um número arbitrário de adversários, e até possivelmente novos adversários que chegam em um ponto aleatório no tempo, ou cores que aparecem sem uma área inicial (embora isso não tenha absolutamente nenhum uso prático).
O manuseio do inimigo em uma base de cor por cor também permitiria que duas instâncias do bot cooperassem (usando coordenadas de pixel para passar um sinal de reconhecimento secreto).
Parece divertido, provavelmente vou tentar isso :).
O processamento pesado de computação é feito assim que novos dados estão disponíveis (após uma notificação de movimentação) e algumas otimizações (a atualização da fronteira) são feitas logo após uma resposta ter sido dada (para fazer o máximo de computação possível durante os outros turnos de bots )
Aqui, novamente, poderia haver maneiras de fazer coisas mais sutis se houvesse mais de um adversário (como interromper uma computação se novos dados se tornarem disponíveis), mas, de qualquer forma, não consigo ver onde a multitarefa é necessária, desde que o algoritmo seja capaz de trabalhar em carga máxima.
Problemas de desempenho
Tudo isso não pode funcionar sem acesso rápido aos dados (e mais poder computacional do que todo o programa Appolo, ou seja, seu PC comum, quando fazia mais do que postar alguns tweets).
A velocidade é fortemente dependente do compilador. Normalmente, o GNU vence a Microsoft com uma margem de 30% (esse é o número mágico que notei em outros 3 desafios relacionados a códigos), mas essa milhagem pode variar, é claro.
O código em seu estado atual mal suou na arena 4. O perfmeter do Windows relata cerca de 4 a 7% de uso da CPU, portanto, deve ser capaz de lidar com um mapa de 1000x1000 dentro do prazo de resposta de 100ms.
No cerne de quase todos os algoritmos de caminho encontra-se um FIFO (possivelmente proritizado, embora não nesse caso), que por sua vez requer uma alocação rápida de elementos.
Como o OP obrigatoriamente estabeleceu um limite para o tamanho da arena, fiz algumas contas e vi que as estruturas de dados fixas dimensionadas para o máximo (ou seja, 1.000.000 pixels) não consumiriam mais do que algumas dúzias de megabytes, que o seu PC comum come no café da manhã.
De fato, no Win7 e compilado com o MSVC 2013, o código consome cerca de 14Mb na arena 4, enquanto os dois threads do Swallower estão usando mais de 20Mb.
Comecei com os contêineres STL para facilitar a criação de protótipos, mas o STL tornou o código ainda menos legível, pois não desejava criar uma classe para encapsular cada bit de dados para ocultar a ofuscação (seja devido às minhas próprias inabilidades). lidar com o STL é deixado à apreciação do leitor).
Independentemente disso, o resultado foi tão atrozmente lento que a princípio pensei que estava criando uma versão de depuração por engano.
Eu acho que isso se deve em parte à implementação incrivelmente ruim da STL da Microsoft (onde, por exemplo, vetores e conjuntos de bits fazem verificações vinculadas ou outras operações criptográficas no operador [], em violação direta das especificações) e em parte ao design da STL em si.
Eu poderia lidar com as questões atrozes de sintaxe e portabilidade (por exemplo, Microsoft vs GNU) se as performances estivessem presentes, mas esse certamente não é o caso.
Por exemplo, deque
é inerentemente lento, porque embaralha muitos dados da contabilidade aguardando a ocasião para fazer seu redimensionamento super inteligente, sobre o qual eu não poderia me importar menos.
Claro que eu poderia ter implementado um alocador personalizado e o que outros bits de modelo personalizados, mas um alocador personalizado sozinho custa algumas centenas de linhas de código e a maior parte do dia para testar, com a dúzia de interfaces que ele precisa implementar, enquanto um A estrutura equivalente artesanal tem cerca de zero linhas de código (embora mais perigosa, mas o algoritmo não funcionaria se eu não soubesse - ou pense que soubesse - o que estava fazendo de qualquer maneira).
Por fim, mantive os contêineres da STL em partes não críticas do código e construí meu próprio alocador brutal e FIFO com duas matrizes de cerca de 1970 e três shorts não assinados.
Engolir o engolidor
Como o autor confirmou, os padrões irregulares do Swallower são causados por atrasos entre notificações e atualizações dos movimentos do inimigo a partir do fio do caminho.
O medidor de desempenho do sistema mostra claramente o segmento de processamento que consome 100% da CPU o tempo todo, e os padrões irregulares tendem a aparecer quando o foco da luta muda para uma nova área. Isso também é bastante aparente nas animações.
Uma otimização simples, mas eficaz
Depois de observar as épicas brigas de cães entre Swallower e meu lutador, lembrei-me de um velho ditado do jogo Go: defender de perto, mas atacar de longe.
Há sabedoria nisso. Se você tentar se ater ao seu adversário demais, desperdiçará movimentos preciosos tentando bloquear cada caminho possível. Pelo contrário, se você ficar a apenas um pixel de distância, provavelmente evitará preencher pequenas lacunas que ganhariam muito pouco e usará seus movimentos para combater ameaças mais importantes.
Para implementar essa idéia, eu simplesmente estendi os movimentos de um inimigo (marcando os 4 vizinhos de cada movimento como um pixel inimigo).
Isso interrompe o algoritmo de desvio a um pixel da fronteira do inimigo, permitindo que meu lutador contorne um adversário sem ser pego em muitas brigas de cães.
Você pode ver a melhoria
(embora todas as execuções não sejam tão bem-sucedidas, você pode observar os contornos muito mais suaves):