44
\$\begingroup\$

This challenge has ended. To see the final scores of the competitors, click here

In this challenge, each submission is one bot. Each bot should be a Javascript function. Bots will fight to obtain the highest total worth in gold. Gold can be farmed, or earned from killing other bots, and is used to upgrade healing, attacking, shielding, and farming.

Objective:

Over a number of rounds containing up to 1000 turns (ends when only one bot is left), the bot with the highest total worth (the sum of all gold obtained) is the winner.

Turns:

In each turn, every bot which is alive (>0 HP) will be run once. It can return a move, which can be one of the following:

  • Heal: Regains HP
  • Attack: Removes HP from another bot
  • Shield: Defends against later attacks
  • Stun: Skips another bot's next turn
  • Farm: Earns gold at the cost of HP
  • Upgrade: Make certain moves better

All bots will return their move before any are executed, so a stun, heal, attack, shield, etc. will not affect any bots moving later in that turn. For example, if Bot A stuns Bot B, and Bot B is after Bot A in the turn order, Bot B will still move later in that same turn and the stun will occur on the next turn.

Combat, Farming, and Upgrading:

Each bot has a maximum HP of 100, and an assigned UID between 0 and 99. This UID changes after every round, and is how bots keep track of each other.

Healing is one of the simplest moves, adding an amount of HP determined by its level (starts at 5 HP). A bot cannot heal past 100 HP.

Attacking a bot by its UID is another possible move, with a base damage of 5 HP at level 0. Bots can also be stunned, skipping their next turn, which also uses UIDs.

Bots have additional shield HP, which has no limit. This shield HP will absorb damage from direct attacks from other bots, and is added by shielding. At level 0, shielding adds 5 shield HP.

Farming will earn 5 gold at level 0, at the cost of 2 HP. This 2 HP cannot be shielded. The only use for gold (beyond winning) is to upgrade moves. Healing, attacking, and shielding have a base value of 5 HP, and farming starts at 5 gold. Each of those moves have individual levels, which start at 0. These formulas will determine the value in HP or gold of a move, where L is the level:

  • Healing: L + 5
  • Attacking: 1.25L + 5
  • Shielding: 1.5L + 5
  • Farming: 2L + 5

The cost of upgrading any move is the same for a certain level, and is determined by 2.5L² + 2.5L + 10, where L is the current level. A bot can use the function cost(currentLevel) as a shortcut to determine this.

Bots start with 25 gold, allowing them to quickly upgrade either two moves to level 1, or one move to level 2. This beginning gold does not count towards a bots total worth. Killing a bot gives you half of its total worth in gold, rounded up, and if two bots kill another in the same turn, they both get the reward.

Input/Output:

In order to communicate with the controller, the return value of the function is used to send move information. One of these should be returned:

  • Heal: heal()
  • Attack: attack(uid)
  • Shield: shield()
  • Stun: stun(uid)
  • Farm: farm()
  • Upgrade: upgrade("heal" / "attack" / "shield" / "farm")

To skip a turn (do nothing), return nothing, or return a falsy value.

To get the current turn number (starts at 1), use turn().

The arguments of your function will include information about your bot, UIDs of other bots, and between-turn storage. The first argument is an object with the following properties: uid, hp, gold, and shield. These are copies of your bot's current information. There is also an nested object levels, with the level numbers of heal, attack, shield, and farm.

The second argument is a shuffled array of all alive bots other than yours, formatted as an object containing properties uid, hp (plus shield), worth, and attack (attack level). The third argument is an empty object which can be used for between-turn storage.

Example Bots:

This bot will farm until it can upgrade its attack to level 5, then attack a random bot each turn until it dies (or wins). Not very effective due to lack of healing/shielding.

function freeTestBotA(me, others, storage) {
    if (me.levels.attack < 5) {
        if (me.gold < cost(me.levels.attack))
            return farm();
        return upgrade("attack");
    }
    return attack(others[0].uid);
}

This bot has two modes: offensive and defensive. It will either stun a random bot or heal when in defensive mode, and it will either attack or shield when in offensive mode. It will attempt to upgrade its attacks whenever possible.

function freeTestBotB(me, others, storage) {
    if (me.gold >= cost(me.levels.attack))
        return upgrade("attack");
    if (me.hp < 50)
        if (Math.random() < 0.5)
            return stun(others[0].uid);
        else
            return heal();
    else
        if (Math.random() < 0.5)
            return attack(others[0].uid);
        else
            return shield();
}

Rules:

  • Standard Loopholes forbidden
  • Bots may not read, modify, or add any variables outside of their scope, may not attempt to cheat, and may not call any controller-defined or DOM functions
  • Return value must be falsy, or one of the above function outputs
  • Bots should not be designed to target a specific bot, but can be designed to take advantage of common strategies
  • Bots may not attack themselves (discovered due to a comment by @Ness)
  • Bots must be sufficiently different from any other bots that they can be reasonably considered separate entries
  • Teaming is now not allowed
  • Controller can be found here
  • Chatroom

New Controller Debugging:

Using the file gold-battle-log.js, you can set the value of the debug property of a bot in botData to 0 (no logging), 1 (log moves), or 2 (log moves, hp, gold, levels, etc.)

Challenge ends at 1700 UTC on Friday, August 9th

\$\endgroup\$
  • 4
    \$\begingroup\$ Created a gist with all bots. gist.github.com/Draco18s/2efbf95edcf98d6b1f264e26bbb669d1 I will endeavor to keep it updated (but if not it's a decent start). \$\endgroup\$ – Draco18s Aug 4 at 14:52
  • 4
    \$\begingroup\$ Auto-updating controller with bots included: redwolfprograms.com/koth \$\endgroup\$ – Redwolf Programs Aug 7 at 15:36
  • 4
    \$\begingroup\$ I'm voting to close this question because it's already de-facto closed to new answers ("This challenge has ended. To see the final scores ...") \$\endgroup\$ – pppery Sep 3 at 20:29
  • 3
    \$\begingroup\$ @pppery Could you not? I'd be fine with non-competitive answers, and the [closed] at the end is likely to make casual viewers skip over reading my challenge since they;d assume it's low quality or off-topic. \$\endgroup\$ – Redwolf Programs Sep 4 at 3:11
  • 5
    \$\begingroup\$ @pppery I've never heard of a challenge being closed for being finished until today, and I'd argue the social restriction you want to enforce doesn't even exist. There's no need to close it, and I don't want it closed. To me, that seems like closing for the sake of closing, rather than for the good of the site. If someone wants to post an answer to an old question, they should be able to. There's no note after the serious contender rule saying it has to be a serious contender when it's posted; an answer can still be a serious contender for the challenge even if it's not a contender for winning \$\endgroup\$ – Redwolf Programs Sep 4 at 3:35

43 Answers 43

16
\$\begingroup\$

Unkillable

forked from Undyable.

function UnkillableBot(me){
    if(me.hp <= 100 - (me.levels.heal + 5)){
        return heal()
    }else if(turn() % 10 == 0 && me.shield < 800) {
        return shield()
    }else{
        if(me.gold >= cost(me.levels.shield) && me.levels.shield <= 9){
            return upgrade("shield")
        }else if(me.gold >= cost(me.levels.farm)){
            return upgrade("farm")
        }else{
            if(me.shield < 500 && me.levels.shield > 4) {
                return shield()
            }
            return farm()
        }
    }
}

Given the exponential costs of upgrades, may as well upgrade farming if we can't upgrade healing, allowing the bot to collect gold more efficiently.

\$\endgroup\$
  • \$\begingroup\$ Absolutely crushing the competition in my tests \$\endgroup\$ – Redwolf Programs Aug 1 at 19:20
  • 1
    \$\begingroup\$ I feel this bot might be a little stronger were that first if statement used <= - currently it will never heal to full. \$\endgroup\$ – Scoots Aug 2 at 13:30
  • \$\begingroup\$ @Scoots Not sure how much it'll matter, but I'll change that. \$\endgroup\$ – Draco18s Aug 2 at 13:41
  • 2
    \$\begingroup\$ @Draco18s I'm sure it matters very very little - but isn't this site all about tiny little practically insignificant improvements? :) \$\endgroup\$ – Scoots Aug 2 at 13:43
  • \$\begingroup\$ @Scoots Healing to max health does not matter much in this challenge because there are no real attacking threats. The only truly offensive bot is bullybot, and you cannot really do anything about him. It might actually decrease performance to stay at full health. \$\endgroup\$ – B0RDERS Aug 2 at 14:58
13
\$\begingroup\$

ThanosBot

function ThanosBot(me, others, storage){
    if(turn()==1){
        storage.origPopulation = others.length;
        return upgrade("attack");
    }

    if (others.length < storage.origPopulation / 2)
    {
        if(me.hp <= 100 - (me.levels.heal + 5)){
            return heal();
        }
        else {
            return farm();
        }
    }

    if(me.hp <= 100 - (me.levels.heal + 5)){
        return heal()
    }else{
        if(me.gold >= cost(me.levels.attack)){
            return upgrade("attack")
        }else if(me.gold >= cost(me.levels.heal)){
            return upgrade("heal")
        }else if(me.gold >= cost(me.levels.farm)){
            return upgrade("farm")
        }else{
            if(Math.random() < 0.5){
                return attack(others[0].uid);
            }
            else{
                return farm();
            }
        }
    }
}

There are too many bots, not enough gold to go around. This bot proposes a solution.

