Who can get more points? (Card game KoTH)

5

4

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]           */

Redwolf Programs

Posted 2018-09-06T19:35:14.513

Reputation: 2 561

Is there an upper limit to points? – HyperNeutrino – 2018-09-06T20:19:05.510

@HyperNeutrino No, but the max possible in a 500-turn game is 2500 – Redwolf Programs – 2018-09-06T20:20:24.347

2Being a card game enthusiast, this challenge being restricted to Javascript makes me sad. – J. Sallé – 2018-09-06T20:46:14.303

@J.Sallé Program it in another language, and I'll do like I did in my last challenge and translate. I don't want to restrict people's ability to enter the challenge, it just makes it easier on my part if they;re all in one language. – Redwolf Programs – 2018-09-06T20:47:13.183

Shielding protects you for 5 turns. If you're already shielded, should this add to the duration? The controller currently just refreshes the count to 5. – Veskah – 2018-09-06T21:41:28.363

@Veskah Huh, you're right. It should indeed! – Redwolf Programs – 2018-09-06T22:10:24.567

This is a bit unclear - Can I choose any card from the 11 that are available to play each turn? – Quintec – 2018-09-07T00:03:24.813

@Quintec Yes you can. I think it might be better to phrase them as moves, rather than cards. – Don Thousand – 2018-09-07T00:17:42.137

@RushabhMehta Originally you would have 5 "cards" to pick from randomly, but I removed that part so that there would be less randomness – Redwolf Programs – 2018-09-07T01:25:13.807

@RedwolfPrograms Does "E", refilling 5 energy and costing 10 points, count as a -10 score? – Quintec – 2018-09-07T02:33:15.230

3This could be really interesting as more than a 1v1 contest – MickyT – 2018-09-07T02:41:16.263

Are you only playing a single game? And are games actually 1v1? Because the controller only plays games between the first two players, nobody else. – Nathan Merrill – 2018-09-07T02:49:35.020

There's a typo in the controller: } else if (resB == 'r' && BotB.energy >= 0.1) { the BotB should be botB. – Omegastick – 2018-09-07T04:33:32.200

5What 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. – isaacg – 2018-09-07T05:50:50.713

Attempting to lower the opponent's score past 0 does not work. There is no upper limit to energy, or lower limit to points. -- could you clarify? – Jonathan Frech – 2018-09-07T06:28:23.680

I'm going through the controller some more, and it's completely broken. There are so many things that don't work. None of the 'affects[sic]' work because you add them to affectAdd and read them from affectAct. When adding up the score modifier, sometimes you increment modA or modB and sometimes modA.points or modB.points. Did you even try running this before you posted it? – Omegastick – 2018-09-07T06:55:57.410

@Omegastick affectAct and affectAdd things are OK because the effects have to be applied at the next turn, not this turn. But you're right about modA.points and modB.points. They should definitely be modA and modB instead. The rest looks fine to me; at least I can run my 2500-scoring bot against the "greedy" without problems after fixing modX myself. – Bubbler – 2018-09-07T07:42:21.783

Oh, there's also modB.energy. That one should be botB.energy instead. – Bubbler – 2018-09-07T07:43:59.693

Does doubling your next score stack? As in can I double for two rounds to get x4? – Barbarian772 – 2018-09-07T07:46:36.673

@RedwolfPrograms The controller has some incredibly silly code duplication. I fixed it up a bit, but I'm sure there are ways to make this even better. (I haven't tested for correctness, though, since I just threw this together in 5 minutes. I can't imagine screwing anything up, but treat this as more of a proof of concept rather than a replacement anyway. (This is in response Redwolf's "I needed to have 1 if for every card for both bots" - you didn't ;) ))

– Alion – 2018-09-07T10:01:57.150

4What 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. – Peter Taylor – 2018-09-07T10:07:30.270

Should it be possible for negative scores to happen? I've found a lot of examples when running Doom vs Greedier... – Alion – 2018-09-07T10:28:04.967

@RedwolfPrograms Also, what if my move renders the opponent with not enough score to execute his move? Whose move executes first? – Don Thousand – 2018-09-07T11:46:06.913

@RedwolfPrograms "Attempting to lower the opponent's score past 0 does not work." Doom disagrees - OX on an opponent with 0 score results in a negative score... – Alion – 2018-09-07T11:55:12.573

Speaking of which, I think @Bubbler 's Doom has successfully doomed this challenge to failure - I don't think you can win against it. – Alion – 2018-09-07T11:58:45.670

@J.Sallé Or someone else. If the bot is interesting enough. /// (or just write a simple other-language wrapper...) – user202729 – 2018-09-07T15:35:19.360

@PeterTaylor X is the energy reducing card. – Redwolf Programs – 2018-09-07T21:48:03.260

