AI:Need To Know

From OpenTTD
Revision as of 11:21, 21 July 2008 by TrueBrain (Talk | contribs)

Jump to: navigation, search


Testing an AI

How to start only my AI?

The easiest way is to set the difficulty setting to No AIs, and in a new game type in the console:

start_ai <the name of your AI>

The AI of your choice will start immediatly.

To list all the AIs in-game, type in the console:


The list of AIs detected by OpenTTD is also available via:

./openttd --help

Loading and restarting AI's

All AIs are reloaded when starting a new game. There is no need to restart the game; abandoning the current level and starting again is usually the easiest method to restart the AI from a scratch map (try newgame from console), or loading a scenario setup to be the same each time. The AI Debug Window also allows restarting individual AIs.

To load a specific scenario immediately, use:

./openttd -g relative/path/to/scenario.scn

Logging and Debug Panel

AI Debug panel is accessible from the Land Area Information (red question mark) labelled "AI Debug Console". All outputs of AILog.XXX() go to here, and can be selected per AI. The AI can also be restarted here. When restarting, the company of that AI is removed (with all his property), and a new AI controlled by the same script (which is reloaded from disk) is started. So when changing the AI, it is enough to just hit the Reload button to get your new AI to work.

AI Debug Panel

Developer Console Usage

