5
\$\begingroup\$

A few days ago, I came up with a fun card game to play with my friends. We were having fun playing it, when I thought, "Why not make this a KoTH?" So, here it is!

Overview

In this game, the objective is to get the most points. Your bot starts with 0 points and 20 energy. Every turn (500 in a game), both bots play one card. Some earn you points, some take the opponent's points.

Cards

A=Add 5 points to your score     [Costs 0.1 energy]
R=Remove 5 points from opponent  [Costs 0.1 energy]
H=Half your next score           [Costs 1 energy]
Y=Half opponent's next score     [Costs 1 energy]
D=Double your next score         [Costs 2 energy]
T=Double opponent's next score   [Costs 2 energy]
N=Negate your next score         [Costs 3 energy]
O=Negate opponent's next score   [Costs 3 energy]
S=Shield for 5 turns             [Costs 15 energy]
X=Take five energy from opponent [Gives opponent 10 points]
E=Refill five energy             [Costs 10 points]

How it works

First how would halving your next score or doubling your opponent's next score come in handy? Well, imagine you get 5 points taken away on your next turn. Instead, 2.5 points get taken away. Cool, right? Or, negating your next score would give you 5. If you think your opponent will give themself points, negate their next move!

The order of operations for modifying point values is:

  1. Add all positive or negative point changes

  2. Half if necessary

  3. Double if necessary

  4. Negate if necessary

  5. If shielding and result is negative, change to 0

Attempting to lower the opponent's energy past 0 does not work. There is no upper limit to energy, or lower limit to points. Energy reducing cards are played before other cards, so if Bot A with 17 energy runs shield and Bot B runs take 5 energy, Bot A cannot shield.

Creating a bot

In Javascript, create a new function with whatever name you wish. This function should take 3 parameters:

