JavaScript for bots

This guide will teach you the constructs used in writing bots for the competition. We'll do this by introducing simple example bots and dissecting their code.
The primary audience for this guide is people who are already familiar with programming languages other than JavaScript.

Here's one of the simplest bots – it always cooperates with its opponent.

alwaysCooperate.js

export default function bot() {const move = "C"const memory = nullreturn [move, memory]}

Bots have to be written in a specific way. They are run by a competition engine, which needs to be able to understand how to talk with your bot. This bot consists of 1 function. Functions, like in most programming languages, allow you to group together a set of instructions and give them a name. Additionally, they can define some parameters which they may take as input, and they can return a value.

Highlighted below is the beginning and end of the function. This one is named bot, and the empty set of parentheses after the name indicate that it defines no parameters, and thus takes no arguments as input.

alwaysCooperate.js

export default function bot() {const move = "C"const memory = nullreturn [move, memory] }

2 keywords are placed before the definition of the function. The export keyword is used to make the function available to some other code outside of the file – in this case, that will be the competition engine, which needs to be able to call (run) your bot. The default keyword is used to indicate that this is the primary/only exported function of the file.

The function doesn't have to be called bot for the competition engine to be able to access it. It does, however, need to be exported and available as the default function. There are a few different ways to declare and export a function in JavaScript:

exports.js

// As is done in most of our examplesexport default function bot() {}// As an unnamed or "anonymous" functionexport default function() {}// Declaring the function, and then exporting it laterfunction bot() {}export default bot// Using short definition syntax, `() => {}` instead of `function() {}`// Often called "arrow functions" due to the `=>`export default () => {}

Oh yeah, also comments are written starting with // for single-line comments, which extend to the end of the line.
We'll stick to the 1st style in these guides for coherence, though you can use whichever you prefer. Next, there are some constant declarations:

alwaysCooperate.js

export default function bot() { const move = "C" const memory = nullreturn [move, memory]}

Each declaration consists of the declaration keyword (in this case const), followed by a variable name and a value to assign to it. The 1st, move, has the value of "C", a string of 1 character. The 2nd, memory, has the value of null. null is a special value in JavaScript that indicates an explicit absence of a value.
null can often be confused with undefined, which indicates an implicit absence of a value. Not many other programming languages have this distinction. Fun!

There are also a few ways to declare constants and variables in JavaScript. These are const and let. The difference is that const does not allow for reassigning a value to the variable, while let does. We've used const here because we don't need to change the values of move or memory after they are assigned.

Finally, there's a return value, preceded by the return keyword. In JavaScript, functions don't have to return anything, though if we omitted this then our bot wouldn't be able to make a move.

alwaysCooperate.js

export default function bot() {const move = "C"const memory = null return [move, memory]}

