diff --git a/core/src/com/unciv/logic/civilization/CivilizationInfo.kt b/core/src/com/unciv/logic/civilization/CivilizationInfo.kt index b05ffb23e1..77bbe076cd 100644 --- a/core/src/com/unciv/logic/civilization/CivilizationInfo.kt +++ b/core/src/com/unciv/logic/civilization/CivilizationInfo.kt @@ -280,9 +280,9 @@ class CivilizationInfo { fun isAtWarWith(otherCiv:CivilizationInfo): Boolean { if (otherCiv.civName == civName) return false // never at war with itself if (otherCiv.isBarbarian() || isBarbarian()) return true - if (!diplomacy.containsKey(otherCiv.civName)) // not encountered yet - return false - return getDiplomacyManager(otherCiv).diplomaticStatus == DiplomaticStatus.War + val diplomacyManager = diplomacy[otherCiv.civName] + ?: return false // not encountered yet + return diplomacyManager.diplomaticStatus == DiplomaticStatus.War } fun isAtWar() = diplomacy.values.any { it.diplomaticStatus== DiplomaticStatus.War && !it.otherCiv().isDefeated() } @@ -452,13 +452,13 @@ class CivilizationInfo { } fun canEnterTiles(otherCiv: CivilizationInfo): Boolean { - if(otherCiv==this) return true - if(nation.isBarbarian() && gameInfo.turns >= gameInfo.difficultyObject.turnBarbariansCanEnterPlayerTiles) return true - if(!diplomacy.containsKey(otherCiv.civName)) // not encountered yet - return false - if(isAtWarWith(otherCiv)) return true - if(getDiplomacyManager(otherCiv).hasOpenBorders) return true - return false + if (otherCiv==this) return true + if (otherCiv.isBarbarian()) return true + if (nation.isBarbarian() && gameInfo.turns >= gameInfo.difficultyObject.turnBarbariansCanEnterPlayerTiles) + return true + val diplomacyManager = diplomacy[otherCiv.civName] + ?: return false // not encountered yet + return (diplomacyManager.hasOpenBorders || diplomacyManager.diplomaticStatus == DiplomaticStatus.War) } fun addNotification(text: String, location: Vector2?, color: Color) { diff --git a/core/src/com/unciv/logic/map/MapUnit.kt b/core/src/com/unciv/logic/map/MapUnit.kt index ec37d0315a..cbad7ee45f 100644 --- a/core/src/com/unciv/logic/map/MapUnit.kt +++ b/core/src/com/unciv/logic/map/MapUnit.kt @@ -39,6 +39,9 @@ class MapUnit { @Transient var doubleMovementInCoast = false @Transient var doubleMovementInForestAndJungle = false @Transient var doubleMovementInSnowTundraAndHills = false + @Transient var canEnterIceTiles = false + @Transient var cannotEnterOceanTiles = false + @Transient var cannotEnterOceanTilesUntilAstronomy = false lateinit var owner: String lateinit var name: String @@ -134,11 +137,14 @@ class MapUnit { uniques.addAll(promotions.promotions.map { currentTile.tileMap.gameInfo.ruleSet.unitPromotions[it]!!.effect }) tempUniques = uniques - if("Ignores terrain cost" in uniques) ignoresTerrainCost = true - if("Rough terrain penalty" in uniques) roughTerrainPenalty = true - if("Double movement in coast" in uniques) doubleMovementInCoast = true - if("Double movement rate through Forest and Jungle" in uniques) doubleMovementInForestAndJungle = true - if("Double movement in Snow, Tundra and Hills" in uniques) doubleMovementInSnowTundraAndHills = true + ignoresTerrainCost = ("Ignores terrain cost" in uniques) + roughTerrainPenalty = ("Rough terrain penalty" in uniques) + doubleMovementInCoast = ("Double movement in coast" in uniques) + doubleMovementInForestAndJungle = ("Double movement rate through Forest and Jungle" in uniques) + doubleMovementInSnowTundraAndHills = ("Double movement in Snow, Tundra and Hills" in uniques) + canEnterIceTiles = ("Can enter ice tiles" in uniques) + cannotEnterOceanTiles = ("Cannot enter ocean tiles" in uniques) + cannotEnterOceanTilesUntilAstronomy = ("Cannot enter ocean tiles until Astronomy" in uniques) } fun hasUnique(unique:String): Boolean { diff --git a/core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt b/core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt index 27316627c1..4caae2447c 100644 --- a/core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt +++ b/core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt @@ -325,8 +325,7 @@ class UnitMovementAlgorithms(val unit:MapUnit) { && !(tile.isCityCenter() && tile.isCoastalTile())) return false - if (tile.terrainFeature == Constants.ice - && !unit.baseUnit.uniques.contains("Can enter ice tiles")) + if (tile.terrainFeature == Constants.ice && !unit.canEnterIceTiles) return false if (tile.isWater && unit.type.isLandUnit()) { @@ -335,8 +334,8 @@ class UnitMovementAlgorithms(val unit:MapUnit) { return false } if (tile.isOcean && unit.civInfo.nation.unique != UniqueAbility.WAYFINDING) { - if (unit.baseUnit.uniques.contains("Cannot enter ocean tiles")) return false - if (unit.baseUnit.uniques.contains("Cannot enter ocean tiles until Astronomy") + if (unit.cannotEnterOceanTiles) return false + if (unit.cannotEnterOceanTilesUntilAstronomy && !unit.civInfo.tech.isResearched("Astronomy")) return false } @@ -350,12 +349,9 @@ class UnitMovementAlgorithms(val unit:MapUnit) { // AIs won't enter city-state's border. } - val unitsInTile = tile.getUnits() - if (unitsInTile.isNotEmpty()) { - val firstUnit = unitsInTile.first() - if (firstUnit.civInfo != unit.civInfo && unit.civInfo.isAtWarWith(firstUnit.civInfo)) - return false - } + val firstUnit = tile.getUnits().firstOrNull() + if (firstUnit != null && firstUnit.civInfo != unit.civInfo && unit.civInfo.isAtWarWith(firstUnit.civInfo)) + return false return true } diff --git a/tests/src/com/unciv/logic/map/UnitMovementAlgorithmsTests.kt b/tests/src/com/unciv/logic/map/UnitMovementAlgorithmsTests.kt new file mode 100644 index 0000000000..ecc29f5bf1 --- /dev/null +++ b/tests/src/com/unciv/logic/map/UnitMovementAlgorithmsTests.kt @@ -0,0 +1,238 @@ +// Taken from https://github.com/TomGrill/gdx-testing +package com.unciv.logic.map + +import com.unciv.Constants +import com.unciv.logic.city.CityInfo +import com.unciv.logic.civilization.CivilizationInfo +import com.unciv.logic.civilization.diplomacy.DiplomacyManager +import com.unciv.logic.civilization.diplomacy.DiplomaticStatus +import com.unciv.models.ruleset.Nation +import com.unciv.models.ruleset.Ruleset +import com.unciv.models.ruleset.RulesetCache +import com.unciv.models.ruleset.unit.BaseUnit +import com.unciv.models.ruleset.unit.UnitType +import com.unciv.testing.GdxTestRunner +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(GdxTestRunner::class) +class UnitMovementAlgorithmsTests { + + private var tile = TileInfo() + private var civInfo = CivilizationInfo() + private var ruleSet = Ruleset() + private var unit = MapUnit() + + @Before + fun initTheWorld() { + RulesetCache.loadRulesets() + ruleSet = RulesetCache.getBaseRuleset() + tile.ruleset = ruleSet + civInfo.tech.techsResearched.addAll(ruleSet.technologies.keys) + civInfo.tech.embarkedUnitsCanEnterOcean = true + civInfo.tech.unitsCanEmbark = true + civInfo.nation = Nation().apply { + name = "My nation" + cities = arrayListOf("The Capital") + } + unit.civInfo = civInfo + } + + @Test + fun canPassThroughPassableTerrains() { + for (terrain in ruleSet.terrains.values) { + tile.baseTerrain = terrain.name + tile.setTransients() + + unit.baseUnit = BaseUnit().apply { unitType = UnitType.Melee } + + Assert.assertTrue(terrain.name, terrain.impassable != unit.movement.canPassThrough(tile)) + } + } + + @Test + fun unitCanEnterTheCity() { + val map = TileMap() + tile.baseTerrain = Constants.hill + tile.tileMap = map + tile.setTransients() + + val otherTile = tile.clone() + otherTile.baseTerrain = Constants.coast + otherTile.position.y = 1f + + map.tileMatrix.add(arrayListOf(tile, otherTile)) + + val city = CityInfo() + city.location = tile.position + city.civInfo = civInfo + tile.owningCity = city + + for (type in UnitType.values()) + { + unit.owner = civInfo.civName + unit.baseUnit = BaseUnit().apply { unitType = type } + Assert.assertTrue(type.name, unit.movement.canPassThrough(tile)) + } + } + + @Test + fun waterUnitCanNOTEnterLand() { + for (terrain in ruleSet.terrains.values) { + if (terrain.impassable) continue + tile.baseTerrain = terrain.name + tile.setTransients() + + for (type in UnitType.values()) { + if (type == UnitType.City) continue + unit.baseUnit = BaseUnit().apply { unitType = type } + Assert.assertTrue("%s cannot be at %s".format(type, terrain.name), + (type.isWaterUnit() && tile.isLand) != unit.movement.canPassThrough(tile)) + } + } + } + + @Test + fun canNOTEnterIce() { + tile.baseTerrain = Constants.ocean + tile.terrainFeature = Constants.ice + tile.setTransients() + + for (type in UnitType.values()) { + unit.baseUnit = BaseUnit().apply { unitType = type } + + if (type == UnitType.WaterSubmarine) { + unit.baseUnit.uniques.add("Can enter ice tiles") + } + unit.updateUniques() + + Assert.assertTrue("$type cannot be in Ice", + (type == UnitType.WaterSubmarine) == unit.movement.canPassThrough(tile)) + } + } + + @Test + fun canNOTEnterNaturalWonder() { + tile.baseTerrain = Constants.plains + tile.naturalWonder = "Wonder Thunder" + tile.setTransients() + + for (type in UnitType.values()) { + unit.baseUnit = BaseUnit().apply { unitType = type } + + Assert.assertFalse("$type must not enter Wonder tile", unit.movement.canPassThrough(tile)) + } + } + + @Test + fun canNOTEnterCoastUntilProperTechIsResearched() { + + civInfo.tech.unitsCanEmbark = false + + tile.baseTerrain = Constants.coast + tile.setTransients() + + for (type in UnitType.values()) { + unit.baseUnit = BaseUnit().apply { unitType = type } + + Assert.assertTrue("$type cannot be in Coast", + unit.type.isLandUnit() != unit.movement.canPassThrough(tile)) + } + } + + @Test + fun canNOTEnterOceanUntilProperTechIsResearched() { + + civInfo.tech.embarkedUnitsCanEnterOcean = false + + tile.baseTerrain = Constants.ocean + tile.setTransients() + + for (type in UnitType.values()) { + unit.baseUnit = BaseUnit().apply { unitType = type } + + Assert.assertTrue("$type cannot be in Ocean", + unit.type.isLandUnit() != unit.movement.canPassThrough(tile)) + } + } + + @Test + fun canNOTEnterOceanWithLimitations() { + + tile.baseTerrain = Constants.ocean + tile.setTransients() + + for (type in UnitType.values()) { + unit.baseUnit = BaseUnit().apply { + unitType = type + if (type == UnitType.Melee) + uniques.add("Cannot enter ocean tiles") + if (type == UnitType.Ranged) + uniques.add("Cannot enter ocean tiles until Astronomy") + } + unit.updateUniques() + + Assert.assertTrue("$type cannot be in Ocean", + (type == UnitType.Melee) != unit.movement.canPassThrough(tile)) + + civInfo.tech.techsResearched.remove("Astronomy") + + Assert.assertTrue("$type cannot be in Ocean until Astronomy", + (type == UnitType.Melee || + type == UnitType.Ranged) != unit.movement.canPassThrough(tile)) + + civInfo.tech.techsResearched.add("Astronomy") + } + } + + @Test + fun canNOTPassThroughTileWithEnemyUnits() { + tile.baseTerrain = Constants.grassland + tile.setTransients() + + val otherCiv = CivilizationInfo() + otherCiv.civName = "Barbarians" // they are always enemies + otherCiv.nation = Nation().apply { name = "Barbarians" } + val otherUnit = MapUnit() + otherUnit.civInfo = otherCiv + tile.militaryUnit = otherUnit + + for (type in UnitType.values()) { + unit.baseUnit = BaseUnit().apply { unitType = type } + + Assert.assertFalse("$type must not enter occupied tile", unit.movement.canPassThrough(tile)) + } + } + + @Test + fun canNOTPassForeignTiles() { + tile.baseTerrain = Constants.desert + tile.setTransients() + + val otherCiv = CivilizationInfo() + otherCiv.civName = "Other civ" + otherCiv.nation = Nation().apply { name = "Other nation" } + + val city = CityInfo() + city.location = tile.position.cpy().add(1f,1f) + city.civInfo = otherCiv + tile.owningCity = city + + unit.baseUnit = BaseUnit().apply { unitType = UnitType.Melee } + unit.owner = civInfo.civName + + Assert.assertFalse("Unit must not enter other civ tile", unit.movement.canPassThrough(tile)) + + city.location = tile.position + + Assert.assertFalse("Unit must not enter other civ city", unit.movement.canPassThrough(tile)) + + city.hasJustBeenConquered = true + civInfo.diplomacy["Other civ"] = DiplomacyManager(otherCiv, "Other civ") + civInfo.getDiplomacyManager(otherCiv).diplomaticStatus = DiplomaticStatus.War + + Assert.assertTrue("Unit can capture other civ city", unit.movement.canPassThrough(tile)) + } +} \ No newline at end of file