Writing your own bot
A bot is a short piece of text in the game's formula language. The text is a judgement: given a position, it says how good that position is. The game does the rest. Each time the bot has a decision to make, the game lists every move it may legally make, works out the position each move leads to, and asks your formula to score each one. The bot makes the move whose position scores highest.
That one idea covers the whole game: where to settle, what to build, when to buy a development card, when to end the turn. You can also give the bot its own say in five particular decisions: which trades to offer, which to accept, whom to close a trade with, where to move the robber, and which cards to discard after a seven.
Every bot on this page is complete and can be copied into the workshop as it stands. If you are asking an assistant to write a bot for you, give it this page: it describes everything the language can do.
The smallest bot
score: 100 * victory_points + 60 * cards_per_roll
victory_points is the seat's victory points. cards_per_roll is the number of resource cards the seat can expect from each roll of the dice. This bot values a victory point at 100 and one more expected card per roll at 60, so it plays any move that raises either, and prefers points when it has to choose.
The formula is applied to every seat at the table, each from what your seat can see of it, and the bot plays to finish as far ahead of the best of its rivals as it can. A move that sets a rival back more than it helps your bot still scores well. Inside the formula, victory_points and every other built-in value describe the seat being scored at that moment.
When the score is all you write, the word score: may be left off: 100 * victory_points + 60 * cards_per_roll on its own is the same bot.
Numbers and sums
You can write numbers (3, 0.5, .25), add +, subtract -, multiply * and divide /, and use brackets. Multiplying and dividing come before adding and subtracting, as usual. Dividing by zero gives 0.
Comparisons give 1 when they hold and 0 when they do not: <, <=, >, >=, == (equal) and != (not equal). Sums are worked out first, so victory_points + 1 > 5 compares victory_points + 1 with 5. Compare two things at a time: a range is written and(2 < cards_in_hand, cards_in_hand < 8).
| Write | Gives |
|---|---|
min(a, b) | the smaller of two numbers |
max(a, b) | the larger of two numbers |
if(condition, then, else) | then when the condition is anything but 0, otherwise else |
and(a, b) | 1 when both are anything but 0 |
or(a, b) | 1 when either is anything but 0 |
not(a) | 1 when a is 0, and 0 when it is anything else |
A line starting with #, or the rest of a line after one, is a note to yourself and the game skips it.
Naming a value
let lead = victory_points - max(s in rivals: s.points);
let rich = cards_per_roll > 0.5;
score: 100 * victory_points + if(rich, 20 * cities_left, 5 * settleable_corners) + 10 * lead
A let gives a value a name that the lines after it can use. Each ends with ;. A name is letters, digits and _, starting with a letter or _, and each is given a value once. You can have up to 16.
The built-in values
Eighteen names are ready-made measurements of a seat's position. Each one measures a single thing and says nothing about whether more of it is good: a weight of your choosing decides that. Each is written out in the library (below) in this same language, so you can see exactly how it is counted and write a version of your own.
A pip is a dot printed on a number token: the number of ways two dice can roll that number, out of 36. A 6 or an 8 has 5 pips, a 2 or a 12 has 1. A corner's pips are the pips of the land hexes around it.
| Name | What it measures |
|---|---|
victory_points | the seat's victory points, including Victory Point cards it has not revealed |
cards_per_roll | resource cards the seat can expect per roll, all resources added together; the hex under the robber counts as producing nothing |
resource_types | how many of the five resources the seat produces at all, from 0 to 5 |
longest_road_chain | the number of roads in the seat's longest unbroken chain |
knights_played | Knight cards the seat has played |
holds_longest_road | 1 while the seat holds Longest Road, otherwise 0 |
holds_largest_army | 1 while the seat holds Largest Army, otherwise 0 |
settleable_corners | corners where the seat could build a settlement right now |
harbour_advantage | how much better than the bank's four-for-one the seat's harbours let it trade, added up over the five resources |
cards_in_hand | resource cards in the seat's hand |
cards_over_seven | resource cards above seven: the cards a roll of seven puts at risk |
settlements_left | settlements the seat has not built yet, from 5 down to 0 |
cities_left | cities the seat has not built yet, from 4 down to 0 |
roads_left | roads the seat has not built yet, from 15 down to 0 |
most_pips_settleable_corner | the pips of the corner with the most pips among those where the seat could build a settlement now, or 0 when there is none |
most_pips_one_road_away | the pips of the corner with the most pips that the seat could build on after one more road |
hand_build_progress | how close the hand is to paying for a settlement and a city, from 0 (nothing it can use) to 1 (enough for both) |
development_cards | development cards the seat holds and has not played, Victory Point cards included |
Bots written before 26 October 2026 use shorter names for some of these. The game still reads each one as the value it stood for, so those bots play exactly as they did:
| Earlier name | Name now |
|---|---|
point | victory_points |
production | cards_per_roll |
diversity | resource_types |
road_length | longest_road_chain |
army | knights_played |
longest_road | holds_longest_road |
largest_army | holds_largest_army |
open_spots | settleable_corners |
port_saving | harbour_advantage |
hand | cards_in_hand |
hand_over_limit | cards_over_seven |
best_spot | most_pips_settleable_corner |
reach1 | most_pips_one_road_away |
hand_synergy | hand_build_progress |
dev_cards | development_cards |
The board
Beyond the built-in values, a bot can read everything a player sitting in its seat can see, each by name.
| Name | What it is |
|---|---|
me | the seat being scored: in score:, each seat in turn, as described under "The smallest bot"; in a decision, your bot's own seat |
seats | every seat, in turn order |
rivals | every seat except me |
nobody | the owner of an empty corner or edge; everything about it reads 0 |
hexes | every land hex |
corners | every corner, where settlements and cities stand |
edges | every edge, where roads go |
harbours | every harbour |
resources | lumber, brick, wool, grain and ore |
numbers | the dice totals 2 to 12 |
game | the turn, the dice, the bank and the rest of the table |
lumber, brick, wool, grain, ore | the five resources |
A dot reads a part of something: me.points, game.robber.number, me.hand.ore.
A seat has:
| Part | What it is |
|---|---|
points | victory points everyone can see (hidden victory point cards are not in it) |
cards | how many resource cards it holds |
hand | its cards by resource: hand.ore, hand.total, hand[r]. Only your own; for any other seat every count reads 0 |
devs | how many development cards it holds |
dev | its development cards by kind: dev.knight, dev.victory_point, dev.road_building, dev.year_of_plenty, dev.monopoly, dev.total. Only your own |
playable | 1 for each kind of development card you hold and did not buy this turn, by kind as dev is. Only your own |
knights | knights played |
road_length | its longest chain of roads |
longest_road, largest_army | 1 while it holds the card |
settlements, cities, roads | pieces it has on the board |
settlements_left, cities_left, roads_left | pieces it has not built |
ratio | the rate it trades each resource to the bank at, 4, 3 or 2: ratio.wool, ratio[r] |
leads | 1 when no seat has more points that everyone can see |
turn | 1 when it is this seat's turn |
next | the seat that plays after it |
A hex has resource, desert (1 for the desert), number (0 on the desert), pips (the ways two dice roll its number, 1 to 5), robber (1 while the robber is on it), corners and edges.
A corner has hexes (the one to three land hexes around it), neighbours (the corners one edge away), edges, building (0 empty, 1 settlement, 2 city), settlement, city, owner (a seat, or nobody), harbour (1 when a building there uses a harbour), ratio (the rate a building there trades each resource at), buildable (1 when me may put a settlement there: it is empty, nothing is built next to it, and one of me's roads reaches it), open (1 when it is empty and nothing is built next to it, whoever could reach it) and coast.
An edge has ends (its two corners), hexes (the land hexes either side), road (1 when a road is on it), owner and buildable (1 when me may put a road there).
A harbour has corners, generic (1 for three-for-one), resource (what a two-for-one takes) and ratio (2 or 3).
A dice total (from numbers) has value (2 to 12), pips (the ways to roll it out of 36) and rolled (how many times it has come up this game).
The game has turn (0 during the opening, then 1, 2, ...), rolled (1 once the dice are rolled this turn), dice (the last total rolled), bank (cards left in the bank, by resource), deck (development cards left to buy), robber (the hex it stands on), current (whose turn it is), offers (trade offers made this turn), target (the points that win) and setup (1 while the opening pieces are being placed).
A resource can be compared with another (h.resource == ore) and used to read a count: me.hand[r], game.bank[r]. Seats, hexes, corners, edges and harbours can be compared too: c.owner == me, h == game.robber.
Cards counted by resource show lumber, brick, wool, grain, ore and total. Cards counted by kind show knight, victory_point, road_building, year_of_plenty, monopoly and total.
Walking the board
To ask about many things at once, walk a list:
score: 100 * victory_points + sum(c in corners where c.owner == me: c.building * 2) + count(e in edges where e.owner == me)
sum(c in corners where c.owner == me: c.building * 2) goes through every corner, keeps the ones you own, and adds up c.building * 2 for each. The where part is optional; the part after the colon is what each one is worth.
| Walk | Gives |
|---|---|
sum(x in list: value) | the values added up |
max(x in list: value) | the largest value, or 0 when nothing is left to walk |
min(x in list: value) | the smallest value, or 0 when nothing is left to walk |
count(x in list) | how many there are |
any(x in list: condition) | 1 when the condition holds for at least one |
all(x in list: condition) | 1 when it holds for every one, or when there are none |
count(c in corners where c.buildable) and count(c in corners: c.buildable) say the same thing.
The lists are seats, rivals, hexes, corners, edges, harbours, resources and numbers, and the lists something has: c.hexes, c.neighbours, c.edges, h.corners, h.edges, e.ends, e.hexes and a harbour's corners.
You can walk several lists in one go, each with its own where, and each can use the ones before it:
score: 100 * victory_points + max(e in edges where e.buildable, c in e.ends where c.open: sum(h in c.hexes where not(h.robber): h.pips))
That is the best corner you could reach with one more road, counted in pips.
Functions
fn pips_at(c: corner) = sum(h in c.hexes where not(h.robber): h.pips);
fn safe(c: corner) = count(n in c.neighbours where n.owner == nobody) == count(n in c.neighbours);
score: 100 * victory_points + 60 * cards_per_roll + max(c in corners where and(c.buildable, safe(c)): pips_at(c))
fn names a calculation with values to fill in. Say what each value is: a number (the kind you get when you do not say), resource, seat, hex, corner, edge or harbour. A function that takes nothing can be used by its name alone. Functions can use each other in any order, but a function may never lead back to itself, directly or through others. A function works only with what it is given, the board and other functions; your let values are not visible inside it.
You can define up to 24 functions, each taking up to 8 values.
The library
The eighteen built-in values are written in this same language, and so are the helpers they are built from, which you can use in any bot:
# The pips a settlement on corner c would collect now: the pips of the hexes
# around it, leaving out the hex the robber stands on.
fn spot(c: corner) = sum(h in c.hexes where not(h.robber): h.pips);
# Cards of resource r you expect per roll from your settlements and cities.
# A city collects twice. The robber's hex pays nothing.
fn produces(r: resource) = sum(c in corners where c.owner == me, h in c.hexes where and(h.resource == r, not(h.robber)): c.building * h.pips / 36);
# How far a hand holding `have` of something is from `want` of it: 0 when
# there are enough, 1 when there are none.
fn short(have, want) = max(want - have, 0) / want;
# How close your hand is to a settlement (1 lumber, 1 brick, 1 wool, 1 grain),
# from 0 to 1.
fn settlement_ready() = 1 - (short(me.hand.lumber, 1) + short(me.hand.brick, 1) + short(me.hand.wool, 1) + short(me.hand.grain, 1)) / 4;
# How close your hand is to a city (2 grain, 3 ore), from 0 to 1.
fn city_ready() = 1 - (short(me.hand.grain, 2) + short(me.hand.ore, 3)) / 2;
score: 100 * victory_points + 30 * city_ready + sum(r in resources: produces(r) * 10)
And here is each built-in value as the game writes it:
fn victory_points() = me.points + me.dev.victory_point;
fn cards_per_roll() = sum(r in resources: produces(r));
fn resource_types() = count(r in resources where produces(r) > 0);
fn longest_road_chain() = me.road_length;
fn knights_played() = me.knights;
fn holds_longest_road() = me.longest_road;
fn holds_largest_army() = me.largest_army;
fn settleable_corners() = count(c in corners where c.buildable);
fn harbour_advantage() = sum(r in resources: 4 - min(me.ratio[r], 4));
fn cards_in_hand() = me.hand.total;
fn cards_over_seven() = max(me.hand.total - 7, 0);
fn settlements_left() = me.settlements_left;
fn cities_left() = me.cities_left;
fn roads_left() = me.roads_left;
fn most_pips_settleable_corner() = max(c in corners where c.buildable: spot(c));
fn most_pips_one_road_away() = max(e in edges where e.buildable, c in e.ends where c.open: spot(c));
fn hand_build_progress() = (settlement_ready() + city_ready()) / 2;
fn development_cards() = me.dev.total;
score: 100 * victory_points + 60 * cards_per_roll + 8 * most_pips_settleable_corner
To change how a built-in value is counted, copy its line into your bot and edit it. Your version then takes its place everywhere in your bot. For example, a most_pips_settleable_corner that also counts a harbour at the corner:
fn most_pips_settleable_corner() = max(c in corners where c.buildable: spot(c) + 3 * c.harbour);
score: 100 * victory_points + 60 * cards_per_roll + 8 * most_pips_settleable_corner
Decisions
Besides score:, a bot can have its own say in five decisions. Each is a part of the bot that starts with its name and a colon and ends with ;:
score: 100 * victory_points + 60 * cards_per_roll + 8 * most_pips_settleable_corner;
answer: default + 30 * pays.city;
robber: default + 5 * victim.points;
discard: default - me.hand[card]
A part is worked out for every choice the decision has, and the choice with the highest value is taken. When two are equal the bot picks one of them at random. A decision your bot has no part for is made the usual way: the move whose position scores best.
Every part can read default: the value the usual judgement gives that choice. So answer: default is exactly the usual answer, answer: default + 30 * pays.city is the usual answer with a liking for trades that finish a city, and answer: gain > 20 throws the usual answer away and writes a new one.
| Part | When | Its own values |
|---|---|---|
offer: | your turn, for each offer of one or two cards of one resource for one card of another | give, get, partner (always nobody: the offer goes to the table), pays, gain, default |
answer: | someone offers you a trade | give, get, partner (who offered), pays, gain, default |
confirm: | people accepted your offer, for each of them | give, get, partner (who accepted), pays, gain, default |
robber: | you move the robber, for each hex and each seat you could rob there | hex, victim (or nobody), default |
discard: | a seven made you discard, one card at a time | card (the resource), default |
In a trade:
giveandgetare the cards you would give and get, by resource:give.ore,get.total,give[r].payssays what the trade lets you build this turn that you could not before:pays.road,pays.settlement,pays.city,pays.dev, each 1 or 0.gainis how much better your score finds your position with the trade than without it, after the builds the cards pay for.defaultis above zero exactly when the usual judgement would say yes.
A trade happens only when its value is above zero: answer: accepts, offer: offers the best of them and confirm: confirms with the best of the people who accepted, or cancels when none is above zero. Whatever your bot says, it makes at most two offers a turn and never the same offer twice in one turn.
These values are only known inside their own parts, never in score:, a let or a function.
What a bot cannot see
A bot sees what you would see sitting in its seat. Another seat's cards are a number, never which cards they are; a hidden victory point card is hidden; the development deck is a number of cards, never their order; and nothing says what the next roll will be. When the game looks at what a move would lead to, it imagines what nobody can know (the dice, the next card off the deck, which cards a rival holds) fairly from what everyone can know, so a bot cannot learn a secret by looking ahead.
Limits
A bot is at most 8000 characters, with brackets at most 16 deep, up to 16 let values and up to 24 functions.
A bot also has to make up its mind quickly, because people at the table are waiting for it. The game works out, before your bot ever plays, the most work it could take to judge one position on the biggest board, and refuses a bot that could take more than 60,000 steps, telling you how many yours would take. The whole library above, every feature written out at once, fits. Every corner against every corner fits only when little is done for each pair, and three lists inside each other never do. The count assumes every where lets everything through, because it cannot know in advance how few will pass. The usual way out is to walk a shorter list (a corner's neighbours rather than every corner), a function that walks less, or a let that works a value out once.
A whole decision has a limit too, counted in steps: every position the bot judges while making it, looking ahead included, and the reading of each position as well as your bot's own steps. At a table with other people, and in every game that is measured or rated, a decision may take 3,500,000 steps. A bot you let think longer may take ten times as many where only you are waiting for it: when you watch it play or ask why it chose, and at your own tables with no one else seated. The count is the same on every phone and computer, so a bot plays the same moves wherever it sits.
When a bot runs out of steps it still plays its own move. A bot that looks ahead plays the best move of the deepest look it finished, so one set to look four moves ahead may play what it found looking three or two moves ahead, or the move that scores best one move ahead. A bot that judges one move ahead plays the best of the moves it had judged, taken in the order the game lists them. Either way it makes no offer on that move, and only if it had not judged a single move is the move made the usual house way. Looking three or four moves ahead is where a bot most often runs out.
Three whole bots
A bot that spreads out and settles good corners:
# Room to grow is worth nearly as much as points.
let spread = count(c in corners where c.buildable) + count(e in edges where e.buildable) / 4;
score: 100 * victory_points + 50 * cards_per_roll + 12 * resource_types + 10 * most_pips_settleable_corner + 5 * most_pips_one_road_away + 3 * spread + 20 * settlement_ready
A bot that builds cities on ore and grain:
fn ore_and_grain() = produces(ore) + produces(grain);
score: 100 * victory_points + 40 * cards_per_roll + 120 * ore_and_grain + 40 * city_ready + 8 * most_pips_settleable_corner - 10 * cards_over_seven;
answer: default + 25 * pays.city;
discard: default + 3 * (card == ore) + 3 * (card == grain)
A bot that trades hard and robs the leader:
score: 100 * victory_points + 60 * cards_per_roll + 8 * most_pips_settleable_corner + 20 * hand_build_progress;
offer: default - 2 * give.total;
answer: if(partner.leads, default - 40, default + 10 * (get.total - give.total));
robber: default + if(victim.leads, 30, 0) + 2 * hex.pips
When the game says no
If something in your bot is wrong, the workshop says what and points at the character where it went wrong: a name that does not exist (with the names that do), a bracket that is not closed, a value of the wrong kind (a corner where a number should be), a function that leads back to itself, or a bot that would take too long to think.