17
\$\begingroup\$

AND THE WINNER IS....

Command Master's "Ball Pulling"!

Note: Competition is over, but more submissions are welcome!

Ball game is a game I invented where two players start with 100 energy each. Every turn, each player independently chooses an amount of energy to spend. Whoever spends more energy brings the ball 1 unit closer to their side - if they spend the same, it's a tie. If the ball is on someone's side, 4 units away from the center, the person who's side it's on wins. If both players run out of energy before it has reached one side or the other, it's a draw.

An example game might look like this:

rem P1  3  2  1  0  1  2  3  P2 rem
75  25              o        33  67
50  25           o            8  59
25  25        o               8  51
0   25     o                 18  33
0   0         o              15  18
0   0            o            6  12
0   0               o        12   0
                DRAW               

Your job is to make a bot for this game, in javascript.

Rules

The bot is a function taking three values:

The player's energy, as a number.
The player's previous moves, as an array of numbers.
The ball position, as a number between -3 and 3 inclusive. Positive is closer to the player, negative is further. The function must return an integer between 1 and 100 (Not NaN).

No messing with other bots.

No global variable access, as in standard KoTH rules.

Controller

This controller can do two things: It can 1v1 two bots, and it can run a tournament with each bot facing every other bot 100 times. The 1v1 setting generates a log of the game, and the tournament setting creates a leaderboard. The tournament is scored by +1 point for winning, nothing for drawing or losing.

Bots should be added to the bots object.

