Added Caching for civ Resources, Stats and city Happiness information for massive performance improvements in some late games

This commit is contained in:
Yair Morgenstern 2019-06-20 23:03:28 +03:00
parent beedcc5896
commit e0f72af06d
19 changed files with 86 additions and 53 deletions

View File

@ -211,7 +211,10 @@ class GameInfo {
for(unit in civInfo.getCivUnits())
unit.updateViewableTiles() // this needs to be done after all the units are assigned to their civs and all other transients are set
}
for (civInfo in civilizations){
// We need to determine the GLOBAL happiness state in order to determine the city stats
for(cityInfo in civInfo.cities) cityInfo.cityStats.updateCityHappiness()
for (cityInfo in civInfo.cities) cityInfo.cityStats.update()
}
}

View File

@ -43,7 +43,7 @@ class Automation {
if (stats.food <= 2) rank += (stats.food * 1.2f) //food get more value to keep city growing
else rank += (2.4f + (stats.food - 2) / 2) // 1.2 point for each food up to 2, from there on half a point
if (civInfo.gold < 0 && civInfo.getStatsForNextTurn().gold <= 0) rank += stats.gold
if (civInfo.gold < 0 && civInfo.statsForNextTurn.gold <= 0) rank += stats.gold
else rank += stats.gold / 2
rank += stats.production
@ -129,7 +129,7 @@ class Automation {
.minBy{it.cost}
if (goldBuilding!=null) {
val choice = ConstructionChoice(goldBuilding.name,1.2f)
if (cityInfo.civInfo.getStatsForNextTurn().gold<0) {
if (cityInfo.civInfo.statsForNextTurn.gold<0) {
choice.choiceModifier=3f
}
relativeCostEffectiveness.add(choice)
@ -151,8 +151,9 @@ class Automation {
.minBy{it.cost}
if (happinessBuilding!=null) {
val choice = ConstructionChoice(happinessBuilding.name,1f)
if (cityInfo.civInfo.happiness > 5) choice.choiceModifier = 1/2f // less desperate
if (cityInfo.civInfo.happiness < 0) choice.choiceModifier = 3f // more desperate
val civHappiness = cityInfo.civInfo.getHappiness()
if (civHappiness > 5) choice.choiceModifier = 1/2f // less desperate
if (civHappiness < 0) choice.choiceModifier = 3f // more desperate
relativeCostEffectiveness.add(choice)
}
@ -217,7 +218,7 @@ class Automation {
}
//Army
if((!isAtWar && cityInfo.civInfo.getStatsForNextTurn().gold>0 && militaryUnits<cities*2)
if((!isAtWar && cityInfo.civInfo.statsForNextTurn.gold>0 && militaryUnits<cities*2)
|| (isAtWar && cityInfo.civInfo.gold > -50)) {
val militaryUnit = chooseMilitaryUnit(cityInfo)
val unitsToCitiesRatio = cities.toFloat() / (militaryUnits + 1)

View File

@ -75,7 +75,7 @@ class NextTurnAutomation{
}
}
if(civInfo.happiness < 5){
if(civInfo.getHappiness() < 5){
for(cityState in civInfo.gameInfo.civilizations
.filter { it.isCityState() && it.getCityStateType()==CityStateType.Mercantile }){
val diploManager = cityState.getDiplomacyManager(civInfo)
@ -300,7 +300,7 @@ class NextTurnAutomation{
else {
if (enemy.victoryType()!=VictoryType.Cultural
&& enemy.getCivUnits().filter { !it.type.isCivilian() }.size > enemy.cities.size
&& enemy.happiness > 0) {
&& enemy.getHappiness() > 0) {
continue //enemy AI has too large army and happiness. It continues to fight for profit.
}
tradeLogic.acceptTrade()
@ -334,7 +334,7 @@ class NextTurnAutomation{
if (civInfo.cities.isNotEmpty() && civInfo.diplomacy.isNotEmpty()) {
val ourMilitaryUnits = civInfo.getCivUnits().filter { !it.type.isCivilian() }.size
if (!civInfo.isAtWar() && civInfo.happiness > 0
if (!civInfo.isAtWar() && civInfo.getHappiness() > 0
&& ourMilitaryUnits >= civInfo.cities.size) { //evaluate war
val ourCombatStrength = Automation().evaluteCombatStrength(civInfo)
val enemyCivsByDistanceToOurs = civInfo.getKnownCivs()
@ -404,7 +404,7 @@ class NextTurnAutomation{
if(civInfo.isAtWar()) return // don't train settlers when you could be training troops.
if(civInfo.victoryType()==VictoryType.Cultural && civInfo.cities.size >3) return
if (civInfo.cities.any()
&& civInfo.happiness > civInfo.cities.size + 5
&& civInfo.getHappiness() > civInfo.cities.size + 5
&& civInfo.getCivUnits().none { it.name == Constants.settler }
&& civInfo.cities.none { it.cityConstructions.currentConstruction == Constants.settler }) {

View File

@ -47,8 +47,9 @@ class BattleDamage{
}
//https://www.carlsguides.com/strategy/civilization5/war/combatbonuses.php
if (combatant.getCivInfo().happiness < 0)
modifiers["Unhappiness"] = max(0.02f * combatant.getCivInfo().happiness,-0.9f) // otherwise it could exceed -100% and start healing enemy units...
val civHappiness = combatant.getCivInfo().getHappiness()
if (civHappiness < 0)
modifiers["Unhappiness"] = max(0.02f * civHappiness,-0.9f) // otherwise it could exceed -100% and start healing enemy units...
if(combatant.getCivInfo().policies.isAdopted("Populism") && combatant.getHealth() < 100){
modifiers["Populism"] = 0.25f

View File

@ -17,7 +17,7 @@ class CityConstructions {
@Transient lateinit var cityInfo: CityInfo
@Transient private var builtBuildingObjects=ArrayList<Building>()
var builtBuildings = ArrayList<String>()
var builtBuildings = HashSet<String>()
val inProgressConstructions = HashMap<String, Int>()
var currentConstruction: String = "Monument" // default starting building!
var currentConstructionIsUserSet = false
@ -213,7 +213,9 @@ class CityConstructions {
getConstruction(buildingName).postBuildEvent(this)
if (currentConstruction == buildingName)
cancelCurrentConstruction()
cityInfo.cityStats.update()
cityInfo.civInfo.updateDetailedCivResources() // this building could be a resource-requiring one
}
fun addCultureBuilding() {

View File

@ -95,6 +95,9 @@ class CityExpansionManager {
if(cityInfo.workedTiles.contains(tileInfo.position))
cityInfo.workedTiles = cityInfo.workedTiles.withoutItem(tileInfo.position)
tileInfo.owningCity=null
cityInfo.civInfo.updateDetailedCivResources()
cityInfo.cityStats.update()
}
fun takeOwnership(tileInfo: TileInfo){
@ -105,6 +108,7 @@ class CityExpansionManager {
cityInfo.tiles = cityInfo.tiles.withItem(tileInfo.position)
tileInfo.owningCity = cityInfo
cityInfo.population.autoAssignPopulation()
cityInfo.civInfo.updateDetailedCivResources()
cityInfo.cityStats.update()
for(unit in tileInfo.getUnits())

View File

@ -166,7 +166,7 @@ class CityStats {
// needs to be a separate function because we need to know the global happiness state
// in order to determine how much food is produced in a city!
// -3 happiness per city
fun getCityHappiness(): LinkedHashMap<String, Float> {
fun updateCityHappiness(){
val civInfo = cityInfo.civInfo
val newHappinessList = LinkedHashMap<String,Float>()
var unhappinessModifier = civInfo.getDifficulty().unhappinessModifier
@ -212,7 +212,6 @@ class CityStats {
// we don't want to modify the existing happiness list because that leads
// to concurrency problems if we iterate on it while changing
happinessList=newHappinessList
return newHappinessList
}
fun getStatsOfSpecialist(stat:Stat, policies: HashSet<String>): Stats {
@ -305,7 +304,7 @@ class CityStats {
stats.culture += 33f
if (policies.contains("Commerce") && cityInfo.isCapital())
stats.gold += 25f
if (policies.contains("Sovereignty") && cityInfo.civInfo.happiness >= 0)
if (policies.contains("Sovereignty") && cityInfo.civInfo.getHappiness() >= 0)
stats.science += 15f
if (policies.contains("Total War") && currentConstruction is BaseUnit && !currentConstruction.unitType.isCivilian() )
stats.production += 15f
@ -369,6 +368,7 @@ class CityStats {
}
fun update() {
updateCityHappiness()
updateBaseStatList()
updateStatPercentBonusList()
@ -389,7 +389,7 @@ class CityStats {
newCurrentCityStats.science *= 1 + statPercentBonusesSum.science / 100
newCurrentCityStats.culture *= 1 + statPercentBonusesSum.culture / 100
val isUnhappy = cityInfo.civInfo.happiness < 0
val isUnhappy = cityInfo.civInfo.getHappiness() < 0
if (!isUnhappy) // Regular food bonus revoked when unhappy per https://forums.civfanatics.com/resources/complete-guide-to-happiness-vanilla.25584/
newCurrentCityStats.food *= 1 + statPercentBonusesSum.food / 100
@ -424,6 +424,8 @@ class CityStats {
if (cityInfo.resistanceCounter > 0)
currentCityStats = Stats().add(Stat.Production,1f) // You get nothing, Jon Snow
else currentCityStats = newCurrentCityStats
cityInfo.civInfo.updateStatsForNextTurn()
}
private fun updateFoodEaten() {

View File

@ -44,9 +44,10 @@ class CivilizationInfo {
/** This is for performance since every movement calculation depends on this, see MapUnit comment */
@Transient var hasActiveGreatWall = false
@Transient var statsForNextTurn = Stats()
@Transient var detailedCivResources = ResourceSupplyList()
var gold = 0
var happiness = 15
@Deprecated("As of 2.11.1") var difficulty = "Chieftain"
var playerType = PlayerType.AI
var civName = ""
@ -78,7 +79,6 @@ class CivilizationInfo {
fun clone(): CivilizationInfo {
val toReturn = CivilizationInfo()
toReturn.gold = gold
toReturn.happiness = happiness
toReturn.playerType = playerType
toReturn.civName = civName
toReturn.tech = tech.clone()
@ -132,7 +132,9 @@ class CivilizationInfo {
else return VictoryType.Neutral
}
fun getStatsForNextTurn():Stats = getStatMapForNextTurn().values.toList().reduce{a,b->a+b}
fun updateStatsForNextTurn(){
statsForNextTurn = getStatMapForNextTurn().values.toList().reduce{a,b->a+b}
}
fun getStatMapForNextTurn(): HashMap<String, Stats> {
val statMap = HashMap<String,Stats>()
@ -154,7 +156,7 @@ class CivilizationInfo {
}
}
for (entry in getHappinessForNextTurn()) {
for (entry in getHappinessBreakdown()) {
if (!statMap.containsKey(entry.key))
statMap[entry.key] = Stats()
statMap[entry.key]!!.happiness += entry.value
@ -220,7 +222,9 @@ class CivilizationInfo {
return transportationUpkeep
}
fun getHappinessForNextTurn(): HashMap<String, Float> {
fun getHappiness() = getHappinessBreakdown().values.sum().roundToInt()
fun getHappinessBreakdown(): HashMap<String, Float> {
val statMap = HashMap<String,Float>()
statMap["Base happiness"] = getDifficulty().baseHappiness.toFloat()
@ -230,7 +234,7 @@ class CivilizationInfo {
.count { it.resourceType === ResourceType.Luxury } * happinessPerUniqueLuxury
for(city in cities.toList()){
for(keyvalue in city.cityStats.getCityHappiness()){
for(keyvalue in city.cityStats.happinessList){
if(statMap.containsKey(keyvalue.key))
statMap[keyvalue.key] = statMap[keyvalue.key]!!+keyvalue.value
else statMap[keyvalue.key] = keyvalue.value
@ -258,23 +262,23 @@ class CivilizationInfo {
}
/**
* Returns list of all resources and their origin - for most usages you'll want getCivResources().totalSupply(),
* Returns list of all resources and their origin - for most usages you'll want updateDetailedCivResources().totalSupply(),
* which unifies al the different sources
*/
fun getCivResources(): ResourceSupplyList {
val newResourceSupplyList=ResourceSupplyList()
for(resourceSupply in getDetailedCivResources())
for(resourceSupply in detailedCivResources)
newResourceSupplyList.add(resourceSupply.resource,resourceSupply.amount,"All")
return newResourceSupplyList
}
fun getDetailedCivResources(): ResourceSupplyList {
val civResources = ResourceSupplyList()
for (city in cities) civResources.add(city.getCityResources())
for (dip in diplomacy.values) civResources.add(dip.resourcesFromTrade())
fun updateDetailedCivResources() {
val newDetailedCivResources = ResourceSupplyList()
for (city in cities) newDetailedCivResources.add(city.getCityResources())
for (dip in diplomacy.values) newDetailedCivResources.add(dip.resourcesFromTrade())
for(resource in getCivUnits().mapNotNull { it.baseUnit.requiredResource }.map { GameBasics.TileResources[it]!! })
civResources.add(resource,-1,"Units")
return civResources
newDetailedCivResources.add(resource,-1,"Units")
detailedCivResources = newDetailedCivResources
}
/**
@ -294,16 +298,24 @@ class CivilizationInfo {
fun getCivUnits(): List<MapUnit> = units
fun addUnit(mapUnit: MapUnit){
fun addUnit(mapUnit: MapUnit, updateCivInfo:Boolean=true){
val newList = ArrayList(units)
newList.add(mapUnit)
units=newList
if(updateCivInfo) {
// Not relevant when updating tileinfo transients, since some info of the civ itself isn't yet available,
// and in any case it'll be updated once civ info transients are
updateStatsForNextTurn() // unit upkeep
updateDetailedCivResources()
}
}
fun removeUnit(mapUnit: MapUnit){
val newList = ArrayList(units)
newList.remove(mapUnit)
units=newList
updateStatsForNextTurn() // unit upkeep
}
fun getIdleUnits() = getCivUnits().filter { it.isIdle() }
@ -431,6 +443,7 @@ class CivilizationInfo {
setCitiesConnectedToCapitalTransients()
updateViewableTiles()
updateHasActiveGreatWall()
updateDetailedCivResources()
}
fun updateHasActiveGreatWall(){
@ -439,6 +452,8 @@ class CivilizationInfo {
}
fun startTurn(){
updateStatsForNextTurn() // for things that change when turn passes e.g. golden age, city state influence
// Generate great people at the start of the turn,
// so they won't be generated out in the open and vulnerable to enemy attacks before you can control them
if (cities.isNotEmpty()) { //if no city available, addGreatPerson will throw exception
@ -450,14 +465,13 @@ class CivilizationInfo {
setCitiesConnectedToCapitalTransients()
for (city in cities) city.startTurn()
happiness = getHappinessForNextTurn().values.sum().roundToInt()
getCivUnits().toList().forEach { it.startTurn() }
}
fun endTurn() {
notifications.clear()
val nextTurnStats = getStatsForNextTurn()
val nextTurnStats = statsForNextTurn
policies.endTurn(nextTurnStats.culture.toInt())
@ -484,7 +498,7 @@ class CivilizationInfo {
city.endTurn()
}
goldenAges.endTurn(happiness)
goldenAges.endTurn(getHappiness())
getCivUnits().forEach { it.endTurn() }
diplomacy.values.forEach{it.nextTurn()}
updateHasActiveGreatWall()
@ -604,6 +618,7 @@ class CivilizationInfo {
val currentPlayerCiv = UnCivGame.Current.gameInfo.getCurrentPlayerCivilization()
currentPlayerCiv.gold -= giftAmount
otherCiv.getDiplomacyManager(currentPlayerCiv).influence += giftAmount/10
updateStatsForNextTurn()
}
//endregion

View File

@ -92,7 +92,7 @@ class PolicyManager {
}
for (cityInfo in civInfo.cities)
cityInfo.cityStats.update()
cityInfo.cityStats.update() // This ALSO has the side-effect of updating the CivInfo startForNextTurn so we don't need to call it explicitly
if(!canAdoptPolicy()) shouldOpenPolicyPicker=false
}

View File

@ -61,7 +61,7 @@ class TechManager {
fun turnsToTech(techName: String): Int {
return Math.ceil( remainingScienceToTech(techName).toDouble()
/ civInfo.getStatsForNextTurn().science).toInt()
/ civInfo.statsForNextTurn.science).toInt()
}
fun isResearched(TechName: String): Boolean = techsResearched.contains(TechName)

View File

@ -221,8 +221,12 @@ class DiplomacyManager() {
for(trade in trades.toList()){
for(offer in trade.ourOffers.union(trade.theirOffers).filter { it.duration>0 }) {
offer.duration--
if(offer.duration==0)
civInfo.addNotification("["+offer.name+"] from [$otherCivName] has ended",null, Color.GOLD)
if(offer.duration==0) {
civInfo.addNotification("[" + offer.name + "] from [$otherCivName] has ended", null, Color.GOLD)
civInfo.updateStatsForNextTurn() // if they were bringing us gold per turn
civInfo.updateDetailedCivResources() // if they were giving us resources
}
}
if(trade.ourOffers.all { it.duration<=0 } && trade.theirOffers.all { it.duration<=0 }) {

View File

@ -522,10 +522,10 @@ class MapUnit {
(actions.random())()
}
fun assignOwner(civInfo:CivilizationInfo){
fun assignOwner(civInfo:CivilizationInfo, updateCivInfo:Boolean=true){
owner=civInfo.civName
this.civInfo=civInfo
civInfo.addUnit(this)
civInfo.addUnit(this,updateCivInfo)
}
//endregion

View File

@ -296,7 +296,7 @@ open class TileInfo {
if(civilianUnit!=null) civilianUnit!!.currentTile = this
for (unit in getUnits()) {
unit.assignOwner(tileMap.gameInfo.getCivilization(unit.owner))
unit.assignOwner(tileMap.gameInfo.getCivilization(unit.owner),false)
unit.setTransients()
}
}

View File

@ -106,7 +106,7 @@ class TradeEvaluation{
TradeType.City -> {
val city = tradePartner.cities.first { it.name==offer.name }
val stats = city.cityStats.currentCityStats
if(civInfo.happiness + city.cityStats.happinessList.values.sum() < 0)
if(civInfo.getHappiness() + city.cityStats.happinessList.values.sum() < 0)
return 0 // we can't really afford to go into negative happiness because of buying a city
val sumOfStats = stats.culture+stats.gold+stats.science+stats.production+stats.happiness+stats.food
return sumOfStats.toInt() * 100

View File

@ -45,7 +45,7 @@ class TradeLogic(val ourCivilization:CivilizationInfo, val otherCivilization: Ci
}
offers.add(TradeOffer("Gold".tr(), TradeType.Gold, 0, civInfo.gold))
offers.add(TradeOffer("Gold per turn".tr(), TradeType.Gold_Per_Turn, 30, civInfo.getStatsForNextTurn().gold.toInt()))
offers.add(TradeOffer("Gold per turn".tr(), TradeType.Gold_Per_Turn, 30, civInfo.statsForNextTurn.gold.toInt()))
if (!civInfo.isCityState() && !otherCivilization.isCityState()) {
for (city in civInfo.cities.filterNot { it.isCapital() })
offers.add(TradeOffer(city.name, TradeType.City, 0))
@ -111,6 +111,8 @@ class TradeLogic(val ourCivilization:CivilizationInfo, val otherCivilization: Ci
from.getDiplomacyManager(nameOfCivToDeclareWarOn).declareWar()
}
}
to.updateStatsForNextTurn()
to.updateDetailedCivResources()
}
transferTrade(ourCivilization,otherCivilization,currentTrade)

View File

@ -89,7 +89,7 @@ class EmpireOverviewScreen : CameraStageBaseScreen(){
centerTable.pack()
}
topTable.add(setResourcesButton)
if(currentPlayerCivInfo.getDetailedCivResources().isEmpty())
if(currentPlayerCivInfo.detailedCivResources.isEmpty())
setResourcesButton.disable()
topTable.pack()
@ -140,12 +140,12 @@ class EmpireOverviewScreen : CameraStageBaseScreen(){
happinessTable.defaults().pad(5f)
happinessTable.add("Happiness".toLabel().setFontSize(24)).colspan(2).row()
happinessTable.addSeparator()
for (entry in currentPlayerCivInfo.getHappinessForNextTurn()) {
for (entry in currentPlayerCivInfo.getHappinessBreakdown()) {
happinessTable.add(entry.key.tr())
happinessTable.add(entry.value.roundToInt().toString()).row()
}
happinessTable.add("Total".tr())
happinessTable.add(currentPlayerCivInfo.getHappinessForNextTurn().values.sum().roundToInt().toString())
happinessTable.add(currentPlayerCivInfo.getHappinessBreakdown().values.sum().roundToInt().toString())
happinessTable.pack()
return happinessTable
}
@ -349,7 +349,7 @@ class EmpireOverviewScreen : CameraStageBaseScreen(){
private fun getResourcesTable(): Table {
val resourcesTable=Table().apply { defaults().pad(10f) }
val resourceDrilldown = currentPlayerCivInfo.getDetailedCivResources()
val resourceDrilldown = currentPlayerCivInfo.detailedCivResources
// First row of table has all the icons
resourcesTable.add()

View File

@ -133,7 +133,7 @@ class CityInfoTable(private val cityScreen: CityScreen) : Table(CameraStageBaseS
for(stats in unifiedStatList.values) stats.happiness=0f
// add happiness to stat list
for(entry in cityStats.getCityHappiness().filter { it.value!=0f }){
for(entry in cityStats.happinessList.filter { it.value!=0f }){
if(!unifiedStatList.containsKey(entry.key))
unifiedStatList[entry.key]= Stats()
unifiedStatList[entry.key]!!.happiness=entry.value

View File

@ -106,7 +106,6 @@ class WorldScreen : CameraStageBaseScreen() {
gameClone.setTransients()
val cloneCivilization = gameClone.getCurrentPlayerCivilization()
kotlin.concurrent.thread {
currentPlayerCiv.happiness = gameClone.getCurrentPlayerCivilization().getHappinessForNextTurn().values.sum().toInt()
gameInfo.civilizations.forEach { it.setCitiesConnectedToCapitalTransients() }
}
@ -336,7 +335,7 @@ class WorldScreen : CameraStageBaseScreen() {
&& currentPlayerCiv.viewableTiles.any { it.getUnits().any { unit -> unit.civInfo.isBarbarianCivilization() } })
displayTutorials("BarbarianEncountered")
if(currentPlayerCiv.cities.size > 2) displayTutorials("SecondCity")
if(currentPlayerCiv.happiness < 0) displayTutorials("Unhappiness")
if(currentPlayerCiv.getHappiness() < 0) displayTutorials("Unhappiness")
if(currentPlayerCiv.goldenAges.isGoldenAge()) displayTutorials("GoldenAge")
if(gameInfo.turns >= 100) displayTutorials("ContactMe")
val resources = currentPlayerCiv.getCivResources()

View File

@ -128,7 +128,7 @@ class WorldScreenTopBar(val screen: WorldScreen) : Table() {
turnsLabel.setText("Turn".tr()+" " + civInfo.gameInfo.turns + " | "+ abs(year)+(if (year<0) " BC" else " AD"))
val nextTurnStats = civInfo.getStatsForNextTurn()
val nextTurnStats = civInfo.statsForNextTurn
val goldPerTurn = "(" + (if (nextTurnStats.gold > 0) "+" else "") + Math.round(nextTurnStats.gold) + ")"
goldLabel.setText(Math.round(civInfo.gold.toFloat()).toString() + goldPerTurn)
@ -136,7 +136,7 @@ class WorldScreenTopBar(val screen: WorldScreen) : Table() {
happinessLabel.setText(getHappinessText(civInfo))
if (civInfo.happiness < 0) {
if (civInfo.getHappiness() < 0) {
happinessLabel.setFontColor(malcontentColor)
happinessImage.clearChildren()
happinessImage.addActor(malcontentGroup)
@ -160,7 +160,7 @@ class WorldScreenTopBar(val screen: WorldScreen) : Table() {
}
private fun getHappinessText(civInfo: CivilizationInfo): String {
var happinessText = civInfo.happiness.toString()
var happinessText = civInfo.getHappiness().toString()
if (civInfo.goldenAges.isGoldenAge())
happinessText += " "+"GOLDEN AGE".tr()+"(${civInfo.goldenAges.turnsLeftForCurrentGoldenAge})"
else