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.
(myE,myPrev,pos)=>myPrev.length?100:-1000000
(but more subtle) \$\endgroup\$Math.random
with something constant \$\endgroup\$