14
\$\begingroup\$

In this challenge, bots must survive as long as possible. In each turn, the bot with the most HP is eliminated.

This challenge is inspired by the Smallest unique number KoTH.

Results

After 100,000 rounds:

Jack: 63390
The Follower: 4942
The Algorithm: 2452
The Randomiser: 140
Toe Tipping: 0
EZ: 0
The Average: 0
LowProfile2: 0
Lower Profile: 0
The Bandwagon: 0
Fair Share: 0
Low Profile: 0

Game Details

  • In each game, all bots start with 100 HP
  • In each turn:
    • All bots select an amount of HP to lose (minimum 1, inclusive)
    • Each bot loses the amount of HP they returned
    • All bots with 0 HP or less are eliminated
    • The bot with the highest HP is eliminated
    • If there is a tie for highest HP, all bots that tied are eliminated
  • The last bot alive wins 1 point
  • If all remaining bots are eliminated in the same turn, no points are awarded
  • After a number of games (default is 1000), the winner is the bot with the most points

Bot Format

Each bot should be a JavaScript function, which is run once per turn. This function will be provided three arguments:

  • The bot's HP
  • The HP of all other bots as of the last turn, or 0 if dead
    • Formatted as an array, will be randomly shuffled for each game
  • An object which can be used for storage between turns

The return value of this function should be a number (minimum 1, inclusive), representing the amount of HP to take. If the return value is not a number or is less than 1, it defaults to 1.

Example Bot

Example Bot tries to keep its HP 2 below the previous turn's average.

function(hp, others, storage) {
    var alive = others.filter(bot => bot > 0);
    var avg = alive.reduce((a, b) => a + b, 0) / alive.length;
    return hp - (avg - 2);
}

Controller

//Bots go in this object:

const bots = {
    "Example Bot": function(hp, others, storage) {
        var alive = others.filter(bot => bot > 0);
        var avg = alive.reduce((a, b) => a + b, 0) / alive.length;
        return hp - (avg - 2);
    }
};

//games: Number of games to run
//log: An array of bot names to log info about, or empty array for game overviews

function runGames(games = 1000, log = null) {
    let p = performance.now();
    let botData = [];
    for (let bot in bots)
        botData.push({
            name: bot,
            run: bots[bot],
            uid: 0,
            hp: 0,
            points: 0,
            storage: {},
        });
    for (let g = 0; g < games; g++) {
        let uids = new Array(botData.length).fill(0).map((a, b) => b);
        let turn = 0;
        if (log)
            console.log("[0] Starting game " + (g + 1));
        for (let j, i = uids.length - 1; i > 0; i--) {
            j = Math.floor(Math.random() * (i + 1));
            [uids[i], uids[j]] = [uids[j], uids[i]];
        }
        for (let i = 0; i < botData.length; i++) {
            botData[i].uid = uids[i];
            botData[i].hp = 100;
            botData[i].storage = {};
        }
        do {
            let hps = [];
            let max, alive;
            turn++;
            for (let hp, b, i = 0; i < botData.length; i++) {
                b = botData[i];
                if (!b.hp)
                    continue;
                try {
                    hp = Number(b.run(
                        b.hp,
                        botData.map(a => [a.uid, a.hp]).sort((a, b) => a[0] - b[0]).filter(a => a[0] != b.uid).map(a => a[1]),
                        b.storage
                    ));
                    if (log && log.includes(b.name))
                        console.log("[" + turn + "] " + b.name + " (" + b.hp + "): " + hp);
                } catch(e) {
                    hp = 100;
                    if (log && log.includes(b.name))
                        console.warn(b.name + " (" + JSON.stringify(b.storage) + "):\n" + (e.stack || e.message));
                }
                hps[i] = Number.isNaN(hp) ? 1 : Math.max(1, hp);
            }
            for (let i = 0; i < botData.length; i++) {
                if (hps[i]) {
                    botData[i].hp = Math.max(0, botData[i].hp - hps[i]);
                    if (!botData[i].hp && log)
                        console.log("[" + turn + "] Eliminated: " + botData[i].name + " (0)");
                }
            }
            alive = botData.filter(a => a.hp);
            if (alive.length == 1)
                alive[0].points += 1;
            if (alive.length <= 1)
                break;
            max = Math.max(...botData.map(a => a.hp));
            for (let i = 0; i < botData.length; i++) {
                if (botData[i].hp == max) {
                    if (log)
                        console.log("[" + turn + "] Eliminated: " + botData[i].name + " (" + botData[i].hp + ")");
                    botData[i].hp = 0;
                }
            }
            alive = botData.filter(a => a.hp);
            if (alive.length == 1)
                alive[0].points += 1;
            if (alive.length <= 1)
                break;
        } while (1);
    }
    console.log(games + " game(s) completed (" + ((performance.now() - p) / 1000).toFixed(3) + "s):\n" + botData.map(a => [a.name, a.points]).sort((a, b) => b[1] - a[1]).map(a => a[0] + ": " + a[1]).join("\n"));
}