Genocide, yes, but random, dispassionate, fair to rich and poor alike.

They called him a madman.

ThanosBot wants the best for the bot community, and is willing to go all the way. In the beginning, he will upgrade his attack, farming and healing, to more efficiently gather resources and win battles. Prograssively, he will start attacking people randomly while still gathering resources, for upcoming battles. He will keep improving his army, his weapons and himself.

Once 50% of the population has been eliminated, bots born will only know full bellies and clear skies, he will retire to a life of farming, and watch the sun rise on a grateful universe. He will become completely pacifist, only healing himself with vegetable soups and farming.

\$\endgroup\$
  • 6
    \$\begingroup\$ I'm tempted to rename "attack" to "snap" \$\endgroup\$ – Redwolf Programs Aug 5 at 19:08
11
\$\begingroup\$

Kill Stealer

function killStealer({hp, gold, attack:atck, shield:shld, levels:{heal:lHeal, shield:lShld, farm:lFarm, attack:lAtck}}, es, S) {
  let saneReduce = (a, f, n) => a.length? a.reduce(f) : n;
  let t = turn();
  if (t===1) {
    S.worth = 0;
    S.pHP = 100;
    S.pGold = 0;
    S.stat = {};
    S.pT = 0;
    for (let e of es) S.stat[e.uid] = {kills:0, seen:0};
  }

  let pT = S.pT;
  S.pT = t;

  let shp = shld+hp;

  let healP = lHeal      + 5;
  let shldP = lShld*1.5  + 5;
  let farmP = lFarm*2    + 5;
  let atckP = lAtck*1.25 + 5;
  let pheal = () => hp<5  ||  Math.min(100, hp+healP)-hp > shldP? heal() : shield();

  let attacked = S.pHP-hp-shld > 2;
  S.pHP = hp+shld;

  if (gold>S.pGold  &&  t!=1) S.worth+= gold-S.pGold;
  S.pGold = gold;

  let pes = S.pEs;
  let ces = {};
  for (let e of es) ces[e.uid] = {uid:e.uid, hp:e.hp, worth:e.worth};
  S.pEs = ces;

  if (t === 1) return shield(); // to not break things depending on previous frame

  if (t == pT+1) {
    for (let uidE in pes) {
      let e = pes[uidE];
      if (!ces[uidE]) { // dead
        if (e.worth < 30) continue; // don't bother, because others probably won't
        for (let a of es) {
          let pa = pes[a.uid];
          if (a.worth >= pa.worth + e.worth/2 - 2) {
            S.stat[a.uid].kills++;
          }
          if (a.worth != pa.worth || a.hp > pa.hp) S.stat[a.uid].seen++;
        }
      }
    }
  }


  let attackers = es.filter(c => {
    let k = S.stat[c.uid].kills;
    let s = S.stat[c.uid].seen;
    return k > 1  &&  k > s*.7;
  });
  let maxDmg = es.map(c=>c.attack).reduce((a, b) => Math.max(a, b), 0)*1.25 + 5;
  for (let e of es) {
    if (e.worth < farmP) continue;
    let p = pes[e.uid];
    let dmg = p.hp-e.hp;
    if (e.hp <= atckP) {
      return attack(e.uid);
    }
    if (e.hp-dmg-atckP <= 0) {
      return attack(e.uid);
    }
    if (e.hp-maxDmg-atckP <= 0) {
      return attack(e.uid);
    }
    if (e.hp-maxDmg-dmg <= 0) {
      return attack(e.uid);
    }
  }
  if (attackers.length>0 && t>50) {
    for (let e of es) {
      if (e.hp - maxDmg*2 - atckP <= 0  &&  e.worth > 200) {
        let worst = saneReduce(attackers.filter(c => c.hp > 80), (a, b)=>a.worth>b.worth? a : b, null);
        if (worst) return stun(worst.uid);
      }
    }
  }



  if (t < 60  &&  t%5 == 1) return shield();
  if (t === 2) return upgrade("heal");
  if (t === 3) return upgrade("farm");
  if (t%10 == 1) return shield();

  if (gold>=cost(lShld) && lFarm>-2) return upgrade("shield");
  if (gold>=cost(lFarm) && !attacked) return upgrade("farm");

  if (es.length > 2) {
    let notDead = es.filter(c => c.hp > 20);
    if (notDead.length !== 0) {
      notDead.sort((a, b) => a.hp-b.hp);
      if (notDead[Math.min(2, notDead.length-1)].hp > shp) {
        return pheal();
      }
    }
  }


  if (gold>=cost(lHeal)  &&  lHeal+5 < lFarm) return upgrade("heal");
  if (gold>=cost(lAtck)  &&  lAtck+5 < lFarm  &&  es.every(c=>c.attack<=lAtck+2)) return upgrade("attack");

  if (lShld>5  &&  shp < 205+healP+t  &&  shp < 600+t*5) return pheal();
  if (es.every(c => c.worth < S.worth+farmP) && es.length>2 && t<100 && lShld<6) return pheal();
  if (shp<=120  ||  hp<5) return pheal();
  return farm();
}

Now not only steals kills, but steals stealing kills too!

This bot doesn't do much except farm, and when it notices the possibility, joins in doing the final blow to a dying enemy, and somehow manages to be very good.

\$\endgroup\$
  • \$\begingroup\$ It works because all bots involved in a killing blow get the full reward. \$\endgroup\$ – Draco18s Aug 4 at 3:35
  • \$\begingroup\$ @Draco18s I understand why it could be good, I just didn't expect such a simple idea to get on average 2x the score of the next best bot (at the time of making it). \$\endgroup\$ – dzaima Aug 4 at 3:38
  • \$\begingroup\$ Hehe, that's fair. I'll have to download all the bots when I can and see if I can find another solution. \$\endgroup\$ – Draco18s Aug 4 at 4:02
9
\$\begingroup\$

The Equalizer

This bot seeks to restore peace in the bot community. He relentlessly targets the bots with the highest attack, giving up only if the bot's healing is better than his own attack. Once no bots with healing worse than its attack are left, he will retire to a life of peaceful farming.

function equalizer(me, others, storage){
  if(storage.agroKilled == null)storage.agroKilled = false;
  if(!storage.agroKilled){
    if(storage.blacklist == null)storage.blacklist = [];
    if(storage.lastAttack == null)storage.lastAttack = -1;
    var maxAtk = 0;
    var maxAtkUid = -1;
    var maxAtkHealth = 0;
    for(var i = 0; i < others.length; i++)if(others[i].uid == storage.lastAttack){
      maxAtk = others[i].attack*1.25+5;
      maxAtkUid = storage.lastAttack;
      maxAtkHealth = others[i].hp;
    }
    for(var i = 0; i < others.length; i++){
      if(storage.lastAttack == others[i].uid && others[i].hp >= storage.lastHealth){
        maxAtk = 0;
        maxAtkUid = -1;
        maxAtkHealth = 0;
        storage.blacklist.push(others[i].uid);
      }
    }
    storage.lastAttack = -1;
    var willHeal;
    for(var i = 0; i < others.length; i++)if(others[i].attack*1.25+5 > maxAtk){
      willHeal = false
      for(var j = 0; j < storage.blacklist.length; j++)if(others[i].uid==storage.blacklist[j])willHeal = true;
      if(!willHeal){
        maxAtk = others[i].attack*1.25+5;
        maxAtkUid = others[i].uid;
        maxAtkHealth = others[i].hp;
      }
    }
    if(me.hp < maxAtk) return heal();
    if(me.hp <= 100 - me.levels.heal - 5) return heal();
    var target = -1;
    var targetWorth = me.levels.farm * 2 + 5;
    for(var i = 0; i < others.length; i++) {
      if (others[i].hp <= maxAtk && others[i].worth / 2 > targetWorth) {
        target= others[i].uid;
          targetWorth = others[i].worth / 2;
      }
    }
    if(target!=-1) return attack(target);
    if(me.gold >= cost(me.levels.attack)) return upgrade("attack");
    if(me.levels.heal + 7 < me.levels.attack && me.levels.heal < 9 && me.gold >= cost(me.levels.heal)) return upgrade("heal");
    if(maxAtkUid!=-1){
      storage.lastAttack = maxAtkUid;
      storage.lastHealth = maxAtkHealth;
      return attack(maxAtkUid);
    }
    storage.agroKilled = true;
  }
  if(me.hp < 30) return heal();
  if(me.gold > cost(me.levels.farm)) return upgrade("farm");
  return farm();
}
\$\endgroup\$
8
\$\begingroup\$

Optimist

function Optimist(me, others, storage) {
    if (me.hp < 10)
        return heal();
    if ( (me.hp + me.shield) < 50 )
        return shield();
    if (me.gold >= cost(me.levels.farm) && cost(me.levels.farm) < 0.8 * (1000 - turn()))
        return upgrade("farm");
    rich_bots = others.sort( (x,y) => y.worth - x.worth );
    potential_victim = rich_bots.find( bot => bot.hp <= me.levels.attack * 1.25 + 5 );
    if (potential_victim)
        return attack(potential_victim.uid);
    if (me.gold < rich_bots[0].worth + cost(me.levels.farm) + 25)
        return farm();
    if (me.levels.heal < me.levels.farm)
        return upgrade("heal");
    if (me.levels.shield < me.levels.heal)
        return upgrade("shield");
    if (me.levels.attack < me.levels.shield)
        return upgrade("attack");
    return shield();
}

