ColorFighter - C ++ - mange quelques avaleurs au petit déjeuner
MODIFIER
- nettoyé le code
- ajout d'une optimisation simple mais efficace
- ajouté des animations GIF
Dieu que je déteste les serpents (prétendez qu'ils sont des araignées, Indy)
En fait, j'adore Python. J'aimerais être moins paresseux et commencer à bien apprendre, c'est tout.
Tout cela étant dit, je devais lutter avec la version 64 bits de ce serpent pour que le juge fonctionne. Pour que PIL fonctionne avec la version 64 bits de Python sous Win7, il faut plus de patience que ce que j'étais prêt à consacrer à ce défi. J'ai donc finalement basculé (péniblement) vers la version Win32.
En outre, le juge a tendance à se bloquer brutalement lorsqu'un bot est trop lent pour réagir.
Étant donné que je ne connaissais pas bien Python, je n’ai pas résolu le problème, mais il s’agit de lire une réponse vide après une temporisation sur stdin.
Une amélioration mineure consisterait à placer la sortie stderr dans un fichier pour chaque bot. Cela faciliterait le traçage pour le débogage post-mortem.
Hormis ces problèmes mineurs, j’ai trouvé le juge très simple et agréable à utiliser.
Bravo pour encore un autre défi inventif et amusant.
Le code
#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();
}
Construire l'exécutable
J'ai utilisé LODEpng.cpp et LODEpng.h pour lire des images png.
J'ai trouvé le moyen le plus simple d'enseigner à ce langage C ++ retardé comment lire une image sans avoir à construire une demi-douzaine de bibliothèques.
Il suffit de compiler et de lier LODEpng.cpp avec le principal et Bob, votre oncle.
J'ai compilé avec MSVC2013, mais comme je n'utilisais que quelques conteneurs STL de base (deque et vecteurs), cela pourrait fonctionner avec gcc (si vous avez de la chance).
Si ce n'est pas le cas, je pourrais essayer une version MinGW, mais franchement, j'en ai assez des problèmes de portabilité C ++.
J’ai beaucoup travaillé sur le C / C ++ portable à l’époque (sur des compilateurs exotiques pour divers processeurs de 8 à 32 bits, ainsi que pour SunOS, Windows de 3.11 à Vista et Linux, depuis ses débuts jusqu’à Ubuntu, roucoulant de zèbres, etc. J'ai une assez bonne idée de ce que signifie portabilité), mais à l'époque, il n'était pas nécessaire de mémoriser (ni de découvrir) les innombrables divergences entre les interprétations GNU et Microsoft des spécifications cryptiques et gonflées du monstre STL.
Résultats contre Swallower
Comment ça marche
À la base, il s’agit d’un simple chemin d’inondation en force brute.
La frontière de la couleur du joueur (c'est-à-dire les pixels ayant au moins un voisin blanc) est utilisée comme germe pour exécuter l'algorithme classique d'inondation de distance.
Lorsqu'un point atteint le voisinage d'une couleur ennemie, une trajectoire en arrière est calculée pour produire une chaîne de pixels se déplaçant vers le point ennemi le plus proche.
Le processus est répété jusqu'à ce que suffisamment de points aient été rassemblés pour une réponse de la longueur souhaitée.
Cette répétition est incroyablement chère, surtout lorsque vous combattez près de l'ennemi.
Chaque fois qu'une chaîne de pixels menant de la frontière à un pixel ennemi a été trouvée (et qu'il nous faut plus de points pour compléter la réponse), le remplissage de l'inondation est refait depuis le début, avec le nouveau chemin ajouté à la frontière. Cela signifie que vous pourriez avoir à effectuer 5 remplissages d’inondation ou plus pour obtenir une réponse de 10 pixels.
Si aucun autre pixel ennemi n'est accessible, les voisins arbitraire des pixels de frontière sont sélectionnés.
L'algorithme est dévolu à un remplissage d'inondation plutôt inefficace, mais cela ne se produit qu'une fois que l'issue du jeu a été décidée (c'est-à-dire qu'il n'y a plus de territoire neutre à défendre).
Je l'ai optimisé pour que le juge ne passe pas des heures à remplir la carte une fois la compétition réglée. Dans son état actuel, le temps d'exécution est négligeable par rapport au juge lui-même.
Comme les couleurs de l'ennemi ne sont pas connues au début, l'image d'arène initiale est conservée afin de copier les zones de départ de l'ennemi lors de son premier mouvement.
Si le code est lu en premier, il remplira simplement quelques pixels arbitraires.
Cela rend l'algorithme capable de combattre un nombre arbitraire d'adversaires, voire de nouveaux adversaires arrivant à un moment aléatoire, ou de couleurs apparaissant sans zone de départ (bien que cela n'ait absolument aucune utilisation pratique).
La gestion ennemie couleur par couleur permettrait également de faire coopérer deux instances du bot (en utilisant les coordonnées de pixels pour transmettre un signe de reconnaissance secret).
Ca a l'air amusant, je vais probablement essayer ça :).
Le cheminement lourd en calcul est effectué dès que de nouvelles données sont disponibles (après une notification de déplacement), et certaines optimisations (la mise à jour de la frontière) sont effectuées juste après qu'une réponse a été donnée (pour effectuer le plus de calculs possible pendant les autres tours de bots ).
Là encore, il pourrait y avoir moyen de faire des choses plus subtiles s'il y avait plus d'un adversaire (par exemple, abandonner un calcul si de nouvelles données devenaient disponibles), mais de toute façon, je ne vois pas où le multitâche est nécessaire, tant que l'algorithme est capable de travailler à pleine charge.
Les problèmes de performance
Tout cela ne peut fonctionner sans un accès rapide aux données (et à une puissance de calcul supérieure à celle du programme Appolo complet, c’est-à-dire à votre PC moyen utilisé pour autre chose que poster quelques tweets).
La vitesse dépend fortement du compilateur. Généralement, GNU bat Microsoft par une marge de 30% (c’est le chiffre magique que j’ai remarqué sur 3 autres problèmes de code lié aux chemins), mais ce kilométrage peut varier bien sûr.
Le perfmètre Windows signale environ 4 à 7% d'utilisation du processeur. Il devrait donc être capable de gérer une carte 1000x1000 dans le délai de réponse de 100 ms.
Au cœur de chaque algorithme de cheminement se trouve une FIFO (éventuellement proritisée, mais pas dans ce cas), qui à son tour nécessite une allocation rapide d'éléments.
Comme le PO fixait obligatoirement une limite à la taille de l'arène, j'ai fait quelques calculs et constaté que des structures de données fixes dimensionnées au maximum (1 000 000 pixels) ne consommeraient pas plus d'une vingtaine de mégaoctets, ce que votre PC moyen mange au petit déjeuner.
En effet sous Win7 et compilé avec MSVC 2013, le code consomme environ 14 Mo sur l’arène 4, alors que les deux threads de Swallower utilisent plus de 20 Mo.
J'ai commencé avec les conteneurs STL pour un prototypage plus facile, mais STL a rendu le code encore moins lisible, car je ne souhaitais pas créer une classe pour encapsuler chaque bit de données afin de masquer l'obfuscation (que cela soit dû à mes propres inaptitudes à faire face au TSL est laissé à l'appréciation du lecteur).
Quoi qu'il en soit, le résultat était si terriblement lent que j'ai d'abord pensé créer une version de débogage par erreur.
Je pense que cela est dû en partie à la très mauvaise implémentation de la STL par Microsoft (où, par exemple, les vecteurs et les bits effectuent des contrôles liés ou d’autres opérations cryptées sur l’opérateur [], en violation directe des spécifications), et en partie à la conception de la STL. lui-même.
Je pouvais faire face aux problèmes de syntaxe et de portabilité atroces (Microsoft vs GNU) si les performances étaient là, mais ce n'est certainement pas le cas.
Par exemple, elle deque
est intrinsèquement lente, car elle mélange beaucoup de données de comptabilité en attendant l'occasion de procéder à son redimensionnement très intelligent, ce dont je me moquais bien.
Bien sûr, j'aurais pu implémenter un allocateur personnalisé et d'autres types de gabarits personnalisés, mais un allocateur personnalisé coûte à lui seul quelques centaines de lignes de code et la plus grande partie de la journée à tester, avec la douzaine d'interfaces qu'il doit implémenter, alors qu'un La structure équivalente faite à la main correspond à zéro ligne de code (bien que plus dangereuse, mais l'algorithme n'aurait pas fonctionné si je ne savais pas - ou ne pensais pas savoir ce que je faisais de toute façon).
Alors, finalement, j'ai conservé les conteneurs STL dans des parties non critiques du code et construit mon propre allocateur brutal et FIFO avec deux tableaux vers 1970 et trois courts métrages non signés.
Avaler le avaleur
Comme son auteur l'a confirmé, les schémas irréguliers de Swallower sont dus à un décalage entre les notifications de mouvements ennemis et les mises à jour du fil pathing.
Le perfmeter du système indique clairement que le thread en route consomme à 100% le processeur en permanence et que les motifs en dents de scie tendent à apparaître lorsque l'objectif de la lutte se déplace vers un nouveau domaine. Ceci est également assez évident avec les animations.
Une optimisation simple mais efficace
Après avoir regardé les combats épiques entre Swallower et mon combattant, je me suis souvenu d'un vieil adage du jeu Go: défendre de près, mais attaquer à distance.
Il y a de la sagesse dans cela. Si vous essayez de trop vous en tenir à votre adversaire, vous perdrez de précieux mouvements en essayant de bloquer chaque chemin possible. Au contraire, si vous restez à un pixel de distance, vous éviterez probablement de combler de petits écarts qui ne gagneraient que très peu et utiliserez vos gestes pour contrer des menaces plus importantes.
Pour mettre en œuvre cette idée, j'ai simplement étendu les mouvements d'un ennemi (en marquant les 4 voisins de chaque mouvement comme un pixel ennemi).
Cela stoppe l'algorithme de trajectoire à un pixel de la frontière ennemie, permettant ainsi à mon combattant de contourner un adversaire sans se faire prendre à trop de combats aériens.
Vous pouvez voir l'amélioration
(bien que toutes les exécutions ne soient pas aussi réussies, vous pouvez remarquer les contours beaucoup plus lisses):