make ships claim targets
This commit is contained in:
@@ -83,6 +83,7 @@ struct Fixture
|
||||
[](const std::string&) -> bool { return true; },
|
||||
rng)
|
||||
, ships(cfg, admin)
|
||||
, ai(cfg)
|
||||
, salvager(admin)
|
||||
, repair(admin)
|
||||
, scraps(admin)
|
||||
@@ -351,6 +352,100 @@ TEST_CASE("BehaviorSystem: player combat ship ignores enemy beyond engagement ra
|
||||
REQUIRE_FALSE(f.admin.get<AttackBehavior>(player).currentTarget.has_value());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AttackBehavior — overclaim penalty & hysteresis
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Absent claims, the nearer enemy wins; once another ship claims that enemy, the
|
||||
// overclaim penalty steers the deciding ship to the unclaimed, equidistant one.
|
||||
TEST_CASE("BehaviorSystem: overclaim penalty steers a ship off a claimed target",
|
||||
"[behavior]")
|
||||
{
|
||||
SECTION("no claim: the nearer enemy is chosen")
|
||||
{
|
||||
Fixture f;
|
||||
const entt::entity player = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
||||
const entt::entity nearEnemy = f.ships.spawn("interceptor", 1, QVector2D(8.0f, 0.0f),
|
||||
/*isEnemy=*/true);
|
||||
f.ships.spawn("interceptor", 1, QVector2D(0.0f, 12.0f), /*isEnemy=*/true);
|
||||
|
||||
f.decide();
|
||||
|
||||
REQUIRE(f.admin.get<AttackBehavior>(player).currentTarget == nearEnemy);
|
||||
}
|
||||
|
||||
SECTION("enemyA already claimed: penalty redirects to the unclaimed enemyB")
|
||||
{
|
||||
const float d = 10.0f;
|
||||
Fixture f;
|
||||
const entt::entity player = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
||||
const entt::entity enemyA = f.ships.spawn("interceptor", 1, QVector2D(d, 0.0f),
|
||||
/*isEnemy=*/true);
|
||||
const entt::entity enemyB = f.ships.spawn("interceptor", 1, QVector2D(0.0f, d),
|
||||
/*isEnemy=*/true);
|
||||
|
||||
// A second player ship already commits to enemyA, registering a claim, so
|
||||
// the penalised score of enemyA falls below the unclaimed equidistant enemyB.
|
||||
const entt::entity claimant = f.ships.spawn("interceptor", 1, QVector2D(d, 1.0f));
|
||||
f.admin.get<AttackBehavior>(claimant).currentTarget = enemyA;
|
||||
|
||||
f.decide();
|
||||
|
||||
REQUIRE(f.admin.get<AttackBehavior>(player).currentTarget == enemyB);
|
||||
}
|
||||
}
|
||||
|
||||
// Hysteresis keeps a ship on its committed target when a fresh candidate is only
|
||||
// marginally better — and self-exclusion means the ship's own claim never counts
|
||||
// against the target it already holds.
|
||||
TEST_CASE("BehaviorSystem: hysteresis keeps a ship on its own claimed target",
|
||||
"[behavior]")
|
||||
{
|
||||
const float d = 10.0f;
|
||||
Fixture f;
|
||||
const entt::entity player = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
||||
const entt::entity enemyA = f.ships.spawn("interceptor", 1, QVector2D(d, 0.0f),
|
||||
/*isEnemy=*/true);
|
||||
f.ships.spawn("interceptor", 1, QVector2D(0.0f, d), /*isEnemy=*/true);
|
||||
|
||||
// The ship already holds enemyA; enemyB is equidistant. Without self-exclusion
|
||||
// the ship's own claim would penalise enemyA and flip the choice.
|
||||
f.admin.get<AttackBehavior>(player).currentTarget = enemyA;
|
||||
|
||||
f.decide();
|
||||
|
||||
REQUIRE(f.admin.get<AttackBehavior>(player).currentTarget == enemyA);
|
||||
}
|
||||
|
||||
// When the held target becomes heavily overclaimed by others, its penalised score
|
||||
// drops far enough that an equidistant unclaimed enemy beats the hysteresis margin
|
||||
// and the ship switches.
|
||||
TEST_CASE("BehaviorSystem: a ship switches off a heavily overclaimed target",
|
||||
"[behavior]")
|
||||
{
|
||||
const float d = 10.0f;
|
||||
Fixture f;
|
||||
const entt::entity player = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
||||
const entt::entity enemyA = f.ships.spawn("interceptor", 1, QVector2D(d, 0.0f),
|
||||
/*isEnemy=*/true);
|
||||
const entt::entity enemyB = f.ships.spawn("interceptor", 1, QVector2D(0.0f, d),
|
||||
/*isEnemy=*/true);
|
||||
|
||||
f.admin.get<AttackBehavior>(player).currentTarget = enemyA;
|
||||
|
||||
// Five other ships also commit to enemyA, saturating the claim penalty (0.5).
|
||||
for (int i = 0; i < 5; ++i)
|
||||
{
|
||||
const entt::entity other =
|
||||
f.ships.spawn("interceptor", 1, QVector2D(d, static_cast<float>(2 + i)));
|
||||
f.admin.get<AttackBehavior>(other).currentTarget = enemyA;
|
||||
}
|
||||
|
||||
f.decide();
|
||||
|
||||
REQUIRE(f.admin.get<AttackBehavior>(player).currentTarget == enemyB);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AttackBehavior — enemy ships
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -84,6 +84,13 @@ TEST_CASE("ConfigLoader loads the committed bin/config/ configs end-to-end", "[c
|
||||
REQUIRE(cfg.world.waves.threatRateFormula.evaluate(1.0) == Approx(1.0));
|
||||
REQUIRE(cfg.world.waves.threatRateFormula.evaluate(5.0) == Approx(5.0));
|
||||
|
||||
// targeting: distance score 1/(1+x) and overclaim penalty max(0.5, 1-0.1*x).
|
||||
REQUIRE(cfg.world.targeting.hysteresis == Approx(0.10));
|
||||
REQUIRE(cfg.world.targeting.targetScoreFormula.evaluate(0.0) == Approx(1.0));
|
||||
REQUIRE(cfg.world.targeting.targetScoreFormula.evaluate(1.0) == Approx(0.5));
|
||||
REQUIRE(cfg.world.targeting.overclaimPenaltyFormula.evaluate(0.0) == Approx(1.0));
|
||||
REQUIRE(cfg.world.targeting.overclaimPenaltyFormula.evaluate(5.0) == Approx(0.5));
|
||||
|
||||
// buildings.toml
|
||||
REQUIRE(cfg.buildings.buildings.size() >= 8);
|
||||
const auto minerIt = std::find_if(
|
||||
|
||||
Reference in New Issue
Block a user