Rules

  • Accessing the controller or other bots is not allowed
  • There's no time limit, but keep within reason
  • Any bots that error are eliminated
  • Clarification: Fractional HP is allowed
  • Deadline: Wednesday, March 25, 12:00 UTC (8:00 EDT) Closed to submissions!
\$\endgroup\$
  • \$\begingroup\$ I think I found a bug in the controller; if there are two bots remaining, and one of them drops its health to zero, no points are awarded (what should happen is the bot that didn't drop to zero earns a point) \$\endgroup\$ – pppery Mar 19 at 4:25
  • \$\begingroup\$ @pppery Intentional, although it might make more sense if it did work that way...should I change it? \$\endgroup\$ – Redwolf Programs Mar 19 at 4:41
  • \$\begingroup\$ I agree with @pppery because the current runner seems to give "no winner" too often. \$\endgroup\$ – Bubbler Mar 19 at 4:53
  • 1
    \$\begingroup\$ On account of the new cheating answer that exploits a controller bug you should probably say that submitting NaN answers is not allowed. \$\endgroup\$ – Value Ink Mar 23 at 4:33
  • 1
    \$\begingroup\$ Are we allowed to keep state between games? If not, why are the enemies randomly shuffled? I have an idea for a strategy that will track trends over multiple games, and would like to know if it is allowed. \$\endgroup\$ – MegaTom Mar 23 at 5:06

11 Answers 11

5
\$\begingroup\$

Jack

Assumes that half of the bots are stupid. So they will kill theirselves or will end up being killed simultaneously with another bot. We just need to make sure to have enough HP to survive it for enough rounds knowing that.

function(hp, others, storage) {
    var nrOfBots = others.filter(bot => bot > 0).length;
    var hpToTake = 1;
    if (hp == 100) { 
        hpToTake = hp - (Math.round(nrOfBots / 2) + 0.0001);
    }
    return hpToTake;
}
| improve this answer | |
\$\endgroup\$
  • 2
    \$\begingroup\$ First place: Wins 63.39% of games \$\endgroup\$ – Redwolf Programs Mar 25 at 15:28
9
\$\begingroup\$

Low Profile

function(hp, others, storage) {
    var alive = others.filter(bot => bot > 0);
    return hp - alive.length
}

Sets its health to be always equal to the number of bots. Any bot that undercuts this one risks eventually hitting zero health.

| improve this answer | |
\$\endgroup\$
  • 1
    \$\begingroup\$ I'm honestly not sure if it's even possible for this bot to lose -- but I would love to be proven wrong. \$\endgroup\$ – pppery Mar 18 at 23:44
  • 2
    \$\begingroup\$ It may lose if the other bots cut their HP faster than this but tightly enough, so that it becomes the highest HP at some point (e.g. two of the other bots use low profile - 1). \$\endgroup\$ – Bubbler Mar 19 at 1:56
  • \$\begingroup\$ @Bubbler Since you appear to have deleted your answers while I was typing (note: I didn't say that they were invalid), I believe your trick doesn't work because one of the two answers is inevitably not a serious contender. \$\endgroup\$ – pppery Mar 19 at 4:01
4
\$\begingroup\$

The Follower

function(hp, others, storage) {
    if (!storage.prev){
        storage.prev = others.slice();
        return hp * (Math.random() * 0.1 + 0.8);
    }
    let [decSum, decCnt] = others.reduce(([s,n],v2,i) => {
        if (v2 <= 0) return [s, n];
        return [s * (storage.prev[i]-v2), n + 1];
    }, [1,0]);
    storage.prev = others.slice();
    let avg = decSum ** (1 / decCnt);
    while (avg > hp) avg /= 2;
    return hp - avg;
}

Uses the geometric mean of the decrements chosen by alive opponents (in the previous turn) to decide its next move. There is no such information at the first turn, so it cuts a very large part of its own HP to be safe.

| improve this answer | |
\$\endgroup\$
  • 1
    \$\begingroup\$ Second place: Wins 4.942% of games \$\endgroup\$ – Redwolf Programs Mar 25 at 15:28
3
\$\begingroup\$

LowProfile2

function(hp, others, storage) {
        var epsilon = 1;
        while (hp + 1 - (epsilon / 2) != hp + 1) {
            epsilon = epsilon / 2;
        }
        var alive = others.filter(bot => bot > 0);
        return hp - alive.length + 1 - epsilon
}

Thanks to the discovery of fractional HP, I realized my old integer-based LowProfile submission could be beaten by one using it. I therefore present LowProfile2, which dynamically calculates the very limits of how close to death one can be!

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ I'm unsure whether this should have been an edit or a separate answer -- one could make an argument for both. \$\endgroup\$ – pppery Mar 19 at 4:33
2
\$\begingroup\$

Fair Share

function (hp, others, storage) {
    return hp / (1 + others.filter(bot => bot > 0).length) | 0;
}

The more bots are left, the more of its hp it has to ditch in order to try to keep ahead.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Your bot seems to lose less HP when there are more bots...fixing it would probably require choosing some constant to multiply by the number of living bots. \$\endgroup\$ – Redwolf Programs Mar 19 at 3:16
2
\$\begingroup\$

The Bandwagon

function(hp, others, storage) {
    if (hp > 4)
        return 48;
    return Math.max(...others) - 1.5;
}

It's a bit slow to catch onto the trend, and it's not very low profile about it.

It bases its strategy on the assumption that, after 2 turns, it's the endgame. It assumes all the others are going to return 1, and ensures it doesn't die by returning 1.5.

| improve this answer | |
\$\endgroup\$
  • 3
    \$\begingroup\$ Wait, fractional HP is a thing? \$\endgroup\$ – pppery Mar 19 at 4:10
  • \$\begingroup\$ @pppery Yes, I guess I'll make that more clear in the challenge spec \$\endgroup\$ – Redwolf Programs Mar 19 at 4:11
2
\$\begingroup\$

Average

function(hp, others, storage) { return Math.floor(hp/3); }

Loses a third of its HP every turn, dying in 13 turns if it doesn't end up as highest. (returning zero gets silently interpreted as returning 1 by the controller)

| improve this answer | |
\$\endgroup\$
2
\$\begingroup\$

The Randomiser

function(hp, others, storage) {
    var highestAlive = Math.max(...others);
    var returnVal = highestAlive * Math.random();
    return hp > returnVal ? returnVal
                          : hp * Math.random();
}

Returns a random number \$0\dots hp\$ of the highest bot alive (if we have enough \$hp\$ else a random number \$0\dots hp\$ of our \$hp\$).

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Fourth place: Wins 0.14% of games \$\endgroup\$ – Redwolf Programs Mar 25 at 15:30
  • \$\begingroup\$ @RedwolfPrograms Wow, that was pretty random! T_T \$\endgroup\$ – Noodle9 Mar 25 at 15:57
1
\$\begingroup\$

Lower Profile

function(hp, others, storage) {
    var alive = others.filter(bot => bot > 0);
    return hp - alive.length + 1
}

Tries to get lower than, but close to, Low Profile. But looks like it can't win by itself. :(

With an ally having + 2 instead of + 1 (I call it Lowest Profile), snipes Low Profile and this bot wins. Try it online!

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ What happened to lowest profile? \$\endgroup\$ – Redwolf Programs Mar 19 at 4:09
  • 1
    \$\begingroup\$ @RedwolfPrograms Lowest Profile was not a serious contender, for one thing. \$\endgroup\$ – pppery Mar 19 at 4:09
  • \$\begingroup\$ It appears that Follower can act as LowestProfile and collude to keep my LowProfile (or, more accurately, LowProfile2, which beats LowProfile under all circumstances) bot from winning. \$\endgroup\$ – pppery Mar 19 at 13:17
  • \$\begingroup\$ @pppery Fun thing is that Follower got the second place, while LowerProfile won zero times in the end... \$\endgroup\$ – Bubbler Mar 27 at 13:50
1
\$\begingroup\$

The Algorithm

function(hp, others, storage) {
    var rate = (storage.rate || (storage.rate = Math.random() * 0.25 + 0.3));
    var alive = others.filter(bot => bot > 0);
    var avg = alive.reduce((a, b) => a + b, 0) / alive.length;
    return hp > avg * rate ? hp - avg * rate : 1;
}

Picks a random value for each game, and will attempt to set its HP to that value multiplied by the current average.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Third place: Wins 2.452% of games \$\endgroup\$ – Redwolf Programs Mar 25 at 15:29
1
\$\begingroup\$

Toe Tipping

function(hp, others, storage) {
    var highestAlive = Math.max(...others);
    var returnVal = highestAlive / 2 - 0.1;
    return Math.min(returnVal, hp-1);
}

Looks for the highest bot alive, and returns 0.1 below halve that bot's HP.

| improve this answer | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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