Assumes it will be able to spend 80% of its time peacefully farming, so it starts off by maxing out farming, and only then starts paying any attention to its combat skills. Surely nothing will go wrong!

\$\endgroup\$
8
\$\begingroup\$

Kill Assist

function KillAssist(me, others, storage) {
  let t = turn();
  if (t===1) {
    storage.worth = 0;
    storage.pHP = 100;
    storage.pGold = 0;
  }
  let hp = me.hp;
  let gold = me.gold;
  let shld = me.shield;
  let lHeal = me.levels.heal+0.25;
  let lFarm = me.levels.farm;
  let lShld = me.levels.shield;
  let lAtck = me.levels.attack;
  let healPower = lHeal      + 4.75;
  let shldPower = lShld*1.5  + 5;
  let farmPower = lFarm*2    + 5;
  let atckPower = lAtck*1.25 + 5;

  let dmgTaken = storage.pHP-(hp+shld);
  let attacked = dmgTaken > 2;
  storage.pHP = (hp+shld);

  if (gold > storage.pGold) storage.worth+= gold-storage.pGold;
  if (gold-storage.pGold > farmPower+5)  storage.lastAtck = -10;
  storage.pGold = gold;
  let pOthers = storage.pOthers;
  storage.pOthers = {};
  for (let o of others) {
    storage.pOthers[o.uid] = {hp: o.hp, uid: o.uid, worth: o.worth};
  } 

  if (t === 1 || t === 2) return upgrade("shield");
  if (t === 3) return shield();

  let maxdmg = others.map(c=>c.attack).reduce((a, b) => Math.max(a, b))*1.25 + 5;
  let lowhp = others.map(c=>c.hp).reduce((a, b) => Math.min(a, b));
  let lowhpid = others.find(c=>c.hp == lowhp).uid;
  let maxAttacker = others.find(o => o.attack*1.25 + 5 == maxdmg).uid;
  for (let o of others) {
    if (o.hp < atckPower  &&  o.worth > farmPower) {
      storage.dead = o.uid;
      storage.deadWorth = o.worth;
      return attack(o.uid);
    }
    let pO = pOthers[o.uid];
    let dmg = pO.hp - o.hp;
    if (o.hp - dmg - atckPower <= atckPower && o.worth >= farmPower) {
      storage.dead = o.uid;
      storage.deadWorth = o.worth;
      return attack(o.uid);
    }
    if (o.hp - maxdmg - atckPower <= atckPower && o.worth >= farmPower) {
      storage.deadWorth = o.worth;
      return attack(o.uid); 
    }
  }
  let lowhpdiff = Math.max(pOthers[lowhpid].hp - others.find(o => o.uid == lowhpid).hp,0);
  if (others.some(o => o.hp > maxdmg && o.hp < lowhpdiff*2+atckPower+maxdmg && o.worth > farmPower)) {
    let bad = others.reduce((a, b) => a.worth>b.worth? a : b);
    let bad2 = others.reduce((a, b) => bad.uid == b.uid ? a : (bad.uid == a.uid ? b : (a.worth>b.worth ? a : b)));
    if(bad.worth < bad2.worth*3 && bad.hp >= (maxdmg+atckPower)*2 && bad.uid != maxAttacker && bad.uid != lowhpid) {
      return stun(bad.uid);
    }
    if(bad2.hp >= (maxdmg+atckPower)*2 && bad2.uid != maxAttacker && bad.uid != lowhpid) {
      return stun(bad2.uid);
    }
  }

  if (t%10 == 9  &&  lShld>4) return shield(); // slowly build up shield just in case
  if (shld+hp < 100) return shldPower>healPower || hp >= 100-healPower? shield() : heal();

  var bon = shldPower-maxdmg < 3 && t < 700 ? lShld/2 : 0;
  var bon2 = t/100;
  if (gold>=cost(lFarm) && lShld+2 > lFarm && bon == 0 && !attacked) return upgrade("farm"); // farm first, but make sure it doesn't get too far ahead
  if (gold>=cost(lShld) && t>20 && (lShld<10+bon || lShld+5+bon2 < lFarm+bon) && t < 900) return upgrade("shield");
  if (gold>=cost(lFarm)) return upgrade("farm"); // try upgrading farming again, because shield upgrading can be picky
  if (gold>=cost(lHeal) && (lHeal<3)) return upgrade("heal"); // healing isn't that important

  if (shld<200 && attacked || shld<500 && t>20 && others.filter(c=>c.hp>=100).every(o=>o.hp+10 > hp+shld)) return shldPower>healPower || hp >= 100-healPower? shield() : heal();

  let hpdelta = attacked ? dmgTaken+shldPower : maxdmg
  if (shld<lShld*60 && (1000-t)*(hpdelta) > shld+hp) return shield(); // we want to look impressive & terrifying
  if (hp<=100-healPower) return heal();

  return farm();
}

Why upgrade attack value when you can do plink damage and still get full credit?

Once more back to piggybacking off Kill Stealer. I was able to simplify several blocks of code where statements were always true and fiddle with some numbers that resulted in massive gains over the original.

I have to hand it to @dzaima for realizing that stunning a wealthy opponent that is likely to be involved in an assist the turn before a kill happens is quite clever. One of the (very) few times Stun() has a positive sum outcome. Once again I was able to improve upon the idea, as knowing that Kill Stealer will be running similar logic, Kill Assist looks for a "second best" target (with some discretion) and stuns them instead.

Minor update to prevent stunning the bot-about-to-die and prevent stunning the bot-most-likely-to-make-the-kill.

Sample results (truncated top 5 after 1000 games)

VM2406:1629 Kill Assist: 39495.679
VM2406:1629 The Accountant: 29990.267
VM2406:1629 Kill Stealer: 23530.153
VM2406:1629 Unkillable: 12722.604
VM2406:1629 captFarmer: 12232.466
\$\endgroup\$
  • \$\begingroup\$ Wait, in what world does Captain Farmer get 14k gold? \$\endgroup\$ – Redwolf Programs Aug 6 at 21:15
  • \$\begingroup\$ This one: runGame(1) results: [...] captFarmer: 13768 \$\endgroup\$ – Draco18s Aug 6 at 21:23
  • \$\begingroup\$ That's pretty unexpectedly high...it usually gets around 10k in my tests \$\endgroup\$ – Redwolf Programs Aug 7 at 1:12
  • \$\begingroup\$ *shrugh* No idea. I'll do an automated gist update just to insure everything's clean. \$\endgroup\$ – Draco18s Aug 7 at 2:22
  • \$\begingroup\$ My favorite bot by the end of the deadline. \$\endgroup\$ – Night2 Aug 9 at 18:27
7
\$\begingroup\$

Undyable Bot (v3)

function undyableBot(me, others, storage){    

    if(me.hp < 100 - (me.levels.heal + 5)*2){
        return heal()
    }else{
        if(me.levels.heal < 10 && cost(me.levels.heal) / 2 < cost(me.levels.farm)){
            if(me.gold >= cost(me.levels.heal)){
                return upgrade("heal")
            }else{
                return farm()
            }
        }else{
            if(me.gold >= cost(me.levels.farm)){
                return upgrade("farm")
            }else{
                return farm()
            }
        }        
    }   
}

\$\endgroup\$
6
\$\begingroup\$

PatientStrategistBot

I tried to write a bot that starts out framing and defending as needed and then switches to killing other high value bots later in the game.

Currently this doesn't seem to work properly since its either killed by a gang of murder-bots in the start of the game or gets stuck somewhere in its offensive mode.

Still pretty happy with this being my first JS code, so ... (I did steal code snippets from here & there cause that was faster than googling all of the JS basic syntax)

