9
\$\begingroup\$

Contest Finished! Read comments on blobs to view their score.

This KoTH is loosely inspired by Primer's Natural Selection Simulation. Your bot is a blob. In order to survive, you must eat pellets to regain energy, which is used to move. With extra energy, blobs can split into two.

Energy and Movement

Your blob starts off each round with 100 energy, and it has no limit on the amount of energy it can collect. Each round is run in turns, with each blob having the option to move North, East, South, or West in any given turn, or stand still. Moving uses 1 energy, and standing still uses 0.25 energy. The map's side length is ceil(0.25 * blobCount) * 2 - 1 units, with a minimum of 9 units. All blobs start on the edge of the map, with one placed in each corner and every subsequent blob being placed 2 units away from any others. Every 30 turns, a wave of pellets are placed in random spots around the map, at least 1 unit from any edge. Each time a wave of pellets appears, the quantity of pellets (originally twice the number of blobs or the width of the map, whichever is larger) in the next wave is decreased by 1, forcing the number of blobs to decrease over time. Each pellet restores between 5 and 15 energy. When a blob's energy is less than or equal to 0, it dies.

Eating

If two or more blobs attempt to occupy the same location, the one with the most energy will eat the others, receiving their energy. If both have equal energy, both vanish.

Detection and Information

Blobs can see any pellets or other blobs within a distance of 4 units. When their functions are called, blobs are provided with:

  • The side length of the map
  • The position of the blob on the map
  • The positions of all pellets within their search radius, as well as their values
  • The positions of all blobs within their search radius, as well as their energy and UIDs
  • The energy, UID, and locations of the blob whose function is being executed
  • A storage object unique to the blob
  • A storage object shared by all blobs related to the blob through splitting

Splitting

If a blob has more than 50 energy, it can choose to split. Splitting costs 50 energy, and any remaining energy is divided evenly between the two blobs. All blobs are either originals or split copies, with every copy tracing back to an original. All of these together are "relatives." All relatives have one communal storage object. Relatives can still eat each other, and can split, use their own storage object, or collect energy without affecting others.

Energy Transfer

