AI:Need To Know

From OpenTTD
Jump to: navigation, search



Contents

Testing an AI

How to start only my AI?

The easiest way is to set number of AIs to zero in the AI/Game Script settings dialog, and in a new game type in the console:

start_ai <the name of your AI>

The AI of your choice will start immediately.

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

list_ai

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

Break on log message

Feature availability

<1.0

1.0-1.2

1.3

1.4

1.5-1.7

Nightly

As of revision r19544 there has been added a few new controls at the bottom of the debug panel for breaking an AI when certain log messages come up. In order to use it you type a string into the text box and when your AI prints a log message that matches this string, the AI will be suspended after the AILog call and at the end of the current tick, OpenTTD will be paused. When you hit the continue button or the regular pause button, your AI will continue to run. If you temporarily want to disable the matching without clearing the edit box, you can use the small toggle button to the left of the edit box.

To enable this feature you need to have the setting "gui.ai_developer_tools" enabled. To enable it you can open the in-game console and enter "set gui.ai_developer_tools 1".

AI Debug Panel - break on string

AIController::Break

From documentation: Break execution of the script when script developer tools are active (setting "gui.ai_developer_tools" is enabled). For other users, nothing will happen when you call this function. To resume the script, you have to click on the continue button in the AI debug window.

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 script=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 script=5 will send all information to the stdout.

Logging

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

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

Or by inputting debug_level script=5 in the console.

AI Fundamentals

GetTick()

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 its 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 built, 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
SetCompanyName()
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.

Opcodes

Remember back in the Introduction when we told you that the AI would be suspended after X opcodes? Well, that was an half-truth. To dwell a little in the details, an opcode is, basically, one squirrel instruction. The framework simply counts the number of squirrel instructions your AI does and puts it to sleep after a certain amount. What's insidious about this is that it is possible to call C++ code from within squirrel, which instructions aren't counted because, well, it's not squirrel.

For example, if I were to play on a 2048x2048 map and make use of a Valuator to valuate all tiles on the map using a valuator written in C++, I would steal all the CPU resources and "freeze" the game for some time. (Thanks to Morloth for the explanation and example!)

Game Mechanics

Map Coordinates

The AI placed this airport, using the top right most coordinate, which is also its 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 coordinate is (0,0), the bottom-most coordinate is (Max_X-1, Max_Y-1). 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.

However, an AI cannot access all coordinates as tiles. The outside edge of the map is reserved as non-buildable tiles. These are tiles in which the X or Y value is either 0, Max_X-1 (if X), or Max_Y-1 (if Y). The API will treat tiles with these coordinates as invalid.

This behavior varies, however, if the freeform_edges option is enabled. If freeform_edges is turned off, then the upper two edges (those where X or Y is 0) become accessible.

So with freeform_edges on, the top-most tile the AI can use is (1, 1). But with freeform_edges off, the top-most tile is (0, 0).

The bottom-most tile accessible to the AI is always (Max_X-2, Max_Y-2).

To get Max_X and Max_Y, the AIMap.GetMapSizeX() and AIMap.GetMapSizeY() functions can be used, respectively.

When 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));

Because the Tile object in C++ is just an Integer, it's possible to wrap around the borders of the map using this trick. To avoid wrapping around the edge of the map, you should check the distance to the the new tile and makes sure it's within the distance you expect.

Cargos

CargoID, like any ID, is an index. 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 allow changing of cargos in all kinds of ways. 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. Then 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 to industry).

list.Valuate(AICargo.IsFreight);
list.KeepValue(0);

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();

When using this method, you can get other types of cargo when loading certain NewGRFs. Nevertheless, there is always a cargo you can transport from and to towns, and there is always one that is most profitable. So it is not important if it is passengers or not. It doesn't matter what your AI transports as long as it makes profit (>=20k USD).


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.IsEnd(); cargo = list.Next()) {
  if (AICargo.HasCargoClass(cargo, AICargo.CC_PASSENGERS)) {
    pass_cargo = cargo;
    break;
  }
}
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);
cargoList.KeepValue(1);
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 need a truck station. So:

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

Towns

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