yairm210 / Unciv

Open-source Android/Desktop remake of Civ V
Mozilla Public License 2.0
8.26k stars 1.55k forks source link

AI plays always on Chieftan difficulty #10830

Closed tuvus closed 1 month ago

tuvus commented 8 months ago

Game Version

since forever to 4.9.13

Describe the bug

There are a few comments looking through the code talking about how the AI is never unhappy. They found new cities very early, which is impossible for the player to do. And when using the new AutoPlay feature to play as a Civ against the AIs, I don't have the same benefits they do. Even on Prince difficulty, the AI swamps the AutoPlay Civ even though they supposedly have only an 85% maintenance modifier difference.

After some investigation, I found that in Civilization.kt lines 303 to 309 tell quite the story.

    fun getDifficulty(): Difficulty {
        if (isHuman()) return gameInfo.getDifficulty()
        // TODO We should be able to mark a difficulty as 'default AI difficulty' somehow
        val chieftainDifficulty = gameInfo.ruleset.difficulties["Chieftain"]
        if (chieftainDifficulty != null) return chieftainDifficulty
        return gameInfo.ruleset.difficulties.values.first()
    }

It turns out that the AI has been playing against us with the Cheftain difficulty all along! And if it can't find the chieftain difficulty, then it finds an even easier difficulty.

Unless there is any other background that I am missing, it seems like we should change this to be more in line with the expectations given to the players. Any thoughts?

Steps to Reproduce

  1. Start a new default game on prince difficulty
  2. Use AutoPlay until your civ dies
SeventhM commented 8 months ago

This is a direct copy of how it works in Civ 5. If there's actually a want to fix it, in theory it likely should be allowing a mod to specify what difficulty the AI play on by default. But it playing on chieftain is in fact, intentional

Edit: at least Firaxis realized for Brave New World that the AI was way too happy and made the AI difficulty a bit less easy for the AI

yairm210 commented 8 months ago

Sounds like a modoption to me, but unlike most modoptions this is actually one that should be applied to our base rulesets. Something like "AI always plays at [difficulty] difficulty" And once that's settled in, we can change default behavior to "AI plays on game difficulty" and have it overridable. That way we retain Civ V compatibility and allow for differences.

Unrelatedly, a difficulty-type conditional sounds interesting, to customize object behaviour... 🤔

tuvus commented 8 months ago

Okay, it's a little weird, though, how the players can't copy or learn from the AI at the beginning since they are not equal. I know that making a good AI is very hard and will never be as good as a good player. Can we at least state this clearly in the Civilopedia somewhere?

yairm210 commented 8 months ago

Sure, I don't think it's that interesting to the player what logic the AI is using to play with, though. I only know this because I needed to know in order to program it, it never affected me as a player.

yairm210 commented 8 months ago

In general if your strategy is 'learn from the AI' then it really doesn't matter what difficulty it's on, since it doesn't really change its behavior between difficulties. But that's for another time...

SomeTroglodyte commented 7 months ago

