Image Bataille de Couleurs

33

FÉLICITATIONS à @kuroineko pour la meilleure candidature et au gain de 200 $ de @TheBestOne (excellente sportivité!).

Ecrivez un programme pour colorer autant d'images que possible avant les programmes d'opposition.

Règles en bref

  • Votre programme recevra une image, votre couleur et le nombre entier N.
  • Chaque tour, d'autres programmes vous envoient des mises à jour de pixels et vous demandent vos mises à jour de N.
  • Vous pouvez mettre à jour tout pixel blanc situé à côté d'un pixel de votre couleur.
  • Le programme qui a ajouté le plus de pixels gagne.

Règles en détail

Votre programme recevra un nom de fichier d'image PNG, une couleur de base et un nombre N. Le nombre N est le nombre maximal de pixels que votre programme peut colorier à chaque tour.

Exemple: MyProg arena.png (255,0,0) 30

L'image d'entrée sera un rectangle avec des côtés compris entre 20 et 1000 pixels de long. Il sera composé de pixels noirs, blancs et de couleur. Votre programme peut choisir une séquence de pixels blancs à colorier, à la condition que chaque nouveau pixel ait au moins un de ses quatre pixels voisins de votre propre couleur. L'image aura initialement au moins un pixel de votre couleur. Il peut également avoir des pixels de couleurs auxquels aucun programme n'est affecté. Le canal alpha n'est pas utilisé.

Votre but est de bloquer vos adversaires et d’écrire votre couleur en autant de pixels que vous pouvez.

À chaque tour, votre programme acceptera une ou plusieurs lignes de message sur STDIN et écrira une ligne composée de coordonnées de pixels sur STDOUT. N'oubliez pas d'affecter STDOUT comme non tamponné ou de vider la mémoire tampon de STDOUT à chaque tour.

L'ordre des joueurs appelés à chaque tour sera attribué au hasard. Cela signifie qu'un adversaire (ou votre programme) peut avoir 2 tours d'affilée.

Des colour (N,N,N) chose X,Y X,Y ... X,Ymessages d’information décrivant les pixels renseignés par les programmes du joueur seront envoyés à votre programme . Si un joueur ne fait pas ou pas de coups valides, aucun message concernant les coups de ce joueur ne vous sera envoyé. Votre programme recevra également un message sur vos mouvements acceptés (si vous avez spécifié au moins un mouvement valide). Le pixel 0,0 est dans le coin supérieur gauche de l'image.

À la réception pick pixels, votre programme affichera X,Y X,Y ... X,Yjusqu'à N pixels (une chaîne vide composée uniquement d'un '\ n' est autorisée). Les pixels doivent être dans l'ordre du tracé. Si un pixel est invalide, il sera ignoré et ne sera pas dans le rapport aux joueurs. Votre programme a 2 secondes pour s’initialiser après le démarrage, mais seulement 0,1 seconde pour répondre avec une réponse à chaque tour, sinon il ratera ce tour. Une mise à jour de pixel envoyée après 0,1 seconde enregistre une erreur. Après 5 erreurs, votre programme est suspendu et aucune mise à jour ou pick pixelsdemande ne sera envoyée .

Lorsque le programme du juge reçoit un choix de pixels vide ou non valide de chaque programme de joueur non suspendu, l'image est considérée comme terminée et les programmes reçoivent le message "exit". Les programmes doivent se terminer après avoir reçu "exit".

Notation

Le juge marquera des points une fois l'image terminée. Votre score sera votre nombre de pixels mis à jour divisé par la capture de pixels moyenne de ce tour, exprimée en pourcentage.

Le nombre de pixels ajoutés à l'image par votre lecteur est A. Le nombre total de pixels ajoutés par tous les lecteurs P est égal à T. avg = T/P score = 100*A/avg

Affichage des scores

Un adversaire de référence "The Blob" est donné. Pour chaque réponse, nommez votre bot avec un nom, une langue et votre score (moyenne des arènes 1 à 4) contre l'adversaire de référence. Une image ou une animation de l’une de vos batailles serait également utile. Le gagnant est le programme avec le score le plus élevé contre le bot de référence.

Si le blob s'avère trop facile à battre, je pourrais ajouter un deuxième tour avec un adversaire de référence plus puissant.

Vous voudrez peut-être aussi expérimenter avec 4 programmes de joueur ou plus. Vous pouvez également tester votre bot contre d'autres bots postés comme réponses.

Le juge

Le programme juge nécessite la bibliothèque commune d'imagerie Python (PIL) et doit être facile à installer à partir du gestionnaire de packages de votre système d'exploitation sous Linux. J'ai signalé que PIL ne fonctionnait pas avec Python 64 bits sous Windows 7, veuillez donc vérifier si PIL fonctionnera pour vous avant de commencer ce défi (mis à jour le 2015-01-29).

#!/usr/bin/env python
# Judge Program for Image Battle challenge on PPCG.
# Runs on Python 2.7 on Ubuntu Linux. May need edits for other platforms.
# V1.0 First release.
# V1.1 Added Java support
# V1.2 Added Java inner class support
# usage: judge cfg.py
import sys, re, random, os, shutil, subprocess, datetime, time, signal
from PIL import Image

ORTH = ((-1,0), (1,0), (0,-1), (0,1))
def place(loc, colour):
    # if valid, place colour at loc and return True, else False
    if pix[loc] == (255,255,255):
        plist = [(loc[0]+dx, loc[1]+dy) for dx,dy in ORTH]
        if any(pix[p]==colour for p in plist if 0<=p[0]<W and 0<=p[1]<H):
            pix[loc] = colour
            return True
    return False

def updateimage(image, msg, bot):
    if not re.match(r'(\s*\d+,\d+)*\s*', msg):
        return []
    plist = [tuple(int(v) for v in pr.split(',')) for pr in msg.split()]
    plist = plist[:PIXELBATCH]
    return [p for p in plist if place(p, bot.colour)]