function PatientStratgistBot(me, others, storage) {

    //set up some stuff in first turn
    if (turn() == 1) {
    storage.selfWorth = 0;
    storage.attackMode = false;
    storage.expectHP = 100;
    storage.expectShield = 0;
    storage.shieldTarget = 0;
    storage.targetUid = "None";
    storage.attackRounds = 0;
    storage.targetStartHP = 100;

        return upgrade("farm");
    }

    let farmPower = me.levels.farm * 2 + 5;

    //defensive Actions

    var maxAtk = Math.max(...others.map(o => o.attack));

    storage.shieldTarget = Math.ceil(maxAtk * 1.25 / 1.5) + 1;

    if (me.levels.shield < storage.shieldTarget && me.gold >= cost(me.levels.shield) && me.levels.shield < me.levels.farm)
        return upgrade("shield");

    if (turn() >= 7 && me.shield < 10 && me.levels.shield * 1.5 >= me.levels.heal) return shield();

    if (turn() >= 15 && me.shield < 15 && me.levels.shield * 1.5 >= me.levels.heal) return shield();

    if (turn() >= 30 && me.shield < 20 && me.levels.shield * 1.5 >= me.levels.heal) return shield();

    //attack mode
    // check if there any targets worth to go for

    function findTarget(potentialTargets, baseR){
    var targetUID = "None";
    var best = 0;
    for( var i = 0; i < potentialTargets.length; i++) {
        //We upgrade to attack lvl12, so 20 dmg; assume an enemy can heal/shield up to 15 per round
        var killRounds = Math.ceil(potentialTargets[i].hp / 5)
        var gain = potentialTargets[i].worth / ( 2 * ( killRounds + baseR) )
        //console.log(me, turn(), potentialTargets[i], killRounds, baseR, gain, farmPower)
        if (gain > farmPower * ( killRounds + baseR ) && gain > best)
            targetUID = potentialTargets[i].uid;
            storage.targetStartHP =  potentialTargets[i].hp;
    }
    return targetUID;
    }


    if (turn() >= 600) {


    //check if a current target is dead
    const uids = others.map(x=>x.uid);
        if(storage.targetUid != "None" && !uids.includes(storage.targetUid)) {
        storage.targetUid = "None";
        storage.attackMode = false;
        storage.attackRounds = 0;
    }


    // check if we are doing enough damage to current target
    if (storage.targetUid != "None" && storage.attackRounds >= 3) {

        var deltaHP = storage.targetStartHP - others[storage.targetUid].hp

        if (deltaHP / storage.attackRounds < 5) {
            storage.targetUid = "None";
            storage.attackMode = false;
            storage.attackRounds = 0;

        }

    }

    var investCost = 0
    for( var i = me.levels.attack; i < 12; i++) investCost += cost(i);

    if (storage.attackMode == true && me.gold >= investCost && me.levels.attack < 12) return upgrade("attack");

    if (storage.attackMode == false) {
        baseRounds = investCost / farmPower * 1.2; //overestimation with the heal level we should have at this point

        if (findTarget(others, baseRounds) != "None")
            storage.attackMode = true;

        var betterThanMe = others.filter(o => o.worth >= storage.selfWorth);

        if (betterThanMe.length > 0)
            storage.attackMode = true;

        //storage.attackMode = true;


    }

    }

    if (storage.attackMode == true && me.levels.attack == 12) {

    if (storage.targetUid == "None") {

        var target = findTarget(others, 0)
        storage.targetUid = target;
        storage.attackRounds = 0;
        return attack(target);

    }

    return attack(storage.targetUid)

    }



    //otherwise farm

    if (me.hp < 50) {
    storage.expectHP += 5 + me.levels.heal;
        return heal();
    }

    if (me.gold >= cost(me.levels.farm) && storage.attackMode == false)
        return upgrade("farm");

    //upgrade heal, so we can farm more, but increase farm ability faster
    if (me.levels.farm > 5 && me.levels.heal < 10 && me.gold >= 2*cost(me.levels.heal))
        return upgrade("heal");


   //be opportunistic - check if killing someone is more profitable than farming
    killable = others.filter(o => o.hp < me.levels.attack * 1.25 + 5 && o.worth / 2 > farmPower);
    if (killable.length > 0){
    //ideally check for the most worth target here
        return attack(killable[0].uid);
    }

    storage.expectHP -= 2;
    storage.selfWorth += farmPower;
    return farm();

}
\$\endgroup\$
6
\$\begingroup\$

Switzerland

function switzerland(self,others,storage){
    let turnsLeft=999-turn()
    let lowestHpBots=others.sort((a,b)=>a.hp-b.hp)
    if(!storage.worth){
        storage.worth=0
        storage.prevGold=25
    }else if(self.gold>storage.prevGold){
        storage.worth+=self.gold-storage.prevGold
    }
    if(others.length===1&&storage.worth>others[0].worth){
        //stun lock the other bot if there are only 2 left and I can win
        return stun(others[0].uid)
    }else if(self.hp<=(95-self.levels.heal)){
        return heal()
    }else if(lowestHpBots[0]&&lowestHpBots[0].hp<20&&lowestHpBots[0].worth/2>2*self.levels.farm+5&&self.hp+self.shield>=110){
        //kill assist
        return attack(lowestHpBots[0].uid)
    } else if(self.shield<=50||self.shield<=5500/others.length&&self.shield<=1200&&turn()>=20||lowestHpBots[1]&&lowestHpBots[1].hp>self.hp+self.shield){
        return shield()
    }else if(self.gold>=cost(self.levels.shield)&&self.levels.shield<=8){
        return upgrade("shield")
    } else if(self.gold>=cost(self.levels.farm)&&(turnsLeft+1)*(2*(self.levels.farm)+5)<turnsLeft*(2*(self.levels.farm+1)+5)){
        return upgrade("farm")
    } else if(self.gold>=cost(self.levels.heal)&&(turnsLeft+1)/(self.levels.heal+5)*(2*self.levels.farm+5)<turnsLeft/(self.levels.heal+6)*(2*self.levels.farm+5)&&self.levels.heal<=2){
        return upgrade("heal")
    }else{
        return farm()
    }
}

Like the name suggests, this bot is neutral mostly neutral(now it helps kill bots that are going to die) and just farms and heals, slowly building up its gold(just like switzerland)

\$\endgroup\$
6
+100
\$\begingroup\$

The Bot That Farms, Attacks, Shields, And Even Heals But Never Stuns

(Short name is TBTFASAEHBNS, not to be mistaken with TBTPTGCBCBA)

function TBTFASAEHBNS(me, others, storage) {
    this.getLevel = function (type) {
        return (typeof me.levels[type] === 'undefined' ? 0 : me.levels[type]);
    };

    this.getPower = function (type, level) {
        if (typeof level === 'undefined') level = this.getLevel(type);
        if (type === 'heal') return level + 5;
        if (type === 'attack') return (level * 1.25) + 5;
        if (type === 'shield') return (level * 1.5) + 5;
        if (type === 'farm') return (level * 2) + 5;
    };

    this.canUpgrade = function (type) {
        return myGold >= cost(this.getLevel(type));
    };

    this.farmOrUpgradeFarm = function () {
        if (this.canUpgrade('farm')) return upgrade('farm');
        if (myHp < 3) return heal();
        return farm();
    };

    let currentTurn = turn(),
        myGold = me.gold,
        myHp = me.hp,
        myShield = me.shield,
        myTotalHp = myHp + myShield,
        myHealPower = this.getPower('heal'),
        myShieldPower = this.getPower('shield'),
        myAttackPower = this.getPower('attack'),
        myFarmPower = this.getPower('farm'),
        topAttackPower = 0,
        attackOptions1 = [],
        attackOptions3 = [],
        attackOptions2 = [],
        finalTurns = 980;

    if (currentTurn === 1) {
        storage.othersInfo = {};
    }

    others.sort((a, b) => b.attack - a.attack);
    for (let i = 0; i < others.length; i++) {
        let other = others[i];

        if (i < 3) topAttackPower += this.getPower('attack', other.attack);

        if (other.worth > myFarmPower) {
            if (other.hp <= myAttackPower) {
                attackOptions1.push(other);
            } else {
                if (typeof storage.othersInfo[other.uid] !== 'undefined') {
                    let otherHpChange = storage.othersInfo[other.uid].hp - other.hp;

                    if (other.hp - otherHpChange <= 0) {
                        attackOptions2.push(other);
                    } else if (other.hp - (otherHpChange * 3) <= 0) {
                        attackOptions3.push(other);
                    }
                }
            }
        }

        storage.othersInfo[other.uid] = {hp: other.hp};
    }

    if (myTotalHp < (topAttackPower * 7) + 5) return shield();
    if (currentTurn <= 10) return this.farmOrUpgradeFarm();

    if (attackOptions1.length > 0) {
        attackOptions1.sort((a, b) => b.worth - a.worth);
        return attack(attackOptions1[0].uid);
    } else if (attackOptions2.length > 0) {
        attackOptions2.sort((a, b) => b.worth - a.worth);
        return attack(attackOptions2[0].uid);
    } else if (attackOptions3.length > 0) {
        attackOptions3.sort((a, b) => b.worth - a.worth);
        return attack(attackOptions3[0].uid);
    }

    if (currentTurn <= 20) return this.farmOrUpgradeFarm();
    if (currentTurn < finalTurns && myShieldPower < topAttackPower / 2 && Math.random() * 15 < 1 && this.canUpgrade('shield')) return upgrade('shield');
    if (currentTurn < finalTurns && this.canUpgrade('farm')) return upgrade('farm');
    if (currentTurn < finalTurns && myHealPower < 10 && this.canUpgrade('heal')) return upgrade('heal');
    if (myHp < 3) return heal();
    return farm();
}

This bot basically:

  • Builds up on farming at start
  • Defends itself when needed
  • Attacks when it can kill or when it thinks there is a chance to kill someone
  • Upgrades here and then
  • Farms the rest of the time
  • Never stuns

Edit 1: Fixed an issue and improved some tiny things in the bot based on tests with lots of games.

Edit 2: Reduced shield upgrades.

