From 4cfbf181784566593bb3f7a786be4c1f55cf80a3 Mon Sep 17 00:00:00 2001 From: Yair Morgenstern Date: Wed, 9 May 2018 21:31:34 +0300 Subject: [PATCH] Organized unit algorithms Combat units now head towards the closest city with no garrison if exists --- .../unciv/logic/automation/UnitAutomation.kt | 15 +- .../logic/automation/WorkerAutomation.kt | 11 +- core/src/com/unciv/logic/map/MapUnit.kt | 28 ++- .../unciv/logic/map/UnitMovementAlgorithms.kt | 187 +++++++++--------- .../unciv/ui/worldscreen/unit/UnitTable.kt | 5 +- 5 files changed, 125 insertions(+), 121 deletions(-) diff --git a/core/src/com/unciv/logic/automation/UnitAutomation.kt b/core/src/com/unciv/logic/automation/UnitAutomation.kt index 262a8aa7..6a9dade1 100644 --- a/core/src/com/unciv/logic/automation/UnitAutomation.kt +++ b/core/src/com/unciv/logic/automation/UnitAutomation.kt @@ -62,7 +62,7 @@ class UnitAutomation{ if (unitToAttack.getBaseUnit().unitType == UnitType.Civilian) { // kill unitToAttack.civInfo.addNotification("Our " + unitToAttack.name + " was destroyed by an enemy " + unit.name + "!", unitTileToAttack.position) unitTileToAttack.unit = null - unit.headTowards(unitTileToAttack.position) + unit.movementAlgs().headTowards(unitTileToAttack) return } @@ -70,7 +70,7 @@ class UnitAutomation{ if (damageToAttacker < unit.health) { // don't attack if we'll die from the attack if(MapUnitCombatant(unit).isMelee()) - unit.headTowards(unitTileToAttack.position) + unit.movementAlgs().headTowards(unitTileToAttack) Battle(unit.civInfo.gameInfo).attack(MapUnitCombatant(unit), MapUnitCombatant(unitToAttack)) return } @@ -78,6 +78,13 @@ class UnitAutomation{ if(unit.getTile().isCityCenter()) return // It's always good to have a unit in the city center, so if you havn't found annyonw aroud to attack, forget it. + val reachableCitiesWithoutUnits = unit.civInfo.cities.filter { it.getCenterTile().unit==null + && unit.movementAlgs().canReach(it.getCenterTile()) } + if(reachableCitiesWithoutUnits.isNotEmpty()){ + val closestCityWithoutUnit = reachableCitiesWithoutUnits + .minBy { unit.movementAlgs().getShortestPath(it.getCenterTile()).size }!! + unit.movementAlgs().headTowards(closestCityWithoutUnit.getCenterTile()) + } if (unit.health < 80) { healUnit(unit) @@ -94,7 +101,7 @@ class UnitAutomation{ .firstOrNull { attackableTiles.contains(it) } if (closestUnit != null) { - unit.headTowards(closestUnit.position) + unit.movementAlgs().headTowards(closestUnit) return } @@ -132,7 +139,7 @@ class UnitAutomation{ if (unit.getTile() == bestCityLocation) UnitActions().getUnitActions(unit, UnCivGame.Current.worldScreen!!).first { it.name == "Found city" }.action() else { - unit.headTowards(bestCityLocation.position) + unit.movementAlgs().headTowards(bestCityLocation) if (unit.currentMovement > 0 && unit.getTile() == bestCityLocation) UnitActions().getUnitActions(unit, UnCivGame.Current.worldScreen!!).first { it.name == "Found city" }.action() } diff --git a/core/src/com/unciv/logic/automation/WorkerAutomation.kt b/core/src/com/unciv/logic/automation/WorkerAutomation.kt index 1568216c..5e0063d2 100644 --- a/core/src/com/unciv/logic/automation/WorkerAutomation.kt +++ b/core/src/com/unciv/logic/automation/WorkerAutomation.kt @@ -3,18 +3,17 @@ package com.unciv.logic.automation import com.unciv.logic.civilization.CivilizationInfo import com.unciv.logic.map.MapUnit import com.unciv.logic.map.TileInfo -import com.unciv.logic.map.UnitMovementAlgorithms import com.unciv.models.gamebasics.GameBasics import com.unciv.models.gamebasics.TileImprovement class WorkerAutomation { fun automateWorkerAction(unit: MapUnit) { - var tile = unit.getTile() + val tile = unit.getTile() val tileToWork = findTileToWork(unit) if (tileToWork != tile) { - tile = unit.headTowards(tileToWork.position) - unit.doPreTurnAction(tile) + unit.movementAlgs().headTowards(tileToWork) + unit.doPreTurnAction() return } if (tile.improvementInProgress == null) { @@ -39,8 +38,8 @@ class WorkerAutomation { // 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{ - UnitMovementAlgorithms(currentTile.tileMap) - .getShortestPath(currentTile.position, workableTiles.first().position,worker) + worker.movementAlgs() + .getShortestPath(workableTiles.first()) .isNotEmpty()} if (selectedTile != null diff --git a/core/src/com/unciv/logic/map/MapUnit.kt b/core/src/com/unciv/logic/map/MapUnit.kt index 6ec67486..b2ad6d51 100644 --- a/core/src/com/unciv/logic/map/MapUnit.kt +++ b/core/src/com/unciv/logic/map/MapUnit.kt @@ -26,31 +26,32 @@ class MapUnit { fun getDistanceToTiles(): HashMap { val tile = getTile() - return UnitMovementAlgorithms(tile.tileMap).getDistanceToTilesWithinTurn(tile.position,currentMovement, - this) + return movementAlgs().getDistanceToTilesWithinTurn(tile.position,currentMovement) } - fun doPreTurnAction(tile: TileInfo) { + fun doPreTurnAction() { + val currentTile = getTile() if (currentMovement == 0f) return // We've already done stuff this turn, and can't do any more stuff if (action != null && action!!.startsWith("moveTo")) { val destination = action!!.replace("moveTo ", "").split(",").dropLastWhile { it.isEmpty() }.toTypedArray() val destinationVector = Vector2(Integer.parseInt(destination[0]).toFloat(), Integer.parseInt(destination[1]).toFloat()) - val gotTo = headTowards(destinationVector) - if(gotTo==tile) // We didn't move at all + val gotTo = movementAlgs().headTowards(currentTile.tileMap[destinationVector]) + if(gotTo==currentTile) // We didn't move at all return if (gotTo.position == destinationVector) action = null - if (currentMovement != 0f) doPreTurnAction(gotTo) + if (currentMovement != 0f) doPreTurnAction() return } if (action == "automation") WorkerAutomation().automateWorkerAction(this) } - private fun doPostTurnAction(tile: TileInfo) { - if (name == "Worker" && tile.improvementInProgress != null) workOnImprovement(tile) + private fun doPostTurnAction() { + if (name == "Worker" && getTile().improvementInProgress != null) workOnImprovement() } - private fun workOnImprovement(tile: TileInfo) { + private fun workOnImprovement() { + val tile=getTile() tile.turnsToImprovement -= 1 if (tile.turnsToImprovement != 0) return when { @@ -65,9 +66,6 @@ class MapUnit { /** * @return The tile that we reached this turn */ - fun headTowards(destination: Vector2): TileInfo { - return UnitMovementAlgorithms(getTile().tileMap).headTowards(this,destination) - } private fun heal(){ val tile = getTile() @@ -93,13 +91,12 @@ class MapUnit { } fun nextTurn() { - val tile = getTile() - doPostTurnAction(tile) + doPostTurnAction() if(currentMovement==maxMovement.toFloat()){ // didn't move this turn heal() } currentMovement = maxMovement.toFloat() - doPreTurnAction(tile) + doPreTurnAction() } fun hasUnique(unique:String): Boolean { @@ -107,4 +104,5 @@ class MapUnit { return baseUnit.uniques!=null && baseUnit.uniques!!.contains(unique) } + fun movementAlgs() = UnitMovementAlgorithms(this) } \ No newline at end of file diff --git a/core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt b/core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt index 67769ca5..69adc4eb 100644 --- a/core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt +++ b/core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt @@ -2,136 +2,136 @@ package com.unciv.logic.map import com.badlogic.gdx.math.Vector2 -class UnitMovementAlgorithms(val tileMap: TileMap){ - private fun getMovementCostBetweenAdjacentTiles(from:TileInfo, to:TileInfo, unit:MapUnit): Float { +class UnitMovementAlgorithms(val unit:MapUnit) { + val tileMap = unit.getTile().tileMap + + private fun getMovementCostBetweenAdjacentTiles(from: TileInfo, to: TileInfo): Float { if (from.roadStatus === RoadStatus.Railroad && to.roadStatus === RoadStatus.Railroad) return 1 / 10f if (from.roadStatus !== RoadStatus.None && to.roadStatus !== RoadStatus.None) //Road { - if(unit.civInfo.tech.isResearched("Machinery")) return 1/3f - else return 1/2f + if (unit.civInfo.tech.isResearched("Machinery")) return 1 / 3f + else return 1 / 2f } - if(unit.hasUnique("Ignores terrain cost")) return 1f + if (unit.hasUnique("Ignores terrain cost")) return 1f - if(unit.hasUnique("Rough terrain penalty") - && (to.baseTerrain=="Hill" || to.terrainFeature=="Forest" || to.terrainFeature=="Jungle")) + if (unit.hasUnique("Rough terrain penalty") + && (to.baseTerrain == "Hill" || to.terrainFeature == "Forest" || to.terrainFeature == "Jungle")) return 4f return to.lastTerrain.movementCost.toFloat() // no road } - fun getDistanceToTilesWithinTurn(origin: Vector2, unitMovement:Float, unit: MapUnit): HashMap { - val distanceToTiles = HashMap() - val unitTile = tileMap[origin] - distanceToTiles[unitTile] = 0f - var tilesToCheck = listOf(unitTile) + fun getDistanceToTilesWithinTurn(origin: Vector2, unitMovement: Float): HashMap { + val distanceToTiles = HashMap() + val unitTile = tileMap[origin] + distanceToTiles[unitTile] = 0f + var tilesToCheck = listOf(unitTile) - while (!tilesToCheck.isEmpty()) { - val updatedTiles = ArrayList() - for (tileToCheck in tilesToCheck) - for (neighbor in tileToCheck.neighbors) { - if(neighbor.getOwner() != null && neighbor.getOwner() != unit.civInfo - && neighbor.isCityCenter()) - continue // Enemy city, can't move through it! + while (!tilesToCheck.isEmpty()) { + val updatedTiles = ArrayList() + for (tileToCheck in tilesToCheck) + for (neighbor in tileToCheck.neighbors) { + if (neighbor.getOwner() != null && neighbor.getOwner() != unit.civInfo + && neighbor.isCityCenter()) + continue // Enemy city, can't move through it! - val distanceBetweenTiles = getMovementCostBetweenAdjacentTiles(tileToCheck,neighbor,unit) + val distanceBetweenTiles = getMovementCostBetweenAdjacentTiles(tileToCheck, neighbor) - var totalDistanceToTile = distanceToTiles[tileToCheck]!! + distanceBetweenTiles - if (!distanceToTiles.containsKey(neighbor) || distanceToTiles[neighbor]!! > totalDistanceToTile) { - if (totalDistanceToTile < unitMovement) - updatedTiles += neighbor - else - totalDistanceToTile = unitMovement - distanceToTiles[neighbor] = totalDistanceToTile - } - } + var totalDistanceToTile = distanceToTiles[tileToCheck]!! + distanceBetweenTiles + if (!distanceToTiles.containsKey(neighbor) || distanceToTiles[neighbor]!! > totalDistanceToTile) { + if (totalDistanceToTile < unitMovement) + updatedTiles += neighbor + else + totalDistanceToTile = unitMovement + distanceToTiles[neighbor] = totalDistanceToTile + } + } - tilesToCheck = updatedTiles - } + tilesToCheck = updatedTiles + } - return distanceToTiles - } + return distanceToTiles + } - fun getShortestPath(origin: Vector2, destination: Vector2, unit:MapUnit): List { - if(origin.equals(destination)) return listOf(tileMap[origin]) // edge case that's needed, so that workers will know that they can reach their own tile. *sig + fun getShortestPath(destination: TileInfo): List { + val currentTile = unit.getTile() + if (currentTile.position.equals(destination)) return listOf(currentTile) // edge case that's needed, so that workers will know that they can reach their own tile. *sig - var tilesToCheck: List = listOf(tileMap[origin]) - val movementTreeParents = HashMap() // contains a map of "you can get from X to Y in that turn" - movementTreeParents[tileMap[origin]] = null + var tilesToCheck: List = listOf(currentTile) + val movementTreeParents = HashMap() // contains a map of "you can get from X to Y in that turn" + movementTreeParents[currentTile] = null - var distance = 1 - while (true) { - val newTilesToCheck = ArrayList() - val distanceToDestination = HashMap() - val movementThisTurn = if (distance == 1) unit.currentMovement else unit.maxMovement.toFloat() - for (tileToCheck in tilesToCheck) { - val distanceToTilesThisTurn = getDistanceToTilesWithinTurn(tileToCheck.position, movementThisTurn, unit) - for (reachableTile in distanceToTilesThisTurn.keys) { - if(reachableTile.position == destination) - distanceToDestination[tileToCheck] = distanceToTilesThisTurn[reachableTile]!! - else { - if (movementTreeParents.containsKey(reachableTile)) continue // We cannot be faster than anything existing... - if (reachableTile.position != destination && reachableTile.unit != null) continue // This is an intermediary tile that contains a unit - we can't go there! - movementTreeParents[reachableTile] = tileToCheck - newTilesToCheck.add(reachableTile) - } - } - } + var distance = 1 + while (true) { + val newTilesToCheck = ArrayList() + val distanceToDestination = HashMap() + val movementThisTurn = if (distance == 1) unit.currentMovement else unit.maxMovement.toFloat() + for (tileToCheck in tilesToCheck) { + val distanceToTilesThisTurn = getDistanceToTilesWithinTurn(tileToCheck.position, movementThisTurn) + for (reachableTile in distanceToTilesThisTurn.keys) { + if (reachableTile == destination) + distanceToDestination[tileToCheck] = distanceToTilesThisTurn[reachableTile]!! + else { + if (movementTreeParents.containsKey(reachableTile)) continue // We cannot be faster than anything existing... + if (reachableTile != destination && reachableTile.unit != null) continue // This is an intermediary tile that contains a unit - we can't go there! + movementTreeParents[reachableTile] = tileToCheck + newTilesToCheck.add(reachableTile) + } + } + } - if (distanceToDestination.isNotEmpty()) { - val path = mutableListOf(tileMap[destination]) // Traverse the tree upwards to get the list of tiles leading to the destination, - var currentTile = distanceToDestination.minBy { it.value }!!.key - while (currentTile.position != origin) { - path.add(currentTile) - currentTile = movementTreeParents[currentTile]!! - } - return path.reversed() // and reverse in order to get the list in chronological order - } + if (distanceToDestination.isNotEmpty()) { + val path = mutableListOf(destination) // Traverse the tree upwards to get the list of tiles leading to the destination, + // Get the tile from which the distance to the final tile in least - + // this is so that when we finally get there, we'll have as many movement points as possible + var intermediateTile = distanceToDestination.minBy { it.value }!!.key + while (intermediateTile != currentTile) { + path.add(intermediateTile) + intermediateTile = movementTreeParents[intermediateTile]!! + } + return path.reversed() // and reverse in order to get the list in chronological order + } - if(newTilesToCheck.isEmpty()) return emptyList() // there is NO PATH (eg blocked by enemy units) + if (newTilesToCheck.isEmpty()) return emptyList() // there is NO PATH (eg blocked by enemy units) - // no need to check tiles that are surrounded by reachable tiles, only need to check the edgemost tiles. - // Because anything we can reach from intermediate tiles, can be more easily reached by the edgemost tiles, - // since we'll have to pass through an edgemost tile in order to reach the diestination anyway - tilesToCheck = newTilesToCheck.filterNot {tile -> tile.neighbors.all{newTilesToCheck.contains(it) || tilesToCheck.contains(it) } } + // no need to check tiles that are surrounded by reachable tiles, only need to check the edgemost tiles. + // Because anything we can reach from intermediate tiles, can be more easily reached by the edgemost tiles, + // since we'll have to pass through an edgemost tile in order to reach the diestination anyway + tilesToCheck = newTilesToCheck.filterNot { tile -> tile.neighbors.all { newTilesToCheck.contains(it) || tilesToCheck.contains(it) } } - distance++ - } - } + distance++ + } + } /** - * @param origin - * @param destination * @return The tile that we reached this turn */ - fun headTowards(unit:MapUnit,destination: Vector2): TileInfo { + fun headTowards(destination: TileInfo): TileInfo { val currentTile = unit.getTile() - val tileMap = currentTile.tileMap - val finalDestinationTile = tileMap[destination] val distanceToTiles = unit.getDistanceToTiles() - val destinationTileThisTurn:TileInfo - if (distanceToTiles.containsKey(finalDestinationTile)) { // we can get there this turn - if (finalDestinationTile.unit == null) - destinationTileThisTurn = finalDestinationTile + val destinationTileThisTurn: TileInfo + if (distanceToTiles.containsKey(destination)) { // we can get there this turn + if (destination.unit == null) + destinationTileThisTurn = destination else // Someone is blocking to the path to the final tile... { - val destinationNeighbors = tileMap[destination].neighbors - if(destinationNeighbors.contains(currentTile)) // We're right nearby anyway, no need to move + val destinationNeighbors = destination.neighbors + if (destinationNeighbors.contains(currentTile)) // We're right nearby anyway, no need to move return currentTile - val reachableDestinationNeighbors = destinationNeighbors.filter { distanceToTiles.containsKey(it) && it.unit==null } - if(reachableDestinationNeighbors.isEmpty()) // We can't get closer... + val reachableDestinationNeighbors = destinationNeighbors + .filter { distanceToTiles.containsKey(it) && it.unit == null } + if (reachableDestinationNeighbors.isEmpty()) // We can't get closer... return currentTile destinationTileThisTurn = reachableDestinationNeighbors.minBy { distanceToTiles[it]!! }!! } - } - - else { // If the tile is far away, we need to build a path how to get there, and then take the first step - val path = getShortestPath(currentTile.position, destination, unit) + } else { // If the tile is far away, we need to build a path how to get there, and then take the first step + val path = getShortestPath(destination) destinationTileThisTurn = path.first() } @@ -139,7 +139,8 @@ class UnitMovementAlgorithms(val tileMap: TileMap){ return destinationTileThisTurn } + fun canReach(destination: TileInfo): Boolean { + return getShortestPath(destination).isNotEmpty() + } - - -} +} \ No newline at end of file diff --git a/core/src/com/unciv/ui/worldscreen/unit/UnitTable.kt b/core/src/com/unciv/ui/worldscreen/unit/UnitTable.kt index 4bb60749..537561e5 100644 --- a/core/src/com/unciv/ui/worldscreen/unit/UnitTable.kt +++ b/core/src/com/unciv/ui/worldscreen/unit/UnitTable.kt @@ -5,7 +5,6 @@ import com.badlogic.gdx.scenes.scene2d.ui.Label import com.badlogic.gdx.scenes.scene2d.ui.Table import com.unciv.logic.map.MapUnit import com.unciv.logic.map.TileInfo -import com.unciv.logic.map.UnitMovementAlgorithms import com.unciv.logic.map.UnitType import com.unciv.ui.utils.CameraStageBaseScreen import com.unciv.ui.utils.ImageGetter @@ -66,8 +65,8 @@ class UnitTable(val worldScreen: WorldScreen) : Table(){ fun tileSelected(selectedTile: TileInfo) { if(currentlyExecutingAction=="moveTo"){ - if(UnitMovementAlgorithms(selectedTile.tileMap) - .getShortestPath(selectedTile.position, selectedTile.position, selectedUnit!!).isEmpty()) + if(selectedUnit!!.movementAlgs() + .getShortestPath(selectedUnit!!.getTile().position, selectedTile.position).isEmpty()) return // can't reach there with the selected unit, watcha want me to do? val reachedTile = selectedUnit!!.headTowards(selectedTile.position)