class Bot:
    botlist = []
    def __init__(self, name, interpreter=None, colour=None):
        self.prog = name
        self.botlist.append(self)
        callarg = re.sub(r'\.class$', '', name)  # Java fix
        self.call = [interpreter, callarg] if interpreter else [callarg]
        self.colour = colour
        self.colstr = str(colour).replace(' ', '')
        self.faults = 0
        self.env = 'env%u' % self.botlist.index(self)
        try: os.mkdir(self.env)
        except: pass
        if name.endswith('.class'): # Java inner class fix
            rootname = re.sub(r'\.class$', '', name)
            for fn in os.listdir('.'):
                if fn.startswith(rootname) and fn.endswith('.class'):
                    shutil.copy(fn, self.env)
        else:
            shutil.copy(self.prog, self.env)
        shutil.copy(imagename, self.env)
        os.chdir(self.env)
        args = self.call + [imagename, self.colstr, `PIXELBATCH`]
        self.proc = subprocess.Popen(args, stdin=subprocess.PIPE, 
            stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        os.chdir('..')
    def send(self, msg):
        if self.faults < FAULTLIMIT:
            self.proc.stdin.write(msg + '\n')
            self.proc.stdin.flush()
    def read(self, timelimit):
        if self.faults < FAULTLIMIT:
            start = time.time()
            inline = self.proc.stdout.readline()
            if time.time() - start > timelimit:
                self.faults += 1
                inline = ''
            return inline.strip()
    def exit(self):
        self.send('exit')

from cfg import *
for i, (prog, interp) in enumerate(botspec):
    Bot(prog, interp, colourspec[i])

image = Image.open(imagename)
pix = image.load()
W,H = image.size

time.sleep(INITTIME)
total = 0
for turn in range(1, MAXTURNS+1):
    random.shuffle(Bot.botlist)
    nullbots = 0
    for bot in Bot.botlist:
        bot.send('pick pixels')
        inmsg = bot.read(TIMELIMIT)
        newpixels = updateimage(image, inmsg, bot)
        total += len(newpixels)
        if newpixels:
            pixtext = ' '.join('%u,%u'%p for p in newpixels)
            msg = 'colour %s chose %s' % (bot.colstr, pixtext)
            for msgbot in Bot.botlist:
                msgbot.send(msg)
        else:
            nullbots += 1
    if nullbots == len(Bot.botlist):
        break
    if turn % 100 == 0: print 'Turn %s done %s pixels' % (turn, total)
for msgbot in Bot.botlist:
    msgbot.exit()

counts = dict((c,f) for f,c in image.getcolors(W*H))
avg = 1.0 * sum(counts.values()) / len(Bot.botlist)
for bot in Bot.botlist:
    score = 100 * counts[bot.colour] / avg
    print 'Bot %s with colour %s scored %s' % (bot.prog, bot.colour, score)
image.save(BATTLE+'.png')

Exemple de configuration - cfg.py

BATTLE = 'Green Blob vs Red Blob'
MAXTURNS = 20000
PIXELBATCH = 10
INITTIME = 2.0
TIMELIMIT = 0.1
FAULTLIMIT = 5

imagename = 'arena1.png'

colourspec = (0,255,0), (255,0,0)

botspec = [
    ('blob.py', 'python'),
    ('blob.py', 'python'),
    ]

The Blob - l'adversaire de référence

# Blob v1.0 - A reference opponent for the Image Battle challenge on PPCG.
import sys, os
from PIL import Image

image = Image.open(sys.argv[1])
pix = image.load()
W,H = image.size
mycolour = eval(sys.argv[2])
pixbatch = int(sys.argv[3])

ORTH = ((-1,0), (1,0), (0,-1), (0,1))
def canchoose(loc, colour):
    if pix[loc] == (255,255,255):
        plist = [(loc[0]+dx, loc[1]+dy) for dx,dy in ORTH]
        if any(pix[p]==colour for p in plist if 0<=p[0]<W and 0<=p[1]<H):
            return True
    return False

def near(loc):
    plist = [(loc[0]+dx, loc[1]+dy) for dx,dy in ORTH]
    pboard = [p for p in plist if 0<=p[0]<W and 0<=p[1]<H]
    return [p for p in pboard if pix[p] == (255,255,255)]

def updateimage(image, msg):
    ctext, colourtext, chose, points = msg.split(None, 3)
    colour = eval(colourtext)
    plist = [tuple(int(v) for v in pr.split(',')) for pr in points.split()]
    for p in plist:
        pix[p] = colour
        skin.discard(p)
        if colour == mycolour:
            for np in near(p):
                skin.add(np)

board = [(x,y) for x in range(W) for y in range(H)]
skin = set(p for p in board if canchoose(p, mycolour))

while 1:
    msg = sys.stdin.readline()
    if msg.startswith('colour'):
        updateimage(image, msg.strip())
    if msg.startswith('pick'):
        plen = min(pixbatch, len(skin))
        moves = [skin.pop() for i in range(plen)]
        movetext = ' '.join('%u,%u'%p for p in moves)
        sys.stdout.write(movetext + '\n')
        sys.stdout.flush()
    if msg.startswith('exit'):
        break

image.save('blob.png')

Arena 1

arena1.png

Arena 2

arena2.png

Arena 3

arena3.png

Arena 4

arena4.png

Un exemple de bataille - Blob vs Blob

Cette bataille a eu un résultat prévisible:

Bot blob.py with colour (255, 0, 0) scored 89.2883333333
Bot blob.py with colour (0, 255, 0) scored 89.365

Exemple de bataille

Logic Knight
la source
Êtes-vous sûr que cela ne devrait pas être un [roi de la colline]?
Justin
J'ai pensé à ça. Les robots ne s'affrontent pas directement. Ils combattent le bot de référence. Est-ce que cela exclut KOTH?
Logic Knight
Oui, comme c'est, ce n'est pas un KOTH, je vous demandais si vous étiez sûr de vouloir combattre le bot de référence plutôt que l'autre.
Justin
1
@TheBestOne, Ajout du support Java. Non testé avec le programme Java cependant. Faites-moi savoir si cela ne fonctionne pas.
Logic Knight
1
Les 10 pixels étant placés dans l'ordre, les pixels ultérieurs peuvent s'appuyer sur les emplacements précédents. Ils peuvent s'appuyer l'un sur l'autre, comme vous le suggérez.
Logic Knight le

Réponses:

17

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

arena1 arena2 arena3 arena4

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 dequeest 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):

