Development
AI Programming
- API Documentation
- Introduction
- info.nut file
- main.nut file
- Basics
- Using the road pathfinder
- Using the rail pathfinder
- Saving / Loading data
- Things you need to know
- Squirrel
- Lists
- Coping with OTTD errors
- Trams
- AI Libraries
- Forum
- Forum FAQ
- AI Behavior
- ShortNames in use
AIs
All articles in NoAI category
Contents |
Benefits
If you've tried to build a road pathfinder yourself, you may have noticed it's hard to get it correctly. Mainly corners on foundations are a problem. Using the pathfinder from the library will solve that for you, so you don't have to do the hard work yourself.
Initialization
As with any library, before you can use it, you need to import it. You can do this by placing the following line at the top of your code.
import("pathfinder.road", "RoadPathFinder", 3);
This imports the road pathfinder version 3 and names it RoadPathFinder
. You can now instance it like any other class:
local pathfinder = RoadPathFinder();
Now set what kind of road type you want to find a path for:
AIRoad.SetCurrentRoadType(AIRoad.ROADTYPE_ROAD);
Before you start finding a route, you may want to set some parameters first. The available parameters are:
Name | Default value | Meaning |
---|---|---|
cost.max_cost | 2000000000 | The maximum cost for a route. |
cost.tile | 100 | The cost for a single tile. |
cost.no_existing_road | 40 | The cost that is added to _cost_tile if no road exists yet. |
cost.turn | 100 | The cost that is added to _cost_tile if the direction changes. |
cost.slope | 200 | The extra cost if a road tile is sloped. |
cost.bridge_per_tile | 150 | The cost per tile of a new bridge, this is added to _cost_tile. |
cost.tunnel_per_tile | 120 | The cost per tile of a new tunnel, this is added to _cost_tile. |
cost.coast | 20 | The extra cost for a coast tile. |
cost.max_bridge_length | 10 | The maximum length of a bridge that will be build. Note that all existing bridges will be explored, regardless of their length. |
cost.max_tunnel_length | 20 | The maximum length of a tunnel that will be build. Note that all existing tunnels will be explored, regardless of their length. |
To set a parameter, you just use:
pathfinder.cost.tile = 100;
Planning a route
SO, now we know how to create an instance of the pathfinder and to set the cost functions, it's time to put that knowledge to some use. Let's plan our first road. First we need one (or more) source tile(s) and one (or more) goal tile(s). For now let's assume we already have those, as it's outside the scope of this page to explain how to get those. So say for example we want to build a road between tile_a and tile_b. First we call InitializePath
on our pathfinder object:
pathfinder.InitializePath([tile_a], [tile_b]);
The pathfinder expects an array of source tiles and an array of goal tiles, since we only have one source and one goal tile, we'll just create two arrays with one element each. Now the pathfinder is ready to find a path.
local path = false; while (path == false) { path = pathfinder.FindPath(100); AIController.Sleep(1); }
The parameter FindPath
accepts is the number of iterations it should do before returning. 100 is just fine. Don't make that value too big, as you'll notice slowdowns in OpenTTD. The AIController.Sleep(1);
line is really needed. FindPath
returns false if it isn't finished with pathfinding. When it's done, it returns either the path it found or null to indicate no route exists.
Building the planned route
Suppose the pathfinder returned a path, how do start building it? For a start, it's nice to know that FindPath
returns an instance of the AyStar.Path
class. To view the documentation, open bin/ai/library/graph/aystar/main.nut
. Basically, it has three functions: GetParent()
, GetCost()
and GetTile()
. GetCost()
isn't useful for now, so we'll just use GetParent()
and GetTile()
. To build the route found by the pathfinder, we call path.GetParent()
until it returns null. As the pathfinder can also return bridges / tunnels, we need to check the distance between the current node and the previous node. If that distance is more than 1, we should build a bridge / tunnel.
while (path != null) { local par = path.GetParent(); if (par != null) { local last_node = path.GetTile(); if (AIMap.DistanceManhattan(path.GetTile(), par.GetTile()) == 1 ) { if (!AIRoad.BuildRoad(path.GetTile(), par.GetTile())) { /* An error occurred while building a piece of road. TODO: handle it. * Note that this could mean the road was already built. */ } } else { /* Build a bridge or tunnel. */ if (!AIBridge.IsBridgeTile(path.GetTile()) && !AITunnel.IsTunnelTile(path.GetTile())) { /* If it was a road tile, demolish it first. Do this to work around expended roadbits. */ if (AIRoad.IsRoadTile(path.GetTile())) AITile.DemolishTile(path.GetTile()); if (AITunnel.GetOtherTunnelEnd(path.GetTile()) == par.GetTile()) { if (!AITunnel.BuildTunnel(AIVehicle.VT_ROAD, path.GetTile())) { /* An error occured while building a tunnel. TODO: handle it. */ } } else { local bridge_list = AIBridgeList_Length(AIMap.DistanceManhattan(path.GetTile(), par.GetTile()) + 1); bridge_list.Valuate(AIBridge.GetMaxSpeed); bridge_list.Sort(AIList.SORT_BY_VALUE, false); if (!AIBridge.BuildBridge(AIVehicle.VT_ROAD, bridge_list.Begin(), path.GetTile(), par.GetTile())) { /* An error occured while building a bridge. TODO: handle it. */ } } } } } path = par; }
This code loops over all the whole route. For every element, it checks whether it's already connected to the previous tile via road or via a bridge. If it's not connected it tries to build a road. Note that in a real AI you'll probably want to check if AIRoad.BuildRoad
returns true, and if not restart pathfinding.
Note: do not use parent as variable name for GetParent
, it's a keyword, giving peculiar results.
A sample AI
import("pathfinder.road", "RoadPathFinder", 3); class SampleAI extends AIController { } function SampleAI::Start() { /* Get a list of all towns on the map. */ local townlist = AITownList(); /* Sort the list by population, highest population first. */ townlist.Valuate(AITown.GetPopulation); townlist.Sort(AIAbstractList.SORT_BY_VALUE, false); /* Pick the two towns with the highest population. */ local townid_a = townlist.Begin(); local townid_b = townlist.Next(); /* Print the names of the towns we'll try to connect. */ AILog.Info("Going to connect " + AITown.GetName(townid_a) + " to " + AITown.GetName(townid_b)); /* Tell OpenTTD we want to build normal road (no tram tracks). */ AIRoad.SetCurrentRoadType(AIRoad.ROADTYPE_ROAD); /* Create an instance of the pathfinder. */ local pathfinder = RoadPathFinder(); /* Set the cost for making a turn extreme high. */ pathfinder.cost.turn = 5000; /* Give the source and goal tiles to the pathfinder. */ pathfinder.InitializePath([AITown.GetLocation(townid_a)], [AITown.GetLocation(townid_b)]); /* Try to find a path. */ local path = false; while (path == false) { path = pathfinder.FindPath(100); this.Sleep(1); } if (path == null) { /* No path was found. */ AILog.Error("pathfinder.FindPath return null"); } /* If a path was found, build a road over it. */ while (path != null) { local par = path.GetParent(); if (par != null) { local last_node = path.GetTile(); if (AIMap.DistanceManhattan(path.GetTile(), par.GetTile()) == 1 ) { if (!AIRoad.BuildRoad(path.GetTile(), par.GetTile())) { /* An error occured while building a piece of road. TODO: handle it. * Note that is can also be the case that the road was already build. */ } } else { /* Build a bridge or tunnel. */ if (!AIBridge.IsBridgeTile(path.GetTile()) && !AITunnel.IsTunnelTile(path.GetTile())) { /* If it was a road tile, demolish it first. Do this to work around expended roadbits. */ if (AIRoad.IsRoadTile(path.GetTile())) AITile.DemolishTile(path.GetTile()); if (AITunnel.GetOtherTunnelEnd(path.GetTile()) == par.GetTile()) { if (!AITunnel.BuildTunnel(AIVehicle.VT_ROAD, path.GetTile())) { /* An error occured while building a tunnel. TODO: handle it. */ } } else { local bridge_list = AIBridgeList_Length(AIMap.DistanceManhattan(path.GetTile(), par.GetTile()) + 1); bridge_list.Valuate(AIBridge.GetMaxSpeed); bridge_list.Sort(AIAbstractList.SORT_BY_VALUE, false); if (!AIBridge.BuildBridge(AIVehicle.VT_ROAD, bridge_list.Begin(), path.GetTile(), par.GetTile())) { /* An error occured while building a bridge. TODO: handle it. */ } } } } } path = par; } AILog.Info("Done"); }
Create an info.nut
for this AI and give it a try. Play a bit with various cost, for example setting pathfinder.cost.no_existing_road = pathfinder.cost.max_cost
. now the pathfinder will only find routes over already existing road. Not very useful if you want to build a new connection, but it can be nice if you want to check if a previously build connection is still OK.