ColorFighter - C ++ - kahvaltıda birkaç kırlangıç yiyor
DÜZENLE
- kodu temizle
- basit ama etkili bir optimizasyon ekledi
- bazı GIF animasyonları ekledi
Tanrı yılanlardan nefret ederim (sadece örümceklermiş gibi davranın, Indy)
Aslında Python'u seviyorum. Keşke daha az tembel bir çocuk olsaydım ve onu düzgün öğrenmeye başladım, hepsi bu.
Bütün bunlar söyleniyor, yargıcın çalışmasını sağlamak için bu yılanın 64 bit versiyonu ile mücadele etmek zorunda kaldım. PIL'in Win7 altındaki 64 bit Python sürümüyle çalışması için bu zorluğa adamaya hazır olduğumdan daha fazla sabır gerekiyor, bu yüzden sonunda (acı bir şekilde) Win32 sürümüne geçtim.
Ayrıca, bir bot yanıt veremeyecek kadar yavaş olduğunda, Hakim fena çarpma eğilimindedir.
Python meraklısı olmadığım için tamir etmedim, ama stdin zaman aşımından sonra boş bir cevap okumak zorundaydı.
Küçük bir gelişme, stderr çıktısını her bot için bir dosyaya koymak olacaktır. Bu, ölüm sonrası hata ayıklama için izlemeyi kolaylaştırır.
Bu küçük problemler dışında, Hakimi kullanması çok basit ve keyifli buldum.
Başka bir yaratıcı ve eğlenceli mücadele için Kudos.
Kod
#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();
}
Yürütülebilir dosyayı oluşturma
Png görüntüleri okumak için LODEpng.cpp ve LODEpng.h kullandım .
En basit yöntem hakkında, bu gerizekalı C ++ diline yarım düzine kitaplık oluşturmak zorunda kalmadan bir resmin nasıl okunacağını öğrettim.
Sadece ana ile birlikte LODEpng.cpp dosyasını derleyin ve bağlayın.
Ben MSVC2013 ile derlenmiş, ama sadece bir kaç STL temel kaplar (deque ve vektörler) kullanılan bu yana, bu olabilir (eğer şanslıysanız) gcc ile çalışır.
Öyle değil, ben belki bir MinGW yapı deneyin, ama açıkçası ben C ++ taşınabilirlik sorunları yoruldum.
Günlerimde oldukça fazla taşınabilir C / C ++ yaptım (8 - 32 bit işlemcilerin yanı sıra SunOS, Windows 3.11'den Vista'ya ve Linux'un bebeklikten Ubuntu cooing zebra ya da her neyse, egzotik derleyicilerde. Taşınabilirliğin ne anlama geldiğine dair oldukça iyi bir fikrim var), ancak o zamanlar, GNU ve STL canavarının şifreli ve şişirilmiş özelliklerinin Microsoft yorumları arasındaki sayısız tutarsızlığı ezberlemesine gerek yoktu.
Sonuçlar Swallower
Nasıl çalışır
Çekirdek, bu basit bir kaba kuvvetli taşkın doldurma yamasıdır.
Oyuncunun renginin sınırı (yani en az bir beyaz komşusu olan pikseller) klasik mesafe taşkın algoritmasını gerçekleştirmek için bir tohum olarak kullanılır.
Bir nokta bir düşman renginin gerçekliğine ulaştığında, en yakın düşman noktasına doğru hareket eden bir dizi piksel üretmek için geriye doğru bir yol hesaplanır.
İşlem, istenen uzunlukta bir cevap için yeterli puan toplanana kadar tekrar edilir.
Bu tekrarlama, özellikle düşmanın yakınında savaşırken, çok pahalı.
Sınırdan bir düşman piksele giden bir piksel dizisi bulunduğunda (ve cevabı tamamlamak için daha fazla noktaya ihtiyacımız var), taşkın dolgusu baştan başlayarak, yeni sınıra eklenen yeni yolla yeniden yapılır. Bu, 10 piksel yanıt alabilmek için 5 taşkın dolgusu veya daha fazlasını yapmanız gerekebileceği anlamına gelir.
Daha fazla düşman pikseline ulaşılamıyorsa, sınır piksellerinin keyfi komşuları seçilir.
Algoritma, verimsiz bir taşkın dolgusuna gelişti, ancak bu, yalnızca oyunun sonucuna karar verildikten sonra olur (yani, savaşılacak tarafsız bir bölge yoktur).
Hâlihazırda yarışma yapıldıktan sonra haritayı doldurmak için yaşları harcamaması için optimize ettim. Mevcut haliyle, yürütme süresi Hakimin kendisiyle karşılaştırıldığında ihmal edilebilir düzeydedir.
Düşman renkleri başlangıçta bilinmediğinden, ilk arena görüntüsü düşmanın ilk hareketini yaptığı zaman düşmanın başlangıç alanlarını kopyalamak için depoda tutulur.
İlk önce kod oynatılırsa, birkaç rastgele pikseli dolduracak şekilde taşmaya başlar.
Bu, algoritmayı keyfi sayıda rakiple ve hatta zaman zaman rastgele bir noktaya gelen yeni rakiplerle veya başlangıç alanı olmadan ortaya çıkan renkleri (bunun kesinlikle pratik bir kullanımı olmasa da) mücadele edebilecek duruma getirir.
Renk başına renk temelinde düşmanın ele alınması, botun iki örneğinin birlikte çalışmasını sağlar (gizli tanıma işaretini geçmek için piksel koordinatlarını kullanarak).
Kulağa eğlenceli geliyor, muhtemelen deneyeceğim :).
Hesaplamaya yönelik ağır veri, yeni veriler mevcut olur olmaz (bir hareket bildiriminin ardından) yapılır ve bazı optimizasyonlar (sınır güncellemesi) bir cevap verildikten hemen sonra yapılır (diğer botlar dönüşlerde mümkün olduğunca fazla hesaplama yapmak için). ).
Burada yine, 1'den fazla düşman varsa (yeni veriler mevcutsa bir hesaplamayı bırakmak gibi), daha ince şeyler yapmanın yolları olabilir, fakat herhangi bir oranda, algoritma olduğu sürece, çoklu görevlerin gerekli olduğunu göremiyorum. tam yükte çalışabilir.
Performans sorunları
Tüm bunlar hızlı veri erişimi olmadan (ve tüm Appolo programından daha fazla bilgi işlem gücü, yani birkaç tweet göndermek için daha çok iş yaparken kullanılan ortalama PC'niz olmadan) çalışamaz.
Hız büyük ölçüde derleyiciye bağlıdır. Genelde GNU, Microsoft’u% 30 marjla yener (bu, diğer 3 kodlamayla ilgili kodda gördüğüm sihirli sayıdır), ancak bu kilometre elbette değişebilir.
Şu anki durumundaki kod, arena 4'teki terleri zorlukla kırar. Windows perfmeter yaklaşık% 4 ila 7 CPU kullanımı bildirir, bu nedenle 100ms'lik yanıt süresi içinde 1000x1000 haritayla başa çıkabilmelidir.
Yaklaşık her bir algoritma algoritmasının merkezinde bir FIFO bulunur (bu durumda olmasa da büyük olasılıkla proritized), ki bu da hızlı eleman tahsisi gerektirir.
OP, arena boyutuna mecbur bir şekilde sınır koyduğundan, bazı matematikler yaptım ve maks. Boyutta (yani 1.000.000 piksel) boyutlandırılmış sabit veri yapılarının ortalama PC'nizin kahvaltıda yediği birkaç düzine megabayttan fazla tüketmeyeceğini gördüm.
Nitekim Win7 altında ve MSVC 2013 ile derlendiğinde, kod 4 arenada yaklaşık 14Mb tüketirken, Swallower'in iki ipliği 20Mb'den fazla kullanıyor.
Daha kolay prototipleme için STL kapları ile başladım, ancak STL kodu daha az okunabilir hale getirdi, çünkü gizliliği gizlemek için her bir veri parçasını kapsüllemek için bir sınıf oluşturma arzusu yoktu (bu benim kendi engellerimden kaynaklanıyordu. STL ile başa çıkma okuyucunun takdirine bırakılmıştır).
Ne olursa olsun, sonuç o kadar acımasızca yavaştı ki, ilk başta yanlışlıkla bir hata ayıklama sürümü oluşturduğumu sanıyordum.
Bunun kısmen STL'nin inanılmaz derecede zayıf Microsoft uygulamasından kaynaklandığını düşünüyorum (örneğin, vektörler ve bitler, [[]] operatörü üzerindeki spesifik kontrolleri veya diğer şifreleme işlemlerini, spesifikasyonun doğrudan ihlaliyle yapıyor) ve kısmen STL tasarımına bağlı. kendisi.
Performanslar orada olsaydı korkunç sözdizimi ve taşınabilirlik (yani Microsoft vs GNU) sorunları ile başa çıkabilirdi, ama bu kesinlikle böyle değil.
Örneğin deque
, doğası gereği yavaştır; çünkü, daha az umursayamayacağım, süper akıllı yeniden boyutlandırma işlemini yapmayı beklerken etrafta pek çok defter tutma verisi vardır.
Tabii ki özel bir tahsisatçı ve balina avcısı için diğer özel şablon bitleri uygulayabilirdim, ancak özel bir tahsisatçı tek başına birkaç yüz satırlık bir kod satırına ve test etmek için bir günün daha iyi bir kısmına mal olur, ne kadarını uygulamak zorunda olduğu bir düzine el yapımı eşdeğer yapı sıfır kod satırları hakkındadır (daha tehlikeli olsa da, fakat yine de ne yaptığımı bilmeseydim - ya da bildiğimi düşünürsem algoritma işe yaramazdı).
Sonunda STL konteynerlerini kodun kritik olmayan kısımlarında tuttum ve iki acımasız tahsisatçıyı ve FIFO'yu 1970 circa iki dizi ve üç imzasız şortla oluşturdum.
Yutkunmayı yutmak
Yazarının onayladığı gibi, Kırlangıççı'nın düzensiz kalıpları, düşman hamlelerinin bildirimleri ve yamalı iplikten gelen güncellemeler arasındaki gecikmeden kaynaklanır.
Sistem perfmetresi her zaman% 100 CPU tüketen yamalı ipliği açıkça gösterir ve pürüzlü desenler kavga odağı yeni bir alana geçtiğinde ortaya çıkma eğilimindedir. Bu aynı zamanda animasyonlarda oldukça belirgindir.
Basit ama etkili bir optimizasyon
Swallower ve benim dövüşçüm arasındaki destansı köpek dövüşlerine baktıktan sonra, Go oyunundan eski bir deyişi hatırladım: yakın savun, ama uzaktan saldırı.
Bunda bilgelik var. Rakibinize çok fazla bağlı kalmaya çalışırsanız, olası her yolu engellemeye çalışarak değerli hamleleri boşa harcarsınız. Aksine, yalnızca bir piksel uzakta kalırsanız, çok az kazanç sağlayacak küçük boşlukları doldurmaktan kaçınacak ve hamlelerinizi daha önemli tehditlere karşı koymak için kullanacaksınız.
Bu fikri uygulamak için, düşmanın hareketlerini genişlettim (her hareketin 4 komşusunu bir düşman pikseli olarak işaretledim).
Bu, dövüş algoritmasının düşmanın sınırından bir piksel uzakta durmasını sağlar ve dövüşçümün bir savaşçıda çok fazla köpek dövüşüne yakalanmadan çalışmasını sağlar.
Gelişimi görebilirsiniz
(tüm çalışmalar başarılı olmasa da, daha yumuşak ana hatları fark edebilirsiniz):