avant après


la source
1
Sensationnel. Je pensais que rien n'allait battre le Swallower. Excellente solution avec une excellente description. Je me souviens de K & R C du bon vieux temps, mais C est ensuite passé du côté obscur. Je pense que vous aimerez Python .
Logic Knight
Ce fut un réel plaisir de relever un défi alors… bien… stimulant et amusant. Cela m'a permis de tester ce petit bijou de LODEpng à grande échelle, et les résultats sont si prometteurs que je pourrais revoir le png racer, tester encore une fois ma relation amour / haine avec ce tristement célèbre C. post-incrémenté.
1
L’avaleur est parfois un peu erratique afin de respecter le délai imparti. C'est en partie à cela que sert le multi-threading. Bon travail!! Je pense que je vais doubler mon bonus ...
TheNumberOne
1
Oreiller a des téléchargements pour 64 bits. Il peut être utilisé comme PIL.
TheNumberOne
@ TheBestOne Je le pensais bien. Mon peintre brutal profite de ces moments où votre avaleur rassemble des données périmées :). En ce qui concerne PIL, j'ai téléchargé environ toutes les versions d'Amd64 PIL et Pillow disponibles sur le World Wide Web, mais elles ne fonctionneraient pas avec mon noyau Python 63.5 bits, qui était probablement une version bootleg et / ou obsolète. Quoi qu'il en soit, le port Win32 fonctionne aussi bien, et si un jour, j’ai besoin de quelque chose de plus rapide, je dois tout de même passer à PyPy.
21

Profondeur-premier blob contre blob

Langue = Python (3.2)

Score = 111.475388276 153.34210035

Mise à jour: utilisez maintenant une Setclasse personnalisée pour que la pop()méthode produise une sorte de motif de grille qui améliore considérablement la zone couverte au début en coupant de grandes parties de l'image de l'ennemi. Remarque: j’utilise pour ce tableau une grille de 12 x 12 qui, sur un échantillon aléatoire de tailles de grille, semble donner les meilleurs résultats pour arena3 (celui qui a obtenu le plus mauvais score avant la mise à jour). Cependant, il est très probable qu’une la taille de la grille existe pour la sélection donnée d'arénas.

J'ai opté pour une simple modification du bot de référence afin de lui donner la préférence pour choisir des points réalisables bordés par le moins de points de couleur propre possible. Une amélioration consisterait peut-être également à favoriser le choix de points réalisables bordés par autant de points de couleur ennemie que possible.

dfblob.py:

import sys, os
from PIL import Image

class RoomyIntPairHashSet:
    def __init__(self, firstMax, secondMax):
        self.m1 = firstMax
        self.m2 = secondMax
        self.set = [set() for i in range((firstMax - 1) * (secondMax - 1) + 1)]
        self.len = 0

    def add(self, tup):
        subset = self.set[self.gettuphash(tup)]
        self.len -= len(subset)
        subset.add(tup)
        self.len += len(subset)

    def discard(self, tup):
        subset = self.set[self.gettuphash(tup)]
        self.len -= len(subset)
        subset.discard(tup)
        self.len += len(subset)

    def pop(self):
        for s in self.set:
            if len(s) > 0:
                self.len -= 1
                return s.pop()
        return self.set[0].pop()

    def gettuphash(self, tup):
        return (tup[0] % self.m1) * (tup[1] % self.m2)

    def __len__(self):
        return self.len

gridhashwidth = 12
gridhashheight = 12
image = Image.open(sys.argv[1])
pix = image.load()
W,H = image.size
mycolour = eval(sys.argv[2])
pixbatch = int(sys.argv[3])

ORTH = ((-1,0), (1,0), (0,-1), (0,1))
def canchoose(loc, virtualneighbors, colour, num_neighbors):
    if pix[loc] == (255,255,255):
        plist = [(loc[0]+dx, loc[1]+dy) for dx,dy in ORTH]
        actual_num_neighbors = 0
        for p in plist:
            if 0<=p[0]<W and 0<=p[1]<H and pix[p]==colour or p in virtualneighbors:
                actual_num_neighbors += 1
        return num_neighbors == actual_num_neighbors
    return False

def near(loc, exclude):
    plist = [(loc[0]+dx, loc[1]+dy) for dx,dy in ORTH]
    pboard = [p for p in plist if 0<=p[0]<W and 0<=p[1]<H]
    return [p for p in pboard if pix[p] == (255,255,255) and p not in exclude]

def updateimage(image, msg):
    ctext, colourtext, chose, points = msg.split(None, 3)
    colour = eval(colourtext)
    plist = [tuple(int(v) for v in pr.split(',')) for pr in points.split()]
    for p in plist:
        pix[p] = colour
        for i in range(len(skins)):
            skins[i].discard(p)
        if colour == mycolour:
            for np in near(p, []):
                for j in range(len(skins)):
                    skins[j].discard(np)
                    if canchoose(np, [], mycolour, j + 1):
                        skins[j].add(np)


