mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-21 13:18:56 +07:00
Experimental pathfinding! (#10883)
* Experimental pathfinding - still not good enough for main branch * Fixed strange pathfinding and impassible tiles * Fixed weirdest bug in the history of weird bugs - remove() on an object did not remove the object * Avoid damaging and enemy tiles for new pathfinding * Add option for activating experimental movement
This commit is contained in:
@ -785,6 +785,7 @@ Automated units can upgrade =
|
|||||||
Automated units choose promotions =
|
Automated units choose promotions =
|
||||||
Order trade offers by amount =
|
Order trade offers by amount =
|
||||||
Ask for confirmation when pressing next turn =
|
Ask for confirmation when pressing next turn =
|
||||||
|
EXPERIMENTAL movement - use at your own risk! =
|
||||||
Notifications log max turns =
|
Notifications log max turns =
|
||||||
|
|
||||||
## Language tab
|
## Language tab
|
||||||
|
@ -2,6 +2,7 @@ package com.unciv.logic.map.mapunit.movement
|
|||||||
|
|
||||||
import com.badlogic.gdx.math.Vector2
|
import com.badlogic.gdx.math.Vector2
|
||||||
import com.unciv.Constants
|
import com.unciv.Constants
|
||||||
|
import com.unciv.UncivGame
|
||||||
import com.unciv.logic.map.BFS
|
import com.unciv.logic.map.BFS
|
||||||
import com.unciv.logic.map.HexMath.getDistance
|
import com.unciv.logic.map.HexMath.getDistance
|
||||||
import com.unciv.logic.map.mapunit.MapUnit
|
import com.unciv.logic.map.mapunit.MapUnit
|
||||||
@ -9,6 +10,13 @@ import com.unciv.logic.map.tile.Tile
|
|||||||
import com.unciv.models.UnitActionType
|
import com.unciv.models.UnitActionType
|
||||||
import com.unciv.models.ruleset.unique.UniqueType
|
import com.unciv.models.ruleset.unique.UniqueType
|
||||||
import com.unciv.ui.components.UnitMovementMemoryType
|
import com.unciv.ui.components.UnitMovementMemoryType
|
||||||
|
import java.util.TreeSet
|
||||||
|
|
||||||
|
|
||||||
|
fun List<UnitMovement.MovementStep>.toBackwardsCompatiblePath(): List<Tile> {
|
||||||
|
val backwardsCompatiblePath = this.reversed().filter { it.totalCost.movementLeft == 0f || it == this.last() }
|
||||||
|
return backwardsCompatiblePath.map { it.tile }
|
||||||
|
}
|
||||||
|
|
||||||
class UnitMovement(val unit: MapUnit) {
|
class UnitMovement(val unit: MapUnit) {
|
||||||
|
|
||||||
@ -82,11 +90,139 @@ class UnitMovement(val unit: MapUnit) {
|
|||||||
return distanceToTiles
|
return distanceToTiles
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
data class MovementStep(val previousStep: MovementStep?, val tile: Tile, val movementCost:Float, val totalCost: MovementStepTotalCost)
|
||||||
|
|
||||||
|
|
||||||
|
data class MovementStepTotalCost(/** Turn 0 means the initial turn */ val turn: Int, val movementLeft: Float):Comparable<MovementStepTotalCost> {
|
||||||
|
override operator fun compareTo(other: MovementStepTotalCost) =
|
||||||
|
compareValuesBy(this, other, {it.turn}, {-it.movementLeft})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Problem and solution documented at https://yairm210.medium.com/multi-turn-pathfinding-7136bd0bdaf0 */
|
||||||
|
fun getShortestPathNew(destination: Tile, considerZoneOfControl: Boolean = true,
|
||||||
|
/** For allowing optional avoid of damaging tiles, tiles outside borders, etc */ shouldAvoidTile: (Tile) -> Boolean = {false},
|
||||||
|
maxTurns: Int = 25,
|
||||||
|
): List<MovementStep> {
|
||||||
|
if (unit.cache.cannotMove) return listOf()
|
||||||
|
|
||||||
|
val startingTile = unit.getTile()
|
||||||
|
val initialStep = MovementStep(null, startingTile, 0f, MovementStepTotalCost(0, unit.currentMovement))
|
||||||
|
|
||||||
|
if (startingTile.position == destination) {
|
||||||
|
// edge case that's needed, so that workers will know that they can reach their own tile. *sigh*
|
||||||
|
pathfindingCache.setShortestPathCache(destination, listOf(startingTile))
|
||||||
|
return listOf(initialStep)
|
||||||
|
}
|
||||||
|
|
||||||
|
val tileToBestStep = HashMap<Tile, MovementStep>() // contains a map of "you can get from X to Y in that turn"
|
||||||
|
tileToBestStep[startingTile] = initialStep
|
||||||
|
|
||||||
|
val tilesToCheck = TreeSet { t: Tile, t2: Tile ->
|
||||||
|
val tStep = tileToBestStep[t]!!
|
||||||
|
val t2Step = tileToBestStep[t2]!!
|
||||||
|
// This last comparitor is REQUIRED otherwise the tree will think that tiles the same distance away are the same and will throw the second one away!
|
||||||
|
compareValuesBy(tStep, t2Step, {it.totalCost}, {it.tile.aerialDistanceTo(destination)}, {it.tile.position.hashCode()})
|
||||||
|
}
|
||||||
|
|
||||||
|
tilesToCheck.add(startingTile)
|
||||||
|
val canEndTurnInCache = HashMap<Tile, Boolean>()
|
||||||
|
val unitMaxMovement = unit.getMaxMovement().toFloat()
|
||||||
|
val movementCostCache = HashMap<Pair<Tile, Tile>, Float>()
|
||||||
|
val shouldAvoidTileCache = HashMap<Tile, Boolean>()
|
||||||
|
val canPassThroughCache = HashMap<Tile, Boolean>()
|
||||||
|
|
||||||
|
while (tilesToCheck.isNotEmpty()){
|
||||||
|
val currentTileToCheck = tilesToCheck.pollFirst()!!
|
||||||
|
|
||||||
|
val currentTileStep = tileToBestStep[currentTileToCheck]!!
|
||||||
|
|
||||||
|
for (neighbor in currentTileToCheck.neighbors){
|
||||||
|
if (shouldAvoidTileCache.getOrPut(neighbor){ shouldAvoidTile(neighbor) }) continue
|
||||||
|
val currentBestStepToNeighbor = tileToBestStep[neighbor]
|
||||||
|
// If this tile can't beat the current best then no point checking
|
||||||
|
if (currentBestStepToNeighbor!=null && (currentBestStepToNeighbor.totalCost < currentTileStep.totalCost))
|
||||||
|
continue
|
||||||
|
|
||||||
|
if (!canPassThroughCache.getOrPut(neighbor){ canPassThrough(neighbor) }) continue
|
||||||
|
|
||||||
|
val movementBetweenTiles: Float = if (!neighbor.isExplored(unit.civ)) 1f // If we don't know then we just guess it to be 1.
|
||||||
|
else movementCostCache.getOrPut(currentTileToCheck to neighbor) {
|
||||||
|
MovementCost.getMovementCostBetweenAdjacentTiles(unit, currentTileToCheck, neighbor, considerZoneOfControl)
|
||||||
|
}
|
||||||
|
|
||||||
|
val newStep = getNextStep(currentTileStep, neighbor, movementBetweenTiles, canEndTurnInCache, unitMaxMovement)
|
||||||
|
?: continue
|
||||||
|
|
||||||
|
|
||||||
|
if (neighbor == destination){
|
||||||
|
val entirePath = arrayListOf<MovementStep>()
|
||||||
|
var currentStep = newStep
|
||||||
|
// We do NOT include the origin tile in this list
|
||||||
|
while (currentStep.previousStep != null){
|
||||||
|
entirePath.add(currentStep)
|
||||||
|
currentStep = currentStep.previousStep!!
|
||||||
|
}
|
||||||
|
return entirePath.reversed()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentBestStepToNeighbor == null ||
|
||||||
|
newStep.totalCost < currentBestStepToNeighbor.totalCost) { // We have a winner!
|
||||||
|
tileToBestStep[neighbor] = newStep
|
||||||
|
if (newStep.totalCost.movementLeft == 0f && newStep.totalCost.turn == maxTurns) continue // don't schedule further expansion
|
||||||
|
tilesToCheck.add(neighbor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return listOf() // no path
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getNextStep(currentTileStep:MovementStep, neighbor:Tile, movementBetweenTiles:Float,
|
||||||
|
canEndTurnInCache:HashMap<Tile, Boolean>, unitMaxMovement:Float):MovementStep? {
|
||||||
|
|
||||||
|
fun canEndTurnIn(tile:Tile) = canEndTurnInCache.getOrPut(tile) { unit.movement.canMoveTo(tile) }
|
||||||
|
|
||||||
|
fun Float.normalizeMovementLeft() = if (this > Constants.minimumMovementEpsilon) this else 0f
|
||||||
|
|
||||||
|
val currentTile = currentTileStep.tile
|
||||||
|
|
||||||
|
// We can move directly
|
||||||
|
if (currentTileStep.totalCost.movementLeft != 0f || canEndTurnIn(currentTile)) {
|
||||||
|
val newTotalCost = if (currentTileStep.totalCost.movementLeft == 0f) MovementStepTotalCost(
|
||||||
|
currentTileStep.totalCost.turn + 1,
|
||||||
|
(unitMaxMovement - movementBetweenTiles).normalizeMovementLeft()
|
||||||
|
)
|
||||||
|
else MovementStepTotalCost(currentTileStep.totalCost.turn, (currentTileStep.totalCost.movementLeft - movementBetweenTiles).normalizeMovementLeft())
|
||||||
|
return MovementStep(currentTileStep, neighbor, movementBetweenTiles, newTotalCost)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backtracking nonsense - we CANNOT end the turn on the previous tile, AND we have no movement left, that means we need to do some alternate history
|
||||||
|
val previousStep = currentTileStep.previousStep ?: return null
|
||||||
|
if (previousStep.totalCost.movementLeft == 0f) return null // We backtracked until a previous end-of-turn and couldn't find any intermediate tiles
|
||||||
|
if (!canEndTurnIn(previousStep.tile)) return null
|
||||||
|
// We found somewhere we could have rested - let's do some alternate history!
|
||||||
|
val currentTileAlternateHistory = MovementStep(currentTileStep.previousStep, currentTile, currentTileStep.movementCost,
|
||||||
|
MovementStepTotalCost(currentTileStep.previousStep.totalCost.turn+1, (unitMaxMovement - currentTileStep.movementCost).normalizeMovementLeft()))
|
||||||
|
if (currentTileAlternateHistory.totalCost.movementLeft == 0f) return null // We tried, and even if we rest there we STILL have to stop at current tile... :/
|
||||||
|
val newTotalCost = MovementStepTotalCost(currentTileAlternateHistory.totalCost.turn, (currentTileAlternateHistory.totalCost.movementLeft-currentTileStep.movementCost).normalizeMovementLeft())
|
||||||
|
return MovementStep(currentTileAlternateHistory, neighbor, movementBetweenTiles, newTotalCost)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Does not consider if the [destination] tile can actually be entered, use [canMoveTo] for that.
|
* Does not consider if the [destination] tile can actually be entered, use [canMoveTo] for that.
|
||||||
* Returns an empty list if there's no way to get to the destination.
|
* Returns an empty list if there's no way to get to the destination.
|
||||||
*/
|
*/
|
||||||
fun getShortestPath(destination: Tile, avoidDamagingTerrain: Boolean = false): List<Tile> {
|
fun getShortestPath(destination: Tile, avoidDamagingTerrain: Boolean = false): List<Tile> {
|
||||||
|
if (UncivGame.Current.settings.experimentalMovement) {
|
||||||
|
fun shouldAvoidEnemyTile(tile:Tile) = unit.isCivilian() && unit.isAutomated() && tile.isEnemyTerritory(unit.civ)
|
||||||
|
if (avoidDamagingTerrain){
|
||||||
|
val shortestPathWithoutDamagingTiles = getShortestPathNew(destination,
|
||||||
|
shouldAvoidTile = { shouldAvoidEnemyTile(it) || unit.getDamageFromTerrain(it) > 0 })
|
||||||
|
if (shortestPathWithoutDamagingTiles.isNotEmpty()) return shortestPathWithoutDamagingTiles.toBackwardsCompatiblePath()
|
||||||
|
}
|
||||||
|
return getShortestPathNew(destination, shouldAvoidTile = ::shouldAvoidEnemyTile).toBackwardsCompatiblePath()
|
||||||
|
}
|
||||||
if (unit.cache.cannotMove) return listOf()
|
if (unit.cache.cannotMove) return listOf()
|
||||||
|
|
||||||
// First try and find a path without damaging terrain
|
// First try and find a path without damaging terrain
|
||||||
|
@ -32,6 +32,7 @@ class GameSettings {
|
|||||||
var showUnitMovements: Boolean = false
|
var showUnitMovements: Boolean = false
|
||||||
var showSettlersSuggestedCityLocations: Boolean = true
|
var showSettlersSuggestedCityLocations: Boolean = true
|
||||||
|
|
||||||
|
var experimentalMovement: Boolean = false
|
||||||
var checkForDueUnits: Boolean = true
|
var checkForDueUnits: Boolean = true
|
||||||
var autoUnitCycle: Boolean = true
|
var autoUnitCycle: Boolean = true
|
||||||
var singleTapMove: Boolean = false
|
var singleTapMove: Boolean = false
|
||||||
|
@ -4,8 +4,8 @@ import com.badlogic.gdx.scenes.scene2d.ui.Table
|
|||||||
import com.unciv.GUI
|
import com.unciv.GUI
|
||||||
import com.unciv.logic.civilization.PlayerType
|
import com.unciv.logic.civilization.PlayerType
|
||||||
import com.unciv.models.metadata.GameSettings
|
import com.unciv.models.metadata.GameSettings
|
||||||
import com.unciv.ui.components.widgets.UncivSlider
|
|
||||||
import com.unciv.ui.components.extensions.toLabel
|
import com.unciv.ui.components.extensions.toLabel
|
||||||
|
import com.unciv.ui.components.widgets.UncivSlider
|
||||||
import com.unciv.ui.screens.basescreen.BaseScreen
|
import com.unciv.ui.screens.basescreen.BaseScreen
|
||||||
|
|
||||||
fun gameplayTab(
|
fun gameplayTab(
|
||||||
@ -53,6 +53,7 @@ fun gameplayTab(
|
|||||||
) { settings.automatedUnitsChoosePromotions = it }
|
) { settings.automatedUnitsChoosePromotions = it }
|
||||||
optionsPopup.addCheckbox(this, "Order trade offers by amount", settings.orderTradeOffersByAmount) { settings.orderTradeOffersByAmount = it }
|
optionsPopup.addCheckbox(this, "Order trade offers by amount", settings.orderTradeOffersByAmount) { settings.orderTradeOffersByAmount = it }
|
||||||
optionsPopup.addCheckbox(this, "Ask for confirmation when pressing next turn", settings.confirmNextTurn) { settings.confirmNextTurn = it }
|
optionsPopup.addCheckbox(this, "Ask for confirmation when pressing next turn", settings.confirmNextTurn) { settings.confirmNextTurn = it }
|
||||||
|
optionsPopup.addCheckbox(this, "EXPERIMENTAL movement - use at your own risk!", settings.experimentalMovement, true) { settings.experimentalMovement = it }
|
||||||
|
|
||||||
addNotificationLogMaxTurnsSlider(this, settings, optionsPopup.selectBoxMinWidth)
|
addNotificationLogMaxTurnsSlider(this, settings, optionsPopup.selectBoxMinWidth)
|
||||||
}
|
}
|
||||||
|
@ -67,7 +67,7 @@ class ConsoleAction(val format: String, val action: (console: DevConsolePopup, p
|
|||||||
return getAutocompleteString(lastParam, options)
|
return getAutocompleteString(lastParam, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun validateFormat(format: String, params:List<String>){
|
private fun validateFormat(format: String, params:List<String>){
|
||||||
val allParams = format.split(" ")
|
val allParams = format.split(" ")
|
||||||
val requiredParamsAmount = allParams.count { it.startsWith('<') }
|
val requiredParamsAmount = allParams.count { it.startsWith('<') }
|
||||||
val optionalParamsAmount = allParams.count { it.startsWith('[') }
|
val optionalParamsAmount = allParams.count { it.startsWith('[') }
|
||||||
@ -110,7 +110,7 @@ class ConsoleUnitCommands : ConsoleCommandNode {
|
|||||||
"add" to ConsoleAction("unit add <civName> <unitName>") { console, params ->
|
"add" to ConsoleAction("unit add <civName> <unitName>") { console, params ->
|
||||||
val selectedTile = console.getSelectedTile()
|
val selectedTile = console.getSelectedTile()
|
||||||
val civ = console.getCivByName(params[0])
|
val civ = console.getCivByName(params[0])
|
||||||
val baseUnit = console.gameInfo.ruleset.units.values.firstOrNull { it.name.toCliInput() == params[1] }
|
val baseUnit = console.gameInfo.ruleset.units.values.firstOrNull { it.name.toCliInput() == params[1].toCliInput() }
|
||||||
?: throw ConsoleErrorException("Unknown unit")
|
?: throw ConsoleErrorException("Unknown unit")
|
||||||
civ.units.placeUnitNearTile(selectedTile.position, baseUnit)
|
civ.units.placeUnitNearTile(selectedTile.position, baseUnit)
|
||||||
DevConsoleResponse.OK
|
DevConsoleResponse.OK
|
||||||
|
Reference in New Issue
Block a user