The console is accessed using the ` key (next to "1") on the keyboard, and appears in the top part of the screen. This can be scrolled by using the Shift + Page Up/Down.

To get a separate window (Windows Only) use the -d parameter by itself (use -d ai=5 to get only AI information) on a shortcut:

./openttd -d

This provides this output (Windows Only):

Zuu's example of the developer console -d version outputting AI errors.

For linux, using -d ai=5 will send all information to the stdout.


AILog.XXX() commands can be output to the developer console directly by:

./openttd -d ai=5
openttd.exe -d ai=5

Or by inputting debug_level ai=5 in the console.

AI Fundamentals


When your Start() is called for the first time, GetTick() always returns 1. It is not an important value. It is just meant to give an indication of time, as we always find it useful to know when an AI spit out a message (was it ages ago, or was it recent). This said, you of course want to know when it's value is changed by the game:

Of course it changes when you do Sleep(), and with the exact value you specified in the Sleep(). But it also changes when you execute a command. This has to do that in networking it takes time to do something, and as you want to know if something really build, we have to wait till we get a signal back from the server. Now to make your life easier, we made singleplayer and multiplayer simular: they both delay. In singleplayer this is always 1 tick, unless you configured it with SetDelay(). In multiplayer it is at least 1 tick, but depends on the server-configuration. If you set SetDelay() lower than the server-configuration, the server-configuration wins, and else SetDelay() does.

So what does this mean:

GetTick() -> 3
GetTick() -> 4 (or higher)

As you might notice, you can't use GetTick() for anything else but debugging. It isn't a good indicator of time, it isn't usable to do something every 10 ticks or something, nothing of that. You will need to make such a system yourself.

Game Mechanics

Map Coordinates

The AI placed this airport, using the top right most coordinate, which is also it's returned location. The airport size is X=4, Y=3

Map (X, Y) coordinates are done from the top to the bottom - the top most tile is (0,0), the bottom most tile is (Max_X, Max_Y). X is increased by going SW, and Y increased by going SE. Any time a more than one tile dimension object is returned, it'll be the most top coordinate, tending towards the top right.

When for example using AITile.IsBuildableRectangle with Lists, the list will contain those tiles where you can build an object of the size specified, where the returned tile is the one with the lowest X and Y value of the rectangle (or rather: the tile most North).

Distance Calculations

All in-game distances use Manhattan distance calculations, so the distance between (3,3) and (5,5) calculated by the game is 4:

abs(3-5)  + abs(3-5) = 4

Use AITile.GetDistanceManhattanToTile to get this distance between tiles. AITile.GetDistanceSquareToTile is much less necessary, calculating the same distance as 8:

(3-5)^2 + (3-5)^2 = 8

The distance might not be exactly what a "plane would fly", since planes, boats and trains with the correct tracks can move diagonally, lowering the direct distance taken between tiles. Buses always must move Manhattan distances however due to having no type of diagonal roads. For an AI, you'll nearly always want to use AITile.GetDistanceManhattanToTile, as that is the function OpenTTD uses internally for distance calculations.

Tile Arithmetic

As part of various applications (e.g. pathfinding), an AI will need the ability to move around the map. Because of the way tile indices are represented, you can perform simple arithmetic on a tile index to find a new tile relative to its position. Adding or subtracting the result of AIMap.GetTileIndex(x, y) to a tile will yield the index of the tile x and y tiles from the original position.

For instance, the following code snippet will create a list of tiles directly adjacent to tile...

local adjacent = AITileList();
adjacent.AddTile(tile - AIMap.GetTileIndex(1,0));
adjacent.AddTile(tile - AIMap.GetTileIndex(0,1));
adjacent.AddTile(tile - AIMap.GetTileIndex(-1,0));
adjacent.AddTile(tile - AIMap.GetTileIndex(0,-1));

This snippet creates a list containing all tiles within an 11x11 square area centered around tile...

local area = AITileList();
area.AddRectangle(tile - AIMap.GetTileIndex(5, 5), tile + AIMap.GetTileIndex(5, 5));

To avoid falling off the edge of the map, you should ensure that tiles are checked with AIMap.IsValidTile().


CargoID, like any ID, is an index (for people who didn't figure that out yet). An index on its own means nothing, it is just a pointer to a value in an array, and in this case, a cargo-array. The main problem with Cargo, is that you can't trust a value to mean something in all cases. NewGRFs are very powerful, and allows changing of cargos in all kinds of way. Normally, CargoID 0 represents Passengers, but with a single NewGRF, this can be changed to, say, Coal. Now here is the tricky part of it all: no longer you can transport this cargo from and to towns. So, how to make sure your AI is compatible enough that it works in all cases?

Well, say you want to transport stuff from one town to the other town. Than you most likely don't care if it is passengers or mail, just the one which is giving you the most profit, right? Well, that is mail in this case, but how to find out?

First, make a cargo list:

local list = AICargoList();

Now filter what we want.. we want non-freight (short: mail, passengers, ... everything that doesn't go or goes to industry (find a dictionary to lookup the meaning in your language!)).


Next, we want the one that is most profitable for a given length (10 tiles in 2 days is what we use here, but use what ever value you want):

list.Valuate(AICargo.GetCargoIncome, 10, 2);

Now the best cargo to transport is the first entry of the list:

local cargo = list.Begin();

And it is that simple! And you will see, when using this method, you can get other types of cargo when loading certain NewGRFs. Nevertheless, it is always a cargo you can transport from and to towns, and it is always the one that is most profitable. So you shouldn't care if it is passengers or not, although some people claim you should. Your AI shouldn't matter what it transports, just as long as it makes profit!

Of course it is also possible to find a cargo based on the class of the cargo. Don't just use value '0' for passengers, it might not hold for all NewGRFs. What is safe to do, is find the cargo that belongs to AICargo.CC_PASSENGERS. To give a simple code snippet which illustrates this:

local list = AICargoList();
local pass_cargo = -1;
for (local cargo = list.Begin(); list.HasNext(); cargo = list.Next()) {
  if (AICargo.HasCargoClass(cargo, AICargo.CC_PASSENGERS)) {
    pass_cargo = cargo;
if (pass_cargo == -1) AILog.Error("Your game doesn't have any passengers cargo, and as we are a passenger only AI, we can't do anything");

An alternative way:

local cargoList = AICargoList();
cargoList.Valuate(AICargo.HasCargoClass, AICargo.CC_PASSENGERS);
if (cargoList.Count() == 0) AILog.Error("Your game doesn't have any passengers cargo, and as we are a passenger only AI, we can't do anything");
local paxCargo = cargoList.Begin();

At the end, either it gave an error, or pass_cargo is the value of the first cargo belonging to the CC_PASSENGERS class. More complex solutions are possible by using the Valuator and find that cargo belonging to the CC_PASSENGERS class that gives the most profit. Of course, without any NewGRFs this is always Passengers.

Bus station or Truck station

There is one simple way to find out if the cargo you want to transport should go to a bus station or a truck station. And it is the most obvious: if the cargo belongs to the CC_PASSENGERS class, it needs a bus station. All other cases needs a truck station. So:

local truck_station = !AICargo.HasCargoClass(cargo, AICargo.CC_PASSENGERS);

It could not be more simple!


If a town seed tile hasn't been destroyed (a very rare occurrence) the tile returned by AITown.GetLocation() is always a road tile. Note that not all road tiles in a town are guaranteed to be connected to the seed tile.

Personal tools