board = [(x,y) for x in range(W) for y in range(H)]
skins = []
for i in range(1, 1 + len(ORTH)):
    skin = RoomyIntPairHashSet(gridhashwidth, gridhashheight)
    skins.append(skin)
    for p in board:
        if canchoose(p, [], mycolour, i):
            skin.add(p)

while 1:
    msg = sys.stdin.readline()
    print("got message "+ msg, file=sys.stderr)
    if msg.startswith('colour'):
        print("updating image", file=sys.stderr)
        updateimage(image, msg.strip())
        print("updated image", file=sys.stderr)
    if msg.startswith('pick'):
        moves = []
        print("picking moves", file=sys.stderr)
        virtualskins = [RoomyIntPairHashSet(gridhashwidth, gridhashheight) for i in range(len(skins))]
        for i in range(pixbatch):
            for j in range(len(skins)):
                if len(virtualskins[j]) > 0 or len(skins[j]) > 0:
                    move = None
                    if len(virtualskins[j]) > 0:
                        move = virtualskins[j].pop()
                    else:
                        move = skins[j].pop()
                    moves.append(move)
                    print("picking move (%u,%u) " % move, file=sys.stderr)
                    for p in near(move, moves):
                        for k in range(len(skins)):
                            virtualskins[k].discard(p)
                            if canchoose(p, moves, mycolour, k + 1):
                                virtualskins[k].add(p)
                    break
        movetext = ' '.join('%u,%u'%p for p in moves)
        print("picked %u moves" % (len(moves)), file=sys.stderr)
        sys.stdout.write(movetext + '\n')
        sys.stdout.flush()
    if msg.startswith('exit') or len(msg) < 1:
        break

image.save('dfblob.png')

Le juge d'origine a été légèrement modifié pour fonctionner avec Python 3.2 (et pour ajouter une fonctionnalité de journalisation brute aux bots + sauvegarder l'image de l'arène périodiquement pour créer une animation):

import sys, re, random, os, shutil, subprocess, datetime, time, signal, io
from PIL import Image

ORTH = ((-1,0), (1,0), (0,-1), (0,1))
def place(loc, colour):
    # if valid, place colour at loc and return True, else False
    if pix[loc] == (255,255,255):
        plist = [(loc[0]+dx, loc[1]+dy) for dx,dy in ORTH]
        if any(pix[p]==colour for p in plist if 0<=p[0]<W and 0<=p[1]<H):
            pix[loc] = colour
            return True
    return False

def updateimage(image, msg, bot):
    if not re.match(r'(\s*\d+,\d+)*\s*', msg):
        return []
    plist = [tuple(int(v) for v in pr.split(',')) for pr in msg.split()]
    plist = plist[:PIXELBATCH]
    return [p for p in plist if place(p, bot.colour)]

class Bot:
    botlist = []
    def __init__(self, name, interpreter=None, colour=None):
        self.prog = name
        self.botlist.append(self)
        callarg = re.sub(r'\.class$', '', name)
        self.call = [interpreter, callarg] if interpreter else [callarg]
        self.colour = colour
        self.colstr = str(colour).replace(' ', '')
        self.faults = 0
        self.env = 'env%u' % self.botlist.index(self)
        try: os.mkdir(self.env)
        except: pass
        shutil.copy(self.prog, self.env)
        shutil.copy(imagename, self.env)
        os.chdir(self.env)
        args = self.call + [imagename, self.colstr, str(PIXELBATCH)]
        errorfile = 'err.log'
        with io.open(errorfile, 'wb') as errorlog:
            self.proc = subprocess.Popen(args, stdin=subprocess.PIPE, 
                stdout=subprocess.PIPE, stderr=errorlog)
        os.chdir('..')
    def send(self, msg):
        if self.faults < FAULTLIMIT:
            self.proc.stdin.write((msg+'\n').encode('utf-8'))
            self.proc.stdin.flush()
    def read(self, timelimit):
        if self.faults < FAULTLIMIT:
            start = time.time()
            inline = self.proc.stdout.readline().decode('utf-8')
            if time.time() - start > timelimit:
                self.faults += 1
                inline = ''
            return inline.strip()
    def exit(self):
        self.send('exit')

from cfg import *
for i, (prog, interp) in enumerate(botspec):
    Bot(prog, interp, colourspec[i])

image = Image.open(imagename)
pix = image.load()
W,H = image.size
os.mkdir('results')