If two blobs are next to each other (after moving), one of the bots can transfer energy to the other. This is done by returning SendNorth(amt), SendEast(amt), SendSouth(amt), or SendWest(amt), with amt being a number representing the amount sent. This can be any amount that the sender can afford, including all of their energy. It is recommended that the blob who is receiving energy is told to stay still through communal storage, so that it does not move away when the energy is being transferred (though the energy would not be deducted from the sender's total in this case).

Functions, Storage, and UIDs

In order to allow more complex learning behaviors, all blobs will be given an integer UID (Unique Identifer). These UIDs will be randomly generated each map, preventing strategies based on individual targets. When a blob's function is called, it is passed four arguments:

  1. The side length of the map as an integer
  2. An object with two arrays: pellets, and blobs. Both arrays contain objects, both having a pos property containing the pellet or blob's position formatted as [x,y]. Pellets will have an energy property, while blobs will have a uid property and an energy property
  3. An object containing various properties of the blob it is passed to: energy, uid, and pos. The pos array is formatted as [x,y]
  4. An object containing the two storage objects of the blob. A self property contains an individual storage object which can be modified however the blob sees fit (by manipulating properties of the object that is passed), and a communal property which can be modified by any relative.

Blobs are not moved immediately to prevent earlier/later turns having an advantage. All movements are processed in groups (All collisions/eating, then all pellets, then splitting, etc.) If a blob lands on a pellet or smaller blob and, in the process uses its last energy, the blob will still consume the pellet/energy independent of whether that would would bring its total energy above 0.

In order for relative blobs to recognize one another, the communal storage must be used for each blob to record its UID in an array, or through some other system.

Return Values

In order to move or split, the return value of the function is used. First, the meaning of the cardinal directions in terms of coordinates:

  • North = -Y
  • East = +X
  • South = +Y
  • West = -X

Note that [0,0] is the top left corner, and Y increases as you go down. The return value of the function should follow these rules:

  • To do Nothing: Return nothing, 0, null, undefined, false, or any other value that equates to false
  • To Move: Return one of four global variables: North, East, South, or West, which equate to "north", "east", "south", or "west" (which could also be used as a return value)
  • To Split: Return the global variable SplitNorth, SplitEast, SplitSouth, or SplitWest, the direction indicating where to place the new blob

If a split command is returned and the amount of energy required is greater than or equal to the energy of the blob, nothing will happen. Blobs will not be able to leave the map.

Predefined Library Functions

There are a few basic functions available by default, to save some time:

taxiDist(pt1, pt2)

Returns the taxicab distance between two points (X distance plus Y distance).

taxiDist([0, 0], [2, 2]) //4
taxiDist([3, 4], [1, 5]) //3
taxiDist([1.25, 1.3], [1.3, 1.4]) //0.15
taxiDist([0, 0], [5, 2.5], 2.5) //3
taxiDist([0, 0], [2, 4], 2.5) //2.4

hypotDist(pt1, pt2)

Returns distance between two points according to the pythagorean theorem

hypotDist([0, 0], [5, 12]) //13
hypotDist([4, 6], [8, 9]) //5
hypotDist([0, 1], [2, 1]) //2
hypotDist([1, 1], [2, 2]) //sqrt(2)

modDir(dir, amt)

Takes the inputted direction, rotates 90 degrees clockwise amt times, then returns the new value.

modDist(North, 1) //East
modDist(East, 2) //West
modDist(West, 3) //South
modDist(South, 4) //South

Example Blob

This blob will not move until it finds a pellet nearby. Then, it will move in the direction it thinks is most likely to reward it. If its energy is ever above 150, it will split.

function(map, near, me, storage) {
    if (me.energy > 150)
        return SplitNorth;
    if (!near.pellets.length)
        return null;
    var dirs = [0, 0, 0, 0];
    for (let p, i = 0; i < near.pellets.length; i++) {
        p = near.pellets[i];
        dirs[0] += me.pos[1] - p.pos[1];
        dirs[1] += p.pos[0] - me.pos[0];
        dirs[2] += p.pos[1] - me.pos[1];
        dirs[3] += me.pos[0] - p.pos[0];
    }
    return [North, East, South, West][dirs.indexOf(Math.max(...dirs))];
}

Rules

  • Standard Loopholes are prohibited. Also, no Unstandard Loopholes.
  • No blob may attempt to modify or read any data not passed to it via its parameters
  • No blob may attempt to modify a return-value variable to sabotage other blobs
  • A round lasts until the only remaining blobs are relatives
  • No blob may modify data by injecting functions into its parameters which modify values using the this keyword
  • All submissions must either be in Javascript or a language which is not too different from Javascript (Python, for example). All answers will be converted to Javascript for the competition.
  • The winner is the blob which has collected the highest amount of energy in total across all rounds (from either pellets or consuming smaller blobs that are not relatives)

Controller: https://gist.github.com/Radvylf/1facc0afe24c5dfd3ada8b8a2c493242

Chatroom: https://chat.stackexchange.com/rooms/93370/hungry-blobs-koth

\$\endgroup\$
5
  • 1
    \$\begingroup\$ Can you expand this to other languages besides javascript? \$\endgroup\$
    – Gymhgy
    May 7, 2019 at 21:02
  • \$\begingroup\$ @EmbodimentofIgnorance Submit it in whatever language you choose, and I'll do the conversion to JS. \$\endgroup\$ May 7, 2019 at 21:38
  • \$\begingroup\$ Can blobs cross over each other Ex: blob1 at [0][0] moves right and blob2 at [0][1] moves left or will the blob with lower energy be eaten? \$\endgroup\$ May 10, 2019 at 15:20
  • \$\begingroup\$ Related \$\endgroup\$ May 10, 2019 at 15:27
  • \$\begingroup\$ @fəˈnɛtɪk Yes, bots can cross over each other. Also, the related challenge was mine (: \$\endgroup\$ May 10, 2019 at 16:17

5 Answers 5

3
+100
\$\begingroup\$

Introvert

The Introvert doesn't like other blobs. When it sees an unrelated blob, it eats it if it can, and begrudgingly accepts its presence it if it can't, though running away if it sees signs of aggression. When it sees a related blob, it distances itself. However, it can't help but split apart a lot.

Technical Details

The core feature of this blob is to split apart and spread out so as to maximize the combined vision of the blobs. It also employs a system to prevent two of them from competing over a pellet.

function introvert(mapSize, vision, self, storage) {
  if (!storage.communal.friends)
    storage.communal.friends = {};
  if (!storage.communal.claims)
    storage.communal.claims = {};
  storage.communal.friends[self.uid] = true;
  for (var i in storage.communal.claims)
    if (storage.communal.claims[i] === self.uid) {
      storage.communal.claims = {};
      break;
    }
  var food = {};
  for (var p of vision.pellets) {
    var score = p.energy - taxiDist(p.pos, self.pos);
    if (score > 0)
      food[p.pos] = score;
  }
  var danger = {};
  for (var i = 0; i < mapSize; i++) {
    danger['-1,' + i] = true;
    danger[mapSize + ',' + i] = true;
    danger[i + ',' + mapSize] = true;
    danger[i + ',-1'] = true;
  }
  var relatives = {};
  for (var b of vision.blobs) {
    if (b.uid in storage.communal.friends) {
      relatives[b.pos] = true;
    } else if (!storage.self.justSplit && b.energy < self.energy - taxiDist(b.pos, self.pos) * 0.75) {
      var score = b.energy - taxiDist(b.pos, self.pos) * 1.25;
      if (score > 0)
        food[b.pos] = score;
    } else {
      danger[b.pos] = true;
      danger[b.pos[0] + ',' + (b.pos[1] - 1)] = true;
      danger[b.pos[0] + 1 + ',' + b.pos[1]] = true;
      danger[b.pos[0] + ',' + (b.pos[1] + 1)] = true;
      danger[b.pos[0] - 1 + ',' + b.pos[1]] = true;
    }
  }
  storage.self.justSplit = !danger[self.pos] && self.energy > 150;
  function fromData(n) {
    return n.split(',').map(s => parseInt(s));
  }
  function fs(f) {
    return food[f] / taxiDist(f, self.pos);
  }
  var target = Object.keys(food).filter(f => !(f in storage.communal.claims)).map(fromData).sort((a, b) => fs(b) - fs(a))[0];
  if (target)
    storage.communal.claims[target] = self.uid;
  function ms(m) {
    if (danger[m])
      return 99999999;
    var dists = Object.keys(relatives).map(r => hypotDist(fromData(r), m));
    return (target ? taxiDist(target, m) : 0) - (dists.length ? dists.reduce((a, b) => a + b) / dists.length : 0);
  }
  var candidates = [
    {p: self.pos},
    {p: [self.pos[0], self.pos[1] - 1], d: storage.self.justSplit ? SplitNorth : North},
    {p: [self.pos[0] + 1, self.pos[1]], d: storage.self.justSplit ? SplitEast : East},
    {p: [self.pos[0], self.pos[1] + 1], d: storage.self.justSplit ? SplitSouth : South},
    {p: [self.pos[0] - 1, self.pos[1]], d: storage.self.justSplit ? SplitWest : West}
  ];
  if (storage.self.justSplit)
    candidates.shift();
  return candidates.sort((a, b) => ms(a.p) - ms(b.p))[0].d;
}
\$\endgroup\$
3
  • \$\begingroup\$ This looks like a pretty nice bot! The contest should be soon (bounty expires tomorrow). \$\endgroup\$ May 15, 2019 at 21:44
  • \$\begingroup\$ @RedwolfPrograms I actually tested it out in the runner and it always wins by a pretty large margin. \$\endgroup\$
    – RamenChef
    May 15, 2019 at 22:01
  • \$\begingroup\$ Average Score per Round: 357.544 \$\endgroup\$ May 16, 2019 at 3:15
1
\$\begingroup\$

Animated Meal

A simple bot, just to start off the competition. Finds the nearest coin, and goes toward it. Based off the example bot.

function(map, near, me, storage) {
    var targs = near.pellets.map(el => taxiDist(el.pos, me.pos));
    var targ = near.pellets[targs.indexOf(Math.max(...targs))].pos;
    if (targ[0] == me.pos[0])
        return targ[1] < me.pos[1] ? North : South;
    return targ[0] < me.pos[0] ? West : East;
}
\$\endgroup\$
2
  • \$\begingroup\$ Average Score per Round: 24.933 \$\endgroup\$ May 16, 2019 at 3:13
  • \$\begingroup\$ And, in a surprising turn of events, the (modified slightly to reduce bugs) 5-liner wins 2nd \$\endgroup\$ May 16, 2019 at 3:16
1
\$\begingroup\$

bloblib tester

function(map, near, me, storage) {
    // BlobLib, the main purpose of this post
    const bloblib = {
        // Returns only pellets and blobs that are within the immediate neighbourhood (within 1 space of) me
        getNeighbours: (known) => {
            let neighbours = {};
            neighbours.pellets = known.pellets.filter(x => x.pos[0] >= me.pos[0] - 1 && x.pos[0] <= me.pos[0] + 1 && x.pos[1] >= me.pos[1] - 1 && x.pos[1] <= me.pos[1] + 1);
            neighbours.blobs = known.blobs.filter(x => x.pos[0] >= me.pos[0] - 1 && x.pos[0] <= me.pos[0] + 1 && x.pos[1] >= me.pos[1] - 1 && x.pos[1] <= me.pos[1] + 1);
            return neighbours;
        },
        // Gets the blob or pellet at the given location
        getByPos: (pos, known) => {
            let pellets = known.pellets.filter(x => x.pos[0] == pos[0] && x.pos[1] == pos[1]);
            let blobs = known.blobs.filter(x => x.pos[0] == pos[0] && x.pos[1] == pos[1]);
            if (blobs.length) return blobs[0];
            if (pellets.length) return pellets[0];
            return null;
        },
        // Returns a 2d array of size, containing any known blobs or pellets
        areaMatrix: (size, known) => {
            let matrix = [];
            for (let x = 0; x < size; x++) {
                let row = [];
                for (let y = 0; y < size; y++) {
                    let realPos = [me.pos[0] - (x + Math.floor(size / 2)), me.pos[1] - (y + Math.floor(size / 2))];
                    row.push(getByPos(realPos, known));
                }
                matrix.push(row);
            }
            return matrix;
        },
        // Gets a cardinal direction pointing from from to to
        cardDirTo: (to, from = me.pos) => {
            let diff = bloblib.multiDist(from, to);

            if (diff[0] == 0 && diff[1] == 0) return null;

            if (Math.abs(diff[0]) > Math.abs(diff[1])) {
                // Gunna be east or west
                return diff[0] > 0
                    ? East
                    : West;
            } else {
                return diff[1] > 0
                    ? South
                    : North;
            }
        },
        // Returns a vector of the X and Y distances between from and to
        multiDist: (from, to) => {
            return [to[0] - from[0], to[1] - from[1]]
        },
        // Gets the closest object in objs to position to
        getClosest: (objs, to = me.pos) => {
            if (!objs || !objs.length) return null;

            let sorted = objs.concat().sort((a, b) => taxiDist(a.pos, to) - taxiDist(b.pos, to));
            return sorted[0];
        },
        // Should be run at startup. Calculates which directions are unsafe to move in
        dangerSense: (origin) => {
            let neighbours = bloblib.getNeighbours(near);
            let matrix = bloblib.areaMatrix(3, neighbours);

            if (me.pos[1] == 0 || (matrix[1,0] && isThreat(matrix[1,0]))) bloblib.unsafeDirs.push(North);
            if (me.pos[0] == map - 1 || (matrix[2,1] && isThreat(matrix[2,1]))) bloblib.unsafeDirs.push(East);
            if (me.pos[0] == 0 || (matrix[0,1] && isThreat(matrix[0,1]))) bloblib.unsafeDirs.push(West);
            if (me.pos[1] == map - 1 || (matrix[1,2] && isThreat(matrix[1,2]))) bloblib.unsafeDirs.push(South);
        },
        isThreat: (blob) => {
            if (!blob.uid) return false;
            if (storage.communal.blobs.includes(blob.uid)) return true;

            return blob.energy >= me.energy - 1;
        }
        // Attempts to move in the given direction
        // Rotates the direction 90 if it can't safely move
        attemptMove: (dir = North) => {
            for (let i = 0; i < 4; i++) {
                if (bloblib.unsafeDirs.includes(dir)) dir = modDir(dir, i);
                else return dir;
            }
            return null;
        },
        // Attempts to split in the given direction
        // Rotates the direction 90 if it can't safely split
        attemptSplit: (dir = SplitNorth) => {
            for (let i = 0; i < 4; i++) {
                if (bloblib.unsafeDirs.includes(dir)) dir = modDir(dir, i);
                else return dir;
            }
            return null;
        },
        // Returns the next direction in which to move toward pos
        // Don't bother checking if we have enough energy, because if
        // we have < 1 energy we're basically dead anyway
        moveTo: (pos) => {
            return bloblib.performAction(bloblib.attemptMove(bloblib.cardDirTo(pos)));
        },
        // Simply registers the action in communal history, then returns it unmodified
        performAction: (action) => {
            storage.communal.history[me.uid].push(action);
            return action;
        },

        // Stores directions in which there is another blob
        // This wouldn't make sense to store across turns, so we don't bother
        unsafeDirs: []
    };
    bloblib.dangerSense(me.pos);

    // Register this blob
    if (!storage.communal.blobs) storage.communal.blobs = [];
    if (!storage.communal.blobs.includes(me.uid)) storage.communal.blobs.push(me.uid);

    // Register history for this blob
    if (!storage.communal.history) storage.communal.history = {};
    if (!storage.communal.history[me.uid]) storage.communal.history[me.uid] = [];

    // Split if we can and there are fewer than 10 blobs in our community
    if (me.energy > 150 && storage.communal.blobs.length < 10) {
        let split = bloblib.getSplit();
        if (split) return split;
    }

    // If we can't see any pellets or blobs, don't do anything
    if (!near.pellets.length && !near.blobs.length) return null;

    // Move toward the nearest pellet
    return bloblib.moveTo(bloblib.getClosest(near.pellets));
}

The actual bot is fairly simple, but this is more designed as a proof of concept of bloblib, a collection of functions and functionality I plan to use and develop across other bots (feel free to use/expand on it yourself too)

In short, this bot does the following:

If energy > 150 and blobs_in_team < 10: Try to split
If visible_pellets = 0 and visible_blobs = 0: do nothing
Move toward the closest pellet in a safe way
    that avoids moving into other stronger or equal blobs
    or off the edge of the map
\$\endgroup\$
4
  • \$\begingroup\$ You can now see a blob's energy, that might come in handy \$\endgroup\$ May 8, 2019 at 18:08
  • 1
    \$\begingroup\$ @RedwolfPrograms updated bloblib to determine whether enemy blobs are a "threat" based on their energy levels. \$\endgroup\$
    – Mayube
    May 8, 2019 at 18:54
  • \$\begingroup\$ Average Score per Round: 7.913 \$\endgroup\$ May 16, 2019 at 3:13
  • \$\begingroup\$ This system could probably have been used for some good blobs, but this one seemed to act a bit strangely. \$\endgroup\$ May 16, 2019 at 3:17
1
\$\begingroup\$

Greedy Coward

import random

def greedy_coward(map_length, near, me, storage):
    interesting_objects = [] #objects I can eat
    bad_objects = [] #objects that eat me
    allowed_directions = ["North", "East", "South", "West"]

    # add pellets to objects that I'm interested in
    for i in near.pellets:
        interesting_objects.append(i)

    # figure out which blobs are good and which are bad
    for i in near.blobs:
        # if I'm under or equal powered, add it to bad_objects
        if i.energy >= me.energy: 
            bad_objects.append(i)
        # if I can eat it, add it to interesting objects.
        else:
            interesting_objects.append(i)

    # if there are any bad objects, process them.
    if not len(bad_objects) == 0:

        # find the nearest bad object and make sure I don't move towards it
        bad_objects_distances = []
        for i in bad_objects:
            bad_objects_distances.append(taxiDist(i.pos, me.pos))
        worst_object = bad_objects[bad_objects_distances.index(min(bad_objects))]

        # find the direction of the worst object
        bad_object_xy_distance = [worst_object.pos[0] - me.pos[1], worst_object.pos[1] - me.pos[1]]
        closest_number = min(bad_object_xy_distance)
        bad_object_direction_vague = [["West","East"],["North","South"]][bad_object_xy_distance.index(closest_number)]
        if closest_number < 0:
            bad_object_direction = bad_object_direction_vague[1]
        else:
            bad_object_direction = bad_object_direction_vague[0]

        # remove bad object direction from allowed directions
        allowed_directions.remove(bad_object_direction)

    # process interesting objects if they exist
    if not len(interesting_objects) == 0:

        # find the nearest interesting object
        interesting_objects_distances = []
        for i in interesting_objects:
            interesting_objects_distances.append(taxiDist(me.pos, i.pos))
            interesting_object = interesting_objects[interesting_objects_distances.index(min(interesting_objects_distances))]

        # find the direction of the best object
            good_object_xy_distance = [interesrting_object.pos[0] - me.pos[1], interesting_object.pos[1] - me.pos[1]]
            closest_number = min(good_object_xy_distance)
            good_object_direction_vague = [["West","East"],["North","South"]][good_object_xy_distance.index(closest_number)]
            if closest_number < 0:
                good_object_direction = good_object_direction_vague[1]
            else:
                good_object_direction = good_object_direction_vague[0]

        # if the good and bad objects are in the same direction, move randomly in a different direction
        if good_object_direction == bad_object_direction:
            return random.choice(allowed_directions)
        else: # otherwise go towards the good object.
            return good_object_direction

    return 0 # when in doubt, stay still

Or, in JavaScript,

function(map_length, near, me, storage) {
    var interesting_objects = []; //objects I can eat
    var bad_objects = []; //objects that eat me
    var allowed_directions = ["north", "east", "south", "west"];

    //add pellets to objects that I'm interested in
    for (let i in near.pellets) {
        interesting_objects.push(near.pellets[i]);
    }

    //figure out which blobs are good and which are bad
    for (let i in near.blobs) {
        //if I'm under or equal powered, add it to bad_objects
        if (near.blobs[i].energy >= me.energy) {
            bad_objects.push(near.blobs[i]);
        }
        //if I can eat it, add it to interesting objects.
        else {
            interesting_objects.push(near.blobs[i]);
        }
    }

    //if there are any bad objects, process them.
    if (bad_objects.length) {

        //find the nearest bad object and make sure I don't move towards it
        var bad_objects_distances = [];
        for (i in bad_objects) {
            bad_objects_distances.push(taxiDist(bad_objects[i].pos, me.pos));
        }
        var worst_object = bad_objects[bad_objects_distances.indexOf(Math.min(...bad_objects_distances))];

        //find the direction of the worst object
        var bad_object_xy_distance = [worst_object.pos[0] - me.pos[1], worst_object.pos[1] - me.pos[1]];
        var closest_number = Math.min(...bad_object_xy_distance.map(el => Math.abs(el)));
        var bad_object_direction_vague = [["west","east"],["north","south"]][bad_object_xy_distance.map(el => Math.abs(el)).indexOf(closest_number)];
        if (closest_number < 0) {
            var bad_object_direction = bad_object_direction_vague[1];
        } else {
            var bad_object_direction = bad_object_direction_vague[0];
        }

        //remove bad object direction from allowed directions
        allowed_directions = allowed_directions.filter(el => el !== bad_object_direction);

    }

    //process interesting objects if they exist
    if (interesting_objects.length) {

        //find the nearest interesting object
        var interesting_objects_distances = [];
        for (i in interesting_objects) {
            interesting_objects_distances.push(taxiDist(me.pos, interesting_objects[i].pos))
        }
        var interesting_object = interesting_objects[interesting_objects_distances.indexOf(Math.min(...interesting_objects_distances))];

        //find the direction of the best object
        var good_object_xy_distance = [interesting_object.pos[0] - me.pos[1], interesting_object.pos[1] - me.pos[1]];
        var closest_number = Math.min(...good_object_xy_distance.map(el => Math.abs(el)));
        var good_object_direction_vague = [["west","east"],["north","south"]][good_object_xy_distance.map(el => Math.abs(el)).indexOf(closest_number)];
        if (closest_number < 0) {
            var good_object_direction = good_object_direction_vague[1];
        } else {
            var good_object_direction = good_object_direction_vague[0];
        }

        //if the good and bad objects are in the same direction, move randomly in a different direction
        if (good_object_direction == bad_object_direction) {
            return allowed_directions[allowed_directions.length * Math.random() | 0];
        } else{ //otherwise go towards the good object.
            return good_object_direction;
        }

    }

    return 0; //when in doubt, stay still
}

This bot isn't very interesting. It acts according to two priorities:

  1. Don't get eaten.
  2. Eat the nearest thing.

It never spits to maximize its ability to eat other things.

\$\endgroup\$
6
  • \$\begingroup\$ I'll get to work translating this! When I finish, I'll suggest an edit with the JS version. \$\endgroup\$ May 11, 2019 at 18:49
  • \$\begingroup\$ @RedwolfPrograms Sounds good, thank you very much. \$\endgroup\$
    – sporkl
    May 11, 2019 at 18:50
  • \$\begingroup\$ I think you need to add an if/else to check if there actually are any good/bad objects. It's causing several issues in the JS version. \$\endgroup\$ May 11, 2019 at 21:08
  • \$\begingroup\$ @RedwolfPrograms It should be fixed now. I just added an if statement that checks the created lists of interesting and bad objects to make sure they are non-empty. Again, thank you for the help. \$\endgroup\$
    – sporkl
    May 11, 2019 at 23:33
  • \$\begingroup\$ @RedwolfPrograms Do you have the JS version ready? \$\endgroup\$
    – RamenChef
    May 15, 2019 at 21:57
1
\$\begingroup\$

SafetyBlob

This bot uses some of the same logic as Safetycoin from the previous KOTH.

How it functions

This bot will head towards food which it can either reach before any bigger bots do or at the same time/before a smaller bot. If it can't see any food which meets these criteria, it will move in a random direction(biased towards the center). If it gets to 150 energy and can not see safe food, it will split in one of the directions it has labeled as safe to move.

This bot doesn't keep track of its own children but they should not collide anyway due to the safety mechanisms.

 function SafetyBlob(map,local,me,stor){
  var center=(map/2|0)+1;
  var [x,y]=me.pos
  var uid=me.uid
  var others=local.blobs;
  var pellets=local.pellets;
  //Bot doesnt use storage because it just tries to find what it can.
  var willSplit=me.energy>150;
  var bestSafePelletValue=0;
  var bestSafePellet=null;
  var pellet;
  var other;
  //Head towards the best valued pellet (energy/distance) which can be reached before any larger or equal sized blobs or can be reached at the same time as smaller blobs
  for(i=0;i<pellets.length;i++){
    pellet=pellets[i]
    if(bestSafePelletValue<=pellet.energy/taxiDist(pellet.pos,me.pos)){
      for(j=0;j<others.length;j++){
        other=others[j];
        if(other.energy<me.energy){
          if(taxiDist(pellet.pos,me.pos)<=taxiDist(other.pos,pellet.pos)){
            if(taxiDist(pellet.pos,me.pos)<taxiDist(bestSafePellet.pos,me.pos)){
              bestSafePellet=pellet;
              bestSafePelletValue=pellet.energy/taxiDist(pellet.pos,me.pos);
            }
          }
        }
        if(other.energy>=me.energy){
          if(taxiDist(pellet.pos,me.pos)<taxiDist(other.pos,pellet.pos)){
            if(taxiDist(pellet.pos,me.pos)<taxiDist(bestSafePellet.pos,me.pos)){
              bestSafePellet=pellet;
              bestSafePelletValue=pellet.energy/taxiDist(pellet.pos,me.pos);
            }
          }
        }
      }
    }
  }

  if(bestSafePellet){
    [xPellet,yPellet]=bestSafePellet.pos;
    if(x<xPellet&&Math.abs(x-xPellet)>=Math.abs(y-yPellet)){
      return East;
    }
    if(x<xPellet&&Math.abs(x-xPellet)>=Math.abs(y-yPellet)){
      return West;
    }
    if(y<yPellet&&Math.abs(x-xPellet)<Math.abs(y-yPellet)){
      return South;
    }
    if(y<yPellet&&Math.abs(x-xPellet)<Math.abs(y-yPellet)){
      return North;
    }
  }
  
  var validMoves=["North","East","South","West","Stay"];
  var removeIndex=0;
  var safeEnergy;
  if(x==0)
    validMoves.splice(validMoves.indexOf("West"));
  if(x==map)
    validMoves.splice(validMoves.indexOf("East"));
  if(y==0)
    validMoves.splice(validMoves.indexOf("North"));
  if(y==map)
    validMoves.splice(validMoves.indexOf("South"));

  var possibleMoves=[...validMoves];
  possibleMoves.splice(possibleMoves.indexOf("Stay"));
  //If there is no safe pellet try to stick somewhat towards the middle
  //Ignore enemies unless at 2 distance from self and there is no safe pellet
  for(i=0;i<others.length;i++){
    other=others[i];
    safeEnergy=willSplit?(me.energy-50)/2:me.energy;
    if((other.energy>=safeEnergy)&&(taxiDist(me.pos,other.pos)<=2)){
      if(taxiDist(me.pos,other.pos)==1){
        if((removeIndex=validMoves.indexOf("Stay"))>=0){
          validMoves.splice(removeIndex,1)
        }
      }
      if(other.pos[0]<x){
        if((removeIndex=validMoves.indexOf("West"))>=0){
          validMoves.splice(removeIndex,1)
        }
      }
      if(other.pos[1]<y){
        if((removeIndex=validMoves.indexOf("South"))>=0){
          validMoves.splice(removeIndex,1)
        }
      }
      if(other.pos[0]>x){
        if((removeIndex=validMoves.indexOf("East"))>=0){
          validMoves.splice(removeIndex,1)
        }
      }
      if(other.pos[1]>y){
        if((removeIndex=validMoves.indexOf("North"))>=0){
          validMoves.splice(removeIndex,1)
        }
      }
    }
  }
  //If there are no safe moves move in a random direction (Reduce energy as much as possible with a slight chance of survival)
  if(!validMoves.length){
    switch (possibleMoves[Math.random()*possibleMoves.length|0]){
      case "North":
        return North;
      case "South":
        return South;
      case "East":
        return East;
      case "West":
        return West;
    }
  }
  //If there are safe moves bias towards moving towards the center block of 1/3 of the way from the sides
  if(!willSplit){
    //bias moving towards near the center
    biasedMoves=[];
    for(var i=0;i<validMoves.length;i++){
      switch(validMoves[i]){
        case "North":
          biasedMoves=biasedMoves.concat(y>center?"0".repeat(center/3|0).split``:"0".repeat(y-center).split``);
          break;
        case "South":
          biasedMoves=biasedMoves.concat(y<center?"2".repeat(center/3|0).split``:"2".repeat(center-y).split``);
          break;
        case "East":
          biasedMoves=biasedMoves.concat(y>center?"1".repeat(center/3|0).split``:"1".repeat(x-center).split``);
          break;
        case "West":
          biasedMoves=biasedMoves.concat(y<center?"3".repeat(center/3|0).split``:"3".repeat(center-x).split``);
          break;
        case "Stay":
          biasedMoves=biasedMoves.concat(["4"]);
          break;
      }
    }
  }
  if(willSplit){
    switch (biasedMoves[Math.random()*biasedMoves.length|0]){
      case "0":
        return SplitNorth;
      case "2":
        return SplitSouth;
      case "1":
        return SplitEast;
      case "3":
        return SplitWest;
      case "4":
        return Stay;
    }
  }
  else{
    switch (biasedMoves[Math.random()*biasedMoves.length|0]){
      case "0":
        return North;
      case "2":
        return South;
      case "1":
        return East;
      case "3":
        return West;
      case "4":
        return Stay;
    }
  }
}
\$\endgroup\$
3
  • \$\begingroup\$ I've already run the controller, but I might do it again later with this new bot. It's too late to reassign the bounty if it wins, but I'm curious about the result. \$\endgroup\$ May 22, 2019 at 14:05
  • \$\begingroup\$ @RedwolfPrograms The goal wasn't to win the bounty. \$\endgroup\$ May 22, 2019 at 14:11
  • \$\begingroup\$ I know, just making sure you know (: \$\endgroup\$ May 22, 2019 at 14:13

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.