Array of Numbers (Your coins, Opponent's coins)
Array of Numbers (Your energy, Opponent's energy)
Object (Use for storage between rounds)

Note that there is no way of knowing what cards have been played. You must teach the bot to figure it out by itself!

The way you select a card is by returning a string, containing the letter of the card in uppercase or lowercase. Note that Standard Loopholes (obviously) aren't allowed. If any other value is returned, your bot just does nothing.

Scoring

Every bot will be run against each other bot once. The loser will get 0 points, anbd the winner will get the difference in points added to its total score (Between all rounds). The top 8 will compete in a tournament. In other words, they will each fight one other bot, the four winners will fight another bot, and the two remaining bots will fight for first.

Controller

var botDataList = [
    {
        name: "",
        desc: "",
        run: function(){

        }
    }, {
        name: "",
        desc: "",
        run: function(){

        }
    }
];

function playGame() {
    botSetup();
    for (var i = 0; i < 500; i++) {
        runBots();
    }
    var botA = botDataList[0];
    var botB = botDataList[1];
    console.log("Bot " + botA.name + ": " + botA.points);
    console.log("Bot " + botB.name + ": " + botB.points);
}

function botSetup() {
    for (var b, i = 0; i < 2; i++) {
        b = botDataList[i];
        b.points = 0;
        b.energy = 20;
        b.storage = {};
        b.affectAdd = [];
        b.affectAct = [];
        b.shield = 0;
    }
}

function runBots() {
    var botA = botDataList[0];
    var botB = botDataList[1];
    var resA = botA.run([botDataList[0].points, botDataList[1].points], [botDataList[0].energy, botDataList[1].energy], botDataList[0].storage).toLowerCase();
    var resB = botB.run([botDataList[1].points, botDataList[0].points], [botDataList[1].energy, botDataList[0].energy], botDataList[1].storage).toLowerCase();
    var modA = 0;
    var modB = 0;
    if (resA == 'a' && botA.energy >= 0.1) {
        botA.energy -= 0.1;
        modA += 5;
    } else if (resA == 'r' && botA.energy >= 0.1) {
        botA.energy -= 0.1;
        modB -= 5;
    } else if (resA == 'h' && botA.energy >= 1) {
        botA.energy -= 1;
        botA.affectAdd.push('h');
    } else if (resA == 'y' && botA.energy >= 1) {
        botA.energy -= 1;
        botB.affectAdd.push('h');
    } else if (resA == 'd' && botA.energy >= 2) {
        botA.energy -= 2;
        botA.affectAdd.push('d');
    } else if (resA == 't' && botA.energy >= 2) {
        botA.energy -= 2;
        botB.affectAdd.push('d');
    } else if (resA == 'n' && botA.energy >= 3) {
        botA.energy -= 3;
        botA.affectAdd.push('n');
    } else if (resA == 'o' && botA.energy >= 3) {
        botA.energy -= 3;
        botB.affectAdd.push('n');
    } else if (resA == 's' && botA.energy >= 15) {
        botA.energy -= 15;
        botA.shield += 5;
    } else if (resA == 'x') {
        modB += 10;
        botB.energy = (botB.energy >= 5) ? botB.energy - 5 : 0;
    } else if (resA == 'e' && botA.points >= 10) {
        modA -= 10;
        botA.energy += 5;
    }
    if (resB == 'a' && botB.energy >= 0.1) {
        botB.energy -= 0.1;
        modB += 5;
    } else if (resB == 'r' && botB.energy >= 0.1) {
        botB.energy -= 0.1;
        modA -= 5;
    } else if (resB == 'h' && botB.energy >= 1) {
        botB.energy -= 1;
        botB.affectAdd.push('h');
    } else if (resB == 'y' && botB.energy >= 1) {
        botB.energy -= 1;
        botA.affectAdd.push('h');
    } else if (resB == 'd' && botB.energy >= 2) {
        botB.energy -= 2;
        botB.affectAdd.push('d');
    } else if (resB == 't' && botB.energy >= 2) {
        botB.energy -= 2;
        botA.affectAdd.push('d');
    } else if (resB == 'n' && botB.energy >= 3) {
        botB.energy -= 3;
        botB.affectAdd.push('n');
    } else if (resB == 'o' && botB.energy >= 3) {
        botB.energy -= 3;
        botA.affectAdd.push('n');
    } else if (resB == 's' && botB.energy >= 15) {
        botB.energy -= 15;
        botB.shield += 5;
    } else if (resB == 'x') {
        modA += 10;
        botA.energy = (botA.energy >= 5) ? botA.energy - 5 : 0;
    } else if (resB == 'e' && botB.points >= 10) {
        modB -= 10;
        botB.energy += 5;
    }
    if (botA.affectAct.includes('h')) {
        modA *= 0.5;
    }
    if (botA.affectAct.includes('d')) {
        modA *= 2;
    }
    if (botA.affectAct.includes('n')) {
        modA *= -1;
    }
    if (botA.shield > 0) {
        modA = (modA < 0) ? 0 : modA;
        botA.shield--;
    }
    botA.points += modA;
    botA.affectAct = botA.affectAdd;
    botA.affectAdd = [];
    if (botB.affectAct.includes('h')) {
        modB *= 0.5;
    }
    if (botB.affectAct.includes('d')) {
        modB *= 2;
    }
    if (botB.affectAct.includes('n')) {
        modB *= -1;
    }
    if (botB.shield > 0) {
        modB = (modB < 0) ? 0 : modB;
        botB.shield--;
    }
    botB.points += modB;
    botB.affectAct = botB.affectAdd;
    botB.affectAdd = [];
}

/*  A=Add 5 points to your score     [Costs 0.1 energy]
    R=Remove 5 points from opponent  [Costs 0.1 energy]
    H=Half your next score           [Costs 1 energy]
    Y=Half opponent's next score     [Costs 1 energy]
    D=Double your next score         [Costs 2 energy]
    T=Double opponent's next score   [Costs 2 energy]
    N=Negate your next score         [Costs 3 energy]
    O=Negate opponent's next score   [Costs 3 energy]
    S=Shield for 5 turns             [Costs 15 energy]
    X=Take five energy from opponent [Gives opponent 10 points]
    E=Refill five energy             [Takes 10 points]           */
\$\endgroup\$
  • \$\begingroup\$ Is there an upper limit to points? \$\endgroup\$ – HyperNeutrino Sep 6 '18 at 20:19
  • 2
    \$\begingroup\$ Being a card game enthusiast, this challenge being restricted to Javascript makes me sad. \$\endgroup\$ – J. Sallé Sep 6 '18 at 20:46
  • 3
    \$\begingroup\$ This could be really interesting as more than a 1v1 contest \$\endgroup\$ – MickyT Sep 7 '18 at 2:41
  • 5
    \$\begingroup\$ What is the objective of the game? Is it to get the most points, summed over many rounds? Beat the opponent? Something else? I'm going to have to downvote until this is clarified. \$\endgroup\$ – isaacg Sep 7 '18 at 5:50
  • 4
    \$\begingroup\$ What precisely does "next score" mean? What does shield do? In "Energy reducing cards are played before other cards", surely every card except X is an energy reducing card? IMO this question still needs a few days in the sandbox. \$\endgroup\$ – Peter Taylor Sep 7 '18 at 10:07
3
\$\begingroup\$

Doom: OX (Negate opponent - Take energy) Combo

function(coins, energy, obj) {
    let mycoin = coins[0]
    let yourcoin = coins[1]
    let myenergy = energy[0]
    let yourenergy = energy[1]
    if (obj.last === 'O') obj.last = 'X'
    else if (myenergy >= 3 && (yourcoin > 0 || yourenergy > 0)) obj.last = 'O'
    else if (mycoin >= 10 && myenergy < 3) obj.last = 'E'
    else obj.last = 'A'
    return obj.last
}

If a player has zero points AND zero energy, it can do absolutely nothing. This bot, named "Doom", aims to put the opponent in this doomed state, and then happily accumulates its own score.

Here is a run against The Classic. Doom wins against Classic with around 1600 - 1800 score, while Classic is always stuck at 0 score and 0 energy.

Doom is arguably the strongest among the four bots posted so far (Classic, Greedy, Greedier and Doom), but my guess is it's still susceptible to some kind of anti-Doom.

\$\endgroup\$
  • 2
    \$\begingroup\$ I feel like shielding could beat this, but imo shielding seems too expensive to really do you any harm \$\endgroup\$ – Barbarian772 Sep 7 '18 at 9:03
  • \$\begingroup\$ I can't beat this bot even when playing against it myself ._. \$\endgroup\$ – Alion Sep 7 '18 at 11:33
  • 1
    \$\begingroup\$ I don't think an anti-Doom exists, because the intuitive idea of negating Doom's negation doesn't work for some reason (I still get -10'd). There are a lot of different things I've tried as well ([XO], [NR], S..., [AS], H[OX], H..., [X]), but all of them fail. I think this just solves the challenge outright. \$\endgroup\$ – Alion Sep 7 '18 at 12:12
  • 1
    \$\begingroup\$ @Alion agreed. This doesn't seem like a terribly interesting KOTH \$\endgroup\$ – Don Thousand Sep 7 '18 at 12:26
  • 1
    \$\begingroup\$ Actually, after re-reading that a couple of times I'm starting to feel like you yourself don't even know how your game works anymore... I have no clue what you mean by O giving you 10 points. \$\endgroup\$ – Alion Sep 9 '18 at 14:16
3
\$\begingroup\$

Doom-RX

function(coins, energy, obj) {
    let mycoin = coins[0]
    let yourcoin = coins[1]
    let myenergy = energy[0]
    let yourenergy = energy[1]
    if(!obj.last) obj.last = ' RXRX';
    if(obj.last.length > 1) obj.last = obj.last.substring(1, 5)
    else if (obj.last === 'O') obj.last = 'X'
    else if (myenergy >= 3 && (yourcoin > 0 || yourenergy > 0)) obj.last = 'O'
    else if (mycoin >= 10 && myenergy < 3) obj.last = 'E'
    else obj.last = 'A'
    return obj.last.substring(0,1)
}

Almost entirely derivative of Bubbler's answer, but with a hardcoded opening sequence that counters that specific entry. Here's this run against Doom.

Essentially, using R has less energy cost than O, so when both play OX against each other after the 4th turn Doom is stuck at 0/0, while Doom-RX survives at -30/1.8 and is able to recover.

RXRX isn't the only sequence that works against Doom, but it does look to be among the most robust 4-letter sequences and I liked it best aesthetically. I'm sure there's a more effective approach to this than hard coding, though.

Overall OX seems overbearingly powerful; I'd be surprised if an entry that didn't use it could beat this kind of strategy.

\$\endgroup\$
  • \$\begingroup\$ ...it seems that I've managed to bail on this approach in my attempts too early. Nice find! \$\endgroup\$ – Alion Sep 10 '18 at 7:24
  • \$\begingroup\$ The TIO link is Classic vs Doom OX \$\endgroup\$ – Quintec Sep 10 '18 at 20:30
  • \$\begingroup\$ @Quintec Thanks for the heads up, fixed. \$\endgroup\$ – ripkoops Sep 10 '18 at 21:20
  • \$\begingroup\$ Just a heads up that this is now a rock paper scissors game \$\endgroup\$ – Quintec Sep 11 '18 at 0:49
2
\$\begingroup\$

Greedy

function(coins, energy, obj) {
    if (energy[0] < 0.2) return 'E';
    else return 'A';
}

I'm probably misunderstanding the game, but I don't see any reason not to just increase your own points every round.

\$\endgroup\$
  • \$\begingroup\$ Oh, I understood 'negate the next score' to mean 'if your score would change next turn, instead don't change it'. But apparently we were both wrong, it looks like the controller inverts the next modifier, so I would lose 5 points instead of gaining them. A bot that just plays 'O' every turn might beat this, but because of energy problems it might not. \$\endgroup\$ – Omegastick Sep 7 '18 at 6:05
  • \$\begingroup\$ Well, yes. I am sorry, I though it meant your opponent's next acquired score. +1, I currently see no obvious way to get an edge over your bot. \$\endgroup\$ – Jonathan Frech Sep 7 '18 at 6:36
2
\$\begingroup\$

Greedier: NE (Negate self - Refill energy) Combo

function(coins, energy, obj) {
    let mycoin = coins[0]
    let myenergy = energy[0]
    if (mycoin >= 10 && (obj.last === 'N' || myenergy <= 3.1)) obj.last = 'E'
    else if (mycoin >= 20 && myenergy >= 8) obj.last = 'N'
    else obj.last = 'A'
    return obj.last
}

How it (may) work

N : Energy -= 3
E : Energy += 5; Points += -(-10)
---------------------------------
    Energy += 2; Points += 10

So we don't even need to worry about being out of energy.

Here is the result against Greedy. Greedy gets only 2410 since it has to refill its energy from time to time. On the other hand, Greedier achieves full 2500 score if the opponent doesn't interfere.

The code above includes a bit of anti-failure measures:

  • If it's getting out of energy somehow, force refill energy (E).
  • If it's out of points, try getting some basic points (A).
  • Use negate (N) only if it has enough energy to run it even if the opponent plays drain energy (X) i.e. 3 + 5 = 8, AND it has enough coins to run E next turn even if the opponent plays double opponent (T) + reduce opponent 5 (R) i.e. 10 + 5 * 2 = 20.
\$\endgroup\$
2
\$\begingroup\$

Doom EX (Rock, paper, scissors!)

function(coins, energy, obj) {
    let mycoin = coins[0]
    let yourcoin = coins[1]
    let myenergy = energy[0]
    let yourenergy = energy[1]
    if(!obj.last) obj.last = ' EXEX';
    if(obj.last.length > 1) obj.last = obj.last.substring(1, 5)
    else if (obj.last === 'O') obj.last = 'X'
    else if (myenergy >= 3 && (yourcoin > 0 || yourenergy > 0)) obj.last = 'O'
    else if (mycoin >= 10 && myenergy < 3) obj.last = 'E'
    else obj.last = 'A'
    return obj.last.substring(0,1)
}

All credit goes to @Bubbler and @ripkoops, I just wanted to point this out. This version manages to beat Doom RX, but loses to Doom OX. Looks like this KOTH is rock paper scissors in disguise...

RX vs EX

\$\endgroup\$
  • \$\begingroup\$ Now it's getting slightly interesting, except that all the top tiers are variants of Doom... \$\endgroup\$ – Bubbler Sep 10 '18 at 22:56
  • \$\begingroup\$ @Bubbler I'm still looking for the "gun" in rock, paper, scissors... what, you've never cheated in that game? :P \$\endgroup\$ – Quintec Sep 11 '18 at 0:48
  • \$\begingroup\$ An XRAXA or XRXAA variant wins against all 3 convincingly (has plenty of other counters though). Wonder how deep this rabbit hole goes. \$\endgroup\$ – ripkoops Sep 11 '18 at 1:35
  • 2
    \$\begingroup\$ @ripkoops huh, nice. This could be quite interesting actually. Time to make a directed graph... and maybe turn this into a whole other code golf challenge xD \$\endgroup\$ – Quintec Sep 11 '18 at 1:36
1
\$\begingroup\$

The Classic (aka Random Player)

function(coins, energy, obj) {
    if (energy[0]<1) return ['A','R','X'][(Math.random()*3)|0];
    else if (energy[0]<2) return ['A','R','H','Y','X'][(Math.random()*5)|0];
    else if (energy[0]<3) return ['A','R','H','Y','D','T','X'][(Math.random()*7)|0];
    else if (energy[0]<10) return ['A','R','H','Y','D','T','X','N','O'][(Math.random()*9)|0];
    else if (energy[0]<15) return ['A','R','H','Y','D','T','X','N','O','E'][(Math.random()*10)|0];
    else return ['A','R','H','Y','D','T','X','N','O','E','S'][(Math.random()*11)|0];
}
\$\endgroup\$
  • \$\begingroup\$ Are you sure you didn't mean energy[0]? \$\endgroup\$ – Redwolf Programs Sep 7 '18 at 1:26
  • \$\begingroup\$ @RedwolfPrograms Yes I did. Oops, I'll fix momentarily \$\endgroup\$ – Don Thousand Sep 7 '18 at 1:29

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.