@Barbarian772 It does stack. You could also end up with x1/4 – Redwolf Programs – 2018-09-07T21:48:28.267

@RushabhMehta I'm going to fix that part of the code. Sorry, didn't test it more than a few simple games – Redwolf Programs – 2018-09-07T21:49:13.553

Could be way more interesting if the game was more card-game-like, i.e. design a deck and make a bot play with it strategically. Somewhat reminds me of Castle Wars series.

– Bubbler – 2018-09-08T05:58:20.140

@Bubbler I would love a Castle Wars-esque KOTH! – Alion – 2018-09-09T14:00:33.213

@RedwolfPrograms you might havemissed this question: Does "E", refilling 5 energy and costing 10 points, count as a -10 score? – Quintec – 2018-09-09T14:42:05.130

@Quintec yes, so O or N can cause you to get points from it – Redwolf Programs – 2018-09-10T19:10:38.917

playing only a single round guarantees the winner will be chosen primarily by luck. why not play each matchup 1000x times and use avg difference? this is the whole benefit of having computer programs rather than humans playing... – Jonah – 2019-02-17T15:36:14.327

Answers

3

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.

Bubbler

Posted 2018-09-06T19:35:14.513

Reputation: 16 616

2I feel like shielding could beat this, but imo shielding seems too expensive to really do you any harm – Barbarian772 – 2018-09-07T09:03:03.427

I can't beat this bot even when playing against it myself ._. – Alion – 2018-09-07T11:33:25.717

@Bubbler I literally was about to post a bot very similar to this right now. lol I'mma do it anyways – Don Thousand – 2018-09-07T11:40:38.467

1I 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. – Alion – 2018-09-07T12:12:04.180

1@Alion agreed. This doesn't seem like a terribly interesting KOTH – Don Thousand – 2018-09-07T12:26:19.687

@RushabhMehta It's a shame, really. Without this optimal opening that you can't do anything against and that leaves both players in a drawn position and unable to make progress (if used by both of them), it could've been fairly interesting I think. – Alion – 2018-09-07T12:34:51.893

@Alion Yup. You end up in a X then E loop that doesn't end. – Don Thousand – 2018-09-07T12:36:42.610

What about looping E during the X, and A during the O? Then, you'd get energy back and the O would give you the ten points – Redwolf Programs – 2018-09-08T14:20:22.233

@RedwolfPrograms That is a disaster! Doom starts its OX loop immediately, so an AE loop doesn't even complete one iteration - you're only at 5 points when trying to do your first E, but Doom steals 10 on turn 2 and makes you unable to afford an E, leaving you with -5 points and 14.9 energy!! Moreover, you wouldn't be able to afford a 10-point-cost action anyway! – Alion – 2018-09-09T14:10:33.450

1Actually, 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. – Alion – 2018-09-09T14:16:01.273

It seems that Doom is in fact beatable - I'm just incompetent. Whoops...

– Alion – 2018-09-10T07:27:20.777

3

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.

ripkoops

Posted 2018-09-06T19:35:14.513

Reputation: 61

...it seems that I've managed to bail on this approach in my attempts too early. Nice find! – Alion – 2018-09-10T07:24:33.317

The TIO link is Classic vs Doom OX – Quintec – 2018-09-10T20:30:54.667

@Quintec Thanks for the heads up, fixed. – ripkoops – 2018-09-10T21:20:20.143

Just a heads up that this is now a rock paper scissors game – Quintec – 2018-09-11T00:49:27.003

2

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.

Omegastick

Posted 2018-09-06T19:35:14.513

Reputation: 121

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. – Omegastick – 2018-09-07T06:05:45.157

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. – Jonathan Frech – 2018-09-07T06:36:14.697

2

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.

Bubbler

Posted 2018-09-06T19:35:14.513

Reputation: 16 616

2

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

Quintec

Posted 2018-09-06T19:35:14.513

Reputation: 2 801

Now it's getting slightly interesting, except that all the top tiers are variants of Doom... – Bubbler – 2018-09-10T22:56:50.667

@Bubbler I'm still looking for the "gun" in rock, paper, scissors... what, you've never cheated in that game? :P – Quintec – 2018-09-11T00:48:50.793

An XRAXA or XRXAA variant wins against all 3 convincingly (has plenty of other counters though). Wonder how deep this rabbit hole goes. – ripkoops – 2018-09-11T01:35:24.167

2@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 – Quintec – 2018-09-11T01:36:24.250

1

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];
}

Don Thousand

Posted 2018-09-06T19:35:14.513

Reputation: 1 781

Are you sure you didn't mean energy[0]? – Redwolf Programs – 2018-09-07T01:26:24.803

@RedwolfPrograms Yes I did. Oops, I'll fix momentarily – Don Thousand – 2018-09-07T01:29:26.507