Artificial Intelligence (AI)
This document is about Freeciv21’s default AI.
Warning
The contents of this page are over 20 years old and have not been reviewed for correctness.
Introduction
The Freeciv21 AI is capable of developing complex nations and provides a challenging opponent for
beginners. It is, however, still too easy for experienced players, mostly due to it being very predictable.
Code that implements the AI is divided between the ai/
and server/advisors
code
directories. The latter is used also by human players for automatic helpers such as auto-settlers and
auto-explorers.
Long-Term AI Development Goals
The long-term goals for Freeciv21 AI development are:
Want Calculations
Build calculations are expressed through a structure called adv_choice
. This has a variable called
“want”, which determines how much the AI wants whatever item is pointed to by choice->type
.
choice->want
is:
Value |
Result |
---|---|
-199 |
get_a_boat |
< 0 |
an error |
== 0 |
no want, nothing to do |
<= 100 |
normal want |
> 100 |
critical want, used to requisition emergency needs |
> ??? |
probably an error (1024 is a reasonable upper bound) |
> 200 |
Frequently used as a cap. When want exceeds this value, it is reduced to a lower number. |
These are ideal numbers, your mileage while travelling through the code may vary considerably. Technology and Diplomats, in particular, seem to violate these standards.
Amortize
Hard fact: amortize(benefit, delay)
returns
\(\texttt{benefit} \times \left(1 - \frac{1}{\texttt{MORT}}\right)^{\texttt{delay}}\).
Speculation: What is better… to receive $10 annually starting in 5 years from now or $5 annually starting
from this year? How can you take inflation into account? The function amortize()
is meant to help you
answer these questions. To achieve this, it re-scales the future benefit in terms of today’s money.
Suppose we have a constant rate of inflation, \(x\) percent. Then in five years $10 will buy as much
as \(10\left(\frac{100}{100 + x}\right)^5\) will buy today. Denoting \(\frac{100}{100+x}\) by
\(q\) we get the general formula, \(N\) dollars, \(Y\) years from now will be worth
\(N\times q^Y\) in today’s money. If we receive \(N\) every year starting \(Y\) years from now, the
total amount receivable (in today’s money) is \(\frac{\,N\,\times\, q^Y}{1 - q}\). This is the sum of
infinite geometric series. This is exactly the operation that the amortize()
function performs, the
multiplication by some \(q < 1\) raised to power \(Y\). Note that the factor \(\frac{1}{1 - q}\)
does not depend on the parameters \(N\) and \(Y\), and can be ignored. The connection between the
\(\texttt{MORT}\) constant and the inflation rate \(x\) is given by
\(\frac{\texttt{MORT} - 1}{\texttt{MORT}} = q = \frac{100}{100 + x}\). Thus the current value of
\(\texttt{MORT} = 24\) corresponds to the inflation rate, or the rate of expansion of your civilization of
4.3%
Most likely this explanation is not what the authors of amortize()
had in mind, but the basic idea is
correct: the value of the payoff decays exponentially with the delay.
The version of amortize used in the military code (military_amortize()
) remains a complete mystery.
Estimation of Profit From a Military Operation
This estimation is implemented by the kill_desire()
function, which is not perfect, the
multi-victim
part is flawed, plus some corrections. In general:
where \(\texttt{Amortization\_Factor}\) is a function of the estimated time length of the operation and \(\texttt{Operation\_Profit} = \texttt{Battle\_Profit} - \texttt{Maintenance}\), where in turn \(\texttt{Maintenance} = (\texttt{Support} + \texttt{Unhappiness\_Compensation}) \times \texttt{Operation\_Time}\)
Here \(\texttt{Unhappiness\_Compensation}\) is from a military unit being away from home and \(\texttt{Support}\) is the number of Shields spent on supporting this unit per turn.
That is \(\texttt{Battle\_Profit}\) is a probabilistic average. It answers the question: “How much better off, on average, would we be from attacking this enemy unit?”
Selecting Military Units
The code dealing with choosing military units to be built and targets for them is especially messy.
Military units are requested in the military_advisor_choose_build()
function. It first considers the
defensive units and then ventures into selection of attackers (if home is safe). There are two possibilities
here: we just build a new attacker or we already have an attacker which was forced, for some reason, to defend.
In the second case it is easy: we calculate how good the existing attacker is and if it is good, we build a
defender to free it up.
Building a brand new attacker is more complicated. First, the ai_choose_attacker_*
functions are
called to find the first approximation to the best attacker that can be built here. This prototype attacker
is selected using very simple \(\texttt{attack\_power}\times\texttt{speed}\) formula. Then, already in the
kill_something_with()
function, we search for targets for the prototype attacker using the
find_something_to_kill()
function. Having found a target, we do the last refinement by calling the
process_attacker_want()
function to look for the best attacker type to take out the target. This type
will be our attacker of choice. Note that the function process_attacker_want()
function has side-effects
with regards to the Technology selection.
Here is an example:
First the ai_choose_attacker_land()
function selects a Dragoon because it is strong and fast.
Then the find_something_to_kill()
function finds a victim for the (virtual) Dragoon, an enemy
Riflemen standing right next to the city. Then the process_attacker_want()
function figures
out that since the enemy is right beside us, it can be taken out easier using an Artillery. It also
figures that a Howitzer would do this job even better, so bumps up our desire for
Robotics.
Ferry System
The ferry (i.e. boats transporting land units) system of Freeciv21 is probably better described by statistical mechanics than by logic. Both ferries and prospective passengers move around in what looks like a random fashion, trying to get closer to each other. On average, they succeed. This behavior has good reasons behind it. It is hell to debug, but means that small bugs do not affect the overall picture visibly.
Each turn both boats and prospective passengers forget all about prior arrangements (unless the passenger is actually in the boat). Then each will look for the closest partner, exchange cards, and head towards it. This is done in a loop which goes through all units in random order.
Because most units recalculate their destination every turn, ignoring prior arrangements is the only good strategy. It means that a boat will not rely on the prospective passenger to notify it when it is not needed anymore. This is not very effective, but can only be changed when the prospective passengers behave more responsibly. See the Diplomat code for more responsible behavior. They try to check if the old target is still good before trying to find a new one.
When a boat has a passenger, it is a different story. The boat does not do any calculations, instead one of the passengers is given full control and it is the passenger who drives the boat.
Here are the main data fields used by the system. Value of ai.ferry
in the passenger unit is:
FERRY_NONE
: means that the unit has no need of a ferry.FERRY_WANTED
: means that the unit wants aferry >0 : id
of its ferry.
Value of ai.passenger
in the ferry unit can be either of:
FERRY_AVAILABLE
: means that the unit is a ferry and isavailable >0 : id
of its passenger.
When boat-building code stabilizes, it can be seen how many free boats there are, on average, per prospective passenger. If there are more boats than prospective passengers, it makes sense that only prospective passengers should look for boats. If boats are few, they should be the ones choosing. This can be done both dynamically, where both possibilities are coded and the appropriate is chosen every turn, and statically, after much testing only one system remains. Now they exist in parallel, although each developed to a different degree.
Diplomacy
The AI’s diplomatic behaviour is current only regulated by the diplomacy
server setting.
AI proposes Cease-fire on first contact.
AI is not very trusting for NEUTRAL and PEACE modes, but once it hits ALLIANCE, this changes completely, and it will happily hand over any technologies and maps it has to you. The only thing that will make the AI attack you then is if you build a Spaceship.
For people who want to hack at this part of the AI code, please note:
The
pplayers_at_war(p1,p2)
function returnsFALSE
ifp1==p2
The
pplayers_non_attack(p1,p2)
function returnsFALSE
ifp1==p2
The
pplayers_allied(p1,p2)
function returnsTRUE
ifp1==p2
The
pplayer_has_embassy(p1,p2)
function returnsTRUE
ifp1==p2
For example, we do not ever consider a Nation to be at War with themselves, we never consider a Nation to have any kind of non-attack treaty with themselves, and we always consider a Nation to have an Alliance with themself.
The introduction of Diplomacy is fraught with many problems. One is that it usually benefits only human players and not AI players, since humans are so much smarter, and know how to exploit Diplomacy. For AI’s, they mostly only add constraints on what it can do. This means Diplomacy either has to be optional, or have fine-grained controls on who can do what Diplomatic deals to whom, which are set from rulesets. The latter is not yet well implemented.
Difficulty Levels
There are currently seven difficulty levels:
Handicapped
Novice
Easy
Normal
Hard
Cheating
Experimental
The hard
level is no-holds-barred. Cheating
is the same except that it has ruleset defined extra
bonuses, while normal
has a number of handicaps. In easy
, the AI also does random stupid
things through the ai_fuzzy()
function. In novice
the AI researches slower than normal
players. The experimental
level is only for coding. You can gate new code with the H_EXPERIMENTAL
handicap and test experimental
level AI’s against hard
level AI’s.
Other handicaps used are:
Variable |
Result |
---|---|
|
Cannot build offensive Diplomats. |
|
Can get only 25 gold and Barbarians from Huts. |
|
Build defensive buildings without calculating need. |
|
Cannot set its national budget rates beyond government limits. |
|
Cannot target anything it does not know exists. |
|
Does not know which unseen tiles have Huts on them. |
|
Cannot see through fog of War. |
|
Does not build air units. |
|
Only knows |
|
Not very good at Diplomacy. |
|
Cannot skip Anarchy. |
|
Do not like being much larger than human. |
|
Always thinks its city is in danger. |
For an up-to-date list of all handicaps and their use for each difficulty level see ai/handicaps.h
.
Things That Need To Be Fixed
Cities do not realize units are on their way to defend it.
AI builds cities without regard to danger at that location.
AI will not build cross-country roads outside of the city vision radius.
Locally_zero_minimap
is not implemented when wilderness tiles change.If no path to a chosen victim is found, a new victim should be chosen.
Emergencies in two cities at once are not handled properly.
Explorers will not use ferryboats to get to new lands to explore. The AI will also not build units to explore new islands, leaving Huts alone.
AI sometimes believes that wasting a horde of weak military units to kill one enemy is profitable.
Stop building shore defense improvements in landlocked cities with a Lake adjacent.
Fix the AI valuation of Supermarket. It currently never builds it. See the
farmland_food()
andai_eval_buildings()
functions inadvdomestic.cpp
.Teach the AI to coordinate the units in an attack.
Idea Space
Friendly cities can be used as beachheads.
The
Assess_danger()
function should acknowledge positive feedback between multiple attackers.It would be nice for a bodyguard and charge to meet en-route more elegantly.
The
struct choice
should have a priority indicator in it. This will reduce the number of “special” want values and remove the necessity to have want capped, thus reducing confusion.