var bots = {
  "25er": function(myE, myPrev, pos) {
    return 25;
  },
  "random": function(myE, myPrev, pos) {
    return Math.floor(Math.random() * 36) + 1;
  },


  "Ball pulling": function(myE, myPrev, pos) {
    return Math.max(1,Math.floor(myE * [0.10808232641486629,0.30174603004818334,0.2358032944932353,0.10406266397506114,0.25013297998583794,0.4608205872385916,0.5635474208925423][3-pos] + [-25.20386503823533,-4.62324162626722,-7.69195538879287,-5.7156191657689925,-3.221187031407233,-16.758418842577605,-4.233010800850232][3-pos] + Math.random() * [23.13539398461274,-3.257894925941551,-0.09547522996644178,-0.09187251666187485,-1.1010525700900573,-9.874021194357677,2.831769191532314][3-pos]));
},

  "Decay": (energy, previous, position) => {
    return energy == 100 ? 30 : Math.ceil(energy * 0.15);
  },


  "waiter": (myE, myPrev, pos) => {
    if (pos == 3) {
      // If 1 space from winning, spend all energy to try and win
      return myE;
    } else if (pos == 1) {
      return Math.max(1, Math.floor(myE * .06));
    } else if (pos == -3) {
      // If opponent is 1 space from winning, spend 82% energy to try and prevent them from winning, or spend 9 to counter Memory
      return myPrev.length < 9 ? Math.ceil(myE * .82) : Math.min(9, Math.ceil(myE * .82));
    } else if (myPrev.length < 1) {
      // If fewer than 1 turn has been played, "wait" and only spend 1 energy
      return 1;
    } else {
      // Otherwise spend up to 12% of energy
      return Math.max(1, Math.floor(myE * .12));
    }
  },
  "Exhauster": function(myE, myPrev, pos) {
    return pos == -3 ? 28 - myPrev.length : 3;
  },


  "Memory": function(myE, myPrev, pos) {
    if (myPrev.length == 0) return 2;
    prev_pos = 0;
    for (i = 0; i < myPrev.length - 1; i++) {
      if (myPrev[i] > myPrev[i + 1]) {
        prev_pos += 1;
      } else if (myPrev[i] < myPrev[i + 1]) {
        prev_pos -= 1;
      }
    }
    move = myPrev[myPrev.length - 1]
    if (pos == 3) return move;
    if (prev_pos > pos) {
      move = Math.ceil(move * 2.75);
    } else if (prev_pos < pos) {
      move = Math.ceil(move * 0.8);
    }
    return Math.max(1, Math.min(move, myE - 8));
  },
  "just1": (myE, myPrev, pos) => {
    return pos === -3 ? Math.ceil(myE / 4) : 1;
  },


  "Vague Memory": function(myE, myPrev, pos) {
    if (myPrev.length == 0) return 2;
    prev_pos = 0;
    for (i = 0; i < myPrev.length - 1; i++) {
      if (myPrev[i] > myPrev[i + 1]) {
        prev_pos += 1;
      } else if (myPrev[i] < myPrev[i + 1]) {
        prev_pos -= 1;
      }
    }
    move = myPrev[myPrev.length - 1]
    if (pos == 3) return move;
    if (prev_pos > pos) {
      move = Math.ceil(move * 2.75 - 2.4 * Math.random());
    } else if (prev_pos < pos) {
      move = Math.ceil(move * 0.8);
    }
    min_move = Math.round(3.5 * Math.random());
    if (pos < -1) min_move += 2;
    return Math.max(min_move, Math.min(move, myE - 28));
  },
  "table": (myE, myPrev, pos) => {
    if (myE == 0) {
      return 0;
    }
    let w = [
      [-5.6544223, 0.06557736, -0.2641703],
      [0.75869083, -0.011340193, 1.4130216, -1.2559392],
      [0.13409477, 2.9561214, 0.27817196, 0.0030480723, 0.3889832],
      [-2.6868086, -0.3014877, 4.818155, 3.9265578, 1.7512726, 0.097844936],
      [0.68415153, 0.72050923, 1.334225, -1.6574494, 2.4933205, -0.25133085, -1.4409807],
      [1.6410222, -74.064156, -7.7318263, -0.01627013, 2.0059361, -6.7933116, -0.14581795, -1.6378351],
    ];
    let b = [1.8160640001296997, 1.5627140998840332, 3.7942299842834473, -2.4042959213256836, 2.8334028720855713, 312.232177734375];
    let log_weights = [];
    let log_weight_max = -Infinity;
    for (let m = 0; m < myE; m++) {
      let f = [m / (myE - 1), (myE - 50) / 29.15475947, 0.5 * pos, 0, 0, 0, 0, 0, 0];
      for (let l = 0; l < w.length; l++) {
        f[l + 3] = b[l];
        for (let i = 0; i < w[l].length; i++) {
          f[l + 3] += w[l][i] * f[i];
        }
        if (b[l] <= 300) {
          f[l + 3] = Math.max(f[l + 3], 0);
        }
      }
      let log_weight = f[f.length - 1];
      log_weight_max = Math.max(log_weight_max, log_weight);
      log_weights.push(log_weight);
    }
    let Q = 0;
    let M = 0;
    for (let m = 0; m < log_weights.length; m++) {
      let q = Math.exp(log_weights[m] - log_weight_max);
      Q += q;
      if (Math.random() * Q <= q) {
        M = m;
      }
    }
    return M + 1;
  },


  "Pacemaker": (myE, myPrev, pos) => {
  if (myPrev.length === 0) return 4; // https://xkcd.com/221/
  if (pos === 3) return Math.round(myE * 0.99);
  if (pos === -3) return Math.max(1, Math.round(myE * 0.4));
  return Math.max(1, Math.min(myE, Math.round(myE / (7 - pos))));
},

  

 "Fraction": function(myE, myPrev, pos) {
   return ((myPrev.length===3)&&(pos===-3))? 25 : Math.floor(myE/(pos+4)*0.395)
 },
'chair': function(myE, myPrev, pos) {
    let c = 0x69b82a691f395b79b7fb2bb83cb5f2a40ce32e4a4c4a055f7c50e68a3f81e123197280dfbec638e3b036a9b53677122f2fa83447501ece2a63d766e983ff858cda2da01ac621bb3084c1055a2fe83e6a7450c29fa99549d3cc6f1c556550b2397f417e915b65d47a9d6c1b267aaab5171d3e8a7fbad4788e9cb795cdc11759ba232730ad0a3635a8a9c5564afdf5b82c380687a8af1b1f1ca4d36ac668b32cafe39afdda16421be9d955ce70cfab6c987dc407e97792f647a43ecd0f191fecdbc60ef782519410655f3054395cc0e1c68f893f416806ad502b9b945166b61110f7d72c0d5437d32235007c26b64f787f1147e3ce1966a4ff6fb564d47db1b191801e811052dd5c31bee7fed77afae4799debec9349e224155780fbd65684d2503356b0c71805a41994fd48f043ee745d8bc8e12bb8105cb97edef8e08f2d5467649aceb5ee668168420ad4b4b0d9c33cf4577b19827220dec3377c1bdf627fa1b3015b59b0a5e4b456f4d18abbbc4d22bde46c7596a7cbabb4536fa325561e50e1bdffeee5fb17a6222ae7f5d894c824920227b4f45aa369e8d3956b68ad8ba2499c5b4f925b42983544c1c9cbff22bb3774c138c0b04b47be0a91c73164b0729d0fd4521be988e78584d5974fc0083452e6127507a59956104d84ad3bc481325d21295ebfc63ea8c4708251a1df30a3f7e906b9acba9eda44f60091c6765466a6f9dbc5a83862ad358d0a41f7d4d099670b2e6302d9a1d5dbfb54a1698e3f9d9956ad90e53n;
    return myE <= 1 ? myE : Number((c / (83n ** BigInt(myE * 7 + pos - 11))) % 83n + 1n);
},
'Indecisive Tracker': (myE, myPrev, pos) => {
  const RESULT = { WIN: 0, DRAW: 1, LOSE: 2 }
  const RESULT_ENCODING_OFFSET = [0, 1, 2]
  const RESULT_WEIGHT = [1, 0, -1]
  const MIN_SPENDING = 1
  const posToAggressionFactor = [0, 0.3, 0.2, 0, 0, 0, 0.1, 0.8, 0]
  const posToRiskAppetite = [0, 1, 0.4, 0.1, 0.1, 0.1, 0.2, 0.3, 0]
  let encodeResult = (E, lastResult) => E + RESULT_ENCODING_OFFSET[lastResult]
  let decodeResult = (E) => RESULT_ENCODING_OFFSET.findIndex(e => e === (E - MIN_SPENDING) % RESULT_ENCODING_OFFSET.length)
  let getResultFromPos = (posBefore, posAfter) => {
    return posBefore < posAfter ? RESULT.WIN
         : posBefore > posAfter ? RESULT.LOSE
                                : RESULT.DRAW
  }
  let BallMove = (prevPos, currE, nextE, finalPos) => {
    let currResult = nextE == null ? getResultFromPos(prevPos, finalPos)
                                   : decodeResult(nextE)
    let nextPos = prevPos + RESULT_WEIGHT[currResult]
    let enemyE = currResult === RESULT.LOSE ? currE + 1
               : currResult === RESULT.DRAW ? currE
                                            : 1
    return {
      enemyE: enemyE,
      myE: currE,
      posBefore: prevPos,
      posAfter: nextPos,
      result: currResult
    }
  }
  const GOAL = 8
  let normalizedPos = pos + 4
  let goalDistance = GOAL - normalizedPos
  let minimumReservedEnergyPerTileDistance = 3
  let spendableEnergy = myE - ((goalDistance - 1) * minimumReservedEnergyPerTileDistance)
  let prevPos = 0
  let prevResult = RESULT.DRAW
  let gameMoves = new Array()
  for (i = 0; i < myPrev.length; i++) {
    let currMove = BallMove(prevPos, myPrev[i], myPrev[i+1], pos)
    prevPos = currMove.posAfter
    prevResult = currMove.result
    gameMoves.push(currMove)
  }
  let myRandom = (min, max, spacing) => {
    if ((spacing ?? 0) === 0) spacing = 1
    let maxEncodingOffset = Math.max.apply(null, RESULT_ENCODING_OFFSET)
    let adjustedMin = Math.floor((min + maxEncodingOffset) / spacing) * spacing
    let offset = adjustedMin
    let adjustedMax = max - offset - maxEncodingOffset
    let adjustedRange = Math.floor(adjustedMax / spacing) + 1
    return Math.floor(Math.random() * adjustedRange) * spacing + offset
  }
  let minE = spendableEnergy * posToAggressionFactor[normalizedPos]
  let maxE = spendableEnergy * posToRiskAppetite[normalizedPos]
  let nextE = MIN_SPENDING + myRandom(minE, maxE, RESULT_ENCODING_OFFSET.length)
  let encodedNextE = encodeResult(nextE, prevResult)
  return encodedNextE
},



  // add your bot here
}

