From 5cbc04b63ac68446899ddd734c3b8243727f3bea Mon Sep 17 00:00:00 2001 From: Oskar Niesen Date: Thu, 25 Jan 2024 15:27:11 -0600 Subject: [PATCH] Espionage automation (#10974) * Civilizations now send their spies out * Idle spies move to a city even if there is no tech to steal * Fixed moving spies * Game doesn't crash when the city the spy was at is taken over * Fixed crash when no other city is viewable * Spies no longer go to city states again * Added a new line for the test * Spies are now removed from a city when it is captured --- .../civilization/NextTurnAutomation.kt | 14 ++++++-- .../automation/unit/EspionageAutomation.kt | 33 +++++++++++++++++++ core/src/com/unciv/logic/battle/Battle.kt | 11 +++++++ core/src/com/unciv/models/Spy.kt | 3 ++ 4 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 core/src/com/unciv/logic/automation/unit/EspionageAutomation.kt diff --git a/core/src/com/unciv/logic/automation/civilization/NextTurnAutomation.kt b/core/src/com/unciv/logic/automation/civilization/NextTurnAutomation.kt index 77bb465622..ec3e061db6 100644 --- a/core/src/com/unciv/logic/automation/civilization/NextTurnAutomation.kt +++ b/core/src/com/unciv/logic/automation/civilization/NextTurnAutomation.kt @@ -2,6 +2,7 @@ package com.unciv.logic.automation.civilization import com.unciv.logic.automation.Automation import com.unciv.logic.automation.ThreatLevel +import com.unciv.logic.automation.unit.EspionageAutomation import com.unciv.logic.automation.unit.UnitAutomation import com.unciv.logic.city.City import com.unciv.logic.civilization.AlertType @@ -13,6 +14,7 @@ import com.unciv.logic.civilization.diplomacy.DiplomacyFlags import com.unciv.logic.civilization.diplomacy.DiplomaticModifiers import com.unciv.logic.civilization.diplomacy.DiplomaticStatus import com.unciv.logic.civilization.diplomacy.RelationshipLevel +import com.unciv.logic.civilization.managers.EspionageManager import com.unciv.logic.map.mapunit.MapUnit import com.unciv.models.ruleset.MilestoneType import com.unciv.models.ruleset.ModOptionsConstants @@ -68,9 +70,15 @@ object NextTurnAutomation { } automateUnits(civInfo) // this is the most expensive part - if (civInfo.isMajorCiv() && civInfo.gameInfo.isReligionEnabled()) { - // Can only be done now, as the prophet first has to decide to found/enhance a religion - ReligionAutomation.chooseReligiousBeliefs(civInfo) + if (civInfo.isMajorCiv()) { + if (civInfo.gameInfo.isReligionEnabled()) { + // Can only be done now, as the prophet first has to decide to found/enhance a religion + ReligionAutomation.chooseReligiousBeliefs(civInfo) + } + if (civInfo.gameInfo.isEspionageEnabled()) { + // Do after cities are conquered + EspionageAutomation.automateSpies(civInfo) + } } automateCities(civInfo) // second most expensive diff --git a/core/src/com/unciv/logic/automation/unit/EspionageAutomation.kt b/core/src/com/unciv/logic/automation/unit/EspionageAutomation.kt new file mode 100644 index 0000000000..f2ba0cb634 --- /dev/null +++ b/core/src/com/unciv/logic/automation/unit/EspionageAutomation.kt @@ -0,0 +1,33 @@ +package com.unciv.logic.automation.unit + +import com.unciv.logic.civilization.Civilization +import com.unciv.models.SpyAction + +object EspionageAutomation { + + fun automateSpies(civInfo: Civilization) { + val civsToStealFrom: List by lazy { + civInfo.getKnownCivs().filter {otherCiv -> otherCiv.isMajorCiv() && otherCiv.cities.any { it.getCenterTile().isVisible(civInfo) } + && civInfo.espionageManager.getTechsToSteal(otherCiv).isNotEmpty() }.toList() + } + + val getCivsToStealFromSorted: List = + civsToStealFrom.sortedBy { otherCiv -> civInfo.espionageManager.spyList + .count { it.isDoingWork() && it.getLocation()?.civ == otherCiv } + }.toList() + + for (spy in civInfo.espionageManager.spyList) { + if (spy.isDoingWork()) continue + if (civsToStealFrom.isNotEmpty()) { + // We want to move the spy to the city with the highest science generation + // Players can't usually figure this out so lets do highest population instead + spy.moveTo(getCivsToStealFromSorted.first().cities.filter { it.getCenterTile().isVisible(civInfo) }.maxByOrNull { it.population.population }) + continue + } + if (spy.action == SpyAction.None) { + spy.moveTo(civInfo.getKnownCivs().filter { otherCiv -> otherCiv.isMajorCiv() && otherCiv.cities.any { it.getCenterTile().isVisible(civInfo) }} + .toList().randomOrNull()?.cities?.filter { it.getCenterTile().isVisible(civInfo) }?.randomOrNull()) + } + } + } +} diff --git a/core/src/com/unciv/logic/battle/Battle.kt b/core/src/com/unciv/logic/battle/Battle.kt index 9b58f47ca4..377d29f713 100644 --- a/core/src/com/unciv/logic/battle/Battle.kt +++ b/core/src/com/unciv/logic/battle/Battle.kt @@ -527,6 +527,17 @@ object Battle { for (airUnit in airUnits.toList()) airUnit.destroy() } + // Move all spies in the city + if (attackerCiv.gameInfo.isEspionageEnabled()) { + for (civ in attackerCiv.gameInfo.civilizations.filter { it.isMajorCiv() }) { + for (spy in civ.espionageManager.spyList) { + if (spy.getLocation() == city) { + spy.moveTo(null) + } + } + } + } + val stateForConditionals = StateForConditionals(civInfo = attackerCiv, city=city, unit = attacker.unit, ourCombatant = attacker, attackedTile = city.getCenterTile()) for (unique in attacker.getMatchingUniques(UniqueType.CaptureCityPlunder, stateForConditionals, true)) { attackerCiv.addStat( diff --git a/core/src/com/unciv/models/Spy.kt b/core/src/com/unciv/models/Spy.kt index 5dcf4f43c6..c083d3c7ea 100644 --- a/core/src/com/unciv/models/Spy.kt +++ b/core/src/com/unciv/models/Spy.kt @@ -154,6 +154,9 @@ class Spy() : IsPartOfGameInfoSerialization { fun isSetUp() = action !in listOf(SpyAction.Moving, SpyAction.None, SpyAction.EstablishNetwork) + // Only returns true if the spy is doing a helpful and implemented action + fun isDoingWork() = action == SpyAction.StealingTech || action == SpyAction.EstablishNetwork + fun getLocation(): City? { return civInfo.gameInfo.getCities().firstOrNull { it.id == location } }