Skip to content
Merged
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

/**
*
Expand Down Expand Up @@ -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()
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down
27 changes: 27 additions & 0 deletions src/main/kotlin/org/openbase/planetsudo/level/AbstractLevel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
}
Expand Down Expand Up @@ -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.")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,29 @@ interface AgentSpecialInterface<DE : AgentSpecialInterfaceGermanWrapper> : 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.
*
* @return true oder false.
*/
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.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
Original file line number Diff line number Diff line change
Expand Up @@ -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

/**
*
Expand All @@ -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
Expand Down Expand Up @@ -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
}
Expand All @@ -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) {
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
Loading