function game(p1, p2) {
  var ballPos = 0,
    p1moves = [],
    p2moves = [],
    p1e = 100,
    p2e = 100,
    outcome = '';
  var str = 'rem P1  3  2  1  0  1  2  3  P2 rem';

  function string(pos, e, e2) {
    var s = p1e + (p1e < 100 ? (p1e < 10 ? '   ' : '  ') : ' ') + e + (e < 10 ? ' ' : '');
    if (pos == -4) {
      s += '        P1 WINS        '
    } else if (pos == 4) {
      s += '        P2 WINS        '
    } else {
      s += ' '.repeat((pos + 4) * 3 - 1) + 'o' + ' '.repeat(23 - (pos + 4) * 3)
    }
    return s + (e2 < 10 ? ' ' : '') + e2 + (p2e < 100 ? (p2e < 10 ? '   ' : '  ') : ' ') + p2e;
  }
  while (Math.abs(ballPos) < 4) {
    var p1out = p1e ? Math.max(Math.min(p1e, bots[p1](p1e, p1moves, -ballPos)), 1) : 0;
    var p2out = p2e ? Math.max(Math.min(p2e, bots[p2](p2e, p2moves, ballPos)), 1) : 0;

    if (p1out > p2out) {
      ballPos--;
    } else if (p1out < p2out) {
      ballPos++;
    }
    p1e -= p1out;
    p2e -= p2out;
    str += '\n' + string(ballPos, p1out, p2out);
    if (p1e == 0 && p2e == 0 && Math.abs(ballPos) < 4) {
      str += '\n                DRAW               ';
      break
    }
    p1moves.push(p1out);
    p2moves.push(p2out)
  }
  return {
    log: str,
    res: (ballPos == -4 ? 1 : (ballPos == 4 ? 2 : 0))
  }
}