time.sleep(INITTIME)
total = 0
for turn in range(1, MAXTURNS+1):
    random.shuffle(Bot.botlist)
    nullbots = 0
    for bot in Bot.botlist:
        bot.send('pick pixels')
        inmsg = bot.read(TIMELIMIT)
        newpixels = updateimage(image, inmsg, bot)
        total += len(newpixels)
        if newpixels:
            pixtext = ' '.join('%u,%u'%p for p in newpixels)
            msg = 'colour %s chose %s' % (bot.colstr, pixtext)
            for msgbot in Bot.botlist:
                msgbot.send(msg)
        else:
            nullbots += 1
    if nullbots == len(Bot.botlist):
        break
    if turn % 100 == 0:
        print('Turn %s done %s pixels' % (turn, total))
        image.save("results/"+BATTLE+str(turn//100).zfill(3)+'.png')
for msgbot in Bot.botlist:
    msgbot.exit()

counts = dict((c,f) for f,c in image.getcolors(W*H))
avg = 1.0 * sum(counts.values()) / len(Bot.botlist)
for bot in Bot.botlist:
    score = 100 * counts[bot.colour] / avg
    print('Bot %s with colour %s scored %s' % (bot.prog, bot.colour, score))
image.save(BATTLE+'.png')

Les résultats de l'arène suivent. Le bot dfblob a reçu la couleur rouge pour toutes les arènes.

Arena 1:

Bot dfblob.py with colour (255, 0, 0) scored 163.75666666666666
Bot blob.py with colour (0, 255, 0) scored 14.896666666666667

1

Arena 2:

Bot blob.py with colour (0, 255, 0) scored 17.65563547726219
Bot dfblob.py with colour (255, 0, 0) scored 149.57006774236964

2

Arena 3:

Bot blob.py with colour (0, 255, 0) scored 21.09758208782965
Bot dfblob.py with colour (255, 0, 0) scored 142.9732433108277

3

Arena 4:

Bot blob.py with colour (0, 255, 0) scored 34.443810082244205
Bot dfblob.py with colour (255, 0, 0) scored 157.0684236785121

4

SamYonnou
la source
Votre algorithme est le même que celui que j'ai implémenté dans le frère fortifié de Blob, Boxer. J'allais utiliser Boxer si Blob n'était pas assez difficile. Très belles animations aussi.
Logic Knight
Pour utiliser PIL en python 3, utilisez-vous un oreiller ?
Trichoplax
@githubphagocyte Oui
SamYonnou
Quel logiciel avez-vous utilisé pour créer ces GIF?
TheNumberOne
1
@TheBestOne J'ai utilisé ImageMagick spécifiquement pour cette commande, convert -delay 5 -loop 0 result*.png animated.gifmême si certains des gifs ont dû être supprimés manuellement pour pouvoir être téléchargés ici
SamYonnou
18

Avaleur

Langue = Java

Score = 162.3289512601408075 169.4020975612382575

Cherche les ennemis et les entoure. Vous devrez peut-être lui donner une limite de temps plus longue. Pourrait être amélioré un peu. Imprime parfois des pixels non valides.

Mise à jour: Entoure beaucoup plus vite. Utilise un autre thread pour mettre à jour les priorités. Retourne toujours dans un délai de 0,1 seconde. Le score devrait être impossible à battre sans augmenter MAX_TURNS.

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.*;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;

public class Swallower {

    static final byte MY_TYPE = 1;
    static final byte BLANK_TYPE = 0;
    static final byte NEUTRAL_TYPE = 2;
    static final byte ENEMY_TYPE = 3;
    private static final int WHITE = Color.WHITE.getRGB();
    private static final int MAX_TIME = 50;
    private final int color;
    private final int N;
    private final int width;
    private final int height;
    private final BufferedReader in;
    Lock borderLock;
    private final PriorityBlockingQueue<Pixel> border;
    private final Set<Pixel> borderSet;
    private final Thread updater;

    Lock imageLock;
    volatile byte[][] image;
    Lock priorityLock;
    volatile int[][] priority;
    volatile boolean updating;
    volatile private boolean exit;

    class Pixel implements Comparable<Pixel> {

        int x;
        int y;

        public Pixel(int x, int y) {
            this.x = x;
            this.y = y;
        }

        @Override
        public int compareTo(Pixel o) {
            return priority() - o.priority();
        }

        private int priority() {
            priorityLock.lock();
            int p = priority[x][y];
            priorityLock.unlock();
            return p;
        }

        public byte type() {
            imageLock.lock();
            byte i = image[x][y];
            imageLock.unlock();
            return i;
        }

        public boolean isBorder() {
            if (type() != BLANK_TYPE){
                return false;
            }
            for (Pixel p : pixelsAround()){
                if (p.type() == MY_TYPE){
                    return true;
                }
            }
            return false;
        }

        public void setType(byte newType) {
            imageLock.lock();
            image[x][y] = newType;
            imageLock.unlock();
        }

        public void setPriority(int newPriority) {
            borderLock.lock();
            boolean contains = borderSet.remove(this);
            if (contains){
                border.remove(this);
            }
            priorityLock.lock();
            priority[x][y] = newPriority;
            priorityLock.unlock();
            if (contains){
                border.add(this);
                borderSet.add(this);
            }
            borderLock.unlock();
        }

        public List<Pixel> pixelsAround() {
            List<Pixel> pixels = new ArrayList<>(4);
            if (x > 0){
                pixels.add(new Pixel(x - 1, y));
            }
            if (x < width - 1){
                pixels.add(new Pixel(x + 1, y));
            }
            if (y > 0){
                pixels.add(new Pixel(x, y - 1));
            }
            if (y < height - 1){
                pixels.add(new Pixel(x, y + 1));
            }
            return pixels;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            Pixel pixel = (Pixel) o;

            return x == pixel.x && y == pixel.y;

        }

        @Override
        public int hashCode() {
            int result = x;
            result = 31 * result + y;
            return result;
        }
    }

    public static void main(String[] args) throws IOException {
        BufferedImage image = ImageIO.read(new File(args[0]));
        int color = parseColorString(args[1]);
        int N = Integer.parseInt(args[2]);
        new Swallower(image, color, N).start();
    }

    private void start() throws IOException {
        updater.start();
        try {
            while (true) {
                String input = in.readLine();
                if (input.equals("exit")) {
                    exit = true;
                    if (!updating) {
                        updater.interrupt();
                    }
                    return;
                } else if (input.startsWith("colour")) {
                    updateImage(input);
                } else if (input.equals("pick pixels")) {
                    if (updating) {
                        try {
                            synchronized (Thread.currentThread()){
                                Thread.currentThread().wait(MAX_TIME);
                            }
                        } catch (InterruptedException ignored) {
                        }
                    }
                    for (int i = 0; i < N && !border.isEmpty(); i++) {
                        borderLock.lock();
                        Pixel p = border.poll();
                        borderSet.remove(p);
                        borderLock.unlock();
                        if (!p.isBorder()){
                            i--;
                            continue;
                        }
                        updateImage(MY_TYPE, p);
                        System.out.print(p.x + "," + p.y + " ");
                    }
                    System.out.println();
                }
            }
        } catch (Throwable e){
            exit = true;
            if (!updating){
                updater.interrupt();
            }
            throw e;
        }
    }

    private void updateImage(byte type, Pixel... pixels) {
        for (Pixel pixel : pixels){
            pixel.setType(type);
            if (type == MY_TYPE){
                pixel.setPriority(Integer.MAX_VALUE);
            } else {
                pixel.setPriority(0);
            }
        }
        for (Pixel pixel : pixels){
            for (Pixel p : pixel.pixelsAround()){
                if (p.type() == BLANK_TYPE){
                    addPixelToUpdate(p);
                }
                if (type == MY_TYPE && p.isBorder()){
                    borderLock.lock();
                    if (borderSet.add(p)){
                        border.add(p);
                    }
                    borderLock.unlock();
                }
            }
        }
    }

    private synchronized void addPixelToUpdate(Pixel p) {
        if (pixelsToUpdateSet.add(p)) {
            pixelsToUpdate.add(p);
            if (!updating){
                updater.interrupt();
            }
        }
    }

    Queue<Pixel> pixelsToUpdate;
    Set<Pixel> pixelsToUpdateSet;

    private void update(){
        while (true){
            if (exit){
                return;
            }
            if (pixelsToUpdate.isEmpty()){
                try {
                    updating = false;
                    while (!exit) {
                        synchronized (Thread.currentThread()) {
                            Thread.currentThread().wait();
                        }
                    }
                } catch (InterruptedException ignored){}
                continue;
            }
            updating = true;
            Pixel pixel = pixelsToUpdate.poll();
            if (pixel.type() != BLANK_TYPE){
                continue;
            }
            pixelsToUpdateSet.remove(pixel);
            updatePixel(pixel);
        }
    }

    private void updatePixel(Pixel pixel) {
        int originalPriority = pixel.priority();
        int minPriority = Integer.MAX_VALUE;
        List<Pixel> pixelsAround = pixel.pixelsAround();
        for (Pixel p : pixelsAround){
            int priority = p.priority();
            if (priority < minPriority){
                minPriority = priority;
            }
        }
        if (minPriority >= originalPriority){
            pixel.setPriority(Integer.MAX_VALUE);
            pixelsToUpdate.addAll(pixelsAround.stream().filter(p -> p.type() == 0 && p.priority() != Integer.MAX_VALUE).filter(pixelsToUpdateSet::add).collect(Collectors.toList()));
        } else {
            pixel.setPriority(minPriority + 1);
            for (Pixel p : pixelsAround){
                if (p.type() == 0 && p.priority() > minPriority + 2){
                    if (pixelsToUpdateSet.add(p)){
                        pixelsToUpdate.add(p);
                    }
                }
            }
        }

    }

    private void updateImage(String input) {
        String[] inputs = input.split("\\s");
        int color = parseColorString(inputs[1]);
        byte type;
        if (color == this.color){
            return;
        } else {
            type = ENEMY_TYPE;
        }
        Pixel[] pixels = new Pixel[inputs.length - 3];
        for (int i = 0; i < inputs.length - 3; i++){
            String[] coords = inputs[i + 3].split(",");
            pixels[i] = new Pixel(Integer.parseInt(coords[0]), Integer.parseInt(coords[1]));
        }
        updateImage(type, pixels);
    }

    private static int parseColorString(String input) {
        String[] colorString = input.split("[\\(\\),]");
        return new Color(Integer.parseInt(colorString[1]), Integer.parseInt(colorString[2]), Integer.parseInt(colorString[3])).getRGB();
    }

    private Swallower(BufferedImage image, int color, int N){
        this.color = color;
        this.N = N;
        this.width = image.getWidth();
        this.height = image.getHeight();
        this.image = new byte[width][height];
        this.priority = new int[width][height];
        for (int x = 0; x < width; x++){
            for (int y = 0; y < height; y++){
                int pixelColor = image.getRGB(x,y);
                priority[x][y] = Integer.MAX_VALUE;
                if (pixelColor == WHITE){
                    this.image[x][y] = BLANK_TYPE;
                } else if (pixelColor == this.color){
                    this.image[x][y] = MY_TYPE;
                } else {
                    this.image[x][y] = NEUTRAL_TYPE;
                }
            }
        }
        border = new PriorityBlockingQueue<>();
        borderSet = Collections.synchronizedSet(new HashSet<>());
        borderLock = new ReentrantLock();
        priorityLock = new ReentrantLock();
        imageLock = new ReentrantLock();
        for (int x = 0; x < width; x++){
            for (int y = 0; y < height; y++){
                Pixel pixel = new Pixel(x,y);
                if (pixel.type() == BLANK_TYPE){
                    if (pixel.isBorder()){
                        if (borderSet.add(pixel)){
                            border.add(pixel);
                        }
                    }
                }
            }
        }
        in = new BufferedReader(new InputStreamReader(System.in));
        updating = false;
        updater = new Thread(this::update);
        pixelsToUpdate = new ConcurrentLinkedQueue<>();
        pixelsToUpdateSet = Collections.synchronizedSet(new HashSet<>());
        exit = false;
    }

}

Comment ça marche:

Ce bot maintient une file d'attente prioritaire de pixels qu'il peut ajouter. La priorité d'un pixel ennemi est 0. La priorité d'un pixel vide est supérieure de 1 à la priorité la plus basse qui l'entoure. Tous les autres pixels ont la priorité Integer.MAX_VALUE. Le thread de mise à jour met constamment à jour les priorités des pixels. A chaque tour, les N pixels les plus bas sont extraits de la file d'attente prioritaire.

Green Blob vs Red Swallower

Score de Blob = 1.680553372583887225

Score de l'avaleur = 169.4020975612382575

Arena 1:

Bot Blob.py with colour (0, 255, 0) scored 1.2183333333333333
Bot Swallower.class with colour (255, 0, 0) scored 177.435

entrez la description de l'image ici

Arena 2:

Bot Swallower.class with colour (255, 0, 0) scored 149.57829253338517
Bot Blob.py with colour (0, 255, 0) scored 0.5159187091564356

entrez la description de l'image ici

Arena 3:

Bot Blob.py with colour (0, 255, 0) scored 0.727104853136361
Bot Swallower.class with colour (255, 0, 0) scored 163.343720545521

entrez la description de l'image ici

Arena 4:

Bot Swallower.class with colour (255, 0, 0) scored 187.25137716604686
Bot Blob.py with colour (0, 255, 0) scored 4.260856594709419

entrez la description de l'image ici

Green Swallower vs. Red Blob

Score de Blob = 1.6852943642218457375

Score de l'avaleur = 169.3923095387498625

Arena 1:

Bot Blob.py with colour (255, 0, 0) scored 1.3166666666666667
Bot Swallower.class with colour (0, 255, 0) scored 177.33666666666667

entrez la description de l'image ici

Arena 2:

Bot Swallower.class with colour (0, 255, 0) scored 149.57829253338517
Bot Blob.py with colour (255, 0, 0) scored 0.49573058575466195

entrez la description de l'image ici

Arena 3:

Bot Swallower.class with colour (0, 255, 0) scored 163.14367053301788
Bot Blob.py with colour (255, 0, 0) scored 0.9271548656394868

entrez la description de l'image ici

Arena 4:

Bot Swallower.class with colour (0, 255, 0) scored 187.51060842192973
Bot Blob.py with colour (255, 0, 0) scored 4.0016253388265675

entrez la description de l'image ici

Avaleur Rouge vs Vert Profond Premier Blob

Score de l'avaleur = 157.0749775233111925

Profondeur Premier Blob Score = 18.192783547939744

Arena 1:

Bot Swallower.class with colour (255, 0, 0) scored 173.52166666666668
Bot dfblob.py with colour (0, 255, 0) scored 5.131666666666667

entrez la description de l'image ici

Arena 2:

Bot dfblob.py with colour (0, 255, 0) scored 17.25635925887156
Bot Swallower.class with colour (255, 0, 0) scored 149.57829253338517

entrez la description de l'image ici

Arena 3:

Bot Swallower.class with colour (255, 0, 0) scored 153.59801488833747
Bot dfblob.py with colour (0, 255, 0) scored 10.472810510319889

entrez la description de l'image ici

Arena 4:

Bot dfblob.py with colour (0, 255, 0) scored 39.91029775590086
Bot Swallower.class with colour (255, 0, 0) scored 151.60193600485545

entrez la description de l'image ici

Green Swallower vs Red Depth First Blob

Score de l'avaleur = 154.3368355651281075

Profondeur Premier Blob Score = 18.84463249420435425

Arena 1:

Bot Swallower.class with colour (0, 255, 0) scored 165.295
Bot dfblob.py with colour (255, 0, 0) scored 13.358333333333333

entrez la description de l'image ici

Arena 2:

Bot dfblob.py with colour (255, 0, 0) scored 8.91118721119768
Bot Swallower.class with colour (0, 255, 0) scored 149.57829253338517

entrez la description de l'image ici

Arena 3:

Bot Swallower.class with colour (0, 255, 0) scored 157.01136822667206
Bot dfblob.py with colour (255, 0, 0) scored 7.059457171985304

entrez la description de l'image ici

Arena 4:

Bot dfblob.py with colour (255, 0, 0) scored 46.0495522603011
Bot Swallower.class with colour (0, 255, 0) scored 145.4626815004552

entrez la description de l'image ici

Green Blob vs Red Depth First Blob vs Blue Swallower:

Score de Blob = 6.347962032393275525

Profondeur Premier Blob Score = 27.34842554331698275

Score de l'avaleur = 227.720728953415375

Arena 1:

Bot Swallower.class with colour (0, 0, 255) scored 242.54
Bot Blob.py with colour (0, 255, 0) scored 1.21
Bot dfblob.py with colour (255, 0, 0) scored 24.3525

entrez la description de l'image ici

Arena 2:

Bot dfblob.py with colour (255, 0, 0) scored 17.828356088588478
Bot Blob.py with colour (0, 255, 0) scored 0.9252889892479551
Bot Swallower.class with colour (0, 0, 255) scored 224.36743880007776

entrez la description de l'image ici

Arena 3:

Bot dfblob.py with colour (255, 0, 0) scored 7.105141670032893
Bot Swallower.class with colour (0, 0, 255) scored 226.52057245080502
Bot Blob.py with colour (0, 255, 0) scored 12.621905476369092

entrez la description de l'image ici

Arena 4:

Bot dfblob.py with colour (255, 0, 0) scored 60.10770441464656
Bot Blob.py with colour (0, 255, 0) scored 10.634653663956055
Bot Swallower.class with colour (0, 0, 255) scored 217.45490456277872

entrez la description de l'image ici

Voici le juge de Sam Yonnou avec quelques modifications pour que vous spécifiiez les fichiers et la commande séparément:

import sys, re, random, os, shutil, subprocess, datetime, time, signal, io
from PIL import Image

ORTH = ((-1,0), (1,0), (0,-1), (0,1))
def place(loc, colour):
    # if valid, place colour at loc and return True, else False
    if pix[loc] == (255,255,255):
        plist = [(loc[0]+dx, loc[1]+dy) for dx,dy in ORTH]
        if any(pix[p]==colour for p in plist if 0<=p[0]<W and 0<=p[1]<H):
            pix[loc] = colour
            return True
    return False

def updateimage(image, msg, bot):
    if not re.match(r'(\s*\d+,\d+)*\s*', msg):
        return []
    plist = [tuple(int(v) for v in pr.split(',')) for pr in msg.split()]
    plist = plist[:PIXELBATCH]
    return [p for p in plist if place(p, bot.colour)]

class Bot:
    botlist = []
    def __init__(self, progs, command=None, colour=None):
        self.prog = progs[0]
        self.botlist.append(self)
        self.colour = colour
        self.colstr = str(colour).replace(' ', '')
        self.faults = 0
        self.env = 'env%u' % self.botlist.index(self)
        try: os.mkdir(self.env)
        except: pass
        for prog in progs:
            shutil.copy(prog, self.env)
        shutil.copy(imagename, self.env)
        os.chdir(self.env)
        args = command + [imagename, self.colstr, str(PIXELBATCH)]
        errorfile = 'err.log'
        with io.open(errorfile, 'wb') as errorlog:
            self.proc = subprocess.Popen(args, stdin=subprocess.PIPE, 
                stdout=subprocess.PIPE, stderr=errorlog)
        os.chdir('..')
    def send(self, msg):
        if self.faults < FAULTLIMIT:
            self.proc.stdin.write((msg+'\n').encode('utf-8'))
            self.proc.stdin.flush()
    def read(self, timelimit):
        if self.faults < FAULTLIMIT:
            start = time.time()
            inline = self.proc.stdout.readline().decode('utf-8')
            if time.time() - start > timelimit:
                self.faults += 1
                inline = ''
            return inline.strip()
    def exit(self):
        self.send('exit')

from cfg import *
for i, (progs, command) in enumerate(botspec):
    Bot(progs, command, colourspec[i])

image = Image.open(imagename)
pix = image.load()
W,H = image.size
resultdirectory = 'results of ' + BATTLE
os.mkdir(resultdirectory)

time.sleep(INITTIME)
total = 0
image.save(resultdirectory+'/'+'result000.png')
for turn in range(1, MAXTURNS+1):
    random.shuffle(Bot.botlist)
    nullbots = 0
    for bot in Bot.botlist:
        bot.send('pick pixels')
        inmsg = bot.read(TIMELIMIT)
        newpixels = updateimage(image, inmsg, bot)
        total += len(newpixels)
        if newpixels:
            pixtext = ' '.join('%u,%u'%p for p in newpixels)
            msg = 'colour %s chose %s' % (bot.colstr, pixtext)
            for msgbot in Bot.botlist:
                msgbot.send(msg)
        else:
            nullbots += 1
    if nullbots == len(Bot.botlist):
        break
    if turn % 100 == 0:
        print('Turn %s done %s pixels' % (turn, total))
        image.save(resultdirectory+'/result'+str(turn//100).zfill(3)+'.png')
image.save(resultdirectory+'/result999.png')
for msgbot in Bot.botlist:
    msgbot.exit()

resultfile = io.open(resultdirectory+'/result.txt','w')
counts = dict((c,f) for f,c in image.getcolors(W*H))
avg = 1.0 * sum(counts.values()) / len(Bot.botlist)
for bot in Bot.botlist:
    score = 100 * counts[bot.colour] / avg
    print('Bot %s with colour %s scored %s' % (bot.prog, bot.colour, score))
    print('Bot %s with colour %s scored %s' % (bot.prog, bot.colour, score), file=resultfile)
image.save(BATTLE+'.png')

Exemple cfg:

BATTLE = 'Green DepthFirstBlob vs Red Swallower @ arena1'
MAXTURNS = 20000
PIXELBATCH = 10
INITTIME = 2.0
TIMELIMIT = .1
FAULTLIMIT = 5

imagename = 'arena1.png'

colourspec = (0,255,0), (255,0,0)

botspec = [
    (['DepthFirstBlob.py'], ['python', 'DepthFirstBlob.py']),
    (['Swallower.class','Swallower$Pixel.class'], ['java', 'Swallower']),
    ]

Remarque: Toute personne qui parvient à avaler le Swallower obtient une prime de 100 points de réputation. S'il vous plaît poster dans les commentaires ci-dessous si vous réussissez à cela.

Le numéro un
la source
2
@githubphagocyte À la demande.
TheNumberOne
1
Beau travail avec le juge change. Séparer la copie des fichiers et la commande est une bonne idée, et la consignation des erreurs était absolument nécessaire.
Logic Knight le
1
Si vous vouliez dire MAXTURNS, n'hésitez pas à le changer. Cela ne fait pas partie des règles. Cela empêche simplement le juge de courir pour toujours (mais je suppose que les conditions de résiliation l'empêchent de toute façon).
Logic Knight le
1
@githubphagocyte Fixé
TheNumberOne
1
Après avoir regardé vos batailles animées, j'ai commencé à me demander à quoi ressemblerait une bataille Swallower vs Swallower. Serait-ce un piège rapide de l'autre ou s'agirait-il d'une lutte constante pour la domination de l'espace?
Logic Knight
6

Aléatoire, Langue = Java, Score = 0.43012126100275

Ce programme met au hasard des pixels sur l'écran. Certains pixels (sinon tous) ne seront pas valides. Sur une note de côté, il devrait être difficile de faire un programme plus rapide que celui-ci.

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.File;
import java.io.InputStreamReader;

public class Random {

    static BufferedReader in = new BufferedReader(new InputStreamReader(System.in));

    static int n;

    static int height;

    static int width;

    public static void main(String[] args) throws Exception{
        BufferedImage image = ImageIO.read(new File(args[0]));
        height = image.getHeight();
        width = image.getWidth();
        n = Integer.parseInt(args[2]);
        while (true){
            String input = in.readLine();
            if (input.equals("exit")){
                return;
            }
            if (!input.equals("pick pixels")){
                continue;
            }
            for (int i = 0; i < n; i++){
                System.out.print((int) (Math.random() * width) + ",");
                System.out.print((int) (Math.random() * height) + " ");
            }
            System.out.println();
        }
    }
}

Arena 1:

1

Arena 2:

2

Arena 3:

3

Arena 4:

4

Le numéro un
la source
7
Je vois que vous n'êtes pas tombé dans le piège de l' optimisation prématurée .
Logic Knight