diff --git a/core/src/com/unciv/logic/automation/civilization/NextTurnAutomation.kt b/core/src/com/unciv/logic/automation/civilization/NextTurnAutomation.kt index ce4fc18cfd..f5e3a05066 100644 --- a/core/src/com/unciv/logic/automation/civilization/NextTurnAutomation.kt +++ b/core/src/com/unciv/logic/automation/civilization/NextTurnAutomation.kt @@ -32,10 +32,8 @@ import com.unciv.models.Counter import com.unciv.models.ruleset.Belief import com.unciv.models.ruleset.BeliefType import com.unciv.models.ruleset.Building -import com.unciv.models.ruleset.INonPerpetualConstruction import com.unciv.models.ruleset.MilestoneType import com.unciv.models.ruleset.ModOptionsConstants -import com.unciv.models.ruleset.PerpetualConstruction import com.unciv.models.ruleset.Policy import com.unciv.models.ruleset.PolicyBranch import com.unciv.models.ruleset.Victory @@ -47,8 +45,6 @@ import com.unciv.models.ruleset.unit.BaseUnit import com.unciv.models.stats.Stat import com.unciv.models.translations.tr import com.unciv.ui.screens.victoryscreen.RankingType -import java.util.SortedMap -import java.util.TreeMap import kotlin.math.min import kotlin.random.Random @@ -84,7 +80,7 @@ object NextTurnAutomation { chooseTechToResearch(civInfo) automateCityBombardment(civInfo) - useGold(civInfo) + UseGoldAutomation.useGold(civInfo) if (!civInfo.isCityState()) { protectCityStates(civInfo) bullyCityStates(civInfo) @@ -291,149 +287,6 @@ object NextTurnAutomation { civInfo.popupAlerts.clear() // AIs don't care about popups. } - private fun tryGainInfluence(civInfo: Civilization, cityState: Civilization) { - if (civInfo.gold < 250) return // save up - if (cityState.getDiplomacyManager(civInfo).getInfluence() < 20) { - cityState.cityStateFunctions.receiveGoldGift(civInfo, 250) - return - } - if (civInfo.gold < 500) return // it's not worth it to invest now, wait until you have enough for 2 - cityState.cityStateFunctions.receiveGoldGift(civInfo, 500) - return - } - - private fun useGoldForCityStates(civ: Civilization) { - // RARE EDGE CASE: If you ally with a city-state, you may reveal more map that includes ANOTHER civ! - // So if we don't lock this list, we may later discover that there are more known civs, concurrent modification exception! - val knownCityStates = civ.getKnownCivs().filter { it.isCityState() }.toList() - - // canBeMarriedBy checks actual cost, but it can't be below 500*speedmodifier, and the later check is expensive - if (civ.gold >= 330 && civ.getHappiness() > 0 && civ.hasUnique(UniqueType.CityStateCanBeBoughtForGold)) { - for (cityState in knownCityStates.toList() ) { // Materialize sequence as diplomaticMarriage may kill a CS - if (cityState.cityStateFunctions.canBeMarriedBy(civ)) - cityState.cityStateFunctions.diplomaticMarriage(civ) - if (civ.getHappiness() <= 0) break // Stop marrying if happiness is getting too low - } - } - - if (civ.gold < 250) return // skip checks if tryGainInfluence will bail anyway - if (civ.wantsToFocusOn(Victory.Focus.Culture)) { - for (cityState in knownCityStates.filter { it.cityStateFunctions.canProvideStat(Stat.Culture) }) { - val diploManager = cityState.getDiplomacyManager(civ) - if (diploManager.getInfluence() < 40) { // we want to gain influence with them - tryGainInfluence(civ, cityState) - } - } - } - - if (civ.gold < 250 || knownCityStates.none()) return - val cityState = knownCityStates - .filter { it.getAllyCiv() != civ.civName } - .associateWith { valueCityStateAlliance(civ, it) } - .maxByOrNull { it.value }?.takeIf { it.value > 0 }?.key - if (cityState != null) { - tryGainInfluence(civ, cityState) - } - } - - /** allow AI to spend money to purchase city-state friendship, buildings & unit */ - private fun useGold(civ: Civilization) { - if (civ.isMajorCiv()) - useGoldForCityStates(civ) - - for (city in civ.cities.sortedByDescending { it.population.population }) { - val construction = city.cityConstructions.getCurrentConstruction() - if (construction is PerpetualConstruction) continue - if ((construction as INonPerpetualConstruction).canBePurchasedWithStat(city, Stat.Gold) - && city.civ.gold / 3 >= construction.getStatBuyCost(city, Stat.Gold)!!) { - city.cityConstructions.purchaseConstruction(construction, 0, true) - } - } - - maybeBuyCityTiles(civ) - } - - private fun maybeBuyCityTiles(civInfo: Civilization) { - if (civInfo.gold <= 0) - return - // Don't buy tiles in the very early game. It is unlikely that we already have the required - // tech, the necessary worker and that there is a reasonable threat from another player to - // grab the tile. We could also check all that, but it would require a lot of cycles each - // turn and this is probably a good approximation. - if (civInfo.gameInfo.turns < (civInfo.gameInfo.speed.scienceCostModifier * 20).toInt()) - return - - val highlyDesirableTiles: SortedMap> = TreeMap( - compareByDescending { it?.naturalWonder != null } - .thenByDescending { it?.resource != null && it.tileResource.resourceType == ResourceType.Luxury } - .thenByDescending { it?.resource != null && it.tileResource.resourceType == ResourceType.Strategic } - // This is necessary, so that the map keeps Tiles with the same resource as two - // separate entries. - .thenBy { it.hashCode() } - ) - for (city in civInfo.cities.filter { !it.isPuppet && !it.isBeingRazed }) { - val highlyDesirableTilesInCity = city.tilesInRange.filter { - val hasNaturalWonder = it.naturalWonder != null - val hasLuxuryCivDoesntOwn = - it.hasViewableResource(civInfo) - && it.tileResource.resourceType == ResourceType.Luxury - && !civInfo.hasResource(it.resource!!) - val hasResourceCivHasNoneOrLittle = - it.hasViewableResource(civInfo) - && it.tileResource.resourceType == ResourceType.Strategic - && civInfo.getResourceAmount(it.resource!!) <= 3 - - it.isVisible(civInfo) && it.getOwner() == null - && it.neighbors.any { neighbor -> neighbor.getCity() == city } - (hasNaturalWonder || hasLuxuryCivDoesntOwn || hasResourceCivHasNoneOrLittle) - } - for (highlyDesirableTileInCity in highlyDesirableTilesInCity) { - highlyDesirableTiles.getOrPut(highlyDesirableTileInCity) { mutableSetOf() } - .add(city) - } - } - - // Always try to buy highly desirable tiles if it can be afforded. - for (highlyDesirableTile in highlyDesirableTiles) { - val cityWithLeastCostToBuy = highlyDesirableTile.value.minBy { - it.getCenterTile().aerialDistanceTo(highlyDesirableTile.key) - } - val bfs = BFS(cityWithLeastCostToBuy.getCenterTile()) - { - it.getOwner() == null || it.owningCity == cityWithLeastCostToBuy - } - bfs.stepUntilDestination(highlyDesirableTile.key) - val tilesThatNeedBuying = - bfs.getPathTo(highlyDesirableTile.key).filter { it.getOwner() == null } - .toList().reversed() // getPathTo is from destination to source - - // We're trying to acquire everything and revert if it fails, because of the difficult - // way how tile acquisition cost is calculated. Everytime you buy a tile, the next one - // gets more expensive and by how much depends on other things such as game speed. To - // not introduce hidden dependencies on that and duplicate that logic here to calculate - // the price of the whole path, this is probably simpler. - var ranOutOfMoney = false - var goldSpent = 0 - for (tileThatNeedsBuying in tilesThatNeedBuying) { - val goldCostOfTile = - cityWithLeastCostToBuy.expansion.getGoldCostOfTile(tileThatNeedsBuying) - if (civInfo.gold >= goldCostOfTile) { - cityWithLeastCostToBuy.expansion.buyTile(tileThatNeedsBuying) - goldSpent += goldCostOfTile - } else { - ranOutOfMoney = true - break - } - } - if (ranOutOfMoney) { - for (tileThatNeedsBuying in tilesThatNeedBuying) { - cityWithLeastCostToBuy.expansion.relinquishOwnership(tileThatNeedsBuying) - } - civInfo.addGold(goldSpent) - } - } - } - internal fun valueCityStateAlliance(civInfo: Civilization, cityState: Civilization): Int { var value = 0 @@ -532,7 +385,7 @@ object NextTurnAutomation { if (civInfo.tech.techsToResearch.isEmpty()) { val costs = getGroupedResearchableTechs() if (costs.isEmpty()) return - + val cheapestTechs = costs[0] //Do not consider advanced techs if only one tech left in cheapest group val techToResearch: Technology = diff --git a/core/src/com/unciv/logic/automation/civilization/UseGoldAutomation.kt b/core/src/com/unciv/logic/automation/civilization/UseGoldAutomation.kt new file mode 100644 index 0000000000..3e30ac2d70 --- /dev/null +++ b/core/src/com/unciv/logic/automation/civilization/UseGoldAutomation.kt @@ -0,0 +1,177 @@ +package com.unciv.logic.automation.civilization + +import com.unciv.logic.city.City +import com.unciv.logic.civilization.Civilization +import com.unciv.logic.map.BFS +import com.unciv.logic.map.tile.Tile +import com.unciv.models.ruleset.INonPerpetualConstruction +import com.unciv.models.ruleset.PerpetualConstruction +import com.unciv.models.ruleset.Victory +import com.unciv.models.ruleset.tile.ResourceType +import com.unciv.models.ruleset.unique.UniqueType +import com.unciv.models.stats.Stat +import java.util.SortedMap +import java.util.TreeMap + +object UseGoldAutomation { + + + /** allow AI to spend money to purchase city-state friendship, buildings & unit */ + fun useGold(civ: Civilization) { + if (civ.isMajorCiv()) + useGoldForCityStates(civ) + + for (city in civ.cities.sortedByDescending { it.population.population }) { + val construction = city.cityConstructions.getCurrentConstruction() + if (construction is PerpetualConstruction) continue + if ((construction as INonPerpetualConstruction).canBePurchasedWithStat(city, Stat.Gold) + && city.civ.gold / 3 >= construction.getStatBuyCost(city, Stat.Gold)!!) { + city.cityConstructions.purchaseConstruction(construction, 0, true) + } + } + + maybeBuyCityTiles(civ) + } + + + private fun useGoldForCityStates(civ: Civilization) { + // RARE EDGE CASE: If you ally with a city-state, you may reveal more map that includes ANOTHER civ! + // So if we don't lock this list, we may later discover that there are more known civs, concurrent modification exception! + val knownCityStates = civ.getKnownCivs().filter { it.isCityState() }.toList() + + // canBeMarriedBy checks actual cost, but it can't be below 500*speedmodifier, and the later check is expensive + if (civ.gold >= 330 && civ.getHappiness() > 0 && civ.hasUnique(UniqueType.CityStateCanBeBoughtForGold)) { + for (cityState in knownCityStates.toList() ) { // Materialize sequence as diplomaticMarriage may kill a CS + if (cityState.cityStateFunctions.canBeMarriedBy(civ)) + cityState.cityStateFunctions.diplomaticMarriage(civ) + if (civ.getHappiness() <= 0) break // Stop marrying if happiness is getting too low + } + } + + if (civ.gold < 250) return // skip checks if tryGainInfluence will bail anyway + if (civ.wantsToFocusOn(Victory.Focus.Culture)) { + for (cityState in knownCityStates.filter { it.cityStateFunctions.canProvideStat(Stat.Culture) }) { + val diploManager = cityState.getDiplomacyManager(civ) + if (diploManager.getInfluence() < 40) { // we want to gain influence with them + tryGainInfluence(civ, cityState) + } + } + } + + if (civ.gold < 250 || knownCityStates.none()) return + val cityState = knownCityStates + .filter { it.getAllyCiv() != civ.civName } + .associateWith { NextTurnAutomation.valueCityStateAlliance(civ, it) } + .maxByOrNull { it.value }?.takeIf { it.value > 0 }?.key + if (cityState != null) { + tryGainInfluence(civ, cityState) + } + } + + private fun maybeBuyCityTiles(civInfo: Civilization) { + if (civInfo.gold <= 0) + return + // Don't buy tiles in the very early game. It is unlikely that we already have the required + // tech, the necessary worker and that there is a reasonable threat from another player to + // grab the tile. We could also check all that, but it would require a lot of cycles each + // turn and this is probably a good approximation. + if (civInfo.gameInfo.turns < (civInfo.gameInfo.speed.scienceCostModifier * 20).toInt()) + return + + val highlyDesirableTiles: SortedMap> = getHighlyDesirableTilesToCityMap(civInfo) + + // Always try to buy highly desirable tiles if it can be afforded. + for (highlyDesirableTile in highlyDesirableTiles) { + val cityWithLeastCostToBuy = highlyDesirableTile.value.minBy { + it.getCenterTile().aerialDistanceTo(highlyDesirableTile.key) + } + val bfs = BFS(cityWithLeastCostToBuy.getCenterTile()) + { + it.getOwner() == null || it.owningCity == cityWithLeastCostToBuy + } + bfs.stepUntilDestination(highlyDesirableTile.key) + val tilesThatNeedBuying = + bfs.getPathTo(highlyDesirableTile.key).filter { it.getOwner() == null } + .toList().reversed() // getPathTo is from destination to source + + // We're trying to acquire everything and revert if it fails, because of the difficult + // way how tile acquisition cost is calculated. Everytime you buy a tile, the next one + // gets more expensive and by how much depends on other things such as game speed. To + // not introduce hidden dependencies on that and duplicate that logic here to calculate + // the price of the whole path, this is probably simpler. + var ranOutOfMoney = false + var goldSpent = 0 + for (tileThatNeedsBuying in tilesThatNeedBuying) { + val goldCostOfTile = + cityWithLeastCostToBuy.expansion.getGoldCostOfTile(tileThatNeedsBuying) + if (civInfo.gold >= goldCostOfTile) { + cityWithLeastCostToBuy.expansion.buyTile(tileThatNeedsBuying) + goldSpent += goldCostOfTile + } else { + ranOutOfMoney = true + break + } + } + if (ranOutOfMoney) { + for (tileThatNeedsBuying in tilesThatNeedBuying) { + cityWithLeastCostToBuy.expansion.relinquishOwnership(tileThatNeedsBuying) + } + civInfo.addGold(goldSpent) + } + } + } + + private fun getHighlyDesirableTilesToCityMap(civInfo: Civilization): SortedMap> { + val highlyDesirableTiles: SortedMap> = TreeMap( + compareByDescending { it?.naturalWonder != null } + .thenByDescending { it?.resource != null && it.tileResource.resourceType == ResourceType.Luxury } + .thenByDescending { it?.resource != null && it.tileResource.resourceType == ResourceType.Strategic } + // This is necessary, so that the map keeps Tiles with the same resource as two + // separate entries. + .thenBy { it.hashCode() } + ) + + for (city in civInfo.cities.filter { !it.isPuppet && !it.isBeingRazed }) { + val highlyDesirableTilesInCity = city.tilesInRange.filter { + isHighlyDesirableTile(it, civInfo, city) + } + for (highlyDesirableTileInCity in highlyDesirableTilesInCity) { + highlyDesirableTiles.getOrPut(highlyDesirableTileInCity) { mutableSetOf() } + .add(city) + } + } + return highlyDesirableTiles + } + + private fun isHighlyDesirableTile(it: Tile, civInfo: Civilization, city: City): Boolean { + if (!it.isVisible(civInfo)) return false + if (it.getOwner() != null) return false + if (it.neighbors.none { neighbor -> neighbor.getCity() == city }) return false + + fun hasNaturalWonder() = it.naturalWonder != null + + fun hasLuxuryCivDoesntOwn() = + it.hasViewableResource(civInfo) + && it.tileResource.resourceType == ResourceType.Luxury + && !civInfo.hasResource(it.resource!!) + + fun hasResourceCivHasNoneOrLittle() = + it.hasViewableResource(civInfo) + && it.tileResource.resourceType == ResourceType.Strategic + && civInfo.getResourceAmount(it.resource!!) <= 3 + + return (hasNaturalWonder() || hasLuxuryCivDoesntOwn() || hasResourceCivHasNoneOrLittle()) + } + + private fun tryGainInfluence(civInfo: Civilization, cityState: Civilization) { + if (civInfo.gold < 250) return // save up + if (cityState.getDiplomacyManager(civInfo).getInfluence() < 20) { + cityState.cityStateFunctions.receiveGoldGift(civInfo, 250) + return + } + if (civInfo.gold < 500) return // it's not worth it to invest now, wait until you have enough for 2 + cityState.cityStateFunctions.receiveGoldGift(civInfo, 500) + return + } + +} diff --git a/core/src/com/unciv/logic/map/mapgenerator/mapregions/MapRegions.kt b/core/src/com/unciv/logic/map/mapgenerator/mapregions/MapRegions.kt index 79b8dbad21..bc0dc009a5 100644 --- a/core/src/com/unciv/logic/map/mapgenerator/mapregions/MapRegions.kt +++ b/core/src/com/unciv/logic/map/mapgenerator/mapregions/MapRegions.kt @@ -890,6 +890,22 @@ class MapRegions (val ruleset: Ruleset){ } +// For dividing the map into Regions to determine start locations +fun Tile.getTileFertility(checkCoasts: Boolean): Int { + var fertility = 0 + for (terrain in allTerrains) { + if (terrain.hasUnique(UniqueType.OverrideFertility)) + return terrain.getMatchingUniques(UniqueType.OverrideFertility).first().params[0].toInt() + else + fertility += terrain.getMatchingUniques(UniqueType.AddFertility) + .sumOf { it.params[0].toInt() } + } + if (isAdjacentToRiver()) fertility += 1 + if (isAdjacentTo(Constants.freshWater)) fertility += 1 // meaning total +2 for river + if (checkCoasts && isCoastalTile()) fertility += 2 + return fertility +} + fun getRegionPriority(terrain: Terrain?): Int? { if (terrain == null) // ie "hybrid" return 99999 // a big number diff --git a/core/src/com/unciv/logic/map/mapunit/MapUnit.kt b/core/src/com/unciv/logic/map/mapunit/MapUnit.kt index a0edd4dc34..0d5f077b0a 100644 --- a/core/src/com/unciv/logic/map/mapunit/MapUnit.kt +++ b/core/src/com/unciv/logic/map/mapunit/MapUnit.kt @@ -620,6 +620,16 @@ class MapUnit : IsPartOfGameInfoSerialization { fun removeFromTile() = currentTile.removeUnit(this) + + /** Return null if military on tile, or no civilian */ + private fun Tile.getUnguardedCivilian(attacker: MapUnit): MapUnit? { + return when { + militaryUnit != null && militaryUnit != attacker -> null + civilianUnit != null -> civilianUnit!! + else -> null + } + } + fun moveThroughTile(tile: Tile) { // addPromotion requires currentTile to be valid because it accesses ruleset through it. // getAncientRuinBonus, if it places a new unit, does too diff --git a/core/src/com/unciv/logic/map/tile/Tile.kt b/core/src/com/unciv/logic/map/tile/Tile.kt index 13c1ff5361..e2e7837f8b 100644 --- a/core/src/com/unciv/logic/map/tile/Tile.kt +++ b/core/src/com/unciv/logic/map/tile/Tile.kt @@ -205,13 +205,6 @@ open class Tile : IsPartOfGameInfoSerialization { return null } - /** Return null if military on tile, or no civilian */ - fun getUnguardedCivilian(attacker: MapUnit): MapUnit? { - if (militaryUnit != null && militaryUnit != attacker) return null - if (civilianUnit != null) return civilianUnit!! - return null - } - fun getCity(): City? = owningCity @Transient @@ -353,16 +346,6 @@ open class Tile : IsPartOfGameInfoSerialization { // We have to .toList() so that the values are stored together once for caching, // and the toSequence so that aggregations (like neighbors.flatMap{it.units} don't take up their own space - /** Returns the left shared neighbor of `this` and [neighbor] (relative to the view direction `this`->[neighbor]), or null if there is no such tile. */ - fun getLeftSharedNeighbor(neighbor: Tile): Tile? { - return tileMap.getClockPositionNeighborTile(this,(tileMap.getNeighborTileClockPosition(this, neighbor) - 2) % 12) - } - - /** Returns the right shared neighbor of `this` and [neighbor] (relative to the view direction `this`->[neighbor]), or null if there is no such tile. */ - fun getRightSharedNeighbor(neighbor: Tile): Tile? { - return tileMap.getClockPositionNeighborTile(this,(tileMap.getNeighborTileClockPosition(this, neighbor) + 2) % 12) - } - fun getRow() = HexMath.getRow(position) fun getColumn() = HexMath.getColumn(position) @@ -444,16 +427,12 @@ open class Tile : IsPartOfGameInfoSerialization { else -> false // Neutral - unblocks tile; } } - if (isLand) // Only water tiles are blocked if empty return false // For water tiles need also to check neighbors: // enemy military naval units blockade all adjacent water tiles. - for (neighbor in neighbors) { - if (!neighbor.isWater) - continue - + for (neighbor in neighbors.filter { it.isWater }) { val neighborUnit = neighbor.militaryUnit ?: continue // Embarked units do not blockade adjacent tiles @@ -473,22 +452,6 @@ open class Tile : IsPartOfGameInfoSerialization { return workingCity != null && workingCity.lockedTiles.contains(position) } - // For dividing the map into Regions to determine start locations - fun getTileFertility(checkCoasts: Boolean): Int { - var fertility = 0 - for (terrain in allTerrains) { - if (terrain.hasUnique(UniqueType.OverrideFertility)) - return terrain.getMatchingUniques(UniqueType.OverrideFertility).first().params[0].toInt() - else - fertility += terrain.getMatchingUniques(UniqueType.AddFertility) - .sumOf { it.params[0].toInt() } - } - if (isAdjacentToRiver()) fertility += 1 - if (isAdjacentTo(Constants.freshWater)) fertility += 1 // meaning total +2 for river - if (checkCoasts && isCoastalTile()) fertility += 2 - return fertility - } - fun providesResources(civInfo: Civilization): Boolean { if (!hasViewableResource(civInfo)) return false if (isCityCenter()) return true @@ -576,17 +539,10 @@ open class Tile : IsPartOfGameInfoSerialization { resource != null && (tileResource.revealedBy == null || civInfo.tech.isResearched( tileResource.revealedBy!!)) - fun getViewableTilesList(distance: Int): List = - tileMap.getViewableTiles(position, distance) - - fun getTilesInDistance(distance: Int): Sequence = - tileMap.getTilesInDistance(position, distance) - - fun getTilesInDistanceRange(range: IntRange): Sequence = - tileMap.getTilesInDistanceRange(position, range) - - fun getTilesAtDistance(distance: Int): Sequence = - tileMap.getTilesAtDistance(position, distance) + fun getViewableTilesList(distance: Int): List = tileMap.getViewableTiles(position, distance) + fun getTilesInDistance(distance: Int): Sequence = tileMap.getTilesInDistance(position, distance) + fun getTilesInDistanceRange(range: IntRange): Sequence = tileMap.getTilesInDistanceRange(position, range) + fun getTilesAtDistance(distance: Int): Sequence =tileMap.getTilesAtDistance(position, distance) fun getDefensiveBonus(): Float { var bonus = baseTerrainObject.defenceBonus @@ -937,9 +893,7 @@ open class Tile : IsPartOfGameInfoSerialization { owningCity!!.civ.cache.updateCivResources() } - fun isPillaged(): Boolean { - return improvementIsPillaged || roadIsPillaged - } + fun isPillaged(): Boolean = improvementIsPillaged || roadIsPillaged fun setRepaired() { improvementInProgress = null diff --git a/core/src/com/unciv/ui/components/tilegroups/layers/TileLayerBorders.kt b/core/src/com/unciv/ui/components/tilegroups/layers/TileLayerBorders.kt index 2d2eef4643..923a4d705b 100644 --- a/core/src/com/unciv/ui/components/tilegroups/layers/TileLayerBorders.kt +++ b/core/src/com/unciv/ui/components/tilegroups/layers/TileLayerBorders.kt @@ -5,8 +5,8 @@ import com.badlogic.gdx.scenes.scene2d.ui.Image import com.unciv.logic.civilization.Civilization import com.unciv.logic.map.tile.Tile import com.unciv.models.ruleset.unique.LocalUniqueCache -import com.unciv.ui.images.ImageGetter import com.unciv.ui.components.tilegroups.TileGroup +import com.unciv.ui.images.ImageGetter import kotlin.math.PI import kotlin.math.atan @@ -33,6 +33,17 @@ class TileLayerBorders(tileGroup: TileGroup, size: Float) : TileLayer(tileGroup, } } + + /** Returns the left shared neighbor of `this` and [neighbor] (relative to the view direction `this`->[neighbor]), or null if there is no such tile. */ + private fun Tile.getLeftSharedNeighbor(neighbor: Tile): Tile? { + return tileMap.getClockPositionNeighborTile(this,(tileMap.getNeighborTileClockPosition(this, neighbor) - 2) % 12) + } + + /** Returns the right shared neighbor of `this` and [neighbor] (relative to the view direction `this`->[neighbor]), or null if there is no such tile. */ + private fun Tile.getRightSharedNeighbor(neighbor: Tile): Tile? { + return tileMap.getClockPositionNeighborTile(this,(tileMap.getNeighborTileClockPosition(this, neighbor) + 2) % 12) + } + private fun updateBorders() { // This is longer than it could be, because of performance -