function init() {
  var f = document.getElementById('f'),
    f2 = document.getElementById('f2');
  for (var x in bots) {
    var option = document.createElement('option');
    option.innerText = x;
    option.value = x;
    f.appendChild(option);
    var option2 = document.createElement('option');
    option2.innerText = x;
    option2.value = x;
    f2.appendChild(option2)
  }
}
init();

function play() {
  var f = document.getElementById('f'),
    f2 = document.getElementById('f2');
  document.getElementById("g").innerText = game(f.value, f2.value).log
}

function tournament() {
  var leaderboard = {},
    names = [];
  for (var x in bots) {
    names.push(x);
    leaderboard[x] = 0
  }
  for (var t = 0; t < names.length; t++) {
    for (var y of names.slice(t + 1)) {
      for (var r = 0; r < 2000; r++) {
        var res = game(names[t], y);
        if (res.res == 1) {
          leaderboard[names[t]]++
        } else if (res.res == 2) {
          leaderboard[y]++
        }
      }
    }
  }
  var o = [];
  for (var x in leaderboard) {
    o.push({
      n: x,
      s: leaderboard[x]
    })
  }
  o = o.sort((x, y) => y.s - x.s);
  var t = document.getElementById('t');
  t.innerHTML = '';
  t.innerHTML += `<tr><td>Name</td><td>Score</td></tr>`
  for (var d of o) {
    t.innerHTML += `<tr><td>${d.n}</td><td>${d.s}</td></tr>`
  }
}
<select id='f'></select> vs
<select id='f2'></select><button onclick='play()'>Play</button>
<pre><code id='g'></code></pre>Or start tournament: <button onclick='tournament()'>Go!</button>
<table id='t'></table>

Leaderboard so far:

Ranking Name User
1st Ball pulling Command Master
2nd Fraction qwatry
3rd Waiter 79037662
4th Chair user1502040
5th Indecisive Tracker aff
6th Vague Memory histocrat
7th Table user1502040
8th Pacemaker Ethertyte
9th Memory histocrat
10th Exhauster Sheik Yerbouti
11th Decay Redwolf Programs
12th just1 Spitemaster
13th random Me (demo)
14th 25er Me (demo)

Command Master's "Ball pulling" mantains its spot in first. qwatry's new "Fraction" has jumped into second. The apparent tie between "waiter" and "chair" has been resolved by a 1v1 - "waiter" beats "chair", but they both score the same amount of points. aff's "Indecisive Tracker" uses the most complicated algorithm yet, but makes it into 5th place.

\$\endgroup\$
19
  • 2
    \$\begingroup\$ I'd suggest your run bot matchups hundreds of times in tournament, not 3. There will be way too much variance otherwise. It will a tournament of luck rather than skill. \$\endgroup\$ – Jonah Apr 3 at 5:37
  • 1
    \$\begingroup\$ Suggestion: Have the controller enforce "number must be between 1-100" by forcing to bot to spend 1 energy if it tries to spend less than 1 to prevent any trickery like (myE,myPrev,pos)=>myPrev.length?100:-1000000 (but more subtle) \$\endgroup\$ – 79037662 Apr 3 at 19:25
  • 1
    \$\begingroup\$ You should change the controller anyway - it can't change any answers, even future one, because it's only preventing illegal operations. \$\endgroup\$ – Spitemaster Apr 3 at 20:54
  • 1
    \$\begingroup\$ @Spitemaster Done. \$\endgroup\$ – A username Apr 3 at 21:22
  • 1
    \$\begingroup\$ Suggestion: add seedrandom.js and seed Math.random with something constant \$\endgroup\$ – Command Master Apr 4 at 7:10

12 Answers 12

5
\$\begingroup\$

Ball pulling