\$\endgroup\$
  • 2
    \$\begingroup\$ As soon as I saw the name I knew it would be your bot (: \$\endgroup\$ – Redwolf Programs Aug 7 at 16:58
  • \$\begingroup\$ I'm sorry about the long names, but I'm addicted to it! \$\endgroup\$ – Night2 Aug 7 at 16:59
  • 1
    \$\begingroup\$ Maybe it's a sign of a good bot...my tests show it's in 5th place \$\endgroup\$ – Redwolf Programs Aug 7 at 17:00
5
\$\begingroup\$

SniperBot

This bot will only be effective if someone starts adding bots that actually attack on a regular basis. SmartFarmer is my current optimized solution

  1. heals if can get one-shot
  2. heals if under 30
  3. attacks bot if can pick it off and earn more money than farming
  4. upgrades farming if can afford
  5. upgrades healing if is under 80 health and can afford
  6. farms

vultures don't need an attack

function sniperBot(me, others){
    if(me.hp < 30) return heal();
    for(var i = 0; i < others.length; i++)if(others[i].attack > me.hp)return heal();
    var target = -1;
    var targetWorth = me.levels.farm * 2 + 5;
    for(var i = 0; i < others.length; i++) {
        if (others[i].hp <= 1.25 * me.levels.attack + 5 && others[i].worth / 2 > targetWorth) {
            target= others[i].uid;
            targetWorth = others[i].worth / 2;
        }
    }
    if(target!=-1) return attack(target);
    if(me.gold >= cost(me.levels.farm)) return upgrade("farm");
    if(me.hp < 50 && me.gold >= cost(me.levels.heal)) return upgrade("heal");
    return farm();
}
\$\endgroup\$
  • \$\begingroup\$ Unexpected identifier (int) on line 2. ReferenceError: health is not defined. \$\endgroup\$ – Draco18s Aug 1 at 20:06
  • \$\begingroup\$ Should be me.hp? \$\endgroup\$ – mbomb007 Aug 1 at 20:19
  • \$\begingroup\$ sorry. new to javascript. thanks for the help \$\endgroup\$ – B0RDERS Aug 1 at 20:26
  • \$\begingroup\$ Your if(me.hp <30 && ...) could be simplified to just the first clause on account of needing an absurd level of heal for it to matter (lvl 65) \$\endgroup\$ – Veskah Aug 2 at 14:43
  • \$\begingroup\$ @Veskah Thanks for pointing that out. That was a remnant from when the min hp was higher \$\endgroup\$ – B0RDERS Aug 2 at 14:44
5
\$\begingroup\$

BullyDozerBot

function BullyDozerBot(me, others, storage){
    if(me.gold >= cost(me.levels.attack) && (storage.bullyTarget && storage.bullyTarget.hp < 500)) {
        return upgrade("attack");
    }
    if(storage.bullyTarget==null){
        storage.bullyTarget=others.sort((a,b) => a.hp - b.hp)[0];
    }
    potential_victim = others.find( bot => bot.hp <= me.levels.attack * 1.25 + 5 );
    if (potential_victim) {
        return attack(potential_victim.uid);
    }
    var targetlives = false;
    for(var i = 0; i < others.length; i++) {
        if (others[i] == storage.bullyTarget) {
            targetlives = true;
            break;
        }
    }
    if(!targetlives){
        storage.bullyTarget=others.sort((a,b) => a.hp - b.hp)[0];
    }
    if(storage.bullyTarget.hp >= 500) {
        if(me.gold >= cost(me.levels.farm)) {
            return upgrade("farm");
        }
        for(var i = 0; i < others.length; i++){
          if(others[i].attack*1.25+10 > me.hp){
            return heal();
          }
        }
        return farm();
    }
    return attack(storage.bullyTarget.uid);
}

Mashup of BullyBot and some other bits. Optimist had a short and sweet opportunistic attack chunk that I yoinked (though other bots do similar calculations).

Instead of bullying the target by stunning them, it murders them for their sweet, sweet loot. It also targets the weakest in the herd for bullying, but it will give up and just go farming if the weakest target's HP is too high.

\$\endgroup\$
  • \$\begingroup\$ you are farming yourself to death. Accept my edit :) \$\endgroup\$ – B0RDERS Aug 2 at 15:18
  • 1
    \$\begingroup\$ @AndrewBorders Ha, didn't even think about it. Thanks. \$\endgroup\$ – Draco18s Aug 2 at 16:50
  • \$\begingroup\$ This bot was great until those protector bots came around. \$\endgroup\$ – B0RDERS Aug 2 at 19:04
  • \$\begingroup\$ @B0RDERS Shield is very strong, even if it wastes time. \$\endgroup\$ – Draco18s Aug 2 at 19:21
5
\$\begingroup\$

FizzBuzz

function FizzBuzz(me, others, storage) {
    if (!storage.target) storage.target = others[0].uid;
    const uids = others.map(x=>x.uid);
    if(!uids.includes(storage.target) || (turn() % 30 === 0 
        && others[uids.indexOf(storage.target)].hp>30))
        storage.target = others[0].uid;

    if (cost(me.levels.farm) < me.gold) return upgrade("farm");
    if (turn() % 15 === 0) return heal();
    if (turn() % 3 === 0) return farm();
    if (turn() % 5 === 0) return heal();

    if (cost(me.levels.attack) < me.gold) return upgrade("attack");
    return attack(storage.target);
}

Mostly offensive bot. Extremely upset by the fact that it can't truly FizzBuzz so it just Buzzes angrily instead. When it's not Fizzing or Buzzing, it chips away at another bot for 30 turns and gives up and chooses another bot to target if it isn't making progress.

Performs extraordinarily inconsistently. Never mind, updated the controller, now seems to always be around the middle of the pack.

\$\endgroup\$
  • \$\begingroup\$ I like this concept. Regardless of the current situation, it keeps chugging along at its own pace. \$\endgroup\$ – Ness Aug 2 at 19:05
5
\$\begingroup\$

bullyBot

function bullyBot(me, others, storage){
    if(turn()==1){return farm();}
    if(storage.bullyTarget==null){storage.bullyTarget=others[0].uid;}

    var targetlives = false;
    for(var i = 0; i < others.length; i++) {
        if (others[i].uid == storage.bullyTarget) {
            targetlives = true;
            break;
        }
    }
    if(!targetlives){storage.bullyTarget = others[0].uid;}

    return stun(storage.bullyTarget);
}

Try it online!

May not win but will certainly try his damndest to make sure his target doesn't either. bullyBot also farms on the first turn so that if there's no outside influence, he will beat his target 5-0 or tie them 5-5.

\$\endgroup\$
5
\$\begingroup\$

JustFarm

I thought I'd start simple.

function justFarm(me, others){
    return farm();
}
\$\endgroup\$
  • 13
    \$\begingroup\$ This bot will suicide due to the 2HP cost of farming. \$\endgroup\$ – Draco18s Aug 1 at 17:28
  • \$\begingroup\$ @Draco18s Although the round may end before that, depending on the number of bots \$\endgroup\$ – Redwolf Programs Aug 1 at 17:52
  • 1
    \$\begingroup\$ While technically true, a 50 round timer is very very short when the default end time is 1000. \$\endgroup\$ – Draco18s Aug 1 at 17:57
  • \$\begingroup\$ It beat the two example bots, but now there are a few more submissions I might try to come up with something better. \$\endgroup\$ – Anonymous Aug 1 at 18:27
  • \$\begingroup\$ @Anonymous Maybe including healing and farming upgrades will be enough. Since getting the most gold is the ultimate goal, keeping that as the bot's main job could work. There haven't been any bots so far which have "modes" like heal mode and farm mode, might be an interesting approach \$\endgroup\$ – Redwolf Programs Aug 1 at 20:23
4
\$\begingroup\$

ScavengerBot (V2)

Realized it wasn't much of a scavenger before. New strategy is wait until it can kill another bot. If no one can be killed, it sits and builds up shield.

function scavengerBot(me, others) {
    if (me.shield < (me.levels.shield * 1.5 + 5)) {
        return shield();
    }
    var currentAttack = 1.25 * me.levels.attack + 5;
    var hasVictim = false;
    var victimUid = 0;
    var maxWorth = 0;
    for (var i = 0; i < others.length; i++) {
        var hp = others[i].hp;
        var worth = others[i].worth;
        if (hp <= currentAttack && worth > maxWorth) {
            hasVictim = true;
            victimUid = others[i].uid;
            maxWorth = worth;
        }
    }

    if (hasVictim) {
        return attack(victimUid);
    }

    if (me.gold >= cost(me.levels.attack)) {
        return upgrade("attack");
    }

    if (me.gold >= cost(me.levels.shield)) {
        return upgrade("shield");
    }
    return shield();
}
\$\endgroup\$
  • 1
    \$\begingroup\$ me.levels.attacl? \$\endgroup\$ – Draco18s Aug 1 at 18:52
  • \$\begingroup\$ Good catch, fixed \$\endgroup\$ – reffu Aug 1 at 18:59
4
\$\begingroup\$

Moody

function Moody(me, others, storage) {
    health = me.hp + me.shield;
    damage = storage.previous_health - health;
    storage.previous_health = health;
    if( damage > 2 ) {
        storage.fear = 2;
    }
    if( storage.fear ) {
        storage.fear -= 1;
        if( me.gold >= cost(me.levels.heal) )
            return upgrade("heal");
        return heal();
    }
    if ( me.hp <= 50 ) {
        return heal();
    }
    if (cost(me.levels.farm) < 0.15 * (1000 - turn())) {
        if( me.gold >= cost(me.levels.farm) )
            return upgrade("farm");
        if( me.gold >= cost(me.levels.heal) )
            return upgrade("heal");
        return farm();
    }
    rich_bots = others.sort( (x,y) => y.worth - x.worth );
    richest_enemy = rich_bots[0];
    if (richest_enemy.hp >= storage.target_hp) {
        storage.anger = true;
    }
    storage.target_hp = NaN;
    if (storage.anger) {
        if( me.gold >= cost(me.levels.attack) ) {
            storage.anger = 0;
            return upgrade("attack");
        }
        return farm();
    }
    storage.target_hp = richest_enemy.hp;   
    return attack(richest_enemy.uid);   
}

Moody's default strategy is to upgrade farming and healing for a while, then take out the other bots in descending order of worth. However, if it's attacked, it'll get scared and refocus on healing for a bit. If it attacks and "fails", because the victim was healing or shielding more effectively than the attack, it'll get angry and go off to upgrade its attacking abilities.

