Robber Barons

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).

WriteGives
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.

NameWhat it measures
victory_pointsthe seat's victory points, including Victory Point cards it has not revealed
cards_per_rollresource cards the seat can expect per roll, all resources added together; the hex under the robber counts as producing nothing
resource_typeshow many of the five resources the seat produces at all, from 0 to 5
longest_road_chainthe number of roads in the seat's longest unbroken chain
knights_playedKnight cards the seat has played
holds_longest_road1 while the seat holds Longest Road, otherwise 0
holds_largest_army1 while the seat holds Largest Army, otherwise 0
settleable_cornerscorners where the seat could build a settlement right now
harbour_advantagehow much better than the bank's four-for-one the seat's harbours let it trade, added up over the five resources
cards_in_handresource cards in the seat's hand
cards_over_sevenresource cards above seven: the cards a roll of seven puts at risk
settlements_leftsettlements the seat has not built yet, from 5 down to 0
cities_leftcities the seat has not built yet, from 4 down to 0
roads_leftroads the seat has not built yet, from 15 down to 0
most_pips_settleable_cornerthe 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_awaythe pips of the corner with the most pips that the seat could build on after one more road
hand_build_progresshow close the hand is to paying for a settlement and a city, from 0 (nothing it can use) to 1 (enough for both)
development_cardsdevelopment 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 nameName now
pointvictory_points
productioncards_per_roll
diversityresource_types
road_lengthlongest_road_chain
armyknights_played
longest_roadholds_longest_road
largest_armyholds_largest_army
open_spotssettleable_corners
port_savingharbour_advantage
handcards_in_hand
hand_over_limitcards_over_seven
best_spotmost_pips_settleable_corner
reach1most_pips_one_road_away
hand_synergyhand_build_progress
dev_cardsdevelopment_cards

The board

Beyond the built-in values, a bot can read everything a player sitting in its seat can see, each by name.

NameWhat it is
methe seat being scored: in score:, each seat in turn, as described under "The smallest bot"; in a decision, your bot's own seat
seatsevery seat, in turn order
rivalsevery seat except me
nobodythe owner of an empty corner or edge; everything about it reads 0
hexesevery land hex
cornersevery corner, where settlements and cities stand
edgesevery edge, where roads go
harboursevery harbour
resourceslumber, brick, wool, grain and ore
numbersthe dice totals 2 to 12
gamethe turn, the dice, the bank and the rest of the table
lumber, brick, wool, grain, orethe five resources

A dot reads a part of something: me.points, game.robber.number, me.hand.ore.

A seat has:

PartWhat it is
pointsvictory points everyone can see (hidden victory point cards are not in it)
cardshow many resource cards it holds
handits cards by resource: hand.ore, hand.total, hand[r]. Only your own; for any other seat every count reads 0
devshow many development cards it holds
devits development cards by kind: dev.knight, dev.victory_point, dev.road_building, dev.year_of_plenty, dev.monopoly, dev.total. Only your own
playable1 for each kind of development card you hold and did not buy this turn, by kind as dev is. Only your own
knightsknights played
road_lengthits longest chain of roads
longest_road, largest_army1 while it holds the card
settlements, cities, roadspieces it has on the board
settlements_left, cities_left, roads_leftpieces it has not built
ratiothe rate it trades each resource to the bank at, 4, 3 or 2: ratio.wool, ratio[r]
leads1 when no seat has more points that everyone can see
turn1 when it is this seat's turn
nextthe 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.

WalkGives
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.

PartWhenIts own values
offer:your turn, for each offer of one or two cards of one resource for one card of anothergive, get, partner (always nobody: the offer goes to the table), pays, gain, default
answer:someone offers you a tradegive, get, partner (who offered), pays, gain, default
confirm:people accepted your offer, for each of themgive, get, partner (who accepted), pays, gain, default
robber:you move the robber, for each hex and each seat you could rob therehex, victim (or nobody), default
discard:a seven made you discard, one card at a timecard (the resource), default

In a trade:

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.

← Back to the game  ·  Privacy