"Ball pulling": function(myE, myPrev, pos) {
    return Math.max(1,Math.floor(myE * [0.10808232641486629,0.30174603004818334,0.2358032944932353,0.10406266397506114,0.25013297998583794,0.4608205872385916,0.5635474208925423][3-pos] + [-25.20386503823533,-4.62324162626722,-7.69195538879287,-5.7156191657689925,-3.221187031407233,-16.758418842577605,-4.233010800850232][3-pos] + Math.random() * [23.13539398461274,-3.257894925941551,-0.09547522996644178,-0.09187251666187485,-1.1010525700900573,-9.874021194357677,2.831769191532314][3-pos]));
}

JSFiddle

Calculates a linear function of the energy and a random number, with the exact function depending on the ball location.

\$\endgroup\$
12
  • \$\begingroup\$ How does this work? Nice job, winning so far... \$\endgroup\$ – A username Apr 3 at 20:33
  • \$\begingroup\$ @Ausername it works by multiplying the energy by a magic number depending on the ball position. Originally my magic numbers were 0.05, 0.1, 0.15, 0.15, 0.3, 0.3, 0.3, but now there are more bots so I find the magic numbers with a genetic algorithm. \$\endgroup\$ – Command Master Apr 4 at 3:58
  • \$\begingroup\$ Interesting... By changing the array to [0.053,0.095,0.056,0.11,0.23,0.46,0.35] I managed to beat every bot including the current version. \$\endgroup\$ – A username Apr 4 at 10:59
  • \$\begingroup\$ @Ausername I'd expect that, because I don't train it against itself it wouldn't be optimized against itself \$\endgroup\$ – Command Master Apr 4 at 11:20
  • 3
    \$\begingroup\$ If I got it right, the bot doesn't have an intrinsic logic, it just gets constant values by trial and error against the current bots. I am not sure whether it makes sense to have this kind of bots. If everyone would do the same (in different ways) the winner would be the last bot posted. \$\endgroup\$ – Sheik Yerbouti Apr 4 at 19:21
9
\$\begingroup\$

Memory

    "Memory": function(myE, myPrev, pos) {
    if (myPrev.length == 0) return 2;
    prev_pos = 0;
    for(i=0;i<myPrev.length-1;i++) {
        if (myPrev[i] > myPrev[i+1]) {
            prev_pos += 1;
        } else if (myPrev[i] < myPrev[i+1]) {
            prev_pos -=1;
        }
    }
    move = myPrev[myPrev.length-1]
    if (pos==3) return move;
    if(prev_pos > pos) {
        move = Math.ceil(move * 2.75);
    } else if (prev_pos < pos) {
        move = Math.ceil(move * 0.8);
    }
    return Math.max(1,Math.min(move, myE - 8));
}

Infers whether it won or lost last round from how it adjusted in previous rounds, and then adjusts.

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

The demo bots

Random

Outputs a random integer between 1 and 36.

function(myE,myPrev,pos){
    return Math.floor(Math.random() * 36) + 1;
}

25er

Outputs 25 all the time.

function(myE,myPrev,pos){
    return 25;
}

Note: These two are built into the controller - no need to add them.

Why is this the most popular?

\$\endgroup\$
1
  • 7
    \$\begingroup\$ Is 25 chosen by fair dice roll and guranteed to be random? ;-) \$\endgroup\$ – Wasif Apr 3 at 2:07
7
\$\begingroup\$

Table


"table": (myE, myPrev, pos) => {
    if (myE == 0) {
        return 0;
    }
    let w = [
      [-5.6544223, 0.06557736, -0.2641703],
      [0.75869083, -0.011340193, 1.4130216, -1.2559392],
      [0.13409477, 2.9561214, 0.27817196, 0.0030480723, 0.3889832],
      [-2.6868086, -0.3014877, 4.818155, 3.9265578, 1.7512726, 0.097844936],
      [0.68415153, 0.72050923, 1.334225, -1.6574494, 2.4933205, -0.25133085, -1.4409807],
      [1.6410222, -74.064156, -7.7318263, -0.01627013, 2.0059361, -6.7933116, -0.14581795, -1.6378351],
    ];
    let b = [1.8160640001296997, 1.5627140998840332, 3.7942299842834473, -2.4042959213256836, 2.8334028720855713, 312.232177734375];
    let log_weights = [];
    let log_weight_max = -Infinity;
    for (let m = 0; m < myE; m++) {
      let f = [m / (myE - 1), (myE - 50) / 29.15475947, 0.5 * pos, 0, 0, 0, 0, 0, 0];
      for (let l = 0; l < w.length; l++) {
        f[l + 3] = b[l];
        for (let i = 0; i < w[l].length; i++) {
          f[l + 3] += w[l][i] * f[i];
        }
        if (b[l] <= 300) {
          f[l + 3] = Math.max(f[l + 3], 0);
        }
      }
      let log_weight = f[f.length - 1];
      log_weight_max = Math.max(log_weight_max, log_weight);
      log_weights.push(log_weight);
    }
    let Q = 0;
    let M = 0;
    for (let m = 0; m < log_weights.length; m++) {
      let q = Math.exp(log_weights[m] - log_weight_max);
      Q += q;
      if (Math.random() * Q <= q) {
        M = m;
      }
    }
    return M + 1;
  }

