ColorFighter - C ++ - mangia un paio di rondini per colazione
MODIFICARE
- ripulito il codice
- aggiunta un'ottimizzazione semplice ma efficace
- aggiunte alcune animazioni GIF
Dio, odio i serpenti (fingi che siano ragni, Indy)
In realtà adoro Python. Vorrei essere meno un ragazzo pigro e iniziare a impararlo correttamente, tutto qui.
Detto questo, ho dovuto lottare con la versione a 64 bit di questo serpente per far funzionare il giudice. Far funzionare PIL con la versione a 64 bit di Python in Win7 richiede più pazienza di quanto non fossi pronto a dedicare a questa sfida, quindi alla fine sono passato (dolorosamente) alla versione di Win32.
Inoltre, il giudice tende a bloccarsi gravemente quando un bot è troppo lento per rispondere.
Non essendo esperto di Python, non l'ho risolto, ma ha a che fare con la lettura di una risposta vuota dopo un timeout su stdin.
Un piccolo miglioramento sarebbe quello di mettere l'output di stderr in un file per ciascun bot. Ciò faciliterebbe la traccia per il debug post mortem.
Fatta eccezione per questi piccoli problemi, ho trovato il giudice molto semplice e piacevole da usare.
Complimenti per l'ennesima sfida creativa e divertente.
Il codice
#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();
}
Costruire l'eseguibile
Ho usato LODEpng.cpp e LODEpng.h per leggere immagini png.
Circa il modo più semplice che ho trovato per insegnare a questo linguaggio C ++ ritardato come leggere un'immagine senza dover costruire una mezza dozzina di librerie.
Basta compilare e collegare LODEpng.cpp insieme al principale e Bob è tuo zio.
Ho compilato con MSVC2013, ma dal momento che ho usato solo alcuni contenitori di base STL (deque e vettori), potrebbe funzionare con gcc (se sei fortunato).
In caso contrario, potrei provare una build di MinGW, ma francamente mi sto stancando dei problemi di portabilità in C ++.
Ai miei tempi ho fatto un sacco di C / C ++ portatili (su compilatori esotici per vari processori da 8 a 32 bit, nonché SunOS, Windows da 3.11 fino a Vista e Linux dalla sua infanzia a Ubuntu cooing zebra o qualsiasi altra cosa, quindi penso Ho una buona idea di cosa significhi portabilità), ma al momento non era necessario memorizzare (o scoprire) le innumerevoli discrepanze tra le interpretazioni GNU e Microsoft delle specifiche criptiche e gonfie del mostro STL.
Risultati contro Swallower
Come funziona
Al centro, si tratta di un semplice percorso di inondazione a forza bruta.
La frontiera del colore del giocatore (ovvero i pixel che hanno almeno un vicino bianco) viene utilizzata come seme per eseguire il classico algoritmo di inondazione di distanza.
Quando un punto raggiunge la vincinità di un colore nemico, viene calcolato un percorso all'indietro per produrre una serie di pixel che si spostano verso il punto nemico più vicino.
Il processo viene ripetuto fino a quando non sono stati raccolti abbastanza punti per una risposta della lunghezza desiderata.
Questa ripetizione è oscenamente costosa, specialmente quando si combatte vicino al nemico.
Ogni volta che viene trovata una stringa di pixel che porta dalla frontiera a un pixel nemico (e abbiamo bisogno di più punti per completare la risposta), il riempimento dell'inondazione viene rifatto dall'inizio, con il nuovo percorso aggiunto alla frontiera. Significa che potresti dover fare 5 o più inondazioni per ottenere una risposta di 10 pixel.
Se non sono più raggiungibili pixel nemici, vengono selezionati i vicini arbitrari dei pixel di frontiera.
L'algoritmo si trasforma in un inondazione piuttosto inefficiente, ma ciò accade solo dopo che è stato deciso il risultato del gioco (cioè non c'è più un territorio neutrale per cui combattere).
L'ho ottimizzato in modo che il giudice non impieghi anni a riempire la mappa una volta che la competizione è stata affrontata. Allo stato attuale, il tempo di esecuzione è trascurabile rispetto al giudice stesso.
Poiché i colori nemici non sono noti all'inizio, l'immagine iniziale dell'arena viene conservata per copiare le aree di partenza del nemico quando fa la sua prima mossa.
Se il codice viene riprodotto per primo, riempirà semplicemente alcuni pixel arbitrari.
Questo rende l'algoritmo in grado di combattere un numero arbitrario di avversari, e forse anche nuovi avversari che arrivano in un momento casuale, o colori che appaiono senza un'area di partenza (anche se questo non ha assolutamente alcun uso pratico).
La gestione dei nemici su base colore per colore consentirebbe anche di far cooperare due istanze del bot (usando le coordinate pixel per passare un segno di riconoscimento segreto).
Sembra divertente, probabilmente lo proverò :).
Il percorso pesante di calcolo viene eseguito non appena sono disponibili nuovi dati (dopo una notifica di spostamento) e alcune ottimizzazioni (aggiornamento della frontiera) vengono eseguite subito dopo che è stata fornita una risposta (per eseguire il massimo calcolo possibile durante gli altri giri di robot ).
Anche in questo caso, potrebbero esserci modi per fare cose più sottili se ci fossero più di 1 avversario (come interrompere un calcolo se diventano disponibili nuovi dati), ma in ogni caso non riesco a vedere dove è necessario il multitasking, purché l'algoritmo sia in grado di lavorare a pieno carico.
Problemi di prestazione
Tutto ciò non può funzionare senza un rapido accesso ai dati (e una maggiore potenza di calcolo rispetto all'intero programma Appolo, ovvero il tuo PC medio quando viene utilizzato per fare più di pubblicare pochi tweet).
La velocità dipende fortemente dal compilatore. Di solito GNU batte Microsoft con un margine del 30% (questo è il numero magico che ho notato in altre 3 sfide di codice relative al percorso), ma questo chilometraggio può variare ovviamente.
Il codice allo stato attuale rompe a malapena il sudore nell'arena 4. Windows perfmeter riporta circa il 4 al 7% di utilizzo della CPU, quindi dovrebbe essere in grado di far fronte a una mappa 1000x1000 entro il limite di tempo di risposta di 100ms.
Al centro di ogni algoritmo di pathing c'è un FIFO (possibilmente proritizzato, anche se non in quel caso), che a sua volta richiede una rapida allocazione degli elementi.
Dato che l'OP ha imposto obbligatoriamente un limite alla dimensione dell'arena, ho fatto alcuni calcoli e ho visto che le strutture dati fisse dimensionate al massimo (cioè 1.000.000 pixel) non consumerebbero più di una dozzina di megabyte, che il tuo PC medio mangia a colazione.
Infatti sotto Win7 e compilato con MSVC 2013, il codice consuma circa 14 Mb su arena 4, mentre i due thread di Swallower utilizzano più di 20 Mb.
Ho iniziato con i contenitori STL per semplificare la prototipazione, ma STL ha reso il codice ancora meno leggibile, dal momento che non desideravo creare una classe per incapsulare ogni singolo dato per nascondere l'offuscamento (se ciò è dovuto alle mie incapacità di far fronte alla STL è lasciato all'apprezzamento del lettore).
Indipendentemente da ciò, il risultato è stato così atrocemente lento che all'inizio ho pensato di creare una versione di debug per errore.
Ritengo che ciò sia dovuto in parte all'implementazione incredibilmente scarsa di Microsoft dell'STL (dove, ad esempio, vettori e bitets eseguono controlli associati o altre operazioni crittografiche sull'operatore [], in diretta violazione delle specifiche), e in parte al design STL si.
Potrei far fronte alle atroci problemi di sintassi e portabilità (cioè Microsoft vs GNU) se le prestazioni fossero lì, ma questo non è certamente il caso.
Ad esempio, deque
è intrinsecamente lento, perché mescola molti dati di contabilità in attesa dell'occasione per fare il suo ridimensionamento super intelligente, di cui non me ne può fregare di meno.
Certo avrei potuto implementare un allocatore personalizzato e ridurre altri bit di template personalizzati, ma un allocatore personalizzato costa solo poche centinaia di righe di codice e la parte migliore di un giorno per testare, che cosa con la dozzina di interfacce deve implementare, mentre un la struttura equivalente fatta a mano è circa zero righe di codice (anche se più pericolosa, ma l'algoritmo non avrebbe funzionato se non avessi saputo - o penso di sapere - quello che stavo facendo comunque).
Così alla fine ho tenuto i contenitori STL in parti non critiche del codice e ho costruito il mio brutale allocatore e FIFO con due array di circa 1970 e tre cortometraggi non firmati.
Deglutizione del deglutitore
Come confermato dal suo autore, i modelli irregolari di Swallower sono causati da ritardo tra le notifiche delle mosse nemiche e gli aggiornamenti dal thread di percorso.
Il perfmeter di sistema mostra chiaramente il thread di pathing che consuma CPU al 100% in ogni momento, e gli schemi frastagliati tendono ad apparire quando il focus del combattimento si sposta su una nuova area. Questo è anche abbastanza evidente con le animazioni.
Un'ottimizzazione semplice ma efficace
Dopo aver visto gli epici combattimenti tra cani di Swallower e il mio combattente, mi sono ricordato un vecchio detto del gioco di Go: difendi da vicino, ma attacca da lontano.
C'è saggezza in questo. Se provi ad attaccarti troppo all'avversario, sprecherai mosse preziose cercando di bloccare ogni possibile percorso. Al contrario, se rimani a un solo pixel di distanza, probabilmente eviterai di riempire piccoli spazi vuoti che guadagnerebbero molto poco e utilizzerai le tue mosse per contrastare minacce più importanti.
Per implementare questa idea, ho semplicemente esteso le mosse di un nemico (contrassegnando i 4 vicini di ogni mossa come pixel nemico).
Questo ferma l'algoritmo di tracciamento a un pixel di distanza dal confine del nemico, permettendo al mio combattente di aggirare un avversario senza essere catturato in troppi combattimenti.
Puoi vedere il miglioramento
(anche se tutte le esecuzioni non hanno lo stesso successo, puoi notare i contorni molto più fluidi):