[BTS] Development thread

Code Proposal:

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

	if (GET_PLAYER(getOwnerINLINE()).getNumCities() == 0)
	{
/************************************************************************************************/
/* Afforess	                  Start		 08/26/10                                               */
/*                                                                                              */
/* Check Adjacent Tiles for Better Spot                                                         */
/************************************************************************************************/
		if (!hasMoved() && canMove())
		{
			//Force Recalculation
			plot()->setFoundValue(getOwnerINLINE(), -1);
			for (int iPlot = 0; iPlot < NUM_DIRECTION_TYPES; iPlot++)
			{
				CvPlot* pAdjacentPlot = plotDirection(getX_INLINE(), getY_INLINE(), ((DirectionTypes)iPlot));
				if (pAdjacentPlot != NULL)
				{
					pAdjacentPlot->setFoundValue(getOwnerINLINE(), -1);
				}
			}
			
			int iCurrentValue = plot()->getFoundValue(getOwnerINLINE());
			CvPlot* pBestPlot = NULL;
			for (int iPlot = 0; iPlot < NUM_DIRECTION_TYPES; iPlot++)
			{
				CvPlot* pAdjacentPlot = plotDirection(getX_INLINE(), getY_INLINE(), ((DirectionTypes)iPlot));
				if (pAdjacentPlot != NULL)
				{
					int iPlotValue = pAdjacentPlot->getFoundValue(getOwnerINLINE());
					iPlotValue *= GC.getMOVE_DENOMINATOR();
					iPlotValue /= pAdjacentPlot->movementCost(this, plot());
					if (iPlotValue > iCurrentValue)
					{
						iCurrentValue = iPlotValue;
						pBestPlot = pAdjacentPlot;
					}
				}
			}
			if (pBestPlot != NULL)
			{
				getGroup()->pushMission(MISSION_MOVE_TO, pBestPlot->getX_INLINE(), pBestPlot->getY_INLINE(), MOVE_SAFE_TERRITORY, false, false, MISSIONAI_FOUND, pBestPlot);
				return;
			}
		}
/************************************************************************************************/
/* Afforess	                     END                                                            */
/************************************************************************************************/
...

This code forces the AI to look at adjacent tiles the settler can move to without using up all of its moves. So if the AI starts in a spot with a better spot directly next to it, the settler will move to the better spot and then found a city.
 
Code Proposal:

This code forces the AI to look at adjacent tiles the settler can move to without using up all of its moves. So if the AI starts in a spot with a better spot directly next to it, the settler will move to the better spot and then found a city.
I don't think your code does exactly what you say in all cases.
1.) If an adjacent plot has more than twice the foundValue of plot() then the standard settler with only 2 moves will go there, even if it doesn't have any movement points left then.
A non-standard settler unit with only 1 move will move to any adjacent plot that has a higher foundValue, also without caring that founding can't happen this turn.
2.) If the movement cost to one of the adjacent plots if lower than 1 move (road), the settler might decide to move there even if that plot has lower(!) foundValue.

