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.