\$\endgroup\$
4
\$\begingroup\$

Bandit

function Bandit(me, others, storage) {
    // stuff we need
    const epsilon = 0.3; // really high epsilon
    function argmax(xs) {
        var max = 0;
        var argmax = 0;
        for (var i=0; i<xs.length; i++) {
            if (xs[i]>max) {
                max = xs[i];
                argmax = i;
            }
        }
        return argmax;
    }
    function base3ToActionSeries(strategy) {
        const actions = [shield(), farm(), heal()];
        var idxs = []
        var strategy_cut = strategy;
        for (var i = 81; i >= 1; i /= 3) {
            if (strategy_cut >= 2 * i) {idxs.push(2); strategy_cut -= 2*i}
            else if (strategy_cut >= i) {idxs.push(1); strategy_cut -= i}
            else idxs.push(0);
        }
        return idxs.map(idx => actions[idx]);
    }

    // actual logic starts here
    // current strategy and info to calculate reward
    if (!storage.prior)
        storage.prior = [0,0.03325,0,0.0361,0.0361,0.2372,0,0.2372,0,0.00035,0.0361,0.23555,0.01305,0.0361,0.5798,0.23555,0.62065,0.23555,0,0.2372,0,0.20965,0.5841,0.2372,0,0.21905,0,0.0361,0.0361,0.2081,0.0361,0.0361,0.01455,0.000350,0.62065,0.205,0.000350,0.0361,0.3708,0.0361,0.0323,1.018050,0.5798,0.04495,0.5798,0.23555,0.62065,0.23555,0.62065,1.06395,0.62065,0.23555,0.62065,0.23555,0,0.2372,0,0.2372,0.5841,0.2372,0,0.2372,0,0.23555,0.62065,0.13775,0.5798,1.0257,0.5798,0.23555,0.62065,0.23555,0,0.2339,0,0.2372,0.5841,0.2339,0,0.2372,0,0.0342,0.0361,0.2372,0.03515,0.03325,0.6228,0.2372,0.5841,0.2372,0.0361,0.0130599,0.62065,0.03515,0.0361,1.0665,0.62065,0.24050,0.62065,0.23555,0.51465,0.2372,0.6228,1.0257,0.6228,0.2372,0.5841,0.2372,0.0361,0.0361,0.58195,0.0361,0.0313596,1.0614,0.58195,1.02315,0.58195,0.0342,0.0361,1.0206,0.02255,0.0183,0.02595,1.0206,1.5526,1.0206,0.58195,1.02315,0.58195,0.02765,0.0251,1.0614,0.0007,0.02085,0.3088,0.2372,0.5841,0.2273,0.6185,0.02255,0.6228,0.2372,0.5841,0.2372,0.62065,1.06395,0.62065,1.0665,0.0917,1.0665,0.62065,0,0.62065,0.2372,0.5841,0.2372,0.6228,1.0257,0.6228,0.2372,0.5841,0.2372,0,0.2372,0,0.23225,0.5841,0.2372,0,0.2372,0,0.23555,0.62065,0.23555,0.5798,1.0257,0.5798,0.23555,0.6142,0.23555,0,0.22235,0,0.2372,0.5841,0.2372,0,0.2372,0,0.23555,0,0.21905,0.62065,0.02255,0.62065,0.23555,0.61205,0.23555,0.5798,1.05885,0.5798,1.018050,0.03895,1.018050,0.5798,1.05885,0.5798,0.23555,0.62065,0.23555,0.62065,0.0361,0.62065,0.23555,0.62065,0.23555,0,0.2372,0,0.2372,0.3745,0.2372,0,0.2372,0,0.23555,0.62065,0.23555,0.5798,0.9452,0.5798,0.23555,0.5626,0.23555,0,0.2372,0,0.18175,0.5841,0.0138,0,0.2372,0]
    if (storage.lastScore == null)
        storage.lastScore = 0;
    if (storage.bestStrategy == null)
        storage.bestStrategy = argmax(storage.prior);

    if (cost(me.levels.heal) < me.gold) return upgrade("heal");
    if (cost(me.levels.farm) < me.gold) return upgrade("farm");

    // This barely explores and mostly exploits.
    if (turn() % 5 === 0) {
        // update
        const reward = me.gold/2 - storage.lastScore;
        // biased a bit towards later learned rewards
        storage.prior[storage.bestStrategy] += reward*0.01
        storage.prior[storage.bestStrategy] *= 100/101

        // explore
        if (Math.random() < epsilon) {
            storage.bestStrategy = Math.floor(Math.random()*243);
        }
        else { // exploit
            storage.bestStrategy = argmax(storage.prior);
        } 
        storage.lastScore = me.gold/2;
    }

    var action = base3ToActionSeries(storage.bestStrategy)[turn() % 5];
    return action;
}

First attempt at a reinforcement learning bot. Purely defensive for now to narrow down the search space. Sort of a smarter spinoff of FizzBuzz -- it repeats a specific series of five actions over and over; the five actions are what are chosen by RL.

But it's mostly based off of enumeration for now -- I just generated all 3^5=243 permutations of series of five defensive actions that repeated itself over and over, and stored their average scores (divided by 200, to get the average gain over five turns) over 100 iterations in the storage.prior array. Then, during the game, it implements an epsilon-greedy approach to updating those score lists so it's more future-proof. (Also because using epsilon=0.3 did way better than epsilon=0.1 so I just kept it.)

It does okay, consistently placing between scavengerBot and Optimist. I'm currently doing some more training on real games, and looking for better ways to frame the strategy, to see if I can improve it.

\$\endgroup\$
4
\$\begingroup\$

The Opportunist

This one borrows a bit from a few others (notably ScavengerBot (V2) and Unkillable) since they had the same ideas I had in mind, but I generally like well-roundedness and jack-of-all-trades styles more than focusing on only one or two things. This will likely mean that I will not win, but it should be in the middle somewhere (which happens to me most of the time in many things).

So it steals juicy kills; heals if necesssary; upgrades farm, attack, and heal in that order; and farms otherwise.

function Opportunist(me, others, storage) {

    // Initializing and keeping track of selfWorth
    if (turn() == 1) {
        storage.selfWorth = 0;
    }
    else if (storage.previousGold < me.gold) {
        storage.selfWorth += (me.gold - storage.previousGold);
    }
    storage.previousGold = me.gold;

    // Me stats
    var me_attack = 1.25 * me.levels.attack + 5;
    var me_heal = me.levels.heal + 5;

    // Look for the juiciest hunk of loot
    // If there are multiple of the highest worth, the last is chosen
    var choice = others[0].uid;
    var mostWorthy = -1;
    for (var i = 0; i < others.length; i++) {
        worth = others[i].worth
        if (others[i].hp <= me_attack && worth >= mostWorthy) {
            choice = others[i].uid;
            mostWorthy = worth;
        }
    }

    // Actions in order of priority
    // The juicy targets must be worth the action
    if (mostWorthy > (storage.selfWorth * 0.25) ) {
        return attack(choice);
    }
    else if (me.hp <= 100 - me_heal) {
        return heal()
    }
    else if (me.gold >= cost(me.levels.farm)) {
        return upgrade("farm");
    }
    else if (me.gold >= cost(me.levels.attack)) {
        return upgrade("attack");
    }
    else if (me.gold >= cost(me.levels.heal)) {
        return upgrade("heal");
    }
    else {
        return farm();
    }
}
\$\endgroup\$
  • 1
    \$\begingroup\$ 2nd argument should be others \$\endgroup\$ – SuperStormer Aug 2 at 19:10
4
\$\begingroup\$

ScaredBot

  1. It finds other bots:
    • with the highest attack
    • with most wealth and HP lower than own attack
  2. If its HP+shield is lower than the found highest attack * (25% of bots), or it gets near the lower end of HP + shield, then it shields
  3. If it found a bot with lower shield than its own attack, it attacks it.
  4. If its health is < 50, it heals.
  5. If it can upgrade any of shield, heal and farm, it upgrades the one with the lowest level
  6. It farms
function ScaredBot(me, others) {
    const my_attack = me.levels.attack * 1.25 + 5;
    const my_defense = me.hp + me.shield;

    var max_attack_val = 0;
    var min_hp_worth = 0;
    var min_hp_id = null;
    var hp_under_me = 0;
    for (var i=0; i<others.length; i++){
        if (others[i].hp < my_attack && others[i].worth > min_hp_worth){
            min_hp_id = others[i].uid;
            min_hp_worth = others[i].worth;
        }
        if (others[i].attack*1.25+5 > max_attack_val){
            max_attack_val = others[i].attack*1.25+5;
        }
        if (others[i].hp < my_defense && others[i].hp > 0){
            hp_under_me++;
        }
    }
    if (max_attack_val*0.25*others.length > my_defense || hp_under_me < 0.25*others.length){
        return shield();
    }
    else if (min_hp_id != null){
        return attack(min_hp_id);
    }
    else if (me.hp < 50){
        return heal();
    }
    else {
        var min_lvl = NaN;
        var min_name = null;
        const vals = [me.levels.heal, me.levels.shield, me.levels.farm];
        const names = ["heal", "shield", "farm"];
        for (var i=0; i<vals.length; i++){
            if (!(min_lvl < vals[i])){
                min_lvl = vals[i];
                min_name = names[i];
            }
        }
        if (me.gold > cost(min_lvl)){
            return upgrade(min_name);
        }
        return farm();
    }
}