Some improvement suggestions:
Code:
void CvUnitAI::AI_settleMove()
{
	PROFILE_FUNC();

	if (GET_PLAYER(getOwnerINLINE()).getNumCities() == 0)
	{
/************************************************************************************************/
/* Afforess	                  Start		 08/26/10                                               */
/*                                                                                              */
/* Check Adjacent Tiles for Better Spot                                                         */
/************************************************************************************************/
		if (!hasMoved() && canMove())
		{
[COLOR="Gray"][s]			//Force Recalculation
			plot()->setFoundValue(getOwnerINLINE(), -1);
			for (int iPlot = 0; iPlot < NUM_DIRECTION_TYPES; iPlot++)
			{
				CvPlot* pAdjacentPlot = plotDirection(getX_INLINE(), getY_INLINE(), ((DirectionTypes)iPlot));
				if (pAdjacentPlot != NULL)
				{
					pAdjacentPlot->setFoundValue(getOwnerINLINE(), -1);
				}
			}[/s][/COLOR]

			[u][COLOR="Green"]//Force Recalculation[/COLOR][/u]
			[U]plot()->setFoundValue(getOwnerINLINE(), -1);[/U]
			int iCurrentValue = plot()->getFoundValue(getOwnerINLINE());
			CvPlot* pBestPlot = NULL;
			for (int iPlot = 0; iPlot < NUM_DIRECTION_TYPES; iPlot++)
			{
				CvPlot* pAdjacentPlot = plotDirection(getX_INLINE(), getY_INLINE(), ((DirectionTypes)iPlot));
				if (pAdjacentPlot != NULL)
				{
					[COLOR="Green"]//Don't settle on top of a bonus, don't give up coast or river[/COLOR]
					[COLOR="Blue"]if ( pAdjacentPlot->getBonusType(NO_TEAM) != NO_BONUS || (plot()->isRiver() && !pAdjacentPlot->isRiver()) || (plot()->isCoastalLand(GC.getMIN_WATER_SIZE_FOR_OCEAN()) && !pAdjacentPlot->isCoastalLand(GC.getMIN_WATER_SIZE_FOR_OCEAN())) )
						continue;[/COLOR]
					
					[u][COLOR="Green"]//Force Recalculation[/COLOR][/u]
					[u]pAdjacentPlot->setFoundValue(getOwnerINLINE(), -1);[/u]

					int iPlotValue = pAdjacentPlot->getFoundValue(getOwnerINLINE());
[s]					[COLOR="Red"]iPlotValue *= GC.getMOVE_DENOMINATOR();
					iPlotValue /= pAdjacentPlot->movementCost(this, plot());[/COLOR][/s]
					if (iPlotValue > iCurrentValue [B]&& pAdjacentPlot->movementCost(this, plot()) < movesLeft()[/B])
					{
						iCurrentValue = iPlotValue;
						pBestPlot = pAdjacentPlot;
					}
				}
			}
			if (pBestPlot != NULL)
			{
				getGroup()->pushMission(MISSION_MOVE_TO, pBestPlot->getX_INLINE(), pBestPlot->getY_INLINE(), MOVE_SAFE_TERRITORY, false, false, MISSIONAI_FOUND, pBestPlot);
				return;
			}
		}
/************************************************************************************************/
/* Afforess	                     END                                                            */
/************************************************************************************************/
...
 
Okay - Thanks. I am not very familiar on how movement computations actually work in the SDK - so I tried to avoid showing my lack of knowledge. I know this feature had been talked about in the past - I wanted to get the ball rolling. Thanks Fuyu.
 
Movement cost between 2 adjancent tiles is done in movementCost(fromHere, toThere) and for everything else we need the pathfinder. Because what we really want is a path cost, which could in general be lower than movementCost. (Standard BTS example: You are standing on a plot without road, and you want to move to the adjancent hill plot. That normally costs 2 moves, but if that hills plot has a road and a third plot - which is adjacent to both your current plot and the hills plot - has a road too and is flatland, you can move there and to the hills afterwards, which only costs you 1 1/2 moves total.)

I admit, maybe that pathfinding isn't that obvious but I was lacking that knowledge too until 2 minutes ago. Now behold the magic :D

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

	if (GET_PLAYER(getOwnerINLINE()).getNumCities() == 0)
	{
[COLOR="Green"]/************************************************************************************************/
/* Afforess & Fuyu	                  Start      08/26/10                                       */
/*                                                                                              */
/* Check Adjacent Tiles for Better Spot                                                         */
/************************************************************************************************/[/COLOR]
		if (!hasMoved() && canMove())
		{
			[COLOR="Green"]//Force Recalculation[/COLOR]
			plot()->setFoundValue(getOwnerINLINE(), -1);
			int iCurrentValue = plot()->getFoundValue(getOwnerINLINE());
			CvPlot* pBestPlot = NULL;
			for (int iPlot = 0; iPlot < NUM_DIRECTION_TYPES; iPlot++)
			{
				CvPlot* pAdjacentPlot = plotDirection(getX_INLINE(), getY_INLINE(), ((DirectionTypes)iPlot));
				if (pAdjacentPlot != NULL)
				{
					[COLOR="Green"]//Don't settle on top of a bonus, don't give up coast or river[/COLOR]
					if ( pAdjacentPlot->getBonusType(NO_TEAM) != NO_BONUS || (plot()->isRiver() && !pAdjacentPlot->isRiver()) || (plot()->isCoastalLand(GC.getMIN_WATER_SIZE_FOR_OCEAN()) && !pAdjacentPlot->isCoastalLand(GC.getMIN_WATER_SIZE_FOR_OCEAN())) )
						continue;
					
					[COLOR="Green"]//Force Recalculation[/COLOR]
					pAdjacentPlot->setFoundValue(getOwnerINLINE(), -1);

					int iPlotValue = pAdjacentPlot->getFoundValue(getOwnerINLINE());
					if (iPlotValue > iCurrentValue)
					{
						[COLOR="Green"]//Can this unit reach the plot this turn? (getPathLastNode()->m_iData2 == 1)
						//Will this unit still have movement points left to found the city the same turn? (getPathLastNode()->m_iData1 > 0))[/COLOR]
						[B]if (generatePath(pAdjacentPlot) && (getPathLastNode()->m_iData2 == 1) && (getPathLastNode()->m_iData1 > 0))[/B]
						{
							iCurrentValue = iPlotValue;
							pBestPlot = pAdjacentPlot;
						}
					}
				}
			}
			if (pBestPlot != NULL)
			{
				getGroup()->pushMission(MISSION_MOVE_TO, pBestPlot->getX_INLINE(), pBestPlot->getY_INLINE(), MOVE_SAFE_TERRITORY, false, false, MISSIONAI_FOUND, pBestPlot);
				return;
			}
		}
[COLOR="Green"]/************************************************************************************************/
/* Afforess	                     END                                                            */
/************************************************************************************************/[/COLOR]
...

It was talked about in the past and back then I posted some code that couldn't work, I just didn't know how to do it, so thanks for your initiative, I think this one actually does :)
 
Excited to see what the next version looks like. That settler move stuff looks particularly interesting. Also interested to see what changes are made to civic evaluation, since that is something I've played with.

For example, it looked like healthiness on buildings was totally bypassed before:

Spoiler :
Code:
	for (iI = 0; iI < GC.getNumBuildingClassInfos(); iI++)
	{
/* Munch edit 24/11/09 */
/* edit so that healthiness is considered rather than ignored totally */
/* to revert, use the commented line */
//		iTempValue = kCivic.getBuildingHappinessChanges(iI);
		iTempValue = kCivic.getBuildingHappinessChanges(iI)+kCivic.getBuildingHealthChanges(iI);
/* end Munch edit */

This section seems to have been added by the previous better AI without any comments:

Spoiler :
Code:
	if( bWarPlan )
	{
		bWarPlan = false;
		int iEnemyWarSuccess = 0;

		for( int iTeam = 0; iTeam < MAX_CIV_TEAMS; iTeam++ )
		{
			if( GET_TEAM((TeamTypes)iTeam).isAlive() && !GET_TEAM((TeamTypes)iTeam).isMinorCiv() )
			{
				if( GET_TEAM(getTeam()).AI_getWarPlan((TeamTypes)iTeam) != NO_WARPLAN )
				{
					if( GET_TEAM(getTeam()).AI_getWarPlan((TeamTypes)iTeam) == WARPLAN_TOTAL || GET_TEAM(getTeam()).AI_getWarPlan((TeamTypes)iTeam) == WARPLAN_PREPARING_TOTAL )
					{
						bWarPlan = true;
						break;
					}

					if( GET_TEAM(getTeam()).AI_isLandTarget((TeamTypes)iTeam) )
					{
						bWarPlan = true;
						break;
					}

					iEnemyWarSuccess += GET_TEAM((TeamTypes)iTeam).AI_getWarSuccess(getTeam());
				}
			}
		}

		if( !bWarPlan )
		{
			if( iEnemyWarSuccess > std::min(getNumCities(), 4) * GC.getWAR_SUCCESS_CITY_CAPTURING() )
			{
				// Lots of fighting, so war is real
				bWarPlan = true;
			}
			else if( iEnemyWarSuccess > std::min(getNumCities(), 2) * GC.getWAR_SUCCESS_CITY_CAPTURING() )
			{
				if( GET_TEAM(getTeam()).AI_getEnemyPowerPercent() > 120 )
				{
					bWarPlan = true;
				}
			}
		}
	}

	if( !bWarPlan )
	{
		// Aggressive players will stick with war civics
		if( GET_TEAM(getTeam()).AI_getTotalWarOddsTimes100() > 200 )
		{
			bWarPlan = true;
		}
	}

And great general and domestic great general modifiers seem to be considered regardless of bWarPlan etc. So, with a large enough great general modifier, the civic will always be chosen regardless of circumstance (e.g. isolated for hundreds of years) ...

Spoiler :
Code:
	iValue += ((kCivic.getGreatGeneralRateModifier() * getNumMilitaryUnits()) / 50);
	iValue += ((kCivic.getDomesticGreatGeneralRateModifier() * getNumMilitaryUnits()) / 100);

Finally, war weariness appears to be considered at two separate points in the method (each without consideration of bWarPlan or similar)!

Spoiler :
Code:
	iValue += -((kCivic.getWarWearinessModifier() * getNumCities()) / ((bWarPlan) ? 10 : 50));
Code:
	if (kCivic.getWarWearinessModifier() != 0)
		int iAngerPercent = getWarWearinessPercentAnger();
		int iPopulation = 3 + (getTotalPopulation() / std::max(1, getNumCities()));

		int iTempValue = (-kCivic.getWarWearinessModifier() * iAngerPercent * iPopulation) / (GC.getPERCENT_ANGER_DIVISOR() * 100);
 
For war weariness, actually the first line you quote does include bWarPlan, and seems to aim to evaluate possible future gains, while the other parts attempts to evaluate the immediate happiness gains. The last part is outdated as I rewrote the whole happy/health gain evaluation for civics, and the other works reasonably well I think.

Considering bWarPlan for GG point modifiers is a very good idea.
Thank you for your input, and if you can be even more specific: how would you do it? Divide the whole thing by 2 or 3 is not bWarPlan?
 
For war weariness, actually the first line you quote does include bWarPlan, and seems to aim to evaluate possible future gains, while the other parts attempts to evaluate the immediate happiness gains. The last part is outdated as I rewrote the whole happy/health gain evaluation for civics, and the other works reasonably well I think.

Ah yes I must have missed the "/ ((bWarPlan) ? 10 : 50)". In my version I have the first code excerpt commented out, leaving only the second. Do you mean to say then that both these war weariness sections should be left, as they are? Or that a modification is required to take account of your rewritten happy/health evaluation?

Considering bWarPlan for GG point modifiers is a very good idea.
Thank you for your input, and if you can be even more specific: how would you do it? Divide the whole thing by 2 or 3 is not bWarPlan?

I discovered this because in my mod, I had a civic which only granted a great general bonus, and AI players were switching to it despite having no war plans. My primitive solution was simply to wrap the two great general lines within "if(bWarPlan) {}". My reasoning was that great general points are useless you are at war or plan to be at war soon.

However in a recent version of BBAI the bWarPlan section was rewritten, as I alluded to in my previous post. Some testing of this new method seems to show that bWarPlan is now set more frequently than it used to be, with the result being that in some cases bWarPlan does not lead to war. Because of the more frequent bWarPlan I'm not sure checking for bWarPlan is enough, but this is because I am not entirely sure how the new bWarPlan code works.
 
The old warplan code just checked if there was a warplan, the new one checks more, reducing the number of bWarPlan even. Only at the end, all aggressive players get bWarPlan true even when there is none..

I think this part can be savely removed:
Code:
	if( !bWarPlan )
	{
		[COLOR="Green"]// Aggressive players will stick with war civics[/COLOR]
		if( GET_TEAM(getTeam()).AI_getTotalWarOddsTimes100() > 200 )
		{
			bWarPlan = true;
		}
	}


edit:


And making GG points only ever add value if there is a war plan sounds good too. You will see many civs revolting as soon as they adopt a warplan though but one the other hand that's already happening quite frequently.

Ah yes I must have missed the "/ ((bWarPlan) ? 10 : 50)". In my version I have the first code excerpt commented out, leaving only the second. Do you mean to say then that both these war weariness sections should be left, as they are? Or that a modification is required to take account of your rewritten happy/health evaluation?
Both should be left in, as one is for present, and one is for possible future gains.
If you were to use my code, there is no modification required either. Happiness is simply handled with higher precision, and the "/ ((bWarPlan) ? 10 : 50)" is left as-is.
 
Thanks for the clarification, Fuyu, the bWarPlan code makes much more sense now.

As you suggest I will comment out the code which tells aggressive AIs to stick with war civics. I think doing this makes sense from the perspective of bWarPlan, as well as being in the best interests of aggressive AIs (everyone needs a bit of peace time).

I will also re-adopt a simple if(bWarPlan) for the great general points.
 
Final version of the settleMove addition proposed by Afforess:
Spoiler :

Code:
	if (GET_PLAYER(getOwnerINLINE()).getNumCities() == 0)
	{
[COLOR="Green"]/************************************************************************************************/
/* Afforess & Fuyu	                  Start      08/26/10                                       */
/*                                                                                              */
/* Check Adjacent Tiles for Better Spot                                                         */
/************************************************************************************************/[/COLOR]
		if (canMove())
		{
			[COLOR="Green"]//Force Recalculation[/COLOR]
			plot()->setFoundValue(getOwnerINLINE(), -1);
			int iCurrentValue = plot()->getFoundValue(getOwnerINLINE());
			CvPlot* pBestPlot = NULL;
			for (int iPlot = 0; iPlot < NUM_DIRECTION_TYPES; iPlot++)
			{
				CvPlot* pAdjacentPlot = plotDirection(getX_INLINE(), getY_INLINE(), ((DirectionTypes)iPlot));
				if (pAdjacentPlot != NULL)
				{
					[COLOR="Green"]//Don't give up coast or river[/COLOR]
					if ( (plot()->isRiver() && !pAdjacentPlot->isRiver()) || (plot()->isCoastalLand(GC.getMIN_WATER_SIZE_FOR_OCEAN()) && !pAdjacentPlot->isCoastalLand(GC.getMIN_WATER_SIZE_FOR_OCEAN())) )
						continue;
					
					[COLOR="Green"]//Force Recalculation[/COLOR]
					pAdjacentPlot->setFoundValue(getOwnerINLINE(), -1);
					int iPlotValue = pAdjacentPlot->getFoundValue(getOwnerINLINE());

					[COLOR="Green"]//Only settle on top of a bonus if it actually makes sense
					if (pAdjacentPlot->getBonusType(NO_TEAM) != NO_BONUS)[/COLOR]
					{
						if (pAdjacentPlot->calculateNatureYield(YIELD_FOOD, getTeam(), true) > 0)
						{
							iPlotValue *= 90;
							iPlotValue /= 100;
						}
						else
						{
							iPlotValue *= 95;
							iPlotValue /= 100;
						}
					}

					if (iPlotValue > iCurrentValue)
					{
						[COLOR="Green"]//Can this unit reach the plot this turn? (getPathLastNode()->m_iData2 == 1)[/COLOR]
						[COLOR="Green"]//Will this unit still have movement points left to found the city the same turn? (getPathLastNode()->m_iData1 > 0))[/COLOR]
						if (pAdjacentPlot->movementCost(this, plot()) < movesLeft() || generatePath(pAdjacentPlot) && (getPathLastNode()->m_iData2 == 1) && (getPathLastNode()->m_iData1 > 0))
						{
							iCurrentValue = iPlotValue;
							pBestPlot = pAdjacentPlot;
						}
					}
				}
			}
			if (pBestPlot != NULL)
			{
				if( gUnitLogLevel >= 2 )
				{
					logBBAI("    Settler not founding in place but moving to the better adjacent tile %d, %d", pBestPlot->getX_INLINE(), pBestPlot->getY_INLINE());
				}
				getGroup()->pushMission(MISSION_MOVE_TO, pBestPlot->getX_INLINE(), pBestPlot->getY_INLINE(), MOVE_SAFE_TERRITORY, false, false, MISSIONAI_FOUND, pBestPlot);
				return;
			}
		}
[COLOR="Green"]/************************************************************************************************/
/* Afforess & Fuyu	                     END                                                    */
/************************************************************************************************/[/COLOR]

GGP modifiers: I agree with Munch so I'll use
if (bWarPlan || isMinorCiv())
GGPs really are worthless otherwise.
 
Fuyu one thing that's been overlooked since AIAutoplay was created is that fact that when the AI takes over from a human, all of the units have been given the default UNITAI types, and the AI freaks out because it thinks it has huge missing pieces of it's army, since pretty much everything will be UNITAI_CITY_DEFENSE, UNITAI_ATTACK, and UNITAI_COUNTER. Is there anyway, without causing other bugs, to just have units built by a human player recieve the UNITAI an AI would give them with the player's current army composition; basically just run the standard unit creation code for setting the UNITAI without giving the default UNITAI type override when the unit is built by a human player?
 
You are not running any standard AI code if you choose a unit or anything else for city production as a human, and finding out what unitai a unit would have gotten, had it been chosen by the AI, is almost impossible.
There are 2 simple and 2 complicated* solutions
simple 1: Start AIAutoplay at turn 0 - that way all units get the right unitai :p
simple 2: Go into worldbuilder and manually set your own units' unitais to something that makes sense to you, and hopefully the AI too.

complicated 1: Make the AI go through all units at the start of AIAutoplay and reset unitais to whatever makes most sense for the AI.
complicated 2: Adapt the unitais of a human's units while the human plays, acording to how he uses the units,
ie if a human gives a city defender promotion to a unit and fortifies it in a city, give UNITAI_CITY_DEFENCE to the unit if it doesn't have that one already. CR promotion -> UNITAI_ATTACK_CITY, flanking promotion -> UNITAI_ATTACK. If a human unit attacks an enemy unit outside home cultural borders -> UNITAI_ATTACK, if it reveals new tiles -> UNITAI_EXPLORE, etc.

(*Complicated because someone would have to actually write that code, it does not exists yet)
 
complicated 1: Make the AI go through all units at the start of AIAutoplay and reset unitais to whatever makes most sense for the AI.

This seems like the easiest. I glanced through CvUnitAI and CvPlayer, and did a little bit of searching, and didn't come up with anything obvious. Do you know where a function is that performs some logic on a unit and decides the proper UNITAI for that unit?
 
Fuyu one thing that's been overlooked since AIAutoplay was created is that fact that when the AI takes over from a human, all of the units have been given the default UNITAI types, and the AI freaks out because it thinks it has huge missing pieces of it's army, since pretty much everything will be UNITAI_CITY_DEFENSE, UNITAI_ATTACK, and UNITAI_COUNTER. Is there anyway, without causing other bugs, to just have units built by a human player recieve the UNITAI an AI would give them with the player's current army composition; basically just run the standard unit creation code for setting the UNITAI without giving the default UNITAI type override when the unit is built by a human player?

It's not a major problem. Units almost always stick with their default unit AI. I found the only units that do regularly change are for MISSIONARY_SEA, SETTLER_SEA, and the other SEA unit AI's. Assuming your not in the midst of a full scale naval battle, the AI should perform admirably.

Feel free to prove this to yourself, and check were AI_setUnitAIType is called. It's called only 14 times in the SDK. ;)
 
Is anyone of you releasing a new inofficial bbai soon? I like all your suggestions/changes but I think I will miss something when picking them up step by step here in the threads.
 
Top Bottom