mirror of
https://github.com/yairm210/Unciv.git
synced 2025-09-26 13:27:22 -04:00
Add Moddable Policy Priorities (#6390)
* Fixed G&K policy eras Patronage should be Medieval and Freedom should be Industrial in G&K * Added 'priorities' object to policy branch entries * Fixed Vanilla policy eras Also set testing priorities in advance * Partial code formatting * Reworked how the AI chooses a policy to newly adopt * Removed debugging codes * Update Civilization-related-JSON-files.md Added Branch priorities * Assigned actual priorities to each branch Also fixed a debugging value in Ruleset.kt (-1 -> 0)
This commit is contained in:
parent
dd529db297
commit
84561e7ad4
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -14,10 +14,7 @@ import com.unciv.logic.map.MapUnit
|
||||
import com.unciv.logic.map.TileInfo
|
||||
import com.unciv.logic.trade.*
|
||||
import com.unciv.models.Counter
|
||||
import com.unciv.models.ruleset.Belief
|
||||
import com.unciv.models.ruleset.BeliefType
|
||||
import com.unciv.models.ruleset.ModOptionsConstants
|
||||
import com.unciv.models.ruleset.VictoryType
|
||||
import com.unciv.models.ruleset.*
|
||||
import com.unciv.models.ruleset.tech.Technology
|
||||
import com.unciv.models.ruleset.tile.ResourceType
|
||||
import com.unciv.models.ruleset.unique.UniqueType
|
||||
@ -393,35 +390,72 @@ object NextTurnAutomation {
|
||||
}
|
||||
}
|
||||
|
||||
private object PolicyPriorityMap {
|
||||
//todo This should be moddable, and needs an update to include new G&K Policies
|
||||
/** Maps [VictoryType] to an ordered List of PolicyBranch names - the AI will prefer them in that order */
|
||||
val priorities = mapOf(
|
||||
VictoryType.Cultural to listOf("Piety", "Freedom", "Tradition", "Commerce", "Patronage"),
|
||||
VictoryType.Scientific to listOf("Rationalism", "Commerce", "Liberty", "Order", "Patronage"),
|
||||
VictoryType.Domination to listOf("Autocracy", "Honor", "Liberty", "Rationalism", "Commerce"),
|
||||
VictoryType.Diplomatic to listOf("Patronage", "Commerce", "Rationalism", "Freedom", "Tradition")
|
||||
)
|
||||
}
|
||||
private fun adoptPolicy(civInfo: CivilizationInfo) {
|
||||
/*
|
||||
# Branch-based policy-to-adopt decision
|
||||
Basically the AI prioritizes finishing incomplete branches before moving on, \
|
||||
unless a new branch with higher priority is adoptable.
|
||||
|
||||
- If incomplete branches have higher priorities than any newly adoptable branch,
|
||||
- Candidates are the unfinished branches.
|
||||
- Else if newly adoptable branches have higher priorities than any incomplete branch,
|
||||
- Candidates are the new branches.
|
||||
- Choose a random candidate closest to completion.
|
||||
- Pick a random child policy of a chosen branch and adopt it.
|
||||
*/
|
||||
while (civInfo.policies.canAdoptPolicy()) {
|
||||
val incompleteBranches: Set<PolicyBranch> = civInfo.policies.incompleteBranches
|
||||
val adoptableBranches: Set<PolicyBranch> = civInfo.policies.adoptableBranches
|
||||
|
||||
val adoptablePolicies = civInfo.gameInfo.ruleSet.policies.values
|
||||
.filter { civInfo.policies.isAdoptable(it) }
|
||||
// Skip the whole thing if all branches are completed
|
||||
if (incompleteBranches.isEmpty() && adoptableBranches.isEmpty()) return
|
||||
|
||||
// This can happen if the player is crazy enough to have the game continue forever and he disabled cultural victory
|
||||
if (adoptablePolicies.isEmpty()) return
|
||||
val priorityMap: Map<PolicyBranch, Int> = civInfo.policies.priorityMap
|
||||
var maxIncompletePriority: Int? =
|
||||
civInfo.policies.getMaxPriority(incompleteBranches)
|
||||
var maxAdoptablePriority: Int? = civInfo.policies.getMaxPriority(adoptableBranches)
|
||||
|
||||
val policyBranchPriority = PolicyPriorityMap.priorities[civInfo.victoryType()]
|
||||
?: emptyList()
|
||||
val policiesByPreference = adoptablePolicies
|
||||
.groupBy { policy ->
|
||||
policyBranchPriority.indexOf(policy.branch.name).let { if (it == -1) 99 else it }
|
||||
}
|
||||
// This here is a (probably dirty) code to bypass NoSuchElementException error
|
||||
// when one of the priority variables is null
|
||||
if (maxIncompletePriority == null) maxIncompletePriority =
|
||||
maxAdoptablePriority!! - 1
|
||||
if (maxAdoptablePriority == null) maxAdoptablePriority =
|
||||
maxIncompletePriority - 1
|
||||
|
||||
val preferredPolicies = policiesByPreference.minByOrNull { it.key }!!.value
|
||||
// Candidate branches to adopt
|
||||
val candidates: Set<PolicyBranch> =
|
||||
// If incomplete branches have higher priorities than any newly adoptable branch,
|
||||
if (maxAdoptablePriority <= maxIncompletePriority) {
|
||||
// Prioritize finishing one of the unfinished branches
|
||||
incompleteBranches.filter {
|
||||
priorityMap[it] == maxIncompletePriority
|
||||
}.toSet()
|
||||
}
|
||||
// If newly adoptable branches have higher priorities than any incomplete branch,
|
||||
else {
|
||||
// Prioritize adopting one of the new branches
|
||||
adoptableBranches.filter {
|
||||
priorityMap[it] == maxAdoptablePriority
|
||||
}.toSet()
|
||||
}
|
||||
|
||||
// branchCompletionMap but keys are only candidates
|
||||
val candidateCompletionMap: Map<PolicyBranch, Int> =
|
||||
civInfo.policies.branchCompletionMap.filterKeys { key ->
|
||||
key in candidates
|
||||
}
|
||||
// The highest number of adopted child policies within a single candidate
|
||||
val maxCompletion: Int =
|
||||
candidateCompletionMap.maxOf { entry -> entry.value }
|
||||
// The candidate closest to completion, hence the target branch
|
||||
val targetBranch = candidateCompletionMap.filterValues { value ->
|
||||
value == maxCompletion
|
||||
}.keys.random()
|
||||
|
||||
val policyToAdopt: Policy =
|
||||
if (civInfo.policies.isAdoptable(targetBranch)) targetBranch
|
||||
else targetBranch.policies.filter { civInfo.policies.isAdoptable(it) }.random()
|
||||
|
||||
val policyToAdopt = preferredPolicies.random()
|
||||
civInfo.policies.adopt(policyToAdopt)
|
||||
}
|
||||
}
|
||||
@ -441,9 +475,7 @@ object NextTurnAutomation {
|
||||
val unitToDisband = civInfo.getCivUnits()
|
||||
.filter { it.baseUnit.requiresResource(resource) }
|
||||
.minByOrNull { it.getForceEvaluation() }
|
||||
if (unitToDisband != null) {
|
||||
unitToDisband.disband()
|
||||
}
|
||||
unitToDisband?.disband()
|
||||
|
||||
for (city in civInfo.cities) {
|
||||
if (city.hasSoldBuildingThisTurn)
|
||||
|
@ -3,7 +3,7 @@ package com.unciv.logic.civilization
|
||||
import com.unciv.logic.map.MapSize
|
||||
import com.unciv.models.ruleset.Policy
|
||||
import com.unciv.models.ruleset.Policy.PolicyBranchType
|
||||
import com.unciv.models.ruleset.unique.StateForConditionals
|
||||
import com.unciv.models.ruleset.PolicyBranch
|
||||
import com.unciv.models.ruleset.unique.UniqueMap
|
||||
import com.unciv.models.ruleset.unique.UniqueTriggerActivation
|
||||
import com.unciv.models.ruleset.unique.UniqueType
|
||||
@ -26,11 +26,62 @@ class PolicyManager {
|
||||
|
||||
var freePolicies = 0
|
||||
var storedCulture = 0
|
||||
// TODO: 'adoptedPolicies' seems to be an internal API.
|
||||
// Why is it HashSet<String> instead of HashSet<Policy>?
|
||||
internal val adoptedPolicies = HashSet<String>()
|
||||
var numberOfAdoptedPolicies = 0
|
||||
var shouldOpenPolicyPicker = false
|
||||
get() = field && canAdoptPolicy()
|
||||
|
||||
/** A [Map] pairing each [PolicyBranch] to its priority ([Int]). */
|
||||
val priorityMap: Map<PolicyBranch, Int>
|
||||
get() {
|
||||
val value = HashMap<PolicyBranch, Int>()
|
||||
for (branch in branches) {
|
||||
value[branch] = branch.priorities[civInfo.victoryType().name] ?: 0
|
||||
}
|
||||
return value
|
||||
}
|
||||
/** A [Set] of adopted [PolicyBranch]es regardless of its completeness. */
|
||||
val adoptedBranches: Set<PolicyBranch>
|
||||
get() = branches.filter { isAdopted(it.name) }.toSet()
|
||||
/** A [Set] of newly adoptable [PolicyBranch]es. */
|
||||
val adoptableBranches: Set<PolicyBranch>
|
||||
get() = branches.filter { isAdoptable(it) }.toSet()
|
||||
/** A [Set] of incomplete [PolicyBranch]es including newly adoptable ones. */
|
||||
val incompleteBranches: Set<PolicyBranch>
|
||||
get() {
|
||||
val value = HashSet<PolicyBranch>()
|
||||
for (branch in branches) {
|
||||
if (branch.policies.any { isAdoptable(it) }) value.add(branch)
|
||||
}
|
||||
return value
|
||||
}
|
||||
/** A [Set] of completed [PolicyBranch]es. */
|
||||
val completedBranches: Set<PolicyBranch>
|
||||
get() {
|
||||
val value = HashSet<PolicyBranch>()
|
||||
for (branch in branches) {
|
||||
if (branch.policies.all { isAdopted(it.name) }) value.add(branch)
|
||||
}
|
||||
return value
|
||||
}
|
||||
/** A [Map] pairing each [PolicyBranch] to how many of its child branches are adopted ([Int]). */
|
||||
val branchCompletionMap: Map<PolicyBranch, Int>
|
||||
get() {
|
||||
val value = HashMap<PolicyBranch, Int>()
|
||||
for (branch in branches) {
|
||||
value[branch] = adoptedPolicies.count {
|
||||
branch.policies.contains(getPolicyByName(it))
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
/** A [Set] of all [PolicyBranch]es. */
|
||||
private val branches: Set<PolicyBranch>
|
||||
get() = civInfo.gameInfo.ruleSet.policyBranches.values.toSet()
|
||||
|
||||
// Only instantiate a single value for all policy managers
|
||||
companion object {
|
||||
private val turnCountRegex by lazy { Regex("for \\[[0-9]*\\] turns") }
|
||||
@ -51,24 +102,28 @@ class PolicyManager {
|
||||
fun getPolicyByName(name: String): Policy = getRulesetPolicies()[name]!!
|
||||
|
||||
fun setTransients() {
|
||||
for (policyName in adoptedPolicies)
|
||||
addPolicyToTransients(getPolicyByName(policyName))
|
||||
for (policyName in adoptedPolicies) addPolicyToTransients(
|
||||
getPolicyByName(policyName)
|
||||
)
|
||||
}
|
||||
|
||||
fun addPolicyToTransients(policy: Policy) {
|
||||
for (unique in policy.uniqueObjects) {
|
||||
// Should be deprecated together with TimedAttackStrength so I'm putting this here so the compiler will complain if we don't
|
||||
// Should be deprecated together with TimedAttackStrength so
|
||||
// I'm putting this here so the compiler will complain if we don't
|
||||
val rememberToDeprecate = UniqueType.TimedAttackStrength
|
||||
if (!unique.text.contains(turnCountRegex) && unique.conditionals.none { it.type == UniqueType.ConditionalTimedUnique })
|
||||
policyUniques.addUnique(unique)
|
||||
if (!unique.text.contains(turnCountRegex) && unique.conditionals.none { it.type == UniqueType.ConditionalTimedUnique }) policyUniques.addUnique(
|
||||
unique
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun addCulture(culture: Int) {
|
||||
val couldAdoptPolicyBefore = canAdoptPolicy()
|
||||
storedCulture += culture
|
||||
if (!couldAdoptPolicyBefore && canAdoptPolicy())
|
||||
if (!couldAdoptPolicyBefore && canAdoptPolicy()) {
|
||||
shouldOpenPolicyPicker = true
|
||||
}
|
||||
}
|
||||
|
||||
fun endTurn(culture: Int) {
|
||||
@ -80,7 +135,7 @@ class PolicyManager {
|
||||
fun getCultureNeededForNextPolicy(): Int {
|
||||
var policyCultureCost = 25 + (numberOfAdoptedPolicies * 6).toDouble().pow(1.7)
|
||||
// https://civilization.fandom.com/wiki/Map_(Civ5)
|
||||
val worldSizeModifier = with (civInfo.gameInfo.tileMap.mapParameters.mapSize) {
|
||||
val worldSizeModifier = with(civInfo.gameInfo.tileMap.mapParameters.mapSize) {
|
||||
when {
|
||||
radius >= MapSize.Huge.radius -> 0.05f
|
||||
radius >= MapSize.Large.radius -> 0.075f
|
||||
@ -89,12 +144,9 @@ class PolicyManager {
|
||||
}
|
||||
var cityModifier = worldSizeModifier * (civInfo.cities.count { !it.isPuppet } - 1)
|
||||
|
||||
for (unique in civInfo.getMatchingUniques(UniqueType.LessPolicyCostFromCities))
|
||||
cityModifier *= 1 - unique.params[0].toFloat() / 100
|
||||
for (unique in civInfo.getMatchingUniques(UniqueType.LessPolicyCost))
|
||||
policyCultureCost *= unique.params[0].toPercent()
|
||||
if (civInfo.isPlayerCivilization())
|
||||
policyCultureCost *= civInfo.getDifficulty().policyCostModifier
|
||||
for (unique in civInfo.getMatchingUniques(UniqueType.LessPolicyCostFromCities)) cityModifier *= 1 - unique.params[0].toFloat() / 100
|
||||
for (unique in civInfo.getMatchingUniques(UniqueType.LessPolicyCost)) policyCultureCost *= unique.params[0].toPercent()
|
||||
if (civInfo.isPlayerCivilization()) policyCultureCost *= civInfo.getDifficulty().policyCostModifier
|
||||
policyCultureCost *= civInfo.gameInfo.gameParameters.gameSpeed.modifier
|
||||
val cost: Int = (policyCultureCost * (1 + cityModifier)).roundToInt()
|
||||
return cost - (cost % 5)
|
||||
@ -116,7 +168,9 @@ class PolicyManager {
|
||||
if (policy.policyBranchType == PolicyBranchType.BranchComplete) return false
|
||||
if (!getAdoptedPolicies().containsAll(policy.requires!!)) return false
|
||||
if (checkEra && civInfo.gameInfo.ruleSet.eras[policy.branch.era]!!.eraNumber > civInfo.getEraNumber()) return false
|
||||
if (policy.getMatchingUniques(UniqueType.IncompatibleWith).any { adoptedPolicies.contains(it.params[0]) }) return false
|
||||
if (policy.getMatchingUniques(UniqueType.IncompatibleWith)
|
||||
.any { adoptedPolicies.contains(it.params[0]) }
|
||||
) return false
|
||||
if (policy.uniqueObjects.filter { it.type == UniqueType.OnlyAvailableWhen }
|
||||
.any { !it.conditionalsApply(civInfo) }) return false
|
||||
return true
|
||||
@ -125,8 +179,7 @@ class PolicyManager {
|
||||
fun canAdoptPolicy(): Boolean {
|
||||
if (civInfo.cities.isEmpty()) return false
|
||||
|
||||
if (freePolicies == 0 && storedCulture < getCultureNeededForNextPolicy())
|
||||
return false
|
||||
if (freePolicies == 0 && storedCulture < getCultureNeededForNextPolicy()) return false
|
||||
|
||||
//Return true if there is a policy to adopt, else return false
|
||||
return getRulesetPolicies().values.any { civInfo.policies.isAdoptable(it) }
|
||||
@ -138,8 +191,9 @@ class PolicyManager {
|
||||
if (freePolicies > 0) freePolicies--
|
||||
else if (!civInfo.gameInfo.gameParameters.godMode) {
|
||||
val cultureNeededForNextPolicy = getCultureNeededForNextPolicy()
|
||||
if (cultureNeededForNextPolicy > storedCulture)
|
||||
throw Exception("How is this possible??????")
|
||||
if (cultureNeededForNextPolicy > storedCulture) throw Exception(
|
||||
"How is this possible??????"
|
||||
)
|
||||
storedCulture -= cultureNeededForNextPolicy
|
||||
numberOfAdoptedPolicies++
|
||||
}
|
||||
@ -156,34 +210,47 @@ class PolicyManager {
|
||||
}
|
||||
|
||||
for (unique in policy.uniques) {
|
||||
if (unique.equalsPlaceholderText("Triggers the following global alert: []"))
|
||||
triggerGlobalAlerts(policy, unique.getPlaceholderParameters()[0])
|
||||
if (unique.equalsPlaceholderText("Triggers the following global alert: []")) triggerGlobalAlerts(
|
||||
policy, unique.getPlaceholderParameters()[0]
|
||||
)
|
||||
}
|
||||
|
||||
for (unique in policy.uniqueObjects)
|
||||
UniqueTriggerActivation.triggerCivwideUnique(unique, civInfo)
|
||||
for (unique in policy.uniqueObjects) UniqueTriggerActivation.triggerCivwideUnique(
|
||||
unique, civInfo
|
||||
)
|
||||
|
||||
// This ALSO has the side-effect of updating the CivInfo statForNextTurn so we don't need to call it explicitly
|
||||
for (cityInfo in civInfo.cities)
|
||||
cityInfo.cityStats.update()
|
||||
for (cityInfo in civInfo.cities) cityInfo.cityStats.update()
|
||||
|
||||
if (!canAdoptPolicy()) shouldOpenPolicyPicker = false
|
||||
}
|
||||
|
||||
private fun triggerGlobalAlerts(policy: Policy, extraNotificationText: String = "") {
|
||||
/**
|
||||
* Return the highest priority ([Int]) among the given [Set] of [PolicyBranch]es.
|
||||
* Would return null if the given [Set] is empty.
|
||||
*/
|
||||
fun getMaxPriority(branchesToCompare: Set<PolicyBranch>): Int? {
|
||||
val filteredMap = priorityMap.filterKeys { branch -> branch in branchesToCompare }
|
||||
return filteredMap.values.maxOrNull()
|
||||
}
|
||||
|
||||
private fun triggerGlobalAlerts(
|
||||
policy: Policy, extraNotificationText: String = ""
|
||||
) {
|
||||
var extraNotificationTextCopy = extraNotificationText
|
||||
if (extraNotificationText != "") {
|
||||
extraNotificationTextCopy = "\n${extraNotificationText}"
|
||||
}
|
||||
for (civ in civInfo.gameInfo.civilizations.filter { it.isMajorCiv() }) {
|
||||
if (civ == civInfo) continue
|
||||
val defaultNotificationText =
|
||||
if (civ.getKnownCivs().contains(civInfo)) {
|
||||
"[${civInfo.civName}] has adopted the [${policy.name}] policy"
|
||||
} else {
|
||||
"An unknown civilization has adopted the [${policy.name}] policy"
|
||||
}
|
||||
civ.addNotification("${defaultNotificationText}${extraNotificationTextCopy}", NotificationIcon.Culture)
|
||||
val defaultNotificationText = if (civ.getKnownCivs().contains(civInfo)) {
|
||||
"[${civInfo.civName}] has adopted the [${policy.name}] policy"
|
||||
} else {
|
||||
"An unknown civilization has adopted the [${policy.name}] policy"
|
||||
}
|
||||
civ.addNotification(
|
||||
"${defaultNotificationText}${extraNotificationTextCopy}", NotificationIcon.Culture
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,5 +2,6 @@ package com.unciv.models.ruleset
|
||||
|
||||
class PolicyBranch : Policy() {
|
||||
var policies: ArrayList<Policy> = arrayListOf()
|
||||
var priorities: HashMap<String, Int> = HashMap<String, Int>()
|
||||
lateinit var era: String
|
||||
}
|
||||
|
@ -251,17 +251,32 @@ class Ruleset {
|
||||
|
||||
val policiesFile = folderHandle.child("Policies.json")
|
||||
if (policiesFile.exists()) {
|
||||
policyBranches += createHashmap(jsonParser.getFromJson(Array<PolicyBranch>::class.java, policiesFile))
|
||||
policyBranches += createHashmap(
|
||||
jsonParser.getFromJson(Array<PolicyBranch>::class.java, policiesFile)
|
||||
)
|
||||
for (branch in policyBranches.values) {
|
||||
// Setup this branch
|
||||
branch.requires = ArrayList()
|
||||
branch.branch = branch
|
||||
for (victoryType in VictoryType.values()) {
|
||||
if (victoryType.name !in branch.priorities.keys) {
|
||||
branch.priorities[victoryType.name] = 0
|
||||
}
|
||||
}
|
||||
policies[branch.name] = branch
|
||||
|
||||
// Append child policies of this branch
|
||||
for (policy in branch.policies) {
|
||||
policy.branch = branch
|
||||
if (policy.requires == null) policy.requires = arrayListOf(branch.name)
|
||||
if (policy.requires == null) {
|
||||
policy.requires = arrayListOf(branch.name)
|
||||
}
|
||||
policies[policy.name] = policy
|
||||
}
|
||||
branch.policies.last().name = branch.name + Policy.branchCompleteSuffix
|
||||
|
||||
// Add a finisher
|
||||
branch.policies.last().name =
|
||||
branch.name + Policy.branchCompleteSuffix
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
- [Buildings.json](#buildingsjson)
|
||||
- [Nations.json](#nationsjson)
|
||||
- [Policies.json](#policiesjson)
|
||||
- [Branch priorities](#branch-priorities)
|
||||
- [Quests.json](#questsjson)
|
||||
- [Religions.json](#religionsjson)
|
||||
- [Specialists.json](#specialistsjson)
|
||||
@ -109,6 +110,7 @@ Each policy branch can have the following properties:
|
||||
|-----------|------|-----------|-------|
|
||||
| name | String | Required | |
|
||||
| era | String | Required | Unlocking era as defined in [Eras.json](Miscellaneous-JSON-files.md#erasjson) |
|
||||
| priorities | Object | Default empty | Priorities for each victory type, [see here](#branch-priorities)
|
||||
| uniques | List | Default empty | List of effects, [see here](../Modders/Unique-parameter-types.md#general-uniques) |
|
||||
| policies | List | Default empty | List of member policies |
|
||||
|
||||
@ -121,6 +123,16 @@ Each member policy can have the following properties:
|
||||
| requires | List | Default empty | List of prerequisite policy names |
|
||||
| uniques | List | Default empty | List of effects, [see here](../Modders/Unique-parameter-types.md#general-uniques) |
|
||||
|
||||
#### Branch priorities
|
||||
The "priorities" object lists its branch's priorities for each victory type. The AI refers to this when deciding which branch to prioritize, also taking its preferred victory type into consideration. If two or more candidate branches have the same priority, the AI chooses a random branch among the candidates. All values are set to 0 if the object itself is missing or empty.
|
||||
|
||||
| Attribute | Type | Optional? | Notes |
|
||||
|-----------|------|-----------|-------|
|
||||
| Neutral | Int | Default 0 | Priority value when the AI's preferred victory type is Neutral |
|
||||
| Cultural | Int | Default 0 | Priority value when the AI's preferred victory type is Cultural |
|
||||
| Diplomatic | Int | Default 0 | Priority value when the AI's preferred victory type is Diplomatic |
|
||||
| Domination | Int | Default 0 | Priority value when the AI's preferred victory type is Domination|
|
||||
| Scientific | Int | Default 0 | Priority value when the AI's preferred victory type is Scientific |
|
||||
|
||||
## Quests.json
|
||||
[Link to original](/jsons/Civ%20V%20-%20Vanilla/Quests.json)
|
||||
|
Loading…
x
Reference in New Issue
Block a user