I would likely do the marking right in the difficulties json... And make it a Difficulty : RulesetObject on the way - the missing space is enough motivation, and I think sooner or later we will want difficulty-dependent triggerables? (btw, Speed being marked IsPartOfGameInfoSerialization is wrong - it's loaded from json, not saved, right?)

state this clearly

Comes as side effect - almost free.

```patch Index: android/assets/jsons/Civ V - Gods & Kings/Difficulties.json IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/android/assets/jsons/Civ V - Gods & Kings/Difficulties.json b/android/assets/jsons/Civ V - Gods & Kings/Difficulties.json --- a/android/assets/jsons/Civ V - Gods & Kings/Difficulties.json (revision 6e843770900cb7e9c6b9041e5b8a7e4592b9cde5) +++ b/android/assets/jsons/Civ V - Gods & Kings/Difficulties.json (date 1704242979756) @@ -55,7 +55,8 @@ "aiUnhappinessModifier": 1, "aisExchangeTechs": false, "turnBarbariansCanEnterPlayerTiles": 60, - "clearBarbarianCampReward": 40 + "clearBarbarianCampReward": 40, + "uniques": ["AI plays at this difficulty"] }, { "name": "Warlord", Index: core/src/com/unciv/models/ruleset/unique/UniqueTarget.kt IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/core/src/com/unciv/models/ruleset/unique/UniqueTarget.kt b/core/src/com/unciv/models/ruleset/unique/UniqueTarget.kt --- a/core/src/com/unciv/models/ruleset/unique/UniqueTarget.kt (revision 6e843770900cb7e9c6b9041e5b8a7e4592b9cde5) +++ b/core/src/com/unciv/models/ruleset/unique/UniqueTarget.kt (date 1704242724663) @@ -54,6 +54,7 @@ // Other Speed, + Difficulty, Tutorial, CityState(inheritsFrom = Global), ModOptions, @@ -82,7 +83,7 @@ // As Array so it can used in a vararg parameter list. val Displayable = arrayOf( Building, Unit, UnitType, Improvement, Tech, - Terrain, Resource, Policy, Promotion, Nation, Ruins, Speed + Terrain, Resource, Policy, Promotion, Nation, Ruins, Speed, Difficulty ) } } Index: core/src/com/unciv/models/ruleset/nation/Difficulty.kt IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/core/src/com/unciv/models/ruleset/nation/Difficulty.kt b/core/src/com/unciv/models/ruleset/nation/Difficulty.kt --- a/core/src/com/unciv/models/ruleset/nation/Difficulty.kt (revision 6e843770900cb7e9c6b9041e5b8a7e4592b9cde5) +++ b/core/src/com/unciv/models/ruleset/nation/Difficulty.kt (date 1704242724671) @@ -1,34 +1,34 @@ package com.unciv.models.ruleset.nation import com.unciv.models.ruleset.Ruleset +import com.unciv.models.ruleset.RulesetObject import com.unciv.models.ruleset.unique.Unique -import com.unciv.models.stats.INamed +import com.unciv.models.ruleset.unique.UniqueTarget import com.unciv.ui.components.fonts.Fonts import com.unciv.ui.screens.civilopediascreen.FormattedLine -import com.unciv.ui.screens.civilopediascreen.ICivilopediaText -class Difficulty: INamed, ICivilopediaText { +class Difficulty : RulesetObject() { override lateinit var name: String - var baseHappiness: Int = 0 - var extraHappinessPerLuxury: Float = 0f - var researchCostModifier:Float = 1f - var unitCostModifier:Float = 1f - var unitSupplyBase: Int = 5 - var unitSupplyPerCity: Int = 2 - var buildingCostModifier:Float = 1f - var policyCostModifier:Float = 1f - var unhappinessModifier:Float = 1f - var barbarianBonus:Float = 0f + var baseHappiness = 0 + var extraHappinessPerLuxury = 0f + var researchCostModifier = 1f + var unitCostModifier = 1f + var unitSupplyBase = 5 + var unitSupplyPerCity = 2 + var buildingCostModifier = 1f + var policyCostModifier = 1f + var unhappinessModifier = 1f + var barbarianBonus = 0f var barbarianSpawnDelay: Int = 0 var playerBonusStartingUnits = ArrayList() - var aiCityGrowthModifier:Float = 1f - var aiUnitCostModifier:Float = 1f - var aiBuildingCostModifier:Float = 1f - var aiWonderCostModifier:Float = 1f - var aiBuildingMaintenanceModifier:Float = 1f + var aiCityGrowthModifier = 1f + var aiUnitCostModifier = 1f + var aiBuildingCostModifier = 1f + var aiWonderCostModifier = 1f + var aiBuildingMaintenanceModifier = 1f var aiUnitMaintenanceModifier = 1f - var aiUnitSupplyModifier: Float = 0f + var aiUnitSupplyModifier = 0f var aiFreeTechs = ArrayList() var aiMajorCivBonusStartingUnits = ArrayList() var aiCityStateBonusStartingUnits = ArrayList() @@ -39,9 +39,7 @@ // property defined in json but so far unused: // var aisExchangeTechs = false - override var civilopediaText = listOf() - - + override fun getUniqueTarget() = UniqueTarget.Difficulty override fun makeLink() = "Difficulty/$name" private fun Float.toPercent() = (this * 100).toInt() @@ -108,6 +106,11 @@ lines += FormattedLine() lines += FormattedLine("{Turns until barbarians enter player tiles}: $turnBarbariansCanEnterPlayerTiles ${Fonts.turn}") lines += FormattedLine("{Gold reward for clearing barbarian camps}: $clearBarbarianCampReward ${Fonts.gold}") + + if (uniques.isEmpty()) return lines + lines += FormattedLine() + for (unique in uniques) + lines += FormattedLine(unique) return lines } Index: core/src/com/unciv/models/ruleset/unique/UniqueType.kt IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/core/src/com/unciv/models/ruleset/unique/UniqueType.kt b/core/src/com/unciv/models/ruleset/unique/UniqueType.kt --- a/core/src/com/unciv/models/ruleset/unique/UniqueType.kt (revision 6e843770900cb7e9c6b9041e5b8a7e4592b9cde5) +++ b/core/src/com/unciv/models/ruleset/unique/UniqueType.kt (date 1704242979740) @@ -780,6 +780,11 @@ //endregion + ///////////////////////////////////////////// region 11 GAME RULES ///////////////////////////////////////////// + AIDifficulty("AI plays at this difficulty", UniqueTarget.Difficulty), + + //endregion + ///////////////////////////////////////////// region 90 META ///////////////////////////////////////////// HiddenWithoutReligion("Hidden when religion is disabled", UniqueTarget.Unit, UniqueTarget.Building, UniqueTarget.Ruins, UniqueTarget.Tutorial, Index: android/assets/jsons/Civ V - Vanilla/Difficulties.json IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/android/assets/jsons/Civ V - Vanilla/Difficulties.json b/android/assets/jsons/Civ V - Vanilla/Difficulties.json --- a/android/assets/jsons/Civ V - Vanilla/Difficulties.json (revision 6e843770900cb7e9c6b9041e5b8a7e4592b9cde5) +++ b/android/assets/jsons/Civ V - Vanilla/Difficulties.json (date 1704243006201) @@ -55,7 +55,8 @@ "aiUnhappinessModifier": 1, "aisExchangeTechs": false, "turnBarbariansCanEnterPlayerTiles": 60, - "clearBarbarianCampReward": 40 + "clearBarbarianCampReward": 40, + "uniques": ["AI plays at this difficulty"] }, { "name": "Warlord", Index: core/src/com/unciv/logic/civilization/Civilization.kt IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== diff --git a/core/src/com/unciv/logic/civilization/Civilization.kt b/core/src/com/unciv/logic/civilization/Civilization.kt --- a/core/src/com/unciv/logic/civilization/Civilization.kt (revision 6e843770900cb7e9c6b9041e5b8a7e4592b9cde5) +++ b/core/src/com/unciv/logic/civilization/Civilization.kt (date 1704243210745) @@ -302,10 +302,11 @@ //region pure functions fun getDifficulty(): Difficulty { if (isHuman()) return gameInfo.getDifficulty() - // TODO We should be able to mark a difficulty as 'default AI difficulty' somehow - val chieftainDifficulty = gameInfo.ruleset.difficulties["Chieftain"] - if (chieftainDifficulty != null) return chieftainDifficulty - return gameInfo.ruleset.difficulties.values.first() + return gameInfo.ruleset.difficulties.values.run { + firstOrNull { it.hasUnique(UniqueType.AIDifficulty) } + ?: firstOrNull { it.name == "Chieftain " } // backwards mod compatibility + ?: first() + } } fun getDiplomacyManager(civInfo: Civilization) = getDiplomacyManager(civInfo.civName) ```

... anyone care to test? Then add a <when Player uses difficulty [x] or harder> Conditional?

github-actions[bot] commented 4 months ago

This issue is stale because it has been open 90 days with no activity. Remove stale label or comment or this will be closed in 15 days.

SeventhM commented 4 months ago

anyone care to test?

Completely forgot about this. Though, this seems like something that should be in modOptions, not Difficulty

Tbh, considering closing this myself as this only matters for BNW and mods, not any of the rulesets currently implemented

tuvus commented 4 months ago

Completely forgot about this. Though, this seems like something that should be in modOptions

Yes, modability is probably the solution. I don't particularly have the time to work on this now so I am fine with it being closed.

github-actions[bot] commented 1 month ago

This issue is stale because it has been open 90 days with no activity. Remove stale label or comment or this will be closed in 15 days.

github-actions[bot] commented 1 month ago

This issue was closed because it has been stalled for 5 days with no activity.