Earlier version with changes mostly to use Sequences (#1993)

* Fixes Issue #1697 by adding information to the special production constructions.

* Get rid of extra $ sign in the SpecialConstruction tooltips

* Major refactor to use Sequences instead of List to try to improve logic whenever getting a list of tiles at a distance.

* Get rid of extraneous parameter

* get rid of extra exception. slight refactor placeUnitNearTile for readability

* Fix bug of doing intersection instead of union

* Add an extra method to get tiles in distance range

* Update based on comments
This commit is contained in:
Kentalot 2020-02-23 04:22:50 -08:00 committed by GitHub
parent 718473fb31
commit 3c2cb01169
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 309 additions and 289 deletions

View File

@ -164,7 +164,8 @@ class GameInfo {
// Barbarians will only spawn in places that no one can see
val allViewableTiles = civilizations.filterNot { it.isBarbarian() }
.flatMap { it.viewableTiles }.toHashSet()
val tilesWithin3ofExistingEncampment = existingEncampments.flatMap { it.getTilesInDistance(3) }
val tilesWithin3ofExistingEncampment = existingEncampments.asSequence()
.flatMap { it.getTilesInDistance(3) }.toSet()
val viableTiles = tileMap.values.filter {
!it.getBaseTerrain().impassable && it.isLand
&& it.terrainFeature == null

View File

@ -14,14 +14,6 @@ import kotlin.math.sqrt
class Automation {
internal fun rankTile(tile: TileInfo?, civInfo: CivilizationInfo): Float {
if (tile == null) return 0f
val stats = tile.getTileStats(null, civInfo)
var rank = rankStatsValue(stats, civInfo)
if (tile.improvement == null) rank += 0.5f // improvement potential!
if (tile.hasViewableResource(civInfo)) rank += 1.0f
return rank
}
fun rankTileForCityWork(tile:TileInfo, city: CityInfo, foodWeight: Float = 1f): Float {
val stats = tile.getTileStats(city, city.civInfo)
@ -63,20 +55,6 @@ class Automation {
return rank
}
fun rankStatsValue(stats: Stats, civInfo: CivilizationInfo): Float {
var rank = 0.0f
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.statsForNextTurn.gold <= 0) rank += stats.gold
else rank += stats.gold / 3 // 3 gold is much worse than 2 production
rank += stats.production
rank += stats.science
rank += stats.culture
return rank
}
fun trainMilitaryUnit(city: CityInfo) {
val name = chooseMilitaryUnit(city)
city.cityConstructions.currentConstruction = name
@ -128,6 +106,32 @@ class Automation {
}
}
companion object {
internal fun rankTile(tile: TileInfo?, civInfo: CivilizationInfo): Float {
if (tile == null) return 0f
val stats = tile.getTileStats(null, civInfo)
var rank = rankStatsValue(stats, civInfo)
if (tile.improvement == null) rank += 0.5f // improvement potential!
if (tile.hasViewableResource(civInfo)) rank += 1.0f
return rank
}
@JvmStatic
fun rankStatsValue(stats: Stats, civInfo: CivilizationInfo): Float {
var rank = 0.0f
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.statsForNextTurn.gold <= 0) rank += stats.gold
else rank += stats.gold / 3 // 3 gold is much worse than 2 production
rank += stats.production
rank += stats.science
rank += stats.culture
return rank
}
}
}
enum class ThreatLevel{

View File

@ -86,7 +86,7 @@ class BarbarianAutomation(val civInfo: CivilizationInfo) {
if (unit.health < 100 && UnitAutomation().tryHealUnit(unit, unitDistanceToTiles)) return
// 6 - wander
UnitAutomation().wander(unit, unitDistanceToTiles)
UnitAutomation.wander(unit, unitDistanceToTiles)
}
private fun automateScout(unit: MapUnit) {
@ -114,7 +114,7 @@ class BarbarianAutomation(val civInfo: CivilizationInfo) {
if (unit.health < 100 && UnitAutomation().tryHealUnit(unit, unitDistanceToTiles)) return
// 5 - wander
UnitAutomation().wander(unit, unitDistanceToTiles)
UnitAutomation.wander(unit, unitDistanceToTiles)
}
private fun findFurthestTileCanMoveTo(

View File

@ -63,9 +63,10 @@ class BattleHelper {
val tilesInAttackRange =
if (unit.hasUnique("Ranged attacks may be performed over obstacles") || unit.type.isAirUnit())
reachableTile.getTilesInDistance(rangeOfAttack)
else reachableTile.getViewableTiles(rangeOfAttack, unit.type.isWaterUnit())
else reachableTile.getViewableTilesList(rangeOfAttack, unit.type.isWaterUnit())
.asSequence()
attackableTiles += tilesInAttackRange.asSequence().filter { it in tilesWithEnemies }
attackableTiles += tilesInAttackRange.filter { it in tilesWithEnemies }
.map { AttackableTile(reachableTile, it) }
}
return attackableTiles

View File

@ -37,7 +37,7 @@ class SpecificUnitAutomation{
return createImprovementAction()
}
}
else UnitAutomation().tryExplore(unit, unit.movement.getDistanceToTiles())
else UnitAutomation.tryExplore(unit, unit.movement.getDistanceToTiles())
}
fun automateGreatGeneral(unit: MapUnit){
@ -72,14 +72,13 @@ class SpecificUnitAutomation{
fun rankTileAsCityCenter(tileInfo: TileInfo, nearbyTileRankings: Map<TileInfo, Float>,
luxuryResourcesInCivArea: Sequence<TileResource>): Float {
val bestTilesFromOuterLayer = tileInfo.getTilesAtDistance(2)
.asSequence()
.sortedByDescending { nearbyTileRankings[it] }.take(2)
.toList()
val top5Tiles = tileInfo.neighbors.union(bestTilesFromOuterLayer)
.asSequence()
.sortedByDescending { nearbyTileRankings[it] }
.take(5)
var rank = top5Tiles.map { nearbyTileRankings[it]!! }.sum()
var rank = top5Tiles.map { nearbyTileRankings.getValue(it) }.sum()
if (tileInfo.isCoastalTile()) rank += 5
val luxuryResourcesInCityArea = tileInfo.getTilesAtDistance(2).filter { it.resource!=null }
@ -96,7 +95,7 @@ class SpecificUnitAutomation{
fun automateSettlerActions(unit: MapUnit) {
if (unit.getTile().militaryUnit == null) return // Don't move until you're accompanied by a military unit
val tilesNearCities = unit.civInfo.gameInfo.getCities()
val tilesNearCities = unit.civInfo.gameInfo.getCities().asSequence()
.flatMap {
val distanceAwayFromCity =
if (unit.civInfo.knows(it.civInfo)
@ -106,30 +105,32 @@ class SpecificUnitAutomation{
else 3
it.getCenterTile().getTilesInDistance(distanceAwayFromCity)
}
.toHashSet()
.toSet()
// This is to improve performance - instead of ranking each tile in the area up to 19 times, do it once.
val nearbyTileRankings = unit.getTile().getTilesInDistance(7)
.associateBy ( {it},{ Automation().rankTile(it,unit.civInfo) })
.associateBy({ it }, { Automation.rankTile(it, unit.civInfo) })
val possibleCityLocations = unit.getTile().getTilesInDistance(5)
.filter { val tileOwner=it.getOwner()
.filter {
val tileOwner = it.getOwner()
it.isLand && (tileOwner == null || tileOwner == unit.civInfo) && // don't allow settler to settle inside other civ's territory
(unit.movement.canMoveTo(it) || unit.currentTile == it)
&& it !in tilesNearCities }
&& it !in tilesNearCities
}
val luxuryResourcesInCivArea = unit.civInfo.cities.asSequence()
.flatMap { it.getTiles().asSequence() }.filter { it.resource != null }
.map { it.getTileResource() }.filter { it.resourceType == ResourceType.Luxury }
.distinct()
val bestCityLocation: TileInfo? = possibleCityLocations
.asSequence()
.sortedByDescending { rankTileAsCityCenter(it, nearbyTileRankings, luxuryResourcesInCivArea) }
.firstOrNull { unit.movement.canReach(it) }
if (bestCityLocation == null) { // We got a badass over here, all tiles within 5 are taken? Screw it, random walk.
if (UnitAutomation().tryExplore(unit, unit.movement.getDistanceToTiles())) return // try to find new areas
UnitAutomation().wander(unit, unit.movement.getDistanceToTiles()) // go around aimlessly
val unitDistanceToTiles = unit.movement.getDistanceToTiles()
if (UnitAutomation.tryExplore(unit, unitDistanceToTiles)) return // try to find new areas
UnitAutomation.wander(unit, unitDistanceToTiles) // go around aimlessly
return
}
@ -169,9 +170,9 @@ class SpecificUnitAutomation{
&& it.isLand
&& !it.isCityCenter()
&& it.resource==null }
.sortedByDescending { Automation().rankTile(it,unit.civInfo) }.toList()
val chosenTile = tiles.firstOrNull { unit.movement.canReach(it) }
if(chosenTile==null) continue // to another city
.sortedByDescending { Automation.rankTile(it,unit.civInfo) }.toList()
val chosenTile = tiles.firstOrNull { unit.movement.canReach(it) } ?: continue // to another city
unit.movement.headTowards(chosenTile)
if(unit.currentTile==chosenTile && unit.currentMovement > 0)
@ -185,9 +186,9 @@ class SpecificUnitAutomation{
fun automateFighter(unit: MapUnit) {
val tilesInRange = unit.currentTile.getTilesInDistance(unit.getRange())
val enemyAirUnitsInRange = tilesInRange
.flatMap { it.airUnits }.filter { it.civInfo.isAtWarWith(unit.civInfo) }
.flatMap { it.airUnits.asSequence() }.filter { it.civInfo.isAtWarWith(unit.civInfo) }
if(enemyAirUnitsInRange.isNotEmpty()) return // we need to be on standby in case they attack
if (enemyAirUnitsInRange.any()) return // we need to be on standby in case they attack
if (battleHelper.tryAttackNearbyEnemy(unit)) return
// TODO Implement consideration for landing on aircraft carrier
@ -207,13 +208,19 @@ class SpecificUnitAutomation{
if (pathsToCities.isEmpty()) return // can't actually move anywhere else
val citiesByNearbyAirUnits = pathsToCities.keys
.groupBy { it.getTilesInDistance(unit.getRange())
.count{it.airUnits.size>0 && it.airUnits.first().civInfo.isAtWarWith(unit.civInfo)} }
.groupBy { key ->
key.getTilesInDistance(unit.getRange())
.count {
val firstAirUnit = it.airUnits.firstOrNull()
firstAirUnit != null && firstAirUnit.civInfo.isAtWarWith(unit.civInfo)
}
}
if (citiesByNearbyAirUnits.keys.any { it != 0 }) {
val citiesWithMostNeedOfAirUnits = citiesByNearbyAirUnits.maxBy { it.key }!!.value
val chosenCity = citiesWithMostNeedOfAirUnits.minBy { pathsToCities[it]!!.size }!! // city with min path = least turns to get there
val firstStepInPath = pathsToCities[chosenCity]!!.first()
//todo: maybe groupby size and choose highest priority within the same size turns
val chosenCity = citiesWithMostNeedOfAirUnits.minBy { pathsToCities.getValue(it).size }!! // city with min path = least turns to get there
val firstStepInPath = pathsToCities.getValue(chosenCity).first()
unit.movement.moveToTile(firstStepInPath)
return
}
@ -255,6 +262,8 @@ class SpecificUnitAutomation{
}
if (citiesThatCanAttackFrom.isEmpty()) return
//todo: this logic looks similar to some parts of automateFighter, maybe pull out common code
//todo: maybe groupby size and choose highest priority within the same size turns
val closestCityThatCanAttackFrom = citiesThatCanAttackFrom.minBy { pathsToCities[it]!!.size }!!
val firstStepInPath = pathsToCities[closestCityThatCanAttackFrom]!!.first()
airUnit.movement.moveToTile(firstStepInPath)

View File

@ -3,10 +3,7 @@ package com.unciv.logic.automation
import com.badlogic.gdx.graphics.Color
import com.unciv.Constants
import com.unciv.UncivGame
import com.unciv.logic.battle.Battle
import com.unciv.logic.battle.BattleDamage
import com.unciv.logic.battle.CityCombatant
import com.unciv.logic.battle.MapUnitCombatant
import com.unciv.logic.battle.*
import com.unciv.logic.city.CityInfo
import com.unciv.logic.civilization.GreatPersonManager
import com.unciv.logic.civilization.diplomacy.DiplomaticStatus
@ -23,6 +20,41 @@ class UnitAutomation {
companion object {
const val CLOSE_ENEMY_TILES_AWAY_LIMIT = 5
const val CLOSE_ENEMY_TURNS_AWAY_LIMIT = 3f
internal fun tryExplore(unit: MapUnit, unitDistanceToTiles: PathsToTilesWithinTurn): Boolean {
if (tryGoToRuin(unit, unitDistanceToTiles) && unit.currentMovement == 0f) return true
for (tile in unit.currentTile.getTilesInDistance(10))
if (unit.movement.canMoveTo(tile) && tile.position !in unit.civInfo.exploredTiles
&& unit.movement.canReach(tile)
&& (tile.getOwner() == null || !tile.getOwner()!!.isCityState())) {
unit.movement.headTowards(tile)
return true
}
return false
}
private fun tryGoToRuin(unit: MapUnit, unitDistanceToTiles: PathsToTilesWithinTurn): Boolean {
if (!unit.civInfo.isMajorCiv()) return false // barbs don't have anything to do in ruins
val tileWithRuin = unitDistanceToTiles.keys
.firstOrNull {
it.improvement == Constants.ancientRuins && unit.movement.canMoveTo(it)
}
if (tileWithRuin == null)
return false
unit.movement.moveToTile(tileWithRuin)
return true
}
@JvmStatic
fun wander(unit: MapUnit, unitDistanceToTiles: PathsToTilesWithinTurn) {
val reachableTiles = unitDistanceToTiles
.filter { unit.movement.canMoveTo(it.key) && unit.movement.canReach(it.key) }
val reachableTilesMaxWalkingDistance = reachableTiles.filter { it.value.totalDistance == unit.currentMovement }
if (reachableTilesMaxWalkingDistance.any()) unit.movement.moveToTile(reachableTilesMaxWalkingDistance.toList().random().first)
else if (reachableTiles.any()) unit.movement.moveToTile(reachableTiles.keys.random())
}
}
private val battleHelper = BattleHelper()
@ -162,10 +194,9 @@ class UnitAutomation {
return true
}
fun getBombardTargets(city: CityInfo): List<TileInfo> {
return city.getCenterTile().getViewableTiles(city.range, true)
fun getBombardTargets(city: CityInfo): Sequence<TileInfo> =
city.getCenterTile().getTilesInDistance(city.range)
.filter { battleHelper.containsAttackableEnemy(it, CityCombatant(city)) }
}
/** Move towards the closest attackable enemy of the [unit].
*
@ -180,7 +211,7 @@ class UnitAutomation {
var closeEnemies = battleHelper.getAttackableEnemies(
unit,
unitDistanceToTiles,
tilesToCheck = unit.getTile().getTilesInDistance(CLOSE_ENEMY_TILES_AWAY_LIMIT)
tilesToCheck = unit.getTile().getTilesInDistance(CLOSE_ENEMY_TILES_AWAY_LIMIT).toList()
).filter {
// Ignore units that would 1-shot you if you attacked
BattleDamage().calculateDamageToAttacker(MapUnitCombatant(unit),
@ -246,16 +277,13 @@ class UnitAutomation {
if (closestReachableEnemyCity != null) {
val unitDistanceToTiles = unit.movement.getDistanceToTiles()
val tilesInBombardRange = closestReachableEnemyCity.getTilesInDistance(2)
val reachableTilesNotInBombardRange = unitDistanceToTiles.keys.filter { it !in tilesInBombardRange }
val tilesInBombardRange = closestReachableEnemyCity.getTilesInDistance(2).toSet()
val suitableGatheringGroundTiles = closestReachableEnemyCity.getTilesAtDistance(4)
.union(closestReachableEnemyCity.getTilesAtDistance(3))
.filter { it.isLand }
// don't head straight to the city, try to head to landing grounds -
// this is against tha AI's brilliant plan of having everyone embarked and attacking via sea when unnecessary.
val tileToHeadTo = suitableGatheringGroundTiles
val tileToHeadTo = closestReachableEnemyCity.getTilesInDistanceRange(3..4)
.filter { it.isLand }
.sortedBy { it.arialDistanceTo(unit.currentTile) }
.firstOrNull { unit.movement.canReach(it) } ?: closestReachableEnemyCity
@ -263,17 +291,21 @@ class UnitAutomation {
unit.movement.headTowards(tileToHeadTo)
else {
if (unit.getRange() > 2) { // should never be in a bombardable position
val tilesCanAttackFromButNotInBombardRange =
reachableTilesNotInBombardRange.filter { it.arialDistanceTo(closestReachableEnemyCity) <= unit.getRange() }
val tileToMoveTo =
unitDistanceToTiles.asSequence()
.filter { it.key !in tilesInBombardRange
&& it.key.arialDistanceTo(closestReachableEnemyCity) <=
unit.getRange() }
.minBy { it.value.totalDistance }?.key
// move into position far away enough that the bombard doesn't hurt
if (tilesCanAttackFromButNotInBombardRange.any())
unit.movement.headTowards(tilesCanAttackFromButNotInBombardRange.minBy { unitDistanceToTiles[it]!!.totalDistance }!!)
if (tileToMoveTo != null)
unit.movement.headTowards(tileToMoveTo)
} else {
// calculate total damage of units in surrounding 4-spaces from enemy city (so we can attack a city from 2 directions at once)
val militaryUnitsAroundEnemyCity = closestReachableEnemyCity.getTilesInDistance(3)
.filter { it.militaryUnit != null && it.militaryUnit!!.civInfo == unit.civInfo }
.map { it.militaryUnit!! }
.map { it.militaryUnit }.filterNotNull()
var totalAttackOnCityPerTurn = -20 // cities heal 20 per turn, so anything below that its useless
val enemyCityCombatant = CityCombatant(closestReachableEnemyCity.getCity()!!)
for (militaryUnit in militaryUnitsAroundEnemyCity) {
@ -290,28 +322,29 @@ class UnitAutomation {
}
fun tryBombardEnemy(city: CityInfo): Boolean {
if (!city.attackedThisTurn) {
val target = chooseBombardTarget(city)
if (target == null) return false
val enemy = Battle.getMapCombatantOfTile(target)!!
return when {
city.attackedThisTurn -> false
else -> {
val enemy = chooseBombardTarget(city) ?: return false
Battle.attack(CityCombatant(city), enemy)
return true
true
}
}
return false
}
private fun chooseBombardTarget(city: CityInfo): TileInfo? {
var targets = getBombardTargets(city)
if (targets.isEmpty()) return null
val siegeUnits = targets
.filter { Battle.getMapCombatantOfTile(it)!!.getUnitType() == UnitType.Siege }
if (siegeUnits.any()) targets = siegeUnits
else {
val rangedUnits = targets
.filter { Battle.getMapCombatantOfTile(it)!!.getUnitType().isRanged() }
if (rangedUnits.any()) targets = rangedUnits
private fun chooseBombardTarget(city: CityInfo): ICombatant? {
val mappedTargets = getBombardTargets(city).map { Battle.getMapCombatantOfTile(it)!! }
.filter {
val unitType = it.getUnitType()
unitType == UnitType.Siege || unitType.isRanged()
}
return targets.minBy { Battle.getMapCombatantOfTile(it)!!.getHealth() }
.groupByTo(LinkedHashMap()) { it.getUnitType() }
var targets = mappedTargets[UnitType.Siege]?.asSequence()
if (targets == null)
targets = mappedTargets.values.asSequence().flatMap { it.asSequence() }
return targets.minBy { it.getHealth() }
}
private fun tryGarrisoningUnit(unit: MapUnit): Boolean {
@ -352,28 +385,6 @@ class UnitAutomation {
return true
}
private fun tryGoToRuin(unit: MapUnit, unitDistanceToTiles: PathsToTilesWithinTurn): Boolean {
if (!unit.civInfo.isMajorCiv()) return false // barbs don't have anything to do in ruins
val tileWithRuin = unitDistanceToTiles.keys
.firstOrNull { it.improvement == Constants.ancientRuins && unit.movement.canMoveTo(it) }
if (tileWithRuin == null) return false
unit.movement.moveToTile(tileWithRuin)
return true
}
internal fun tryExplore(unit: MapUnit, unitDistanceToTiles: PathsToTilesWithinTurn): Boolean {
if (tryGoToRuin(unit, unitDistanceToTiles) && unit.currentMovement == 0f) return true
for (tile in unit.currentTile.getTilesInDistance(10))
if (unit.movement.canMoveTo(tile) && tile.position !in unit.civInfo.exploredTiles
&& unit.movement.canReach(tile)
&& (tile.getOwner()==null || !tile.getOwner()!!.isCityState())) {
unit.movement.headTowards(tile)
return true
}
return false
}
/** This is what a unit with the 'explore' action does.
It also explores, but also has other functions, like healing if necessary. */
fun automatedExplore(unit: MapUnit) {
@ -383,13 +394,4 @@ class UnitAutomation {
if (tryExplore(unit, unit.movement.getDistanceToTiles())) return
unit.civInfo.addNotification("[${unit.name}] finished exploring.", unit.currentTile.position, Color.GRAY)
}
fun wander(unit: MapUnit, unitDistanceToTiles: PathsToTilesWithinTurn) {
val reachableTiles = unitDistanceToTiles
.filter { unit.movement.canMoveTo(it.key) && unit.movement.canReach(it.key) }
val reachableTilesMaxWalkingDistance = reachableTiles.filter { it.value.totalDistance == unit.currentMovement }
if (reachableTilesMaxWalkingDistance.any()) unit.movement.moveToTile(reachableTilesMaxWalkingDistance.toList().random().first)
else if (reachableTiles.any()) unit.movement.moveToTile(reachableTiles.toList().random().first)
}
}

View File

@ -14,8 +14,7 @@ class WorkerAutomation(val unit: MapUnit) {
fun automateWorkerAction() {
val enemyUnitsInWalkingDistance = unit.movement.getDistanceToTiles().keys
.filter {
it.militaryUnit != null && it.militaryUnit!!.civInfo != unit.civInfo
&& unit.civInfo.isAtWarWith(it.militaryUnit!!.civInfo)
it.militaryUnit != null && it.militaryUnit!!.civInfo.isAtWarWith(unit.civInfo)
}
if (enemyUnitsInWalkingDistance.isNotEmpty()) return // Don't you dare move.
@ -122,20 +121,21 @@ class WorkerAutomation(val unit: MapUnit) {
val workableTiles = currentTile.getTilesInDistance(4)
.filter {
(it.civilianUnit == null || it == currentTile)
&& tileCanBeImproved(it, unit.civInfo) }
.sortedByDescending { getPriority(it, unit.civInfo) }.toMutableList()
&& tileCanBeImproved(it, unit.civInfo)
}
.sortedByDescending { getPriority(it, unit.civInfo) }
// the tile needs to be actually reachable - more difficult than it seems,
// which is why we DON'T calculate this for every possible tile in the radius,
// but only for the tile that's about to be chosen.
val selectedTile = workableTiles.firstOrNull { unit.movement.canReach(it) }
if (selectedTile != null
return if (selectedTile != null
&& getPriority(selectedTile, unit.civInfo) > 1
&& (!workableTiles.contains(currentTile)
|| getPriority(selectedTile, unit.civInfo) > getPriority(currentTile, unit.civInfo)))
return selectedTile
else return currentTile
selectedTile
else currentTile
}
private fun tileCanBeImproved(tile: TileInfo, civInfo: CivilizationInfo): Boolean {

View File

@ -62,9 +62,10 @@ class CityExpansionManager {
fun chooseNewTileToOwn(): TileInfo? {
for (i in 2..5) {
val tiles = cityInfo.getCenterTile().getTilesInDistance(i)
.filter {it.getOwner() == null && it.neighbors.any { tile->tile.getOwner()==cityInfo.civInfo }}
if (tiles.isEmpty()) continue
val chosenTile = tiles.maxBy { Automation().rankTile(it,cityInfo.civInfo) }
.filter { it.getOwner() == null
&& it.neighbors.any { tile -> tile.getOwner() == cityInfo.civInfo } }
val chosenTile = tiles.maxBy { Automation.rankTile(it, cityInfo.civInfo) }
if (chosenTile != null)
return chosenTile
}
return null
@ -80,10 +81,9 @@ class CityExpansionManager {
// It becomes an invisible city and weird shit starts happening
takeOwnership(cityInfo.getCenterTile())
cityInfo.getCenterTile().getTilesInDistance(1)
.filter { it.getCity()==null } // can't take ownership of owned tiles (by other cities)
.forEach { takeOwnership(it) }
for (tile in cityInfo.getCenterTile().getTilesInDistance(1)
.filter { it.getCity() == null }) // can't take ownership of owned tiles (by other cities)
takeOwnership(tile)
}
private fun addNewTileWithCulture() {

View File

@ -292,6 +292,7 @@ class CivilizationInfo {
}
fun isAtWarWith(otherCiv:CivilizationInfo): Boolean {
if (otherCiv.civName == civName) return false // never at war with itself
if (otherCiv.isBarbarian() || isBarbarian()) return true
if (!diplomacy.containsKey(otherCiv.civName)) // not encountered yet
return false

View File

@ -142,9 +142,9 @@ class MapUnit {
fun updateVisibleTiles() {
if(type.isAirUnit()) {
if(hasUnique("6 tiles in every direction always visible"))
viewableTiles = getTile().getTilesInDistance(6) // it's that simple
else viewableTiles = listOf() // bomber units don't do recon
viewableTiles = if (hasUnique("6 tiles in every direction always visible"))
getTile().getTilesInDistance(6).toList() // it's that simple
else listOf() // bomber units don't do recon
}
else {
var visibilityRange = 2
@ -161,7 +161,7 @@ class MapUnit {
val tile = getTile()
if (tile.baseTerrain == Constants.hill && type.isLandUnit()) visibilityRange += 1
viewableTiles = tile.getViewableTiles(visibilityRange, type.isWaterUnit())
viewableTiles = tile.getViewableTilesList(visibilityRange, type.isWaterUnit())
}
civInfo.updateViewableTiles() // for the civ
}
@ -388,9 +388,10 @@ class MapUnit {
if (amountToHealBy == 0) return
if (hasUnique("+10 HP when healing")) amountToHealBy += 10
val adjacentUnits = currentTile.getTilesInDistance(1).flatMap { it.getUnits() }
if (adjacentUnits.isNotEmpty())
amountToHealBy += adjacentUnits.map { it.adjacentHealingBonus() }.max()!!
val maxAdjacentHealingBonus = currentTile.getTilesInDistance(1)
.flatMap { it.getUnits().asSequence() }.map { it.adjacentHealingBonus() }.max()
if (maxAdjacentHealingBonus != null)
amountToHealBy += maxAdjacentHealingBonus
if (hasUnique("All healing effects doubled"))
amountToHealBy *= 2
healBy(amountToHealBy)
@ -539,11 +540,17 @@ class MapUnit {
unit.disband()
}
}
destroy()
if (currentTile.getOwner() == civInfo)
civInfo.gold += baseUnit.getDisbandGold()
if (civInfo.isDefeated()) civInfo.destroy()
for (unit in currentTile.getUnits().filter { it.type.isAirUnit() && it.isTransported }) {
if (unit.movement.canMoveTo(currentTile)) continue // we disbanded a carrier in a city, it can still stay in the city
val tileCanMoveTo = unit.currentTile.getTilesInDistance(unit.getRange())
.firstOrNull { unit.movement.canMoveTo(it) }
if (tileCanMoveTo != null) unit.movement.moveToTile(tileCanMoveTo)
else unit.disband()
}
}
private fun getAncientRuinBonus(tile: TileInfo) {
@ -591,7 +598,7 @@ class MapUnit {
// Map of the surrounding area
actions.add {
val revealCenter = tile.getTilesAtDistance(ANCIENT_RUIN_MAP_REVEAL_OFFSET).random(tileBasedRandom)
val revealCenter = tile.getTilesAtDistance(ANCIENT_RUIN_MAP_REVEAL_OFFSET).toList().random(tileBasedRandom)
val tilesToReveal = revealCenter
.getTilesInDistance(ANCIENT_RUIN_MAP_REVEAL_RANGE)
.filter { Random.nextFloat() < ANCIENT_RUIN_MAP_REVEAL_CHANCE }

View File

@ -68,9 +68,8 @@ open class TileInfo {
return false
}
fun containsUnique(unique: String): Boolean {
return isNaturalWonder() && getNaturalWonder().uniques.contains(unique)
}
fun containsUnique(unique: String): Boolean =
isNaturalWonder() && getNaturalWonder().uniques.contains(unique)
//region pure functions
/** Returns military, civilian and air units in tile */
@ -104,13 +103,8 @@ open class TileInfo {
// This is for performance - since we access the neighbors of a tile ALL THE TIME,
// and the neighbors of a tile never change, it's much more efficient to save the list once and for all!
@Transient private var internalNeighbors : List<TileInfo>?=null
val neighbors: List<TileInfo>
get(){
if(internalNeighbors==null)
internalNeighbors = getTilesAtDistance(1)
return internalNeighbors!!
}
@delegate:Transient
val neighbors: List<TileInfo> by lazy { getTilesAtDistance(1).toList() }
fun getHeight(): Int {
if (baseTerrain == Constants.mountain) return 4
@ -127,18 +121,15 @@ open class TileInfo {
return containingCity.civInfo
}
fun getTerrainFeature(): Terrain? {
return if (terrainFeature == null) null else ruleset.terrains[terrainFeature!!]
}
fun getTerrainFeature(): Terrain? =
if (terrainFeature == null) null else ruleset.terrains[terrainFeature!!]
fun isWorked(): Boolean {
val city = getCity()
return city!=null && city.workedTiles.contains(position)
}
fun getTileStats(observingCiv: CivilizationInfo): Stats {
return getTileStats(getCity(), observingCiv)
}
fun getTileStats(observingCiv: CivilizationInfo): Stats = getTileStats(getCity(), observingCiv)
fun getTileStats(city: CityInfo?, observingCiv: CivilizationInfo): Stats {
var stats = getBaseTerrain().clone()
@ -264,21 +255,20 @@ open class TileInfo {
fun isCoastalTile() = neighbors.any { it.baseTerrain==Constants.coast }
fun hasViewableResource(civInfo: CivilizationInfo): Boolean {
return resource != null && (getTileResource().revealedBy == null || civInfo.tech.isResearched(getTileResource().revealedBy!!))
}
fun hasViewableResource(civInfo: CivilizationInfo): Boolean =
resource != null && (getTileResource().revealedBy == null || civInfo.tech.isResearched(getTileResource().revealedBy!!))
fun getViewableTiles(distance:Int, ignoreCurrentTileHeight:Boolean = false): List<TileInfo> {
return tileMap.getViewableTiles(this.position,distance,ignoreCurrentTileHeight)
}
fun getViewableTilesList(distance:Int, ignoreCurrentTileHeight: Boolean): List<TileInfo> =
tileMap.getViewableTiles(position, distance, ignoreCurrentTileHeight)
fun getTilesInDistance(distance:Int): List<TileInfo> {
return tileMap.getTilesInDistance(position,distance)
}
fun getTilesInDistance(distance: Int): Sequence<TileInfo> =
tileMap.getTilesInDistance(position,distance)
fun getTilesAtDistance(distance:Int): List<TileInfo> {
return tileMap.getTilesAtDistance(position,distance)
}
fun getTilesInDistanceRange(range: IntRange): Sequence<TileInfo> =
tileMap.getTilesInDistanceRange(position, range)
fun getTilesAtDistance(distance:Int): Sequence<TileInfo> =
tileMap.getTilesAtDistance(position, distance)
fun getDefensiveBonus(): Float {
var bonus = getBaseTerrain().defenceBonus

View File

@ -73,22 +73,21 @@ class TileMap {
return get(vector.x.toInt(), vector.y.toInt())
}
fun getTilesInDistance(origin: Vector2, distance: Int): List<TileInfo> {
val tilesToReturn = mutableListOf<TileInfo>()
for (i in 0 .. distance) {
tilesToReturn += getTilesAtDistance(origin, i)
}
return tilesToReturn
}
fun getTilesInDistance(origin: Vector2, distance: Int): Sequence<TileInfo> =
getTilesInDistanceRange(origin, 0..distance)
fun getTilesAtDistance(origin: Vector2, distance: Int): List<TileInfo> {
if(distance==0) return listOf(get(origin))
val tilesToReturn = ArrayList<TileInfo>()
fun getTilesInDistanceRange(origin: Vector2, range: IntRange): Sequence<TileInfo> =
sequence {
for (i in range)
yield(getTilesAtDistance(origin, i))
}.flatMap { it }
fun addIfTileExists(x:Int,y:Int){
if(contains(x,y))
tilesToReturn += get(x,y)
}
fun getTilesAtDistance(origin: Vector2, distance: Int): Sequence<TileInfo> =
if (distance <= 0) // silently take negatives.
sequenceOf(get(origin))
else
sequence {
fun getIfTileExistsOrNull(x: Int, y: Int) = if (contains(x, y)) get(x, y) else null
val centerX = origin.x.toInt()
val centerY = origin.y.toInt()
@ -98,26 +97,25 @@ class TileMap {
var currentY = centerY - distance
for (i in 0 until distance) { // From 6 to 8
addIfTileExists(currentX,currentY)
yield(getIfTileExistsOrNull(currentX, currentY))
// We want to get the tile on the other side of the clock,
// so if we're at current = origin-delta we want to get to origin+delta.
// The simplest way to do this is 2*origin - current = 2*origin- (origin - delta) = origin+delta
addIfTileExists(2*centerX - currentX, 2*centerY - currentY)
yield(getIfTileExistsOrNull(2 * centerX - currentX, 2 * centerY - currentY))
currentX += 1 // we're going upwards to the left, towards 8 o'clock
}
for (i in 0 until distance) { // 8 to 10
addIfTileExists(currentX,currentY)
addIfTileExists(2*centerX - currentX, 2*centerY - currentY)
yield(getIfTileExistsOrNull(currentX, currentY))
yield(getIfTileExistsOrNull(2 * centerX - currentX, 2 * centerY - currentY))
currentX += 1
currentY += 1 // we're going up the left side of the hexagon so we're going "up" - +1,+1
}
for (i in 0 until distance) { // 10 to 12
addIfTileExists(currentX,currentY)
addIfTileExists(2*centerX - currentX, 2*centerY - currentY)
yield(getIfTileExistsOrNull(currentX, currentY))
yield(getIfTileExistsOrNull(2 * centerX - currentX, 2 * centerY - currentY))
currentY += 1 // we're going up the top left side of the hexagon so we're heading "up and to the right"
}
return tilesToReturn
}
}.filterNotNull()
/** Tries to place the [unitName] into the [TileInfo] closest to the given the [position]
*
@ -134,24 +132,32 @@ class TileMap {
): MapUnit? {
val unit = gameInfo.ruleSet.units[unitName]!!.getMapUnit(gameInfo.ruleSet)
fun isTileMovePotential(tileInfo:TileInfo): Boolean {
if(unit.type.isAirUnit()) return true
if(unit.type.isWaterUnit()) return tileInfo.isWater || tileInfo.isCityCenter()
else return tileInfo.isLand
fun isTileMovePotential(tileInfo: TileInfo): Boolean =
when {
unit.type.isAirUnit() -> true
unit.type.isWaterUnit() -> tileInfo.isWater || tileInfo.isCityCenter()
else -> tileInfo.isLand
}
val viableTilesToPlaceUnitInAtDistance1 = getTilesInDistance(position, 1).filter { isTileMovePotential(it) }
val viableTilesToPlaceUnitInAtDistance1 = getTilesAtDistance(position, 1)
.filter { isTileMovePotential(it) }.toSet()
// This is so that units don't skip over non-potential tiles to go elsewhere -
// e.g. a city 2 tiles away from a lake could spawn water units in the lake...Or spawn beyond a mountain range...
val viableTilesToPlaceUnitInAtDistance2 = getTilesAtDistance(position, 2)
.filter { isTileMovePotential(it) && it.neighbors.any { n->n in viableTilesToPlaceUnitInAtDistance1 } }
val viableTilesToPlaceUnitIn = getTilesAtDistance(position, 2)
.filter {
isTileMovePotential(it)
&& it.neighbors.any { n -> n in viableTilesToPlaceUnitInAtDistance1 }
} + viableTilesToPlaceUnitInAtDistance1
val viableTilesToPlaceUnitIn = viableTilesToPlaceUnitInAtDistance1.union(viableTilesToPlaceUnitInAtDistance2)
unit.assignOwner(civInfo, false) // both the civ name and actual civ need to be in here in order to calculate the canMoveTo...Darn
val unitToPlaceTile = viableTilesToPlaceUnitIn.firstOrNull { unit.movement.canMoveTo(it) }
if(unitToPlaceTile!=null) {
if (unitToPlaceTile == null) {
civInfo.removeUnit(unit) // since we added it to the civ units in the previous assignOwner
return null // we didn't actually create a unit...
}
// Remove the tile improvement, e.g. when placing the starter units (so they don't spawn on ruins/encampments)
if (removeImprovement) unitToPlaceTile.improvement = null
// only once we know the unit can be placed do we add it to the civ's unit list
@ -165,18 +171,14 @@ class TileMap {
// And update civ stats, since the new unit changes both unit upkeep and resource consumption
civInfo.updateStatsForNextTurn()
civInfo.updateDetailedCivResources()
}
else {
civInfo.removeUnit(unit) // since we added it to the civ units in the previous assignOwner
return null // we didn't actually create a unit...
}
return unit
}
fun getViewableTiles(position: Vector2, sightDistance: Int, ignoreCurrentTileHeight: Boolean = false): List<TileInfo> {
if (ignoreCurrentTileHeight) return getTilesInDistance(position, sightDistance)
fun getViewableTiles(position: Vector2, sightDistance: Int, ignoreCurrentTileHeight: Boolean)
: List<TileInfo> {
if (ignoreCurrentTileHeight) return getTilesInDistance(position, sightDistance).toList()
val viewableTiles = getTilesInDistance(position, 1).toMutableList()
val currentTileHeight = get(position).getHeight()

View File

@ -43,9 +43,9 @@ class UnitMovementAlgorithms(val unit:MapUnit) {
class ParentTileAndTotalDistance(val parentTile:TileInfo, val totalDistance: Float)
fun getDistanceToTilesWithinTurn(origin: Vector2, unitMovement: Float): PathsToTilesWithinTurn {
if(unitMovement==0f) return PathsToTilesWithinTurn()
val distanceToTiles = PathsToTilesWithinTurn()
if(unitMovement==0f) return distanceToTiles
val unitTile = unit.getTile().tileMap[origin]
distanceToTiles[unitTile] = ParentTileAndTotalDistance(unitTile,0f)
var tilesToCheck = listOf(unitTile)
@ -344,7 +344,8 @@ class UnitMovementAlgorithms(val unit:MapUnit) {
return true
}
fun getDistanceToTiles() = getDistanceToTilesWithinTurn(unit.currentTile.position,unit.currentMovement)
fun getDistanceToTiles(): PathsToTilesWithinTurn
= getDistanceToTilesWithinTurn(unit.currentTile.position,unit.currentMovement)
fun getArialPathsToCities(): HashMap<TileInfo, ArrayList<TileInfo>> {
var tilesToCheck = ArrayList<TileInfo>()

View File

@ -98,7 +98,7 @@ class CityScreen(internal val city: CityInfo): CameraStageBaseScreen() {
updateAnnexAndRazeCityButton()
updateTileGroups()
if (city.getCenterTile().getTilesAtDistance(4).isNotEmpty())
if (city.getCenterTile().getTilesAtDistance(4).any())
displayTutorial(Tutorial.CityRange)
}

View File

@ -251,18 +251,20 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap
tileGroups[unit.getTile()]!!.selectUnit(unit)
val isAirUnit = unit.type.isAirUnit()
val tilesInMoveRange = if(isAirUnit) unit.getTile().getTilesInDistance(unit.getRange())
else unit.movement.getDistanceToTiles().keys
val tilesInMoveRange =
if (isAirUnit)
for(tile in tilesInMoveRange)
tileGroups[tile]!!.showCircle(Color.BLUE,0.3f)
unit.getTile().getTilesInDistance(unit.getRange())
else
unit.movement.getDistanceToTiles().keys.asSequence()
for (tile: TileInfo in tilesInMoveRange)
for (tile in tilesInMoveRange) {
val tileToColor = tileGroups.getValue(tile)
if (isAirUnit)
tileToColor.showCircle(Color.BLUE, 0.3f)
if (unit.movement.canMoveTo(tile))
tileGroups[tile]!!.showCircle(Color.WHITE,
tileToColor.showCircle(Color.WHITE,
if (UncivGame.Current.settings.singleTapMove || isAirUnit) 0.7f else 0.3f)
}
val unitType = unit.type
val attackableTiles: List<TileInfo> = if (unitType.isCivilian()) listOf()
@ -289,7 +291,7 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap
}
private fun updateTilegroupsForSelectedCity(city: CityInfo, playerViewableTilePositions: HashSet<Vector2>) {
val attackableTiles: List<TileInfo> = UnitAutomation().getBombardTargets(city)
val attackableTiles = UnitAutomation().getBombardTargets(city)
.filter { (UncivGame.Current.viewEntireMapForDebug || playerViewableTilePositions.contains(it.position)) }
for (attackableTile in attackableTiles) {
tileGroups[attackableTile]!!.showCircle(colorFromRGB(237, 41, 57))