Closely approximates the Nash equilibrium. Not exploitative enough to top the leader board.

Chair


'chair': function(myE, myPrev, pos) {
    let c = 0x69b82a691f395b79b7fb2bb83cb5f2a40ce32e4a4c4a055f7c50e68a3f81e123197280dfbec638e3b036a9b53677122f2fa83447501ece2a63d766e983ff858cda2da01ac621bb3084c1055a2fe83e6a7450c29fa99549d3cc6f1c556550b2397f417e915b65d47a9d6c1b267aaab5171d3e8a7fbad4788e9cb795cdc11759ba232730ad0a3635a8a9c5564afdf5b82c380687a8af1b1f1ca4d36ac668b32cafe39afdda16421be9d955ce70cfab6c987dc407e97792f647a43ecd0f191fecdbc60ef782519410655f3054395cc0e1c68f893f416806ad502b9b945166b61110f7d72c0d5437d32235007c26b64f787f1147e3ce1966a4ff6fb564d47db1b191801e811052dd5c31bee7fed77afae4799debec9349e224155780fbd65684d2503356b0c71805a41994fd48f043ee745d8bc8e12bb8105cb97edef8e08f2d5467649aceb5ee668168420ad4b4b0d9c33cf4577b19827220dec3377c1bdf627fa1b3015b59b0a5e4b456f4d18abbbc4d22bde46c7596a7cbabb4536fa325561e50e1bdffeee5fb17a6222ae7f5d894c824920227b4f45aa369e8d3956b68ad8ba2499c5b4f925b42983544c1c9cbff22bb3774c138c0b04b47be0a91c73164b0729d0fd4521be988e78584d5974fc0083452e6127507a59956104d84ad3bc481325d21295ebfc63ea8c4708251a1df30a3f7e906b9acba9eda44f60091c6765466a6f9dbc5a83862ad358d0a41f7d4d099670b2e6302d9a1d5dbfb54a1698e3f9d9956ad90e53n;
    return myE <= 1 ? myE : Number((c / (83n ** BigInt(myE * 7 + pos - 11))) % 83n + 1n);
},

Uses a magic constant as a lookup table. Optimized against the current playing field.

\$\endgroup\$
1
  • \$\begingroup\$ Sorry, I didn't notice Chair. Adding now. \$\endgroup\$ – A username Apr 13 at 4:38
6
\$\begingroup\$

Decay

{
    "Decay": (energy, previous, position) => {
        return energy == 100 ? 30 : Math.ceil(energy * 0.15);
    }
}

Starts with 30 energy, which is enough to give it an initial lead. It then uses 15% of its remaining energy each turn.

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

Just1

"just1": (myE, myPrev, pos) => {
    return pos === -3 ? Math.ceil(myE / 4) : 1;
}

Waits out its opponent - it bids 1 unless it'd lose if it doesn't spend more - then it spends 25% of its current energy.

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

Exhauster

"Exhauster": (myE,myPrev,pos) => { 
    return pos == -3 ? 28 - myPrev.length : 3; 
}

It uses 3 units of energy in every turn, except when the opponent is at one step from the victory.

\$\endgroup\$
3
  • \$\begingroup\$ couldn't this return negative integers if the game takes more than 25 turns? \$\endgroup\$ – Command Master Apr 3 at 19:13
  • \$\begingroup\$ @CommandMaster Yes: jsfiddle.net/g8o37sn5 \$\endgroup\$ – 79037662 Apr 3 at 19:20
  • \$\begingroup\$ Yes you are right, it returns negative values after a while. Someone suggested to make the controller to handle cases for energy less than 1. This would also fix my answer. \$\endgroup\$ – Sheik Yerbouti Apr 3 at 21:07
5
\$\begingroup\$

Waiter

