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,11 +164,12 @@ 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
&& it.naturalWonder==null
&& it.terrainFeature == null
&& it.naturalWonder == null
&& it !in tilesWithin3ofExistingEncampment
&& it !in allViewableTiles
}

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 }
@ -94,9 +93,9 @@ class SpecificUnitAutomation{
fun automateSettlerActions(unit: MapUnit) {
if(unit.getTile().militaryUnit==null) return // Don't move until you're accompanied by a military unit
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,34 +105,36 @@ 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()
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 }
.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
}
val luxuryResourcesInCivArea = unit.civInfo.cities.asSequence()
.flatMap { it.getTiles().asSequence() }.filter { it.resource!=null }
.map { it.getTileResource() }.filter { it.resourceType==ResourceType.Luxury }
.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
if (bestCityLocation == null) { // We got a badass over here, all tiles within 5 are taken? Screw it, random walk.
val unitDistanceToTiles = unit.movement.getDistanceToTiles()
if (UnitAutomation.tryExplore(unit, unitDistanceToTiles)) return // try to find new areas
UnitAutomation.wander(unit, unitDistanceToTiles) // go around aimlessly
return
}
if(bestCityLocation.getTilesInDistance(3).any { it.isCityCenter() })
if (bestCityLocation.getTilesInDistance(3).any { it.isCityCenter() })
throw Exception("City within distance")
if (unit.getTile() == bestCityLocation)
@ -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,35 +186,41 @@ 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(battleHelper.tryAttackNearbyEnemy(unit)) return
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
val immediatelyReachableCities = tilesInRange
.filter { it.isCityCenter() && it.getOwner()==unit.civInfo && unit.movement.canMoveTo(it)}
.filter { it.isCityCenter() && it.getOwner() == unit.civInfo && unit.movement.canMoveTo(it) }
for(city in immediatelyReachableCities){
if(city.getTilesInDistance(unit.getRange())
.any { battleHelper.containsAttackableEnemy(it,MapUnitCombatant(unit)) }) {
for (city in immediatelyReachableCities) {
if (city.getTilesInDistance(unit.getRange())
.any { battleHelper.containsAttackableEnemy(it, MapUnitCombatant(unit)) }) {
unit.movement.moveToTile(city)
return
}
}
val pathsToCities = unit.movement.getArialPathsToCities()
if(pathsToCities.isEmpty()) return // can't actually move anywhere else
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 }){
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.
@ -118,24 +117,25 @@ class WorkerAutomation(val unit: MapUnit) {
* Returns the current tile if no tile to work was found
*/
private fun findTileToWork(): TileInfo {
val currentTile=unit.getTile()
val currentTile = unit.getTile()
val workableTiles = currentTile.getTilesInDistance(4)
.filter {
(it.civilianUnit== null || it == currentTile)
&& tileCanBeImproved(it, unit.civInfo) }
.sortedByDescending { getPriority(it, unit.civInfo) }.toMutableList()
(it.civilianUnit == null || it == currentTile)
&& 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) }
val selectedTile = workableTiles.firstOrNull { unit.movement.canReach(it) }
if (selectedTile != null
&& getPriority(selectedTile, unit.civInfo)>1
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
@ -72,7 +73,7 @@ class CityExpansionManager {
//region state-changing functions
fun reset() {
for(tile in cityInfo.getTiles())
for (tile in cityInfo.getTiles())
relinquishOwnership(tile)
// The only way to create a city inside an owned tile is if it's in your territory
@ -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

@ -277,7 +277,7 @@ class CityInfo {
fun setTransients() {
tileMap = civInfo.gameInfo.tileMap
centerTileInfo = tileMap[location]
tilesInRange = getCenterTile().getTilesInDistance( 3).toHashSet()
tilesInRange = getCenterTile().getTilesInDistance(3).toHashSet()
population.cityInfo = this
expansion.cityInfo = this
expansion.setTransients()

View File

@ -292,8 +292,9 @@ class CivilizationInfo {
}
fun isAtWarWith(otherCiv:CivilizationInfo): Boolean {
if(otherCiv.isBarbarian() || isBarbarian()) return true
if(!diplomacy.containsKey(otherCiv.civName)) // not encountered yet
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
return getDiplomacyManager(otherCiv).diplomaticStatus == DiplomaticStatus.War
}

View File

@ -141,10 +141,10 @@ 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
if(type.isAirUnit()) {
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)
@ -441,19 +442,19 @@ class MapUnit {
}
}
fun startTurn(){
fun startTurn() {
currentMovement = getMaxMovement().toFloat()
attacksThisTurn=0
attacksThisTurn = 0
due = true
// Wake sleeping units if there's an enemy nearby
if(isSleeping() && currentTile.getTilesInDistance(2).any {
it.militaryUnit!=null && it.militaryUnit!!.civInfo.isAtWarWith(civInfo)
if (isSleeping() && currentTile.getTilesInDistance(2).any {
it.militaryUnit != null && it.militaryUnit!!.civInfo.isAtWarWith(civInfo)
})
action=null
action = null
val tileOwner = getTile().getOwner()
if(tileOwner!=null && !civInfo.canEnterTiles(tileOwner) && !tileOwner.isCityState()) // if an enemy city expanded onto this tile while I was in it
if (tileOwner != null && !civInfo.canEnterTiles(tileOwner) && !tileOwner.isCityState()) // if an enemy city expanded onto this tile while I was in it
movement.teleportToClosestMoveableTile()
doPreTurnAction()
}
@ -520,7 +521,7 @@ class MapUnit {
civInfo.addNotification("We have captured a barbarian encampment and recovered [${goldGained.toInt()}] gold!", tile.position, Color.RED)
}
fun disband(){
fun disband() {
// evacuation of transported units before disbanding, if possible
if (type.isAircraftCarrierUnit() || type.isMissileCarrierUnit()) {
for(unit in currentTile.getUnits().filter { it.type.isAirUnit() && it.isTransported }) {
@ -539,11 +540,17 @@ class MapUnit {
unit.disband()
}
}
destroy()
if(currentTile.getOwner()==civInfo)
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
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
@ -159,24 +165,20 @@ class TileMap {
unit.currentMovement = unit.getMaxMovement().toFloat()
// Only once we add the unit to the civ we can activate addPromotion, because it will try to update civ viewable tiles
for(promotion in unit.baseUnit.promotions)
for (promotion in unit.baseUnit.promotions)
unit.promotions.addPromotion(promotion, true)
// 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)
unit.getTile().getTilesInDistance(unit.getRange())
else
unit.movement.getDistanceToTiles().keys.asSequence()
if(isAirUnit)
for(tile in tilesInMoveRange)
tileGroups[tile]!!.showCircle(Color.BLUE,0.3f)
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()
@ -281,15 +283,15 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap
else 0.5f
for (tile in tileGroups.values) {
if (tile.icons.populationIcon != null) tile.icons.populationIcon!!.color.a = fadeout
if (tile.icons.improvementIcon != null && tile.tileInfo.improvement!=Constants.barbarianEncampment
&& tile.tileInfo.improvement!=Constants.ancientRuins)
if (tile.icons.improvementIcon != null && tile.tileInfo.improvement != Constants.barbarianEncampment
&& tile.tileInfo.improvement != Constants.ancientRuins)
tile.icons.improvementIcon!!.color.a = fadeout
if (tile.resourceImage != null) tile.resourceImage!!.color.a = fadeout
}
}
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))