Writing a bot
This guide will teach you how to write a bot to participate in battles. Simple bots can be written in a few lines of code, but the best bots are often more complex and require some strategy.
Your bot is a single function. Imagine it as a machine that takes in a list
of moves (the game so far) and some memory, and outputs its next move and an
updated memory.
This move is for a game similar to the classic Prisoner's Dilemma, sometimes called the Peace war game. The number of points for each move varies throughout the event, though
the basic idea is as follows:
- The 2 options for moves are to cooperate, or to defect.
- The best outcome for both you and your oppenent is if you both cooperate.
- The best outcome for you, if your opponent is cooperating, is to defect.
The points are structured in this way because it makes for some interesting gameplay, requires strategy to establish trust, and somewhat reflects many theories on human cooperation and trust. Each player makes a move at the same time, so can't see what the other player did until they have both made their move.
This programme allows you to submit such functions or bots, written in JavaScript or TypeScript. If you know TypeScript syntax, the signature looks like this:
bot.ts
// "C" represents cooperation, "D" represents defection.type Move = "C" | "D"// A match has 2 moves, 1 by you and 1 by your opponent.type Match = {you: Moveopponent: Move}// You can put anything you want in memory, or nothing at all.type Memory = unknowntype State = {history: Match[] // Every round so farmemory: Memory // Whatever you saved to your memory last round}// A bot looks at its existing state (the history and its memory), and outputs its next move and an updated memory.type Bot = (state: State) => [Move, Memory]If you only know JavaScript and not TypeScript, just know that the type annotations used in TypeScript are entirely optional and don't ever affect how the code actually runs. You can write your bot in JavaScript and it will work just fine – the types are for your own understanding and to help you avoid mistakes.
A battle runs for many rounds, always at least 100. Your bot can see the full history of moves, and whatever memory you return will be passed back to you in the next round, allowing you to remember anything you like without having to keep variables outside your bot's function (in the global scope, which won't be persisted).
Let's write a simple bot. Our strategy will be to cooperate 100% of the time, and hope that our opponent doesn't defect, I guess. Let's call it alwaysCooperate. We'll start with a JavaScript file containing a single function.
alwaysCooperate.js
function bot() {}We should define a move to make, and some memory to store. The move we'll
make is to cooperate, which is represented with the "C" string.
We don't need to store any memory, which we can represent with null.
alwaysCooperate.js
function bot() { const move = "C" // Cooperate on every move const memory = null // We don't need to remember anything}Finally, we need to do 2 things with our move and memory: return them from the function, and export the function so that the tournament can use it. The final code looks like this:
alwaysCooperate.js
export default function bot() {const move = "C" // Cooperate on every moveconst memory = null // We don't need to remember anything return [move, memory] // Move must come 1st, then memory 2nd}Writing an opponent
Let's add another bot for our bot to play against. It will be very similar to the alwaysCooperate bot, however it will be a little bit meaner. It will defect every time instead of cooperating every time. We'll call it alwaysDefect.
alwaysDefect.js
export default function bot() { const move = "C" // Cooperate on every move const move = "D" // Defect on every moveconst memory = null // We don't need to remember anythingreturn [move, memory] // Move must come 1st, then memory 2nd}We'll now try battling these 2 bots against each other. You may already be able to guess what will happen. On the 1st round, alwaysCooperate will cooperate and alwaysDefect will defect, so alwaysDefect will get 3 points and alwaysCooperate will get 0 points. On the 2nd round, the same thing will happen, and so on for every round.
alwaysDefect won
This battle lasted for 120 rounds, and the scores displayed above are the mean number of points per round earned by each bot. As expected, alwaysDefect won the battle. If many battles between these bots were played, you would see alwaysDefect increase in Elo while alwaysCooperate would fall.
You can also write a bot that reacts to its opponent's moves. A classic example is the tit-for-tat strategy. This bot cooperates on the first round, and then copies its opponent's last move for every subsequent round. That is, if the opponent cooperated last round, it will cooperate this round, and if the opponent defected last round, it will defect this round.
We'll call this bot titForTat. It's friendly by default, only retaliates if its opponent does first, and always provides the option of going back to friendly cooperation afterwards. This bot wo'nt need to access any memory, though it will need to look at the history of moves to see what its opponent did last round.
titForTat.ts
export default function bot({ history }) {}To start with, we check the length of the history. If it's 0, that means this is the first round, so we cooperate.
titForTat.ts
export default function bot({ history }) { if (history.length === 0) return ["C", null] // Cooperate on the first round}Next we'll get the opponent's move from the last round, and return that as our next move. We'll also return null for memory, since we don't need to store anything.
titForTat.ts
export default function bot({ history }) {if (history.length === 0)return ["C", null] // Cooperate on the first round const move = history.at(-1).opponent return [move, null] // Copy the opponent's last move}This bot seems like it has a pretty decent strategy. It will cooperate with other cooperative bots and defend itself against defectors. Let's put it in the arena and see how it fares against the other bots we've written so far. We'll play a battle against alwaysCooperate and a battle against alwaysDefect to see how it does.
Against alwaysCooperate, both bots cooperate for every move, earning 2 points each per round.
Tie
Against alwaysDefect, titForTat cooperates on the first round, but then defects for every subsequent round. Both bots receive 1 point per round, except for the first round where titForTat receives 0 points and alwaysDefect receives 3 points. alwaysDefect wins the battle, but only by a small margin.
alwaysDefect won
Okay, there's 1 problem with the tit-for-tat strategy, and that's the fact that it can never win a battle. Due to its fairly peaceful-until-provoked nature, it never defects more than its opponent. However, when it does lose, it loses by a only a hair.
We'll implement a 4th and final bot to demonstrate the use of memory, and to actually be able to win a battle. This bot will "hold a grudge": once its opponent has defected against it once, it will defect forever. This bot will need to keep track of whether its opponent has defected against it, so it will use memory to store that information. We'll call this bot grudger.
grudger.js
export default function bot({ history, memory }) {}Its memory starts off as null, so the first thing we'll do is
set it with a variable so that we can know whether our opponent has defected
yet.
grudger.js
export default function bot({ history, memory }) { // If memory is null, set it to an object with a property to track whether the opponent has defected memory = memory ?? { opponentDefected: false }}Next we'll get the opponent's previous move, and if it's a defection, set
the opponentDefected property to true. If it
isn't, nothing will happen, so the property will remain what it was before.
grudger.js
export default function bot({ history, memory }) {// If memory is null, set it to an object with a property to track whether the opponent has defectedmemory = memory ?? { opponentDefected: false } // Get the opponent's last move const lastOpponentMove = history.at(-1)?.opponent if (lastOpponentMove === "D") memory.opponentDefected = true}Finally, we'll choose our move based on whether the opponent has defected yet, and return our move. We'll also return our updated memory so that we can remember whether the opponent has defected in future rounds.
grudger.js
export default function bot({ history, memory }) {// If memory is null, set it to an object with a property to track whether the opponent has defectedmemory = memory ?? { opponentDefected: false }// Get the opponent's last moveconst lastOpponentMove = history.at(-1)?.opponentif (lastOpponentMove === "D")memory.opponentDefected = true const move = memory.opponentDefected ? "D" : "C" return [move, memory]}I'll leave it up to you to decide if this strategy is a good one, and I encourage you to try it and see how it fares. Also try tweaking this bot or any of the others to see if you can make them perform better!
The rules
Bots run in a sandbox with limits to keep the competition fast and safe. These include measures such as:
- Maximum of 10ms per move.
- 1 MB maximum memory (stack/heap size).
- Your code must be a single file, or at least be compiled to a single file.
If a bot times out, returns an invalid move, or throws an error, then it forfeits the match and its opponent wins 3:0. Write something robust!
Additionally, you may have a maximum of 3 bots active at the same time. This measure exists to prevent users deploying, for example, 99 bad bots and 1 good bot to unfairly climb the leaderboard. You can deactivate or reactivate your bots at any time.
Scoring
Each round pays out using a payoff matrix, or score matrix, similar to the original prisoner's dilemma:
- Both cooperate: 2 points each.
- You defect, opponent cooperates: 3 points for you, 0 points for them.
- You cooperate, opponent defects: 0 points for you, 3 points for them.
- Both defect: 1 point each.
This may change in future, possibly later on in the competition. Keep your bots flexible!
The winner is whoever banks more points over the whole battle. Every battle adjusts both bots' Elo (starting at 1000), which is what the leaderboard ranks.
Submitting
Head to Submit a bot, paste your code, and flip it active. It will enter the live tournament immediately, and you can watch it on the leaderboard to inspect individual battles.