"waiter": (myE, myPrev, pos) => {
  if (pos == 3) {
    // If 1 space from winning, spend all energy to try and win
    return myE;
  } else if (pos == 1) {
        return Math.max(1, Math.floor(myE * .06));
    } else if (pos == -3) {
    // If opponent is 1 space from winning, spend 82% energy to try and prevent them from winning, or spend 9 to counter Memory
    return myPrev.length < 9 ? Math.ceil(myE * .82) : Math.min(9, Math.ceil(myE * .82));
  } else if (myPrev.length < 1) {
    // If fewer than 1 turn has been played, "wait" and only spend 1 energy
    return 1;
  } else {
    // Otherwise spend up to 12% of energy
    return Math.max(1, Math.floor(myE * .12));
  }
}

Update 2021-04-03T21:14:04Z - Changing 0.3 to 0.2 seems to improve performance against the current bots: https://jsfiddle.net/jwnsq917/

Update 2021-04-04T16:47:32Z - Changing 0.2 to 0.12 improves performance against the current bots, in particular BallPulling: https://jsfiddle.net/qd80eofc/1/

Update 2021-04-05T13:26:19Z - Changing 2 to 1 improves performance against the current bots: https://jsfiddle.net/3sqftja7/

The trend of "slightly adjust values to pull ahead" continues.

Update 2021-04-05T13:37:16Z - Add case for when opponent is 2 spaces from winning: https://jsfiddle.net/3sqftja7/5/

Update 2021-04-05T15:10:57Z - Add hard counter for Memory: https://jsfiddle.net/egnwLb3r/

Update 2021-04-07T14:06:56Z - More minor adjustments to once again take top spot: https://jsfiddle.net/ceL2as16/1/

I suspect that due to the fact that minor changes can make a bot win against other bots, the winner of this challenge will simply be the last person to make their bot hard-counter the other bots, possibly by updating their bot in secret.

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

Vague Memory

"Vague Memory": function(myE, myPrev, pos) {
    if (myPrev.length == 0) return 2;
    prev_pos = 0;
    for(i=0;i<myPrev.length-1;i++) {
        if (myPrev[i] > myPrev[i+1]) {
            prev_pos += 1;
        } else if (myPrev[i] < myPrev[i+1]) {
            prev_pos -=1;
        }
    }
    move = myPrev[myPrev.length-1]
    if (pos==3) return move;
    if(prev_pos > pos) {
        move = Math.ceil(move * 2.75 - 2.4*Math.random());
    } else if (prev_pos < pos) {
        move = Math.ceil(move*0.8);
    }
    min_move = Math.round(3.5 * Math.random());
    if(pos < -1) min_move += 2;
    return Math.max(min_move,Math.min(move, myE - 28));
}

Same as Memory, this tries to hone in the right value by adjusting up when it loses a round and down when it wins one. But to make it harder to tune against, this one randomly saves energy toward the beginning and randomly spends it toward the end.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ Nice, another non-deterministic program! This will make it a lot harder to easily beat every bot... \$\endgroup\$ – A username Apr 4 at 22:29
2
\$\begingroup\$

Pacemaker

"Pacemaker": (myE, myPrev, pos) => {
  if (myPrev.length === 0) return 4; // https://xkcd.com/221/
  if (pos === 3) return Math.round(myE * 0.99);
  if (pos === -3) return Math.max(1, Math.round(myE * 0.4));
  return Math.max(1, Math.min(myE, Math.round(myE / (7 - pos))));
}

Tries to pace itself based on how many more rounds it expects the game to last. Goes all in if it's one win away from victory, goes in heavy if it's one loss away from complete loss.

I also tried out similar ideas with nondeterminism, but the hardcoded logic one performed better in all my variations.

Update: Increasing the plan-ahead time from 4 to 7 rounds seems to fair better so I've updated the parameter. Previously, initial rounds were too greedy, spending a lot of energy early on.

\$\endgroup\$
3
  • \$\begingroup\$ I think this can return 0 even when the energy isn't zero, causing an infinite loop \$\endgroup\$ – Command Master Apr 5 at 14:27
  • \$\begingroup\$ @CommandMaster Sorry, and thanks for pointing that out. I hope the update fixes the issue, is there anything else I missed? \$\endgroup\$ – Etheryte Apr 5 at 14:36
  • 1
    \$\begingroup\$ Nice! You don't need to make it fit, the controller does that automatically. \$\endgroup\$ – A username Apr 5 at 20:34
2
\$\begingroup\$

Indecisive Tracker

