What I've learned about AI and multiple improvements

Padmewan

King
Joined
Nov 26, 2003
Messages
748
Location
Planet
Sorry if this is old ground well-trodden. Anyone who digs into the AI scripts would probably already know this, but since I can't decipher that stuff yet, I decided to do this the old-fashioned way: experimentation.

Motivation: In our mod, we have 2 improvement types that generate food, hothouses (+1 food, +2 w/ civic) and terraforming, which actually changes the tile and then allows other improvements, but is coded to look like +2 food, -1 hammer to the AI. We found that the AI workers would sit around, waiting for the better improvement to come along, current unimproved status of the land nonwithstanding.

(It turns out the AI doesn't give a hoot about civics when weighing an improvement. I assume but don't know that it DOES account for tech changes, but I haven't run tests on that yet.)

Experiment: I created a "Super Farm" improvement and moved its tech location around.

With Super Farm at Writing, Calendar, and Music, automated workers refused to build regular farms and sat in the city instead. (Note that at Writing and Calendar, the UI displayed Super Farms as on the way, but not at Music, yet the results were the same for the AI).

At Future Tech, workers did go build farms. I didn't feel like trying every tech in between as a 4 tech gap in between is already too much for purposes of our mod.

Changing the Super Farm to -2 hammer didn't cause the workers to build regular Farms. Nor did making Farm+2 and Super Farm+3, nor did giving Roosevelt a +100 preference for Farm.

I would conclude that the AI is only able to handle ONE improvement per terrain type per yield value. When you go through all the vanilla terrain types and think about improvements that go with them, there is at max one per terrain type per yield. If there is more than one, the second is always (a) better, and (b) applies only in a special case (e.g. watermill better than workshop, but is only next to river. Perhaps watermill was coded to be allowed only on one side of the river to prevent the AI from not building cottages?).

If any modders out there have had a different experience, or know how to "fix" this in the AI scripting, we welcome your suggestions.

Otherwise, what I've now learned is KISS: Keep It Simple, Sucka.
 
This looks to be the core of the "what tiles will the AI improve logic", it is from CvUnitAI.

Code:
void CvUnitAI::AI_workerMove()
{
	PROFILE_FUNC();

	CvCity* pCity;
	bool bCanRoute;
	bool bNextCity;

	bCanRoute = canBuildRoute();
	bNextCity = false;

	// XXX could be trouble...
	if (plot()->getOwnerINLINE() != getOwnerINLINE())
	{
		if (AI_retreatToCity())
		{
			return;
		}
	}

	if (!(isHuman()))
	{
		if (plot()->getOwnerINLINE() == getOwnerINLINE())
		{
			if (AI_load(UNITAI_SETTLER_SEA, MISSIONAI_LOAD_SETTLER, UNITAI_SETTLE, 2, -1, -1, 0, MOVE_SAFE_TERRITORY))
			{
				return;
			}
		}
	}

	if (!(getGroup()->canDefend()))
	{
		if (GET_PLAYER(getOwnerINLINE()).AI_getPlotDanger(plot()) > 0)
		{
			if (AI_retreatToCity()) // XXX maybe not do this??? could be working productively somewhere else...
			{
				return;
			}
		}
	}

	if (AI_improveBonus(20))
	{
		return;
	}

	if (AI_improveBonus(10))
	{
		return;
	}

	if (bCanRoute)
	{
		if (AI_connectBonus())
		{
			return;
		}

		if (AI_connectCity())
		{
			return;
		}
	}

	pCity = NULL;

	if (plot()->getOwnerINLINE() == getOwnerINLINE())
	{
		pCity = plot()->getWorkingCity();
	}

	if (pCity == NULL)
	{
		pCity = GC.getMapINLINE().findCity(getX_INLINE(), getY_INLINE(), getOwnerINLINE()); // XXX do team???
	}

	if ((pCity == NULL) || (pCity->countNumImprovedPlots() > pCity->getPopulation()))
	{
		if (AI_improveBonus())
		{
			return;
		}

		if (AI_nextCityToImprove(pCity))
		{
			return;
		}

		bNextCity = true;
	}

	if (pCity != NULL)
	{
		if (AI_improveCity(pCity))
		{
			return;
		}
	}

	if (!bNextCity)
	{
		if (AI_improveBonus())
		{
			return;
		}

		if (AI_nextCityToImprove(pCity))
		{
			return;
		}
	}

	if (bCanRoute)
	{
		if (AI_routeTerritory(true))
		{
			return;
		}

		if (AI_connectBonus(false))
		{
			return;
		}

		if (AI_routeCity())
		{
			return;
		}
	}

	if (AI_irrigateTerritory())
	{
		return;
	}

	if (bCanRoute)
	{
		if (AI_routeTerritory())
		{
			return;
		}
	}

	if (!(isHuman()))
	{
		if (AI_nextCityToImproveAirlift())
		{
			return;
		}
	}

	if (!(isHuman()) && (AI_getUnitAIType() == UNITAI_WORKER))
	{
		if (GET_PLAYER(getOwnerINLINE()).AI_totalAreaUnitAIs(area(), UNITAI_WORKER) > GET_PLAYER(getOwnerINLINE()).AI_neededWorkers(area()))
		{
			if (GC.getGameINLINE().getElapsedGameTurns() > 10)
			{
				if (GET_PLAYER(getOwnerINLINE()).calculateUnitCost() > 0)
				{
					scrap();
					return;
				}
			}
		}
	}

	if (AI_retreatToCity(false, true))
	{
		return;
	}

	if (AI_retreatToCity())
	{
		return;
	}

	if (AI_safety())
	{
		return;
	}

b	getGroup()->pushMission(MISSION_SKIP);
	return;
}

You will notice that the above calls the AI_improveBonus code:

Code:
bool CvUnitAI::AI_improveBonus(int iMinValue)
{
	PROFILE_FUNC();

	CvPlot* pLoopPlot;
	CvPlot* pBestPlot;
	ImprovementTypes eImprovement;
	BuildTypes eBuild;
	BuildTypes eBestBuild;
	BuildTypes eBestTempBuild;
	BonusTypes eNonObsoleteBonus;
	int iPathTurns;
	int iValue;
	int iBestValue;
	int iBestTempBuildValue;
	int iI, iJ;

	iBestValue = 0;
	eBestBuild = NO_BUILD;
	pBestPlot = NULL;

	for (iI = 0; iI < GC.getMapINLINE().numPlotsINLINE(); iI++)
	{
		pLoopPlot = GC.getMapINLINE().plotByIndexINLINE(iI);

		if (AI_plotValid(pLoopPlot))
		{
			if (pLoopPlot->getOwnerINLINE() == getOwnerINLINE()) // XXX team???
			{
				eNonObsoleteBonus = pLoopPlot->getNonObsoleteBonusType(getTeam());

				if (eNonObsoleteBonus != NO_BONUS)
				{
					eImprovement = pLoopPlot->getImprovementType();

					if ((eImprovement == NO_IMPROVEMENT) || !(GET_PLAYER(getOwnerINLINE()).isOption(PLAYEROPTION_SAFE_AUTOMATION)))
					{
						if ((eImprovement == NO_IMPROVEMENT) || !(GC.getImprovementInfo(eImprovement).isImprovementBonusTrade(eNonObsoleteBonus)))
						{
							iBestTempBuildValue = MAX_INT;
							eBestTempBuild = NO_BUILD;

							for (iJ = 0; iJ < GC.getNumBuildInfos(); iJ++)
							{
								eBuild = ((BuildTypes)iJ);

								if (GC.getBuildInfo(eBuild).getImprovement() != NO_IMPROVEMENT)
								{
									if (GC.getImprovementInfo((ImprovementTypes) GC.getBuildInfo(eBuild).getImprovement()).isImprovementBonusTrade(eNonObsoleteBonus))
									{
										if (canBuild(pLoopPlot, eBuild))
										{
											iValue = 10000;

											iValue /= (GC.getBuildInfo(eBuild).getTime() + 1);

											// XXX feature production???

											if (iValue < iBestTempBuildValue)
											{
												iBestTempBuildValue = iValue;
												eBestTempBuild = eBuild;
											}
										}
									}
								}
							}

							if (eBestTempBuild != NO_BUILD)
							{
								if (!(pLoopPlot->isVisibleEnemyUnit(getOwnerINLINE())))
								{
									if (GET_PLAYER(getOwnerINLINE()).AI_plotTargetMissionAIs(pLoopPlot, MISSIONAI_BUILD, getGroup()) == 0)
									{
										if (generatePath(pLoopPlot, 0, true, &iPathTurns)) // XXX should this actually be at the top of the loop? (with saved paths and all...)
										{
											iValue = GET_PLAYER(getOwnerINLINE()).AI_bonusVal(eNonObsoleteBonus);

											if (GET_PLAYER(getOwnerINLINE()).getNumTradeableBonuses(eNonObsoleteBonus) == 0)
											{
												iValue *= 2;
											}

											if (iValue > iMinValue)
											{
												iValue *= 1000;

												iValue /= (iPathTurns + 1);

												if (iValue > iBestValue)
												{
													iBestValue = iValue;
													eBestBuild = eBestTempBuild;
													pBestPlot = pLoopPlot;
												}
											}
										}
									}
								}
							}
						}
					}
				}
			}
		}
	}

	if (pBestPlot != NULL)
	{
		FAssertMsg(eBestBuild != NO_BUILD, "BestBuild is not assigned a valid value");

		getGroup()->pushMission(MISSION_ROUTE_TO, pBestPlot->getX_INLINE(), pBestPlot->getY_INLINE(), 0, false, false, MISSIONAI_BUILD, pBestPlot);
		getGroup()->pushMission(MISSION_BUILD, eBestBuild, -1, 0, (getGroup()->getLengthMissionQueue() > 0), false, MISSIONAI_BUILD, pBestPlot);

		return true;
	}

	return false;
}

Notice the bestBuild logic int he above which is causing the AI to try to figure out what the best potential improvement is for a tile and wait for that one.
 
At 6:30am that code caused my brain to completely fry. I may have to send abother CTD to you Kael as payback!

:eek: Does that clear things up Pad?
 
The above code (the bottom one) seems to be the wrong code to look at. That one only controls if a bonus resource will be used when building another improvement, and which improvement allows this resource to be used at fastest.
It has nothing to do with waiting, Kael. :(

But thats the code we have to manipulate to build the correct nodes. ;)
 
The code you need is:
Code:
// Returns true if the unit found a build for this city...
bool CvUnitAI::AI_bestCityBuild(CvCity* pCity, CvPlot** ppBestPlot, BuildTypes* peBestBuild, CvPlot* pIgnorePlot)
{
    PROFILE_FUNC();

    CvPlot* pLoopPlot;
    BuildTypes eBuild;
    bool bBuild;
    int iValue;
    int iBestValue;
    int iI;

    iBestValue = 0;
    if (ppBestPlot != NULL)
    {
        *ppBestPlot = NULL;
    }
    if (peBestBuild != NULL)
    {
        *peBestBuild = NO_BUILD;
    }

    bBuild = false;

    for (iI = 0; iI < NUM_CITY_PLOTS; iI++)
    {
        pLoopPlot = plotCity(pCity->getX_INLINE(), pCity->getY_INLINE(), iI);

        if (pLoopPlot != NULL)
        {
            if (AI_plotValid(pLoopPlot))
            {
                if (pLoopPlot != pIgnorePlot)
                {
                    if ((pLoopPlot->getImprovementType() == NO_IMPROVEMENT) || !(GET_PLAYER(getOwnerINLINE()).isOption(PLAYEROPTION_SAFE_AUTOMATION)))
                    {
                        iValue = pCity->AI_getBestBuildValue(iI);

                        if (iValue > iBestValue)
                        {
                            eBuild = pCity->AI_getBestBuild(iI);

                            if (eBuild != NO_BUILD)
                            {
                                if (canBuild(pLoopPlot, eBuild))
                                {
                                    if (!(pLoopPlot->isVisibleEnemyUnit(getOwnerINLINE())))
                                    {
                                        // XXX take advantage of range (warning... this could lead to some units doing nothing...)
                                        if (GET_PLAYER(getOwnerINLINE()).AI_plotTargetMissionAIs(pLoopPlot, MISSIONAI_BUILD, getGroup()) == 0)
                                        {
                                            if (generatePath(pLoopPlot, 0, true))
                                            {
                                                iBestValue = iValue;
                                                if (ppBestPlot != NULL)
                                                {
                                                    *ppBestPlot = pLoopPlot;
                                                }
                                                if (peBestBuild != NULL)
                                                {
                                                    *peBestBuild = eBuild;
                                                }

                                                bBuild = true;
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    return bBuild;
}

and the real important function is (CvCityAI.cpp lines 3578 to 3992):

void CvCityAI::AI_bestPlotBuild(CvPlot* pPlot, int* piBestValue, BuildTypes* peBestBuild)

That function is 400 lines and decides which improvement is best for each plot, depending on the city. I won't copy that much code through. And I don't have the time to analyse it now. ...

EDIT: one thing that is easy to see: Stripmines probably won't be built, as the final improvement is considered for the value of each improvement.

EDIT2: There seems to be a lot of room for improvement in this function. This is really somethin one should look into, but i think several issues will be adressed in the Expansion as they are marked by firaxis for improvement.

One question through. Does one or both of your improvements spread irrigation? that might be the major factor in your problem.
 
Uh oh, thread forking! I guess I'll reply specifically here. Thanks, first of all, for all the great input and suggestions.
Chalid said:
EDIT: one thing that is easy to see: Stripmines probably won't be built, as the final improvement is considered for the value of each improvement.
Interesting. I wonder if there's some balance though between present and future value, or else in the modern era the AI will never build watermills?

What the AI really needs is some sense of present discounted value. Oh boy.

As to the actual implementation of stripmines, because the improvement itself is a stub (since it requires terraforming), the value of that stub can be set so that the AI uses it as appropriate. Perhaps it's the same value as a mine, so that only AI leaderheads that have a preference for it will ever build it...?

This may also explain why, when I originally coded terraforming as an upgrading improvement, the AI would both build regular +2 food farms and later upgrading terraforms that went from +1 to +6 food.

Chalid said:
EDIT2: There seems to be a lot of room for improvement in this function. This is really somethin one should look into, but i think several issues will be adressed in the Expansion as they are marked by firaxis for improvement.
I would be keen to see how this develops. I imagine that a truly flexible AI would be beyond their needs, and that instead the AI will be optimized for a certain set of assumptions (one from 1.0 to 1.61 seems to be that there is only one improvement type per terrain type per yield).

Chalid said:
One question through. Does one or both of your improvements spread irrigation? that might be the major factor in your problem.
We repurposed "irrigation" to be a concept used for energy, not food, so no.
 
Padmewan said:
We repurposed "irrigation" to be a concept used for energy, not food, so no.

I fear that won't work very well for the AI without some changes in the Code as there a some functions that link Irrigation to food.

Things like
Code:
 bEmphasizeIrrigation = bEmphasizeFood;
come to mind...
 
I must second Chalid thoughts on this, both that it needs improvement and it is the AI_bestPlotBuild that determines it, however in this function it calls calculateNaturalYield, which only calculates the "natural" yield of a plot. This way civic is not taken into account, but I dont know if tech (or tech in the future) is taken into account. Have to look at the code, when I get back home.
 
Top Bottom