ColorFighter - C ++ - makan beberapa swallower untuk sarapan
EDIT
- membersihkan kode
- menambahkan pengoptimalan yang sederhana namun efektif
- menambahkan beberapa animasi GIF
Ya Tuhan, aku benci ular (pura-pura itu laba-laba, Indy)
Sebenarnya saya suka Python. Seandainya saja saya bukan anak yang malas dan mulai mempelajarinya dengan baik, itu saja.
Semua ini dikatakan, saya harus berjuang dengan versi 64 bit dari ular ini untuk membuat Hakim bekerja. Membuat PIL bekerja dengan versi 64 bit Python di bawah Win7 membutuhkan lebih banyak kesabaran daripada yang saya siapkan untuk tantangan ini, jadi pada akhirnya saya beralih (dengan menyakitkan) ke versi Win32.
Juga, Hakim cenderung crash parah ketika bot terlalu lambat untuk merespon.
Menjadi bukan Python savvy, saya tidak memperbaikinya, tetapi itu ada hubungannya dengan membaca jawaban kosong setelah batas waktu pada stdin.
Perbaikan kecil akan menempatkan output stderr ke file untuk setiap bot. Itu akan memudahkan pelacakan untuk debugging post-mortem.
Kecuali untuk masalah-masalah kecil ini, saya menemukan bahwa Hakim itu sangat sederhana dan menyenangkan untuk digunakan.
Pujian untuk tantangan inventif dan menyenangkan lainnya.
Kode
#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();
}
Membangun executable
Saya menggunakan LODEpng.cpp dan LODEpng.h untuk membaca gambar png.
Tentang cara termudah yang saya temukan untuk mengajarkan bahasa C ++ terbelakang ini cara membaca gambar tanpa harus membangun setengah lusin perpustakaan.
Kompilasi saja & tautkan LODEpng.cpp bersama dengan main dan Bob adalah pamanmu.
Saya dikompilasi dengan MSVC2013, tetapi karena saya hanya menggunakan beberapa kontainer dasar STL (deque dan vektor), mungkin berhasil dengan gcc (jika Anda beruntung).
Jika tidak, saya mungkin mencoba membangun MinGW, tapi terus terang saya bosan dengan masalah portabilitas C ++.
Saya melakukan cukup banyak C / C ++ portabel di hari-hari saya (pada kompiler eksotis untuk berbagai 8 hingga 32 bit prosesor serta SunOS, Windows dari 3,11 hingga Vista dan Linux dari masa kanak-kanak ke Ubuntu cooing zebra atau apa pun, jadi saya pikir Saya memiliki ide yang cukup bagus tentang apa artinya portabilitas), tetapi pada saat itu tidak perlu untuk menghafal (atau menemukan) perbedaan yang tak terhitung banyaknya antara interpretasi GNU dan Microsoft tentang spesifikasi samar dan bengkak dari monster STL.
Hasil melawan Swallower
Bagaimana itu bekerja
Pada intinya, ini adalah jalur penimbunan brute force yang sederhana.
Batas warna pemain (yaitu piksel yang memiliki setidaknya satu tetangga putih) digunakan sebagai seed untuk melakukan algoritma flooding jarak klasik.
Ketika suatu titik mencapai lingkaran warna musuh, jalur mundur dihitung untuk menghasilkan serangkaian piksel bergerak menuju tempat musuh terdekat.
Proses ini diulangi sampai cukup banyak poin telah dikumpulkan untuk tanggapan dengan panjang yang diinginkan.
Pengulangan ini sangat mahal, terutama ketika bertarung di dekat musuh.
Setiap kali serangkaian piksel yang mengarah dari perbatasan ke piksel musuh telah ditemukan (dan kami membutuhkan lebih banyak poin untuk menyelesaikan jawabannya), isi banjir akan dikerjakan ulang dari awal, dengan jalur baru ditambahkan ke perbatasan. Ini berarti Anda harus melakukan 5 pengisian banjir atau lebih untuk mendapatkan jawaban 10 piksel.
Jika tidak ada lagi piksel musuh yang dapat dijangkau, tetangga arbitrer dari piksel perbatasan dipilih.
Algoritma beralih ke pengisian banjir yang agak tidak efisien, tetapi ini hanya terjadi setelah hasil permainan telah diputuskan (yaitu tidak ada wilayah yang lebih netral untuk diperjuangkan).
Saya memang mengoptimalkannya sehingga Hakim tidak menghabiskan waktu lama untuk mengisi peta begitu kompetisi telah ditangani. Dalam kondisi saat ini, waktu eksekusi dapat diabaikan dibandingkan dengan Hakim itu sendiri.
Karena warna musuh tidak diketahui saat start, gambar arena awal disimpan di toko untuk menyalin area awal musuh ketika itu membuat langkah pertama.
Jika kode diputar terlebih dahulu, itu hanya akan mengisi beberapa piksel sembarang.
Hal ini membuat algoritma mampu melawan sejumlah musuh yang sewenang-wenang, dan bahkan mungkin musuh baru tiba pada titik waktu yang acak, atau warna muncul tanpa area awal (meskipun ini sama sekali tidak ada penggunaan praktis).
Penanganan musuh berdasarkan warna-per-warna juga akan memungkinkan dua instance bot bekerja sama (menggunakan koordinat piksel untuk melewati tanda pengakuan rahasia).
Kedengarannya menyenangkan, saya mungkin akan mencobanya :).
Penelusuran berat komputasi dilakukan segera setelah data baru tersedia (setelah pemberitahuan perpindahan), dan beberapa optimisasi (pembaruan perbatasan) dilakukan tepat setelah respons diberikan (untuk melakukan perhitungan sebanyak mungkin selama putaran bot lainnya) ).
Di sini lagi, mungkin ada cara untuk melakukan hal-hal yang lebih halus jika ada lebih dari 1 musuh (seperti membatalkan perhitungan jika data baru tersedia), tetapi bagaimanapun saya gagal untuk melihat di mana multitasking diperlukan, asalkan algoritma itu dapat bekerja dengan beban penuh.
Masalah kinerja
Semua ini tidak dapat bekerja tanpa akses data cepat (dan lebih banyak daya komputasi dari seluruh program Appolo, yaitu PC rata-rata Anda saat digunakan untuk melakukan lebih dari memposting beberapa tweet).
Kecepatannya sangat bergantung pada kompiler. Biasanya GNU mengalahkan Microsoft dengan selisih 30% (itu angka ajaib yang saya perhatikan pada 3 tantangan kode terkait lintasan lainnya), tetapi jarak tempuh ini tentu saja bervariasi.
Kode dalam kondisi saat ini nyaris tidak berkeringat di arena 4. Windows perfmeter melaporkan penggunaan CPU sekitar 4 hingga 7%, sehingga harus mampu mengatasi peta 1000x1000 dalam batas waktu respons 100 ms.
Inti dari setiap algoritma lintasan terletak pada FIFO (mungkin diprioritaskan, meskipun tidak dalam kasus itu), yang pada gilirannya membutuhkan alokasi elemen cepat.
Karena OP wajib menetapkan batas ukuran arena, saya melakukan beberapa matematika dan melihat bahwa struktur data tetap berdimensi maks (yaitu 1.000.000 piksel) tidak akan mengkonsumsi lebih dari beberapa lusin megabita, yang rata-rata PC Anda makan untuk sarapan.
Memang di bawah Win7 dan dikompilasi dengan MSVC 2013, kode ini mengkonsumsi sekitar 14MB di arena 4, sementara dua utas Swallower menggunakan lebih dari 20MB.
Saya mulai dengan wadah STL untuk prototipe yang lebih mudah, tetapi STL membuat kodenya bahkan lebih mudah dibaca, karena saya tidak punya keinginan untuk membuat kelas untuk merangkum setiap bit data untuk menyembunyikan kebingungan menjauh (apakah itu karena ketidakmampuan saya sendiri untuk mengatasi STL diserahkan kepada apresiasi pembaca).
Bagaimanapun, hasilnya sangat lambat sehingga pada awalnya saya pikir saya membangun versi debug secara tidak sengaja.
Saya rasa ini sebagian karena implementasi STL Microsoft yang sangat buruk (di mana, misalnya, vektor dan bitet melakukan pemeriksaan terikat atau operasi crypic lainnya pada operator [], yang melanggar spesifikasi secara langsung), dan sebagian karena desain STL diri.
Saya bisa mengatasi masalah sintaksis dan portabilitas (yaitu Microsoft vs GNU) yang buruk jika pertunjukannya ada di sana, tetapi ini tentu saja bukan masalahnya.
Sebagai contoh, deque
secara inheren lambat, karena mengacak banyak data pembukuan sekitar menunggu kesempatan untuk melakukan perubahan ukuran super pintar, tentang yang saya tidak peduli.
Tentu saya bisa menerapkan pengalokasi kustom dan whatver bit template kustom lainnya, tetapi pengalokasi kustom sendiri biaya beberapa ratus baris kode dan bagian yang lebih baik dari hari untuk menguji, apa dengan selusin antarmuka yang harus diimplementasikan, sementara struktur setara buatan tangan adalah tentang nol baris kode (walaupun lebih berbahaya, tetapi algoritme tidak akan bekerja jika saya tidak tahu - atau berpikir saya tahu - apa yang saya lakukan tetap).
Jadi akhirnya saya menyimpan kontainer STL di bagian kode yang tidak kritis, dan membangun pengalokasi brutal saya sendiri dan FIFO dengan dua array sekitar tahun 1970 dan tiga celana pendek yang tidak ditandatangani.
Menelan burung walet
Seperti yang dikonfirmasikan oleh penulisnya, pola Swallower yang tidak menentu disebabkan oleh jeda antara pemberitahuan pergerakan musuh dan pembaruan dari utas lintasan.
Perfmeter sistem menunjukkan dengan jelas bahwa jalur path mengkonsumsi 100% CPU sepanjang waktu, dan pola bergerigi cenderung muncul ketika fokus pertarungan bergeser ke area baru. Ini juga cukup jelas dengan animasinya.
Optimalisasi sederhana namun efektif
Setelah melihat pertarungan epik antara Swallower dan petarung saya, saya teringat pepatah lama dari permainan Go: bertahan dari dekat, tetapi serang dari jauh.
Ada kebijaksanaan dalam hal itu. Jika Anda mencoba terlalu banyak bertahan pada musuh Anda, Anda akan menyia-nyiakan gerakan berharga mencoba memblokir setiap jalur yang mungkin. Sebaliknya, jika Anda tinggal satu piksel jauhnya, Anda kemungkinan akan menghindari mengisi celah kecil yang akan mendapatkan sangat sedikit, dan menggunakan gerakan Anda untuk menghadapi ancaman yang lebih penting.
Untuk mengimplementasikan ide ini, saya cukup memperluas gerakan musuh (menandai 4 tetangga dari setiap gerakan sebagai piksel musuh).
Ini menghentikan algoritme lintasan satu piksel dari perbatasan musuh, memungkinkan pejuang saya untuk mengatasi musuh tanpa terjebak dalam terlalu banyak pertempuran udara.
Anda dapat melihat peningkatannya
(meskipun semua proses tidak berhasil, Anda dapat melihat garis besar yang jauh lebih lancar):