'Indecisive Tracker': (myE, myPrev, pos) => {
  const RESULT = { WIN: 0, DRAW: 1, LOSE: 2 }
  const RESULT_ENCODING_OFFSET = [0, 1, 2]
  const RESULT_WEIGHT = [1, 0, -1]
  const MIN_SPENDING = 1
  const posToAggressionFactor = [0, 0.3, 0.2, 0, 0, 0, 0.1, 0.8, 0]
  const posToRiskAppetite = [0, 1, 0.4, 0.1, 0.1, 0.1, 0.2, 0.3, 0]
  let encodeResult = (E, lastResult) => E + RESULT_ENCODING_OFFSET[lastResult]
  let decodeResult = (E) => RESULT_ENCODING_OFFSET.findIndex(e => e === (E - MIN_SPENDING) % RESULT_ENCODING_OFFSET.length)
  let getResultFromPos = (posBefore, posAfter) => {
    return posBefore < posAfter ? RESULT.WIN
         : posBefore > posAfter ? RESULT.LOSE
                                : RESULT.DRAW
  }
  let BallMove = (prevPos, currE, nextE, finalPos) => {
    let currResult = nextE == null ? getResultFromPos(prevPos, finalPos)
                                   : decodeResult(nextE)
    let nextPos = prevPos + RESULT_WEIGHT[currResult]
    let enemyE = currResult === RESULT.LOSE ? currE + 1
               : currResult === RESULT.DRAW ? currE
                                            : 1
    return {
      enemyE: enemyE,
      myE: currE,
      posBefore: prevPos,
      posAfter: nextPos,
      result: currResult
    }
  }
  const GOAL = 8
  let normalizedPos = pos + 4
  let goalDistance = GOAL - normalizedPos
  let minimumReservedEnergyPerTileDistance = 3
  let spendableEnergy = myE - ((goalDistance - 1) * minimumReservedEnergyPerTileDistance)
  let prevPos = 0
  let prevResult = RESULT.DRAW
  let gameMoves = new Array()
  for (i = 0; i < myPrev.length; i++) {
    let currMove = BallMove(prevPos, myPrev[i], myPrev[i+1], pos)
    prevPos = currMove.posAfter
    prevResult = currMove.result
    gameMoves.push(currMove)
  }
  let myRandom = (min, max, spacing) => {
    if ((spacing ?? 0) === 0) spacing = 1
    let maxEncodingOffset = Math.max.apply(null, RESULT_ENCODING_OFFSET)
    let adjustedMin = Math.floor((min + maxEncodingOffset) / spacing) * spacing
    let offset = adjustedMin
    let adjustedMax = max - offset - maxEncodingOffset
    let adjustedRange = Math.floor(adjustedMax / spacing) + 1
    return Math.floor(Math.random() * adjustedRange) * spacing + offset
  }
  let minE = spendableEnergy * posToAggressionFactor[normalizedPos]
  let maxE = spendableEnergy * posToRiskAppetite[normalizedPos]
  let nextE = MIN_SPENDING + myRandom(minE, maxE, RESULT_ENCODING_OFFSET.length)
  let encodedNextE = encodeResult(nextE, prevResult)
  return encodedNextE
}

Tries to encode last round's winning/losing information on the spent energy, then deduce (conservatively) the game rounds info. But it fails to decide what to do with the data...

Ultimately, the bot uses lookup value based on the ball's position to decide that round's minimum and maximum factors, which will be fed into a random function.

The lookup values themselves are initially based on intuition and then some test run until I got a good-enough result for me. So, you can say that they are partially based on the other bots' behavior.

\$\endgroup\$
2
  • \$\begingroup\$ Nice algorithm! \$\endgroup\$ – A username Apr 13 at 4:44
  • \$\begingroup\$ Hey, thank you! \$\endgroup\$ – aff Apr 13 at 8:07
2
\$\begingroup\$

Fraction

 "Fraction": function(myE, myPrev, pos) {
   return ((myPrev.length===3)&&(pos===-3))? 25 : Math.floor(myE/(pos+4)*0.395)
 },

I was just messing around and I discovered this simple solution that surprisingly does fairly well!

¯\_(ツ)_/¯

\$\endgroup\$
3
  • \$\begingroup\$ It shouldn't perform any better - It's coerced to an integer anyway. \$\endgroup\$ – A username Apr 13 at 4:36
  • \$\begingroup\$ Nice! Second place! \$\endgroup\$ – A username Apr 13 at 4:43
  • \$\begingroup\$ @Ausername Yeah, you're right. Not sure where I got that result. :( Perhaps I was using an older version of the controller? \$\endgroup\$ – qwatry Apr 13 at 16:26

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.