diff --git a/build.gradle.kts b/build.gradle.kts index 555b55a..ddca6b2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -55,6 +55,7 @@ dependencies { testImplementation(libs.org.jetbrains.kotlin.test.junit5) testImplementation(libs.io.mockk) testImplementation(libs.org.amshove.kluent) + testImplementation(kotlin("test")) } nexusPublishing { diff --git a/src/main/kotlin/org/openbase/planetsudo/game/strategy/DivineStrategy.kt b/src/main/kotlin/org/openbase/planetsudo/game/strategy/DivineStrategy.kt index 327c2a9..1a27b86 100644 --- a/src/main/kotlin/org/openbase/planetsudo/game/strategy/DivineStrategy.kt +++ b/src/main/kotlin/org/openbase/planetsudo/game/strategy/DivineStrategy.kt @@ -5,6 +5,7 @@ import org.openbase.planetsudo.level.LevelSize import org.openbase.planetsudo.level.levelobjects.AgentInterface import org.openbase.planetsudo.level.levelobjects.Resource.ResourceType import org.openbase.planetsudo.level.levelobjects.Tower +import org.openbase.planetsudo.level.levelobjects.WallDistance /** * @@ -41,6 +42,46 @@ class DivineStrategy(agent: AgentInterface) : StrategyLevelLegacy(agent) { "Discover" commander inCase { true } then { agent.goRight(4) } +// // Keep a bit of clearance from the wall while still following it. The distance parameter ensures the +// // agent does not hug the wall tightly when it is supposed to stay parallel to it. +// "Re-find Wall" commander inCase { +// !agent.seeWallAtRight(45, WallDistance.VERY_FAR) && !agent.seeWallAtRight( +// 90, +// WallDistance.VERY_FAR, +// ) && !agent.seeWallAtRight(135, WallDistance.VERY_FAR) +// } then { agent.turnRight(2) } + "Follow Wall With Clearance" commander inCase { + ( + agent.seeWallAtRight(45, WallDistance.CLOSE) && + agent.seeWallAtRight(135, WallDistance.CLOSE) + ) && !agent.seeWallAtRight(0, WallDistance.VERY_FAR) + } then { agent.goLeft(5) } + "Follow Wall Closer" commander inCase { + ( + agent.seeWallAtRight(45, WallDistance.VERY_FAR) && + agent.seeWallAtRight(135, WallDistance.VERY_FAR) + ) && !agent.seeWallAtRight(0, WallDistance.VERY_FAR) + } then { agent.goRight(5) } + "Follow Wall" commander inCase { + ( + agent.seeWallAtRight(45, WallDistance.FAR) && + agent.seeWallAtRight(135, WallDistance.FAR) + ) && !agent.seeWallAtRight(0, WallDistance.VERY_FAR) + } then { agent.go() } + "Follow Corner" commander inCase { + agent.seeWallAtRight(0, WallDistance.FAR) && ( + agent.seeWallAtRight( + 45, + WallDistance.VERY_FAR, + ) || agent.seeWallAtRight(90, WallDistance.VERY_FAR) + ) + } then { agent.goLeft(20) } + "Follow Edge" commander inCase { + agent.seeWallAtRight(135, WallDistance.VERY_FAR) && + !agent.seeWallAtRight(110, WallDistance.VERY_FAR) && + !agent.seeWallAtRight(0, WallDistance.FAR) + } then { agent.turnRight(20) } + "Scan" commander inCase { tower.type == Tower.TowerType.ObservationTower && tower.levelSize == LevelSize.UNKNOWN } then { tower.scanLevelSize() } @@ -352,7 +393,7 @@ class DivineStrategy(agent: AgentInterface) : StrategyLevelLegacy(agent) { ) // --------------------------------------------> createRule( - object : Rule("RepaireMothership") { + object : Rule("RepairMothership") { override fun constraint(): Boolean { return mothership.isDamaged && agent.isAtMothership } @@ -488,7 +529,7 @@ class DivineStrategy(agent: AgentInterface) : StrategyLevelLegacy(agent) { ) // --------------------------------------------> createRule( - object : Rule("Follow Wall", SwatTeam.COMMANDER) { + object : Rule("Avoid Wall Collision", SwatTeam.COMMANDER) { override fun constraint(): Boolean { return agent.isCollisionDetected } diff --git a/src/main/kotlin/org/openbase/planetsudo/level/AbstractLevel.kt b/src/main/kotlin/org/openbase/planetsudo/level/AbstractLevel.kt index dc71079..1d580a0 100644 --- a/src/main/kotlin/org/openbase/planetsudo/level/AbstractLevel.kt +++ b/src/main/kotlin/org/openbase/planetsudo/level/AbstractLevel.kt @@ -7,6 +7,7 @@ import org.openbase.planetsudo.game.GameManager import org.openbase.planetsudo.game.Team import org.openbase.planetsudo.game.strategy.AbstractStrategy import org.openbase.planetsudo.geometry.Base2D +import org.openbase.planetsudo.geometry.Direction2D import org.openbase.planetsudo.geometry.Point2D import org.openbase.planetsudo.level.levelobjects.Agent import org.openbase.planetsudo.level.levelobjects.Mothership @@ -179,6 +180,32 @@ abstract class AbstractLevel : AbstractGameObject, Runnable { return !levelBorderPolygon.contains(bounds) } + /** + * Steps along a direction from start point and returns true when the first wall/border is hit. + * Uses a single reusable rectangle and incremental translation to reduce allocations. + */ + fun hitsWall(start: org.openbase.planetsudo.geometry.Point2D, direction: org.openbase.planetsudo.geometry.Direction2D, maxDistance: Int): Boolean { + val p = start.clone() + val testRect = Rectangle2D.Double(0.0, 0.0, 2.0, 2.0) + val walls = levelWallPolygons + for (i in 1..maxDistance) { + p.translate(direction, 1) + testRect.x = p.xy[0] - 1.0 + testRect.y = p.xy[1] - 1.0 + if (walls != null) { + for (wall in walls) { + if (wall.intersects(testRect) || wall.contains(testRect)) { + return true + } + } + } + if (!levelBorderPolygon.contains(testRect)) { + return true + } + } + return false + } + fun collisionDetected(bounds: Rectangle2D?): Boolean { return containsWall(bounds) } diff --git a/src/main/kotlin/org/openbase/planetsudo/level/levelobjects/Agent.kt b/src/main/kotlin/org/openbase/planetsudo/level/levelobjects/Agent.kt index 4b82b77..6c66d06 100644 --- a/src/main/kotlin/org/openbase/planetsudo/level/levelobjects/Agent.kt +++ b/src/main/kotlin/org/openbase/planetsudo/level/levelobjects/Agent.kt @@ -162,6 +162,20 @@ class Agent( override fun isCollisionDetectedAtRight(beta: Int): Boolean = level.collisionDetected(computeFutureBoundsRight(beta)) + override fun seeWallAtLeft(beta: Int, distance: WallDistance): Boolean = + level.hitsWall( + start = position.clone(), + direction = Direction2D(direction.angle).also { it.angle -= beta }, + maxDistance = distance.pixel, + ) + + override fun seeWallAtRight(beta: Int, distance: WallDistance): Boolean = + level.hitsWall( + start = position.clone(), + direction = Direction2D(direction.angle).also { it.angle += beta }, + maxDistance = distance.pixel, + ) + override val isShifting get(): Boolean = shiftTonic > 0.0 @@ -738,14 +752,15 @@ class Agent( override fun goToSupportAgent() { try { - val agentToSupport = mothership.getAgentToSupport(this) - if (agentToSupport !== this) { - goTo { - agentToSupport.levelView?.getRelativeDirection(this@Agent) - ?.also { angle = it } + mothership.getAgentToSupport(this)?.let { agentToSupport -> + if (agentToSupport !== this) { + goTo { + agentToSupport.levelView?.getRelativeDirection(this@Agent) + ?.also { angle = it } + } + } else { + throw CouldNotPerformException("Could not support itself!") } - } else { - throw CouldNotPerformException("Could not support itself!") } } catch (ex: CouldNotPerformException) { ExceptionPrinter.printHistory(CouldNotPerformException("Could not goToSupportAgent!", ex), LOGGER) @@ -773,7 +788,10 @@ class Agent( level.getAdversaryAgent(this)?.let { enemyAgent -> // do not come too close to the adversary agents if (enemyAgent.levelView!!.getDistance(this) >= (AGENT_SIZE / 2)) { - goTo { turnTo(position, enemyAgent.position) } + goTo { + enemyAgent.levelView?.getRelativeDirection(this@Agent) + ?.also { angle = it } + } } else { performAction { direction.turnTo(position, enemyAgent.position) @@ -930,6 +948,8 @@ class Agent( const val MAX_TONIC: Int = 3 const val AGENT_SIZE: Int = 50 const val AGENT_VIEW_DISTANCE: Int = AGENT_SIZE + + /** Default see distance (in pixels) for agent line-of-sight checks */ const val DEFAULT_AGENT_SPEED: Int = 6 const val SHIFT_EXTRA_SPEED: Int = 3 const val SHIFT_TONIC_CONSUMPTION: Double = 0.01 diff --git a/src/main/kotlin/org/openbase/planetsudo/level/levelobjects/AgentMock.kt b/src/main/kotlin/org/openbase/planetsudo/level/levelobjects/AgentMock.kt index 1df86c2..173ba01 100644 --- a/src/main/kotlin/org/openbase/planetsudo/level/levelobjects/AgentMock.kt +++ b/src/main/kotlin/org/openbase/planetsudo/level/levelobjects/AgentMock.kt @@ -83,6 +83,7 @@ class AgentMock : AgentInterface { override fun goToEnemyAgent() { error("Mock does not offer any functionality.") } + override fun goToTeamAgent() { error("Mock does not offer any functionality.") } @@ -250,6 +251,14 @@ class AgentMock : AgentInterface { error("Mock does not offer any functionality.") } + override fun seeWallAtLeft(beta: Int, distance: WallDistance): Boolean { + error("Mock does not offer any functionality.") + } + + override fun seeWallAtRight(beta: Int, distance: WallDistance): Boolean { + error("Mock does not offer any functionality.") + } + override fun constructTower(type: Tower.TowerType) { error("Mock does not offer any functionality.") } diff --git a/src/main/kotlin/org/openbase/planetsudo/level/levelobjects/AgentSpecialInterface.kt b/src/main/kotlin/org/openbase/planetsudo/level/levelobjects/AgentSpecialInterface.kt index 43e86f4..bdca695 100644 --- a/src/main/kotlin/org/openbase/planetsudo/level/levelobjects/AgentSpecialInterface.kt +++ b/src/main/kotlin/org/openbase/planetsudo/level/levelobjects/AgentSpecialInterface.kt @@ -11,6 +11,14 @@ interface AgentSpecialInterface : Agent */ fun isCollisionDetectedAtLeft(beta: Int): Boolean + /** + * Abfrage, ob sich eine Wand in der linken Sichtweite befindet. + * + * @param beta Winkel in Grad (0 = vorne - 90 = links - 180 = hinten) + * @param distance Sicht-Distanz + */ + fun seeWallAtLeft(beta: Int, distance: WallDistance = WallDistance.FAR): Boolean + /** * Gibt an, ob ein Zusammenstoß mit einer Wand zur rechten Seite bevorsteht. * @@ -18,6 +26,14 @@ interface AgentSpecialInterface : Agent */ fun isCollisionDetectedAtRight(beta: Int): Boolean + /** + * Abfrage, ob sich eine Wand in der rechten Sichtweite befindet. + * + * @param beta Winkel in Grad (0 = vorne - 90 = rechts - 180 = hinten) + * @param distance Sicht-Distanz + */ + fun seeWallAtRight(beta: Int, distance: WallDistance = WallDistance.FAR): Boolean + /** * Abfrage, ob der Agent aktuell unsichtbar ist. */ diff --git a/src/main/kotlin/org/openbase/planetsudo/level/levelobjects/AgentSpecialInterfaceGermanWrapper.kt b/src/main/kotlin/org/openbase/planetsudo/level/levelobjects/AgentSpecialInterfaceGermanWrapper.kt index 92224ed..bb4c5ee 100644 --- a/src/main/kotlin/org/openbase/planetsudo/level/levelobjects/AgentSpecialInterfaceGermanWrapper.kt +++ b/src/main/kotlin/org/openbase/planetsudo/level/levelobjects/AgentSpecialInterfaceGermanWrapper.kt @@ -17,6 +17,24 @@ open class AgentSpecialInterfaceGermanWrapper(private val agent: AgentSpecialInt */ fun istKollisionRechtsErkannt(beta: Int) = agent.isCollisionDetectedAtRight(beta) + /** + * Prüft, ob sich links vom Agenten innerhalb der angegebenen Distanz eine Wand befindet. + * + * @param beta Winkel in Grad (0 = vorne, 90 = links, 180 = hinten) + * @param distanz Maximale Suchdistanz + */ + fun seheWandLinks(beta: Int, distanz: WallDistance = WallDistance.FAR) = + agent.seeWallAtLeft(beta, distanz) + + /** + * Prüft, ob sich rechts vom Agenten innerhalb der angegebenen Distanz eine Wand befindet. + * + * @param beta Winkel in Grad (0 = vorne, 90 = rechts, 180 = hinten) + * @param distanz Maximale Suchdistanz + */ + fun seheWandRechts(beta: Int, distanz: WallDistance = WallDistance.FAR) = + agent.seeWallAtRight(beta, distanz) + /** * Abfrage, ob der Agent aktuell unsichtbar ist. */ diff --git a/src/main/kotlin/org/openbase/planetsudo/level/levelobjects/Mothership.kt b/src/main/kotlin/org/openbase/planetsudo/level/levelobjects/Mothership.kt index a0340f1..32a6247 100644 --- a/src/main/kotlin/org/openbase/planetsudo/level/levelobjects/Mothership.kt +++ b/src/main/kotlin/org/openbase/planetsudo/level/levelobjects/Mothership.kt @@ -320,16 +320,16 @@ class Mothership(id: Int, team: Team, level: AbstractLevel) : } @Throws(CouldNotPerformException::class) - fun getAgentToSupport(helper: Agent): Agent { + fun getAgentToSupport(helper: Agent): Agent? { supportChannelLock.read { if (supportChannel.isEmpty()) { - throw CouldNotPerformException("No support necessary.") + return null } val supportCaller: Agent = supportChannel .filterNot { it === helper } // do not help yourself .minByOrNull { helper.levelView?.getDistance(it) ?: Int.MAX_VALUE } - ?: throw CouldNotPerformException("No support possible.") + ?: return null // remove caller from support channel if support is possible if (supportCaller.bounds.intersects(helper.viewBounds)) { diff --git a/src/main/kotlin/org/openbase/planetsudo/level/levelobjects/WallDistance.kt b/src/main/kotlin/org/openbase/planetsudo/level/levelobjects/WallDistance.kt new file mode 100644 index 0000000..79e7f8a --- /dev/null +++ b/src/main/kotlin/org/openbase/planetsudo/level/levelobjects/WallDistance.kt @@ -0,0 +1,29 @@ +package org.openbase.planetsudo.level.levelobjects + +import org.openbase.planetsudo.level.levelobjects.Agent.Companion.AGENT_SIZE + +/** + * Maximum distance used by [AgentSpecialInterface.seeWallAtLeft] and + * [AgentSpecialInterface.seeWallAtRight] when checking for a wall. + * + * The distance is measured in pixels from the agent's center in the requested direction. + * It is a search limit, not the measured distance to a wall. Each step represents a multiple + * of [AGENT_SIZE]. + * + * @property pixel Maximum search distance in pixels. + */ +enum class WallDistance(val pixel: Int) { + /** Search up to one agent size away. */ + VERY_CLOSE(pixel = agentFactor(1.0)), + + /** Search up to one and a half agent sizes away. */ + CLOSE(pixel = agentFactor(1.5)), + + /** Search up to two agent sizes away. */ + FAR(pixel = agentFactor(2.0)), + + /** Search up to two and a half agent sizes away. */ + VERY_FAR(pixel = agentFactor(2.5)), +} + +private fun agentFactor(factor: Double) = (AGENT_SIZE * factor).toInt() diff --git a/src/main/kotlin/org/openbase/planetsudo/view/level/levelobjects/TowerTopPanel.kt b/src/main/kotlin/org/openbase/planetsudo/view/level/levelobjects/TowerTopPanel.kt index 6ed546d..4b3032f 100644 --- a/src/main/kotlin/org/openbase/planetsudo/view/level/levelobjects/TowerTopPanel.kt +++ b/src/main/kotlin/org/openbase/planetsudo/view/level/levelobjects/TowerTopPanel.kt @@ -14,6 +14,7 @@ import java.awt.geom.Arc2D import java.beans.PropertyChangeEvent import java.beans.PropertyChangeListener import javax.swing.Timer +import kotlin.math.hypot /** * @@ -28,7 +29,7 @@ class TowerTopPanel(tower: Tower, parentPanel: TowerPanel) : DrawLayer.BACKGROUND, ), PropertyChangeListener { - private val tower: Tower + private val tower: Tower? private var gg2: Graphics2D? = null private var direction: Direction2D? = null @@ -70,7 +71,8 @@ class TowerTopPanel(tower: Tower, parentPanel: TowerPanel) : } override fun paintComponent(g2: Graphics2D, gl: Graphics2D) { - // only paint if constructed + // Guard against stale or already-removed tower references during asynchronous Swing repaints. + val currentTower = tower ?: return if (!resource.isConstructed) { return } @@ -80,7 +82,7 @@ class TowerTopPanel(tower: Tower, parentPanel: TowerPanel) : gg2 = g2.create() as Graphics2D try { - paintImageRotated(tower.direction, gg2!!) + paintImageRotated(currentTower.direction, gg2!!) // draw scanning waves if active if (scanning) { @@ -118,7 +120,7 @@ class TowerTopPanel(tower: Tower, parentPanel: TowerPanel) : // parentResourcePanel.removeChild(this); // } // } - if (evt.propertyName == Tower.TOWER_SCAN_LEVEL) { + if (evt.propertyName == Tower.TOWER_SCAN_LEVEL && tower != null) { // start wave animation: compute max radius to level borders val levelBounds = tower.level.levelBorderPolygon.bounds2D val cx = tower.position.x @@ -134,7 +136,7 @@ class TowerTopPanel(tower: Tower, parentPanel: TowerPanel) : for (c in corners) { val dx = c[0] - cx val dy = c[1] - cy - val d = Math.hypot(dx, dy) + val d = hypot(dx, dy) if (d > maxd) maxd = d } scanMaxRadius = maxd