18
\$\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\$
22
  • \$\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\$ Mar 19, 2020 at 4:25
  • \$\begingroup\$ @pppery Intentional, although it might make more sense if it did work that way...should I change it? \$\endgroup\$ Mar 19, 2020 at 4:41
  • \$\begingroup\$ I agree with @pppery because the current runner seems to give "no winner" too often. \$\endgroup\$
    – Bubbler
    Mar 19, 2020 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, 2020 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, 2020 at 5:06

11 Answers 11

12
\$\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.

\$\endgroup\$
3
  • 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\$ Mar 18, 2020 at 23:44
  • 3
    \$\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, 2020 at 1:56
  • 1
    \$\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\$ Mar 19, 2020 at 4:01
10
\$\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;
}
\$\endgroup\$
1
  • 3
    \$\begingroup\$ First place: Wins 63.39% of games \$\endgroup\$ Mar 25, 2020 at 15:28
6
\$\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.

\$\endgroup\$
1
  • 2
    \$\begingroup\$ Second place: Wins 4.942% of games \$\endgroup\$ Mar 25, 2020 at 15:28
4
\$\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.

\$\endgroup\$
2
  • 4
    \$\begingroup\$ Wait, fractional HP is a thing? \$\endgroup\$ Mar 19, 2020 at 4:10
  • 1
    \$\begingroup\$ @pppery Yes, I guess I'll make that more clear in the challenge spec \$\endgroup\$ Mar 19, 2020 at 4:11
4
\$\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!

\$\endgroup\$
1
  • 1
    \$\begingroup\$ I'm unsure whether this should have been an edit or a separate answer -- one could make an argument for both. \$\endgroup\$ Mar 19, 2020 at 4:33
4
\$\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.

\$\endgroup\$
3
\$\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!

\$\endgroup\$
4
  • 1
    \$\begingroup\$ What happened to lowest profile? \$\endgroup\$ Mar 19, 2020 at 4:09
  • 2
    \$\begingroup\$ @RedwolfPrograms Lowest Profile was not a serious contender, for one thing. \$\endgroup\$ Mar 19, 2020 at 4:09
  • 1
    \$\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\$ Mar 19, 2020 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, 2020 at 13:50
3
\$\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)

\$\endgroup\$
3
\$\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\$).

\$\endgroup\$
2
  • \$\begingroup\$ Fourth place: Wins 0.14% of games \$\endgroup\$ Mar 25, 2020 at 15:30
  • \$\begingroup\$ @RedwolfPrograms Wow, that was pretty random! T_T \$\endgroup\$
    – Noodle9
    Mar 25, 2020 at 15:57
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.

\$\endgroup\$
1
  • 1
    \$\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\$ Mar 19, 2020 at 3:16
2
\$\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.

\$\endgroup\$
1
  • \$\begingroup\$ Third place: Wins 2.452% of games \$\endgroup\$ Mar 25, 2020 at 15:29

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.