jdog5000
Revolutionary
Well, after a lot of searching and head scratching, I may have found it ... the (main) root cause for why the AI far too frequently will have a worker on one tile putting a workshop over a cottage, while a worker on an identical adjacent tile puts a cottage over a workshop.
This could be huge for the AI's late game economy, so the fix has to be right.
First, the problem code in CvCityAI::AI_bestPlotBuild ... this section of code is assigning a value to improvements the AI might build on a plot based on yield. This is only part of the code which does this, but it's the main part.
The logic here is that the value of a new improvement is equal to a weighted valuation of the change in yield from the current improvement.
For most improvement types, the array aiFinalYields holds double the food, hammer, and commerce yield of the plot with that improvement on it. The reason it's doubled is so that for cottages it can be the sum of the yields with a cottage plus the yields with a Town, so aiFinalYields/2 would be the average between cottage and town (with integer math, often you want to put off doing division until you've done all the multiplication you're going to do).
Two times the change in food, hammer, and commerce yield versus the tiles current yield is then assigned to aiDiffYields (bolded line).
Finally, the food, hammer, and commerce yield changes for the potential new improvement are given their respective weights and added to iValue.
So, as a example, if a grassland tile has a workshop (1F, 3H, 0C) on it and the AI is considering the value of placing a cottage there, the math would look like:
aiFinalYields for cottage:
4 (2*2)
0 (2*0)
6 (1 + 5)
aiDiffYields for cottage replacing workshop:
2
-6
6
Depending on the weights iFoodPriority, iProductionPriority, and iCommercePriority, the AI may decide to make the switch. Now, imagine it does go ahead and replace the workshop with a cottage. It will then look at the cottage and consider replacing it with a workshop, producing:
aiFinalYields for workshop:
2 (2*1)
6 (2*3)
0 (2*0)
aiDiffYields for workshop replacing cottage:
-2
6
-2
See the problem? It's asymmetric. When the AI is considering replacing a cottage, it uses only the current yield and not the sum/average between current and final. There's a significant range of the iFoodPriority, iProductionPriority, and iCommercePriority values where this will produce a constant flipping between workshops and cottages.
Now, this asymmetry also produces the following twist:
aiDiffYields for cottage replacing cottage:
0
0
4
Of course, the AI will never actually start building a cottage on top of a cottage, no issue there. If the code I've included above was the end of the valuation, then this twist would solve the problem with workshop/cottage swapping:
Cottage replacing workshop case:
iValue of cottage = (iValue of cottage over workshop)/2 + (iValue of town over workshop)/2
= (iValue of cottage over workshop) + (iValue of town over cottage)/2
iValue of workshop = 0
Replace if:
(iValue of cottage) > (iValue of workshop)
which reduces to
(iValue of cottage over workshop) + (iValue of town over cottage)/2 > 0
Workshop replacing cottage case:
iValue of workshop = (iValue of workshop over cottage)
= -(iValue of cottage over workshop)
iValue of cottage = (iValue of town over cottage)/2
Replace if:
(iValue of workshop) > (iValue of cottage)
which reduces to
-(iValue of cottage over workshop) > (iValue of town over cottage)/2
or equivalently
(iValue of cottage over workshop) + (iValue of town over cottage)/2 < 0
------------------------------------
Since (iValue of cottage over workshop) + (iValue of town over cottage)/2 cannot be both > 0 and < 0, there would be no flipping (assuming the priority weights for different yields stayed roughly constant).
However, if the iValue produced by this section of code is > 0 for an improvement, then there are a bunch of extra multipliers and adders that are considered. These extra factors depend on aiDiffYields but are quite different depending on which fields are possitive, so the asymmetry rears its head.
So, my proposed solution to this problem is to change this line:
so that instead of using 2x the current yield for cottage tiles, it uses the sum of the cottage and town yield. This would change the workshop over cottage case to:
Modified Workshop replacing cottage case:
iValue of workshop = (iValue of workshop over cottage) - (iValue of town over cottage)/2
iValue of cottage = 0
------------------------------------
Since this is potentially a big deal, I wanted to get feedback and ideas from all of you:
- Do you see any other simple change which could fix this flipping?
- Do you see any reason not to change this?
- What potential side-effects might there be?
This could be huge for the AI's late game economy, so the fix has to be right.
First, the problem code in CvCityAI::AI_bestPlotBuild ... this section of code is assigning a value to improvements the AI might build on a plot based on yield. This is only part of the code which does this, but it's the main part.
Code:
for (iJ = 0; iJ < NUM_YIELD_TYPES; iJ++)
{
aiFinalYields[iJ] = 2*(pPlot->calculateNatureYield(((YieldTypes)iJ), getTeam(), bIgnoreFeature));
aiFinalYields[iJ] += (pPlot->calculateImprovementYieldChange(eFinalImprovement, ((YieldTypes)iJ), getOwnerINLINE(), false));
aiFinalYields[iJ] += (pPlot->calculateImprovementYieldChange(eImprovement, ((YieldTypes)iJ), getOwnerINLINE(), false));
if (bIgnoreFeature && pPlot->getFeatureType() != NO_FEATURE)
{
aiFinalYields[iJ] -= 2 * GC.getFeatureInfo(pPlot->getFeatureType()).getYieldChange((YieldTypes)iJ);
}
[B]aiDiffYields[iJ] = (aiFinalYields[iJ] - (2 * pPlot->getYield(((YieldTypes)iJ))));[/B]
}
iValue += (aiDiffYields[YIELD_FOOD] * ((100 * iFoodPriority) / 100));
iValue += (aiDiffYields[YIELD_PRODUCTION] * ((60 * iProductionPriority) / 100));
iValue += (aiDiffYields[YIELD_COMMERCE] * ((40 * iCommercePriority) / 100));
iValue /= 2;
The logic here is that the value of a new improvement is equal to a weighted valuation of the change in yield from the current improvement.
For most improvement types, the array aiFinalYields holds double the food, hammer, and commerce yield of the plot with that improvement on it. The reason it's doubled is so that for cottages it can be the sum of the yields with a cottage plus the yields with a Town, so aiFinalYields/2 would be the average between cottage and town (with integer math, often you want to put off doing division until you've done all the multiplication you're going to do).
Two times the change in food, hammer, and commerce yield versus the tiles current yield is then assigned to aiDiffYields (bolded line).
Finally, the food, hammer, and commerce yield changes for the potential new improvement are given their respective weights and added to iValue.
So, as a example, if a grassland tile has a workshop (1F, 3H, 0C) on it and the AI is considering the value of placing a cottage there, the math would look like:
aiFinalYields for cottage:
4 (2*2)
0 (2*0)
6 (1 + 5)
aiDiffYields for cottage replacing workshop:
2
-6
6
Depending on the weights iFoodPriority, iProductionPriority, and iCommercePriority, the AI may decide to make the switch. Now, imagine it does go ahead and replace the workshop with a cottage. It will then look at the cottage and consider replacing it with a workshop, producing:
aiFinalYields for workshop:
2 (2*1)
6 (2*3)
0 (2*0)
aiDiffYields for workshop replacing cottage:
-2
6
-2
See the problem? It's asymmetric. When the AI is considering replacing a cottage, it uses only the current yield and not the sum/average between current and final. There's a significant range of the iFoodPriority, iProductionPriority, and iCommercePriority values where this will produce a constant flipping between workshops and cottages.
Now, this asymmetry also produces the following twist:
aiDiffYields for cottage replacing cottage:
0
0
4
Of course, the AI will never actually start building a cottage on top of a cottage, no issue there. If the code I've included above was the end of the valuation, then this twist would solve the problem with workshop/cottage swapping:
Cottage replacing workshop case:
iValue of cottage = (iValue of cottage over workshop)/2 + (iValue of town over workshop)/2
= (iValue of cottage over workshop) + (iValue of town over cottage)/2
iValue of workshop = 0
Replace if:
(iValue of cottage) > (iValue of workshop)
which reduces to
(iValue of cottage over workshop) + (iValue of town over cottage)/2 > 0
Workshop replacing cottage case:
iValue of workshop = (iValue of workshop over cottage)
= -(iValue of cottage over workshop)
iValue of cottage = (iValue of town over cottage)/2
Replace if:
(iValue of workshop) > (iValue of cottage)
which reduces to
-(iValue of cottage over workshop) > (iValue of town over cottage)/2
or equivalently
(iValue of cottage over workshop) + (iValue of town over cottage)/2 < 0
------------------------------------
Since (iValue of cottage over workshop) + (iValue of town over cottage)/2 cannot be both > 0 and < 0, there would be no flipping (assuming the priority weights for different yields stayed roughly constant).
However, if the iValue produced by this section of code is > 0 for an improvement, then there are a bunch of extra multipliers and adders that are considered. These extra factors depend on aiDiffYields but are quite different depending on which fields are possitive, so the asymmetry rears its head.
So, my proposed solution to this problem is to change this line:
Code:
aiDiffYields[iJ] = (aiFinalYields[iJ] - (2 * pPlot->getYield(((YieldTypes)iJ))));
so that instead of using 2x the current yield for cottage tiles, it uses the sum of the cottage and town yield. This would change the workshop over cottage case to:
Modified Workshop replacing cottage case:
iValue of workshop = (iValue of workshop over cottage) - (iValue of town over cottage)/2
iValue of cottage = 0
------------------------------------
Since this is potentially a big deal, I wanted to get feedback and ideas from all of you:
- Do you see any other simple change which could fix this flipping?
- Do you see any reason not to change this?
- What potential side-effects might there be?