The idea is to stay alive the longest possible and otherwise try getting gold in a safe and cheap way to be able to upgrade.

Upgrade priorities should probably be tweaked, as well as the condition when determining on whether to shield.

\$\endgroup\$
3
\$\begingroup\$

SmartFarmer

Farms, upgrades farming, heals if low health. Farming seemed overpowered until the truly offensive bots arrived. Now my bot gets killed :-(

function smartFarmer(me, others){
    if(me.hp < 13) return heal();
    for(var i = 0; i < others.length; i++)if(others[i].attack * 1.25 + 5 > me.hp)return heal();
    if(me.gold >= cost(me.levels.farm)) return upgrade("farm");
    if(me.levels.heal < 9 && me.levels.farm > me.levels.heal + 7 && me.gold >= cost(me.levels.heal)) return upgrade("heal");
    return farm();
}
\$\endgroup\$
  • 1
    \$\begingroup\$ I was (manually) testing essentially the same strategy to see what the maximum obtainable worth is and the best numbers I could get, was by slightly delaying when heal is upgraded (I used gold >= cost*2) and going up to lvl10 heal. \$\endgroup\$ – Nicolai Aug 2 at 14:26
  • \$\begingroup\$ That price multiplier is a good idea. I added something similar. I would be interested to see what numbers you got \$\endgroup\$ – B0RDERS Aug 2 at 14:37
3
\$\begingroup\$

Mort

function Mort(me, others, storage) {
    if (me.hp <= 100 - (me.levels.heal + 5))
        return heal();
    actions = ["farm", "heal", "attack"].filter(action => cost(me.levels[action]) <= me.gold).map( action => [upgrade(action), 1000 - turn() - cost(me.levels[action]) ] )
    my_damage = me.levels.attack * 1.25 + 5;
    actions = actions.concat(others.map( bot => [ attack(bot.uid), (bot.worth/2)/Math.max(bot.hp/(my_damage-(bot.hp > my_damage ? 5 : 0)),1) ] ));
    actions.push( [farm(), (2 * me.levels.farm + 5)*(1-2/(me.levels.heal+5))] );
    return actions.sort( (x,y) => y[1] - x[1] )[0][0];
}

Each turn, compares the amortized profit of killing each bot to that of farming and healing and picks the best option. Really it should use state to figure out how long it'll take to kill a bot, but for now it just assumes every bot heals or shields an average of 5 points a turn net of damage other bots do.

\$\endgroup\$
3
\$\begingroup\$

Friendly bot

function menShengFaDaCai(me, others) {
  // heal if needed
  const maxAttack = Math.max(...others.map(bot => bot.attack));
  const maxAttackCost = maxAttack * maxAttack + 5;
  const othersHp = others.map(bot => bot.hp).sort();
  const targetHp = othersHp[Math.ceil(othersHp.length / 2)];
  if (me.hp < 95 && me.hp < Math.max(maxAttackCost * 2, targetHp, 50)) return heal();

  // upgrade heal and farm if possible
  const { heal: healLevel, farm: farmLevel } = me.levels;
  const gain = (heal, farm) => ((5 + heal) / 2) * (2 * farm + 5) / ((5 + heal) / 2 + 1);
  const gain0 = gain(healLevel, farmLevel);
  const gainUpgradeHeal = gain(healLevel + 1, farmLevel);
  const gainUpgradeFarm = gain(healLevel, farmLevel + 1);
  const gainUpgradeHealPerGold = (gainUpgradeHeal - gain0) / cost(healLevel);
  const gainUpgradeFarmPerGold = (gainUpgradeFarm - gain0) / cost(farmLevel);
  const preferUpgradeHeal = gainUpgradeHealPerGold > gainUpgradeFarmPerGold;
  const mayOffer = type => me.gold >= cost(me.levels[type]);
  if (preferUpgradeHeal && mayOffer('heal')) return upgrade('heal');
  if (!preferUpgradeHeal && mayOffer('farm')) return upgrade('farm');

  // keep farming
  return farm();
}

others[0].hp is hp + shield instead of hp...

\$\endgroup\$
  • 4
    \$\begingroup\$ Can anyone help me translating the function name to English? ^_^ \$\endgroup\$ – tsh Aug 2 at 3:59
  • 4
    \$\begingroup\$ According to Google Translate, "闷声发大财" means "Muffled". Pretty sure that's not what you want and is in fact yet another Google Translate epic fail... I searched further and all results seem to mention that there's no single English word that can be used here, so it might be better to keep it as is, actually, as it seems to be a Chinese preposition that generally means that one should work silently and let the results speak for themselves and attains to a traditional philosophy. Unfortunately, I don't know Chinese at all to translate it directly. :D \$\endgroup\$ – Erik the Outgolfer Aug 2 at 12:25
  • 1
    \$\begingroup\$ as a native Chinese speaker, it means something like "silently make a huge fortune" :v 闷声 also connotes intentionally being quiet, literally "covering up sound" \$\endgroup\$ – Rin's Fourier transform Aug 2 at 15:39
  • 1
    \$\begingroup\$ Sneaky? UnderTheRadar? DontMindMe? AttentionDeflector? \$\endgroup\$ – Peter Taylor Aug 3 at 8:57
3
\$\begingroup\$

The Accountant

This practical bot calculates the most economically beneficial move, but he likes to keep his attack profile low to avoid trouble from all of the vigilante bots. He doesn't try to pick help the defenseless or prey on them. Rather, he does what helps him the most.

function accountant(me, others, storage) {
    if (turn() == 1) {
        storage.lastHP = me.hp + me.shield;
        storage.hisAttack = 5;
        storage.timesAttacked = 0;
        storage.lastAttack = -1;
        storage.healths = [], storage.uids = [], storage.heals = [];
        for (var i = 0; i < others.length; i++) {
            storage.healths.push(others[i].hp);
            storage.uids.push(others[i].uid);
            storage.heals.push(5);
        }
    }
    storage.timesAttacked++;
    if (storage.lastHP == me.hp + me.shield) storage.timesAttacked = 0;
    else storage.hisAttack = storage.lastHP - me.hp - me.shield;
    storage.lastHP = me.hp + me.shield;
    var attacks = [];
    for (var i = 0; i < others.length; i++) if (others[i].uid != me.uid) attacks[i] = 1.25 * others[i].attack + 5;
    attacks.sort();
    for (var i = 0; i < others.length; i++) {
        storageIndex = storage.uids.indexOf(others[i].uid);
        if (storage.heals[storageIndex] < others[i].hp - storage.healths[storageIndex] + (others[i].uid == storage.lastAttack ? 1.25 * me.levels.attack + 5 : 0)) others[i].hp - storage.healths[storageIndex] + (others[i].uid == storage.lastAttack ? 1.25 * me.levels.attack + 5 : 0);
    }
    var maxProfitTurn = 2 * me.levels.farm + 5, victimID = -1, tempProfit;
    for (var i = 0; i < others.length; i++) {
        storageIndex = storage.uids.indexOf(others[i].uid);
        tempProfit = others[i].worth / 2 * (1.25 * me.levels.attack + 5 - storage.heals[storageIndex]) / others[i].hp;
        if (tempProfit > maxProfitTurn) {
            victimID = others[i].uid;
            maxProfitTurn = tempProfit;
        }
    }
    maxUrgentProfit = 0;
    for (var i = 0; i < others.length; i++) if (maxUrgentProfit < others[i].worth / 2 && others[i].hp <= attacks.slice(0, 4).reduce((a, b) => a + b) + 1.25 * me.levels.attack + 5) {
        maxUrgentProfit = others[i].worth / 2;
        victimID = others[i].uid;
    }
    if (maxUrgentProfit > 0) {
        storage.lastAttack = victimID;
        return attack(victimID);
    }
    storage.lastAttack = -1;
    if (storage.timesAttacked == 0) {
        if (me.levels.shield < 20 && me.gold >= cost(me.levels.shield)) return upgrade("shield");
        if (me.levels.heal < 5 && me.levels.shield >= me.levels.heal + 5 && me.gold >= cost(me.levels.heal)) return upgrade("heal");
        if (Math.random() < Math.pow((me.hp + me.shield) / 100, -2)) {
            storage.lastHP += 1.5 * me.levels.shield + 5;
            return shield();
        }
    }
    else {
        if (Math.random() < .5 || me.hp + me.shield - storage.hisAttack - attacks[0] <= 10) {
            storage.lastHP += 1.5 * me.levels.shield + 5;
            return shield();
        }
        if (me.levels.shield < 20 && me.gold >= cost(me.levels.shield)) return upgrade("shield");
        if (me.hp <= 2) {
            storage.lastHP += me.levels.shield + 5;
            return heal();
        }
        storage.lastHP -= 2;
        return farm();
    }
    if (me.gold >= cost(me.levels.farm)) return upgrade("farm");
    storage.lastAttack = victimID;
    if (victimID != -1) return attack(victimID);
    if (me.hp <= 2) {
        storage.lastHP += me.levels.shield + 5;
        return heal();
    }
    storage.lastHP -= 2;
    return farm();
}
\$\endgroup\$
3
\$\begingroup\$

reallyCommittedTurtle

function reallyCommittedTurtle(me, others, storage) {
    if( storage.previousHP ) {
        others.forEach ( o => {storage.deltaHP[o.uid] = o.hp - storage.previousHP[o.uid]; storage.previousHP[o.uid] = o.hp } );
    }
    else {
        storage.previousHP = {};
        storage.deltaHP = {};
        others.forEach ( o => storage.previousHP[o.uid] = o.hp );
    }
    if (turn() < 3)
        return upgrade("shield");
    if ( me.shield < 400 || others.find( o=> o.deltaHP < -2 ) )
        return shield();
    if (me.hp <= 95 - me.levels.heal) {
        if (me.gold >= cost(me.levels.heal))
            return upgrade("heal");
        return heal();
    }
    rich_bots = others.sort( (x,y) => y.worth - x.worth );
        potential_victim = rich_bots.find( bot => bot.hp + storage.deltaHP[bot.uid] <= me.levels.attack * 1.25 + 5 );
        if (potential_victim && potential_victim.worth/2 > me.levels.farm*2 + 5)
            return attack(potential_victim.uid);
    if (me.gold >= cost(me.levels.farm))
        return upgrade("farm");
    return farm();
}

Here's the thing. It's gotten really dangerous out there. Farming at all increases your worth, making you a target. So it's really not safe to farm until you've built up a huge shield and all the violence has died down. Then you can poke your head out of your shell and start farming. Or kill-assisting. Whatever pays better.

\$\endgroup\$
2
\$\begingroup\$

Guardian

I can have more than one submission, right?

A fork of CampBot. Doesn't shield, instead focusing on attacking. Shows a preference for attacking players with higher attack stats, rather than striking at random like CampBot. Focuses on upgrading its farming rather than healing.

function guardian(self,others,storage){
    if(!storage.victimBlacklist){
        storage.victimBlacklist=[]
    }
    let turnsLeft=999-turn()
    function findVictim(){
        let potentialVictims=others.filter(bot=>!storage.victimBlacklist.includes(bot.uid))
        if(potentialVictims.length>0){
            let victim=potentialVictims.reduce((el, em) => el.attack > em.attack ? el : em);
            storage.victimUid=victim.uid
            storage.victimPrevHp=victim.hp
            storage.prevMove="attack"
            return attack(victim.uid)
        }else{
            storage.prevMove="farm"
            return farm()
        }   
    }
    if(self.hp<=(95-self.levels.heal)){
        storage.prevMove="heal"
        return heal()
    } else if(self.gold>=cost(self.levels.attack)){
        storage.prevMove="upgrade"
        return upgrade("attack")
    } else if(self.gold>=cost(self.levels.farm)&&turnsLeft>100&&self.levels.heal<=1){
        storage.prevMove="upgrade"
        return upgrade("farm")
    } else if(!storage.victimUid){
        return findVictim()
    }else if(Object.values(others).map(bot=>bot.uid).includes(storage.victimUid)){
        let victimCurrHp=Object.values(others).filter(bot=>bot.uid==storage.victimUid)[0].hp
        if(storage.victimPrevHp<victimCurrHp&&storage.prevMove==="attack"){
            storage.victimBlacklist.push(storage.victimUid)
            storage.victimUid=undefined
            return findVictim()
        }else{  
            storage.victimPrevHp=victimCurrHp
            storage.prevMove="attack"
            return attack(storage.victimUid)
        }
    }else{
        storage.victimUid=undefined
        return findVictim()
    }
}
\$\endgroup\$
  • \$\begingroup\$ my bot doesnt randomly attack... \$\endgroup\$ – SuperStormer Aug 2 at 17:02
  • \$\begingroup\$ You can post as many times as you want, the more the merrier I suppose \$\endgroup\$ – Redwolf Programs Aug 2 at 17:04
  • \$\begingroup\$ @SuperStormer I realise yours isn't entirely random, but: let victim=potentialVictims[Math.floor(Math.random()*potentialVictims.length)] \$\endgroup\$ – Anonymous Aug 2 at 17:24
  • \$\begingroup\$ but it first filters out the ones that aren't worth attacking \$\endgroup\$ – SuperStormer Aug 2 at 17:28
  • \$\begingroup\$ I was working on a similar bot named the equalizer when you posted this. I am still fine-tuning it, but I like some of your ideas. \$\endgroup\$ – B0RDERS Aug 2 at 18:21
2
\$\begingroup\$

Rando

This silly guy will choose actions based on uniform randomness with some biases. If a randomly chosen action won't work, then it drops down to the next choice.

So on average, he should attack almost 2/9 of the time and farm almost 3/9 of the time. The rest are about 1/9 chance if he is able to upgrade, or if the healing/shielding is worth it, etc.

He will not likely perform well, but at least there's a small chance that he reigns supreme. And that's the whole purpose of Rando. He just needs to believe in himself! All the options are laid in front of him. He needs only choose what is needed for any given situation.

function Rando(me, others, storage) {

    var rnum = Math.floor(Math.random() * 9);
    switch (rnum) {
        case 0:
            if (me.gold >= cost(me.levels.shield)) {
                return upgrade("shield");
            }
        case 1:
            if (me.hp >= 100 - (me.levels.heal + 5) && me.levels.shield >= me.levels.heal) {
                return shield();
            }
        case 2:
            if (me.hp < 100 - (me.levels.heal + 5)) {
                return heal();
            }
        case 3:
            if (me.gold >= cost(me.levels.farm)) {
                return upgrade("farm");
            }
        case 4:
            if (me.gold >= cost(me.levels.heal)) {
                return upgrade("heal");
            }
        case 5:
            if (me.hp > 2) {
                return farm();
            }
        case 6:
            // Beat down the leader!
            var currentLeader = others[0].uid;
            var leaderWorth = -1;
            for (var i = 0; i < others.length; i++) {
                worth = others[i].worth;
                if (worth > leaderWorth) {
                    currentLeader = others[i].uid;
                    leaderWorth = worth;
                }
            }
            return stun(currentLeader);
        case 7:
            if (me.gold >= cost(me.levels.attack)) {
                return upgrade("attack");
            }
        case 8:
            // Find the juiciest kill (if any), or attack the strongest
            var choice = others[0].uid;
            var choiceWorth = -1;
            var currentLeader = others[0].uid;
            var leaderWorth = -1;
            for (var i = 0; i < others.length; i++) {
                worth = others[i].worth
                if (worth > leaderWorth) {
                    currentLeader = others[i].uid;
                    leaderWorth = worth;
                }
                if (others[i].hp <= (1.25 * me.levels.attack + 5) && worth >= choiceWorth) {
                    choice = others[i].uid;
                    choiceWorth = worth;
                }
            }
            if (choice > -1) {
                return attack(choice);
            }
            else {

                return attack(currentLeader);
            }
        default:
            return false
    }
}

(I know "default" is unnecessary, but I think it is good coding practice for robust code.)

\$\endgroup\$
  • 2
    \$\begingroup\$ "He just needs to believe in himself"...I'm laughing so much right now \$\endgroup\$ – Redwolf Programs Aug 2 at 20:42
2
\$\begingroup\$

Kill Bot

function killBot(me, others, storage) {
    // If I lost health since my last check, shield.
    if (me.hp < storage.hp){
        storage.hp = me.hp;
        return shield();
    }

    storage.hp = me.hp;

    health = Math.min(...others.map(o => o.hp));
    // If I have the least health or can be one-shot, shield.
    if (others.some(o => o.attack * 1.25 + 5 >= me.hp + me.shield) || (health > me.hp + me.shield && health < 500)) return shield();

    // If I can kill someone, kill them!
    targets = others.filter(o => o.hp < me.attack);
    if (targets.length > 0){
        wealth = Math.max(...targets.map(o => o.worth));
        targets = targets.filter(o => o.worth == wealth);
        target = targets[Math.floor(Math.random()*targets.length)];
        return attack(targets[0].uid);
    }

    // If I have the money, upgrade shielding or attack
    if (me.levels.shield <= me.levels.attack){
        if (cost(me.levels.shield) < me.gold) return upgrade("shield");
    } else {
        if (cost(me.levels.attack) < me.gold) return upgrade("attack");
    }

    // Otherwise, attack the weakest!
    targets = others.filter(o => o.hp == health);
    // And if there's a tie, attack the wealthiest.
    wealth = Math.max(...targets.map(o => o.worth));
    targets = targets.filter(o => o.worth == wealth);
    target = targets[Math.floor(Math.random()*targets.length)];
    return attack(targets[0].uid);
}

A simple bot, Kill Bot just wants to kill its enemies. Since shielding is much more efficient than healing (especially when levelled), Kill Bot just tries to always be an unappealing target by shielding itself whenever attacked. Kill Bot does quite well among the weak, pacifistic bots around here (you can feel its scorn for them).

\$\endgroup\$
  • 3
    \$\begingroup\$ Note that o.attack is the attack level, not its damage \$\endgroup\$ – Redwolf Programs Aug 2 at 19:37
2
\$\begingroup\$

FarmHeal Bot

Forked from @Anonymous' JustFarm bot

function farmhealBot(me, others, storage) {
  if (me.hp <= 95)
    return heal();
  else return farm();
}
\$\endgroup\$
2
\$\begingroup\$

Indestructible

A modification of Draco18's bot, using shields (more effective against other bots)

function indestructible(me){
    if (me.hp < 100) {
        return heal();
    } else if (me.shield < 15) {
        return shield();
    } else {
        if (me.gold >= cost(me.levels.shield)) {
            return upgrade("shield");
        } else if (me.gold >= cost(me.levels.farm)) {
            return upgrade("farm");
        } else {
            return farm();
        }
    }
}
\$\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.