Arrays in JavaScript are delimited by square brackets [] (similar to what's sometimes called "lists" or "vectors" in other programming languages). The competition engine expects your bot to return an array with 2 elements: the 1st being the move to make, either "C" for cooperation or "D" for defection, and the 2nd being the memory to pass to your bot on its next turn. We add the constants move and memory to the array in their respective places. It doesn't matter what the constants are named, just their order in the returned array.

The competition engine will run this bot, and it will receive the value ["C", null] in return. It then keeps track of the move the bot wants to make, and stores the memory value. The bot is then rerun on the next round.

Alongside your bot, the competition engine will also run your opponent's bot. So what's the deal with the memory? And how can your bot access the moves your opponent has made?

A more advanced bot

The following is an implementation of a strategy that tests its opponent, and tries to either exploit its opponent or cooperate based on the opponent's response. Because of this, strategies of this family are sometimes called "detectives".

detective.js

export default function bot({ history, memory }) {const currentRound = history.lengthif (currentRound == 0) return ["C", null]if (currentRound == 1) return ["D", null]if (currentRound == 2) return ["C", null]if (memory == null) {const opp0 = history[0].opponentconst opp1 = history[1].opponentconst opp2 = history[2].opponent// if the opponent reacted to our defection, we'll cooperate with themif (opp0 == "C" && opp1 == "C" && opp2 == "D")memory = { strategy: "C" }elsememory = { strategy: "D" }}return [memory.strategy, memory]}

Let's break this down. The first line is the same as usual, declaring a function, though it includes 1 parameter. This parameter is the state, and it's destructured into its component parts, which are the battle history and the bot's memory.

The first line could instead be written as follows:

detective.js

export default function bot({ history, memory }) { export default function bot(state) { const history = state.history const memory = state.memory// or, alternatively, const { history, memory } = state// ...}

The history includes a list of moves by us and our opponent. We'll explore the format of the moves in a little bit, though for now we can find out which round of the battle we're on by looking at the length of this history.

We'll start with a simple test: cooperate, defect, cooperate – and we'll do this by checking the current round and making a move based on that for the first 3 rounds.

detective.js

export default function bot({ history, memory }) { const currentRound = history.length if (currentRound == 0) return ["C", null] if (currentRound == 1) return ["D", null] if (currentRound == 2) return ["C", null]// ...}

So here, the currentRound constant holds the length of the battle history, which will be 0 if it's empty. I suppose that makes the round number look like it's off by 1.
For the next 3 statements, we check if it's round 0, 1, or 2 using the equality operator ==, and early-return the appropriate move and memory. The memory is set to null for now, since we don't need to store any information yet.

JavaScript allows for returning at any point during a function, and nothing after a return will be executed. That is, if one of these if statements is true, none of the remaining ones will be checked.

Next, we'll check our opponent's moves for the first 3 moves they made while we were testing. Each element in the history array is an object with 2 properties: you and opponent. The you property is the move we made, and the opponent property is the move our opponent made.

detective.js

export default function bot({ history, memory }) {// ... if (memory == null) { const opp0 = history[0].opponent const opp1 = history[1].opponent const opp2 = history[2].opponent// ...}return [memory.strategy, memory]}

We first check in the if statement whether we've written to memory already, and if we haven't (because it's null), we proceed.
Next, we index into the history array by placing an integer in square brackets [number] following the constant name (identifier). JavaScript arrays are 0-indexed, so the first element is 0 and the final element is history.length - 1. For each history item, we take our opponent's move from it using dot notation.

Now that we have these values assigned to constants, we'll check what they've done to determine our strategy. This will be simple enough for now: if they cooperated for the beginning 2 rounds and then defected in response to our defection, we'll assume they're a tit-for-tat-type bot and cooperate with them. Otherwise, we'll assume they're either (1) an always-defecting bot that is only worth defecting against, (2) an always-cooperate bot that is worth exploiting, or (3) a random-like bot that is too unpredictable to cooperate with – in any of these cases, we'll defect against them.

detective.js

export default function bot({ history, memory }) {// ...if (memory == null) {// ...// if the opponent reacted to our defection, we'll cooperate with them if (opp0 == "C" && opp1 == "C" && opp2 == "D") memory = { strategy: "C" } else memory = { strategy: "D" }}return [memory.strategy, memory]}

The && operator here is used as a logical AND operator, so all of the conditions must be met for the entire statement to be true.
If we detect a retaliatory defection, we set the memory variable (since it's destructured from the function parameter, it's reassignable by default) to an object with a property strategy set to "C". Otherwise, we set this strategy property to "D".
Objects are the equivalent of what other languages may call "dictionaries", "maps", or "hashmaps". They store pairs of keys and values. The keys can be text strings, numbers, or other applicable types, and the values can be any type.

An additional note on these if statements: the brackets enclosing their bodies are only necessary if a body consists of more than one statement. In the interest of brevity, I've omitted them here everywhere possible.

And to complete our bot, we'll return the move we want to make (the same value as the strategy we determined) and the memory object we created. The competition engine will then store this memory and pass it back to our bot on the next round, so we can continue to cooperate or defect based on our opponent's response.

detective.js

export default function bot({ history, memory }) {// ... return [memory.strategy, memory]}

This bot is designed to cooperate well with bots using tit-for-tat strategies, and to exploit bots that always cooperate. It will also defend itself as much as it can against bots which are random/unpredictable or always defect.

Can you figure out what its weaknesses are? Try to build a bot that can beat this one, or try to improve on this version of the detective strategy.