chore: Separated minor civ placement into a separate object

This commit is contained in:
Yair Morgenstern 2023-10-03 11:22:17 +03:00
parent e1a33f2116
commit a2a5cb7c2c
3 changed files with 243 additions and 152 deletions

View File

@ -147,4 +147,6 @@ object MapRegionResources {
mapOf() mapOf()
} }
} }

View File

@ -672,7 +672,7 @@ class MapRegions (val ruleset: Ruleset){
fun placeResourcesAndMinorCivs(tileMap: TileMap, minorCivs: List<Civilization>) { fun placeResourcesAndMinorCivs(tileMap: TileMap, minorCivs: List<Civilization>) {
placeNaturalWonderImpacts(tileMap) placeNaturalWonderImpacts(tileMap)
assignLuxuries() assignLuxuries()
placeMinorCivs(tileMap, minorCivs) MinorCivPlacer.placeMinorCivs(regions, tileMap, minorCivs, usingArchipelagoRegions, tileData, ruleset)
placeLuxuries(tileMap) placeLuxuries(tileMap)
placeStrategicAndBonuses(tileMap) placeStrategicAndBonuses(tileMap)
} }
@ -783,157 +783,6 @@ class MapRegions (val ruleset: Ruleset){
randomLuxuries.addAll(remainingLuxuries.drop(targetDisabledLuxuries)) randomLuxuries.addAll(remainingLuxuries.drop(targetDisabledLuxuries))
} }
/** Assigns [civs] to regions or "uninhabited" land and places them. Depends on
* assignLuxuries having been called previously.
* Note: can silently fail to place all city states if there is too little room.
* Currently our GameStarter fills out with random city states, Civ V behavior is to
* forget about the discarded city states entirely. */
private fun placeMinorCivs(tileMap: TileMap, civs: List<Civilization>) {
if (civs.isEmpty()) return
// Some but not all city states are assigned to regions directly. Determine the CS density.
val minorCivRatio = civs.size.toFloat() / regions.size
val minorCivPerRegion = when {
minorCivRatio > 14f -> 10 // lol
minorCivRatio > 11f -> 8
minorCivRatio > 8f -> 6
minorCivRatio > 5.7f -> 4
minorCivRatio > 4.35f -> 3
minorCivRatio > 2.7f -> 2
minorCivRatio > 1.35f -> 1
else -> 0
}
val unassignedCivs = civs.shuffled().toMutableList()
if (minorCivPerRegion > 0) {
regions.forEach {
val civsToAssign = unassignedCivs.take(minorCivPerRegion)
it.assignedMinorCivs.addAll(civsToAssign)
unassignedCivs.removeAll(civsToAssign)
}
}
// Some city states are assigned to "uninhabited" continents - unless it's an archipelago type map
// (Because then every continent will have been assigned to a region anyway)
val uninhabitedCoastal = ArrayList<Tile>()
val uninhabitedHinterland = ArrayList<Tile>()
val uninhabitedContinents = tileMap.continentSizes.filter {
it.value >= 4 && // Don't bother with tiny islands
regions.none { region -> region.continentID == it.key }
}.keys
val civAssignedToUninhabited = ArrayList<Civilization>()
var numUninhabitedTiles = 0
var numInhabitedTiles = 0
if (!usingArchipelagoRegions) {
// Go through the entire map to build the data
for (tile in tileMap.values) {
if (!canPlaceMinorCiv(tile)) continue
val continent = tile.getContinent()
if (continent in uninhabitedContinents) {
if (tile.isCoastalTile())
uninhabitedCoastal.add(tile)
else
uninhabitedHinterland.add(tile)
numUninhabitedTiles++
} else
numInhabitedTiles++
}
// Determine how many minor civs to put on uninhabited continents.
val maxByUninhabited = (3 * civs.size * numUninhabitedTiles) / (numInhabitedTiles + numUninhabitedTiles)
val maxByRatio = (civs.size + 1) / 2
val targetForUninhabited = min(maxByRatio, maxByUninhabited)
val civsToAssign = unassignedCivs.take(targetForUninhabited)
unassignedCivs.removeAll(civsToAssign)
civAssignedToUninhabited.addAll(civsToAssign)
}
// If there are still unassigned minor civs, assign extra ones to regions that share their
// luxury type with two others, as compensation. Because starting close to a city state is good??
if (unassignedCivs.isNotEmpty()) {
val regionsWithCommonLuxuries = regions.filter {
regions.count { other -> other.luxury == it.luxury } >= 3
}
// assign one civ each to regions with common luxuries if there are enough to go around
if (regionsWithCommonLuxuries.isNotEmpty() &&
regionsWithCommonLuxuries.size <= unassignedCivs.size
) {
regionsWithCommonLuxuries.forEach {
val civToAssign = unassignedCivs.first()
unassignedCivs.remove(civToAssign)
it.assignedMinorCivs.add(civToAssign)
}
}
}
// Still unassigned civs??
if (unassignedCivs.isNotEmpty()) {
// Add one extra to each region as long as there are enough to go around
while (unassignedCivs.size >= regions.size) {
regions.forEach {
val civToAssign = unassignedCivs.first()
unassignedCivs.remove(civToAssign)
it.assignedMinorCivs.add(civToAssign)
}
}
}
// STILL unassigned civs??
if (unassignedCivs.isNotEmpty()) {
// At this point there is at least for sure less remaining city states than regions
// Sort regions by fertility and put extra city states in the worst ones.
val worstRegions = regions.sortedBy { it.totalFertility }.take(unassignedCivs.size)
worstRegions.forEach {
val civToAssign = unassignedCivs.first()
unassignedCivs.remove(civToAssign)
it.assignedMinorCivs.add(civToAssign)
}
}
// All minor civs are assigned - now place them
// First place the "uninhabited continent" ones, preferring coastal starts
tryPlaceMinorCivsInTiles(civAssignedToUninhabited, tileMap, uninhabitedCoastal)
tryPlaceMinorCivsInTiles(civAssignedToUninhabited, tileMap, uninhabitedHinterland)
// Fallback to a random region for civs that couldn't be placed in the wilderness
for (unplacedCiv in civAssignedToUninhabited) {
regions.random().assignedMinorCivs.add(unplacedCiv)
}
// Now place the ones assigned to specific regions.
for (region in regions) {
tryPlaceMinorCivsInTiles(region.assignedMinorCivs, tileMap, region.tiles.toMutableList())
}
}
/** Attempts to randomly place civs from [civsToPlace] in tiles from [tileList]. Assumes that
* [tileList] is pre-vetted and only contains habitable land tiles.
* Will modify both [civsToPlace] and [tileList] as it goes! */
private fun tryPlaceMinorCivsInTiles(civsToPlace: MutableList<Civilization>, tileMap: TileMap, tileList: MutableList<Tile>) {
while (tileList.isNotEmpty() && civsToPlace.isNotEmpty()) {
val chosenTile = tileList.random()
tileList.remove(chosenTile)
val data = tileData[chosenTile.position]!!
// If the randomly chosen tile is too close to a player or a city state, discard it
if (data.impacts.containsKey(ImpactType.MinorCiv))
continue
// Otherwise, go ahead and place the minor civ
val civToAdd = civsToPlace.first()
civsToPlace.remove(civToAdd)
placeMinorCiv(civToAdd, tileMap, chosenTile)
}
}
private fun canPlaceMinorCiv(tile: Tile) = !tile.isWater && !tile.isImpassible() &&
!tileData[tile.position]!!.isJunk &&
tile.getBaseTerrain().getMatchingUniques(UniqueType.HasQuality).none { it.params[0] == "Undesirable" } && // So we don't get snow hills
tile.neighbors.count() == 6 // Avoid map edges
private fun placeMinorCiv(civ: Civilization, tileMap: TileMap, tile: Tile) {
tileMap.addStartingLocation(civ.civName, tile)
tileData.placeImpact(ImpactType.MinorCiv,tile, 4)
tileData.placeImpact(ImpactType.Luxury, tile, 3)
tileData.placeImpact(ImpactType.Strategic,tile, 0)
tileData.placeImpact(ImpactType.Bonus, tile, 3)
StartNormalizer.normalizeStart(tile, tileMap, tileData, ruleset, isMinorCiv = true)
}
/** Places all Luxuries onto [tileMap]. Assumes that assignLuxuries and placeMinorCivs have been called. */ /** Places all Luxuries onto [tileMap]. Assumes that assignLuxuries and placeMinorCivs have been called. */
private fun placeLuxuries(tileMap: TileMap) { private fun placeLuxuries(tileMap: TileMap) {
// First place luxuries at major civ start locations // First place luxuries at major civ start locations

View File

@ -0,0 +1,240 @@
package com.unciv.logic.map.mapgenerator.mapregions
import com.unciv.logic.civilization.Civilization
import com.unciv.logic.map.TileMap
import com.unciv.logic.map.tile.Tile
import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.unique.UniqueType
import kotlin.math.min
object MinorCivPlacer {
/** Assigns [civs] to regions or "uninhabited" land and places them. Depends on
* assignLuxuries having been called previously.
* Note: can silently fail to place all city states if there is too little room.
* Currently our GameStarter fills out with random city states, Civ V behavior is to
* forget about the discarded city states entirely. */
fun placeMinorCivs(regions: List<Region>, tileMap: TileMap, civs: List<Civilization>, usingArchipelagoRegions:Boolean, tileData: TileDataMap, ruleset: Ruleset) {
if (civs.isEmpty()) return
// Some but not all city states are assigned to regions directly. Determine the CS density.
val unassignedCivs = assignMinorCivsDirectlyToRegions(civs, regions)
// Some city states are assigned to "uninhabited" continents - unless it's an archipelago type map
// (Because then every continent will have been assigned to a region anyway)
val uninhabitedCoastal = ArrayList<Tile>()
val uninhabitedHinterland = ArrayList<Tile>()
val civAssignedToUninhabited = ArrayList<Civilization>()
if (!usingArchipelagoRegions) {
spreadCityStatesBetweenHabitedAndUninhabited(
tileMap,
regions,
tileData,
uninhabitedCoastal,
uninhabitedHinterland,
civs,
unassignedCivs,
civAssignedToUninhabited
)
}
assignCityStatesToRegionsWithCommonLuxuries(regions, unassignedCivs)
spreadCityStatesEvenlyBetweenRegions(unassignedCivs, regions)
assignRemainingCityStatesToWorstFertileRegions(regions, unassignedCivs)
// After we've finished assigning, NOW we actually place them
placeAssignedMinorCivs(
civAssignedToUninhabited,
tileMap,
uninhabitedCoastal,
tileData,
ruleset,
uninhabitedHinterland,
regions
)
}
private fun spreadCityStatesBetweenHabitedAndUninhabited(
tileMap: TileMap,
regions: List<Region>,
tileData: TileDataMap,
uninhabitedCoastal: ArrayList<Tile>,
uninhabitedHinterland: ArrayList<Tile>,
civs: List<Civilization>,
unassignedCivs: MutableList<Civilization>,
civAssignedToUninhabited: ArrayList<Civilization>
) {
val uninhabitedContinents = tileMap.continentSizes.filter {
it.value >= 4 && // Don't bother with tiny islands
regions.none { region -> region.continentID == it.key }
}.keys
var numInhabitedTiles = 0
var numUninhabitedTiles = 0
// Go through the entire map to build the data
for (tile in tileMap.values) {
if (!canPlaceMinorCiv(tile, tileData)) continue
val continent = tile.getContinent()
if (continent in uninhabitedContinents) {
if (tile.isCoastalTile())
uninhabitedCoastal.add(tile)
else
uninhabitedHinterland.add(tile)
numUninhabitedTiles++
} else
numInhabitedTiles++
}
// Determine how many minor civs to put on uninhabited continents.
val maxByUninhabited =
(3 * civs.size * numUninhabitedTiles) / (numInhabitedTiles + numUninhabitedTiles)
val maxByRatio = (civs.size + 1) / 2
val targetForUninhabited = min(maxByRatio, maxByUninhabited)
val civsToAssign = unassignedCivs.take(targetForUninhabited)
unassignedCivs.removeAll(civsToAssign)
civAssignedToUninhabited.addAll(civsToAssign)
}
/** If there are still unassigned minor civs, assign extra ones to regions that share their
* luxury type with two others, as compensation. Because starting close to a city state is good??
*/
private fun assignCityStatesToRegionsWithCommonLuxuries(
regions: List<Region>,
unassignedCivs: MutableList<Civilization>
) {
if (unassignedCivs.isEmpty()) return
val regionsWithCommonLuxuries = regions.filter {
regions.count { other -> other.luxury == it.luxury } >= 3
}
// assign one civ each to regions with common luxuries if there are enough to go around
if (regionsWithCommonLuxuries.isNotEmpty() &&
regionsWithCommonLuxuries.size <= unassignedCivs.size
) {
regionsWithCommonLuxuries.forEach {
val civToAssign = unassignedCivs.first()
unassignedCivs.remove(civToAssign)
it.assignedMinorCivs.add(civToAssign)
}
}
}
/** Add one extra to each region as long as there are enough to go around */
private fun spreadCityStatesEvenlyBetweenRegions(
unassignedCivs: MutableList<Civilization>,
regions: List<Region>
) {
if (unassignedCivs.isEmpty()) return
while (unassignedCivs.size >= regions.size) {
regions.forEach {
val civToAssign = unassignedCivs.first()
unassignedCivs.remove(civToAssign)
it.assignedMinorCivs.add(civToAssign)
}
}
}
/** At this point there is at least for sure less remaining city states than regions
Sort regions by fertility and put extra city states in the worst ones. */
private fun assignRemainingCityStatesToWorstFertileRegions(
regions: List<Region>,
unassignedCivs: MutableList<Civilization>
) {
if (unassignedCivs.isEmpty()) return
val worstRegions = regions.sortedBy { it.totalFertility }.take(unassignedCivs.size)
worstRegions.forEach {
val civToAssign = unassignedCivs.first()
unassignedCivs.remove(civToAssign)
it.assignedMinorCivs.add(civToAssign)
}
}
/** Actually placee the minor civs, after they have been sorted into groups and assigned to regions */
private fun placeAssignedMinorCivs(
civAssignedToUninhabited: ArrayList<Civilization>,
tileMap: TileMap,
uninhabitedCoastal: ArrayList<Tile>,
tileData: TileDataMap,
ruleset: Ruleset,
uninhabitedHinterland: ArrayList<Tile>,
regions: List<Region>
) {
// All minor civs are assigned - now place them
// First place the "uninhabited continent" ones, preferring coastal starts
tryPlaceMinorCivsInTiles(
civAssignedToUninhabited, tileMap, uninhabitedCoastal, tileData, ruleset
)
tryPlaceMinorCivsInTiles(
civAssignedToUninhabited, tileMap, uninhabitedHinterland, tileData, ruleset
)
// Fallback to a random region for civs that couldn't be placed in the wilderness
for (unplacedCiv in civAssignedToUninhabited) {
regions.random().assignedMinorCivs.add(unplacedCiv)
}
// Now place the ones assigned to specific regions.
for (region in regions) {
tryPlaceMinorCivsInTiles(
region.assignedMinorCivs, tileMap, region.tiles.toMutableList(), tileData, ruleset
)
}
}
private fun assignMinorCivsDirectlyToRegions(
civs: List<Civilization>,
regions: List<Region>
): MutableList<Civilization> {
val minorCivRatio = civs.size.toFloat() / regions.size
val minorCivPerRegion = when {
minorCivRatio > 14f -> 10 // lol
minorCivRatio > 11f -> 8
minorCivRatio > 8f -> 6
minorCivRatio > 5.7f -> 4
minorCivRatio > 4.35f -> 3
minorCivRatio > 2.7f -> 2
minorCivRatio > 1.35f -> 1
else -> 0
}
val unassignedCivs = civs.shuffled().toMutableList()
if (minorCivPerRegion > 0) {
regions.forEach {
val civsToAssign = unassignedCivs.take(minorCivPerRegion)
it.assignedMinorCivs.addAll(civsToAssign)
unassignedCivs.removeAll(civsToAssign)
}
}
return unassignedCivs
}
/** Attempts to randomly place civs from [civsToPlace] in tiles from [tileList]. Assumes that
* [tileList] is pre-vetted and only contains habitable land tiles.
* Will modify both [civsToPlace] and [tileList] as it goes! */
private fun tryPlaceMinorCivsInTiles(civsToPlace: MutableList<Civilization>, tileMap: TileMap, tileList: MutableList<Tile>, tileData: TileDataMap, ruleset: Ruleset) {
while (tileList.isNotEmpty() && civsToPlace.isNotEmpty()) {
val chosenTile = tileList.random()
tileList.remove(chosenTile)
val data = tileData[chosenTile.position]!!
// If the randomly chosen tile is too close to a player or a city state, discard it
if (data.impacts.containsKey(MapRegions.ImpactType.MinorCiv))
continue
// Otherwise, go ahead and place the minor civ
val civToAdd = civsToPlace.first()
civsToPlace.remove(civToAdd)
placeMinorCiv(civToAdd, tileMap, chosenTile, tileData, ruleset)
}
}
private fun canPlaceMinorCiv(tile: Tile, tileData: TileDataMap) = !tile.isWater && !tile.isImpassible() &&
!tileData[tile.position]!!.isJunk &&
tile.getBaseTerrain().getMatchingUniques(UniqueType.HasQuality).none { it.params[0] == "Undesirable" } && // So we don't get snow hills
tile.neighbors.count() == 6 // Avoid map edges
private fun placeMinorCiv(civ: Civilization, tileMap: TileMap, tile: Tile, tileData: TileDataMap, ruleset: Ruleset) {
tileMap.addStartingLocation(civ.civName, tile)
tileData.placeImpact(MapRegions.ImpactType.MinorCiv,tile, 4)
tileData.placeImpact(MapRegions.ImpactType.Luxury, tile, 3)
tileData.placeImpact(MapRegions.ImpactType.Strategic,tile, 0)
tileData.placeImpact(MapRegions.ImpactType.Bonus, tile, 3)
StartNormalizer.normalizeStart(tile, tileMap, tileData, ruleset, isMinorCiv = true)
}
}