Layout-aim tests: lookalike-hull lie, asymmetric two-mount Wing hull, Entity assertions without formatting, docked stance through IsHostileTo
UnderALookalikeLieNoGearIsLocated: claimed and true hulls of one type and size with a reactor mount of equal type, position and shape. AMountAimReadsThePositionAxesInOrder and TwoMountsOfOneTypeAreTwoRows use a Wing hull off the diagonal with two Sensors mounts. Assert.Same and DoesNotContain over Entities become ReferenceEquals asserts. The docked-stance test reads IsHostileTo on a hostile-by-default subject instead of _iffOverrides. Test-only; no production change. Not yet built. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
1b0a67069e
commit
3098ff69a5
11 changed files with 142 additions and 33 deletions
|
|
@ -70,6 +70,22 @@ public sealed partial class ContactRevealTests : IDisposable
|
|||
new HardpointData { Type = HardpointType.Sensors, Position = new int2(0, 3), Shape = new Shape() }
|
||||
}
|
||||
});
|
||||
// Layout-aim: a hull off the diagonal with two mounts of one type. The first Sensors mount covers (0,0) and (1,0), the
|
||||
// second (2,3) and (3,3): a transposed position gives another cell, and a rule that matches mounts by type cannot
|
||||
// tell the two apart.
|
||||
var wingMount = new Shape(2, 1);
|
||||
foreach (var cell in wingMount.AllCoordinates) wingMount[cell] = true;
|
||||
var wingHull = new Shape(5, 5);
|
||||
foreach (var cell in wingHull.AllCoordinates) wingHull[cell] = true;
|
||||
cache.Upsert(new HullData
|
||||
{
|
||||
Name = "Wing", HullType = HullType.Ship, Shape = wingHull, Durability = 1000000, Mass = 1000,
|
||||
Hardpoints =
|
||||
{
|
||||
new HardpointData { Type = HardpointType.Sensors, Position = new int2(0, 0), Shape = wingMount },
|
||||
new HardpointData { Type = HardpointType.Sensors, Position = new int2(2, 3), Shape = wingMount }
|
||||
}
|
||||
});
|
||||
var bayReactor = Tool("BayReactor", new ReactorData { Charge = Const(1000), Efficiency = Const(1), OverloadEfficiency = Const(1), ThrottlingFactor = Const(2) });
|
||||
bayReactor.Hardpoint = HardpointType.Reactor;
|
||||
cache.Upsert(bayReactor);
|
||||
|
|
@ -1713,14 +1729,6 @@ public sealed partial class ContactRevealTests : IDisposable
|
|||
Assert.Equal(0L, smallest);
|
||||
}
|
||||
|
||||
// The observer's stored decision about a subject (SetIff), read as the entity holds it: a neutral decision (false) is also
|
||||
// what an unset stance derives to here, so IsHostileTo alone cannot tell it kept from dropped.
|
||||
private static bool? StoredStance(Entity observer, Entity subject)
|
||||
{
|
||||
var overrides = (System.Collections.IDictionary) typeof(Entity).GetField("_iffOverrides", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(observer);
|
||||
return overrides.Contains(subject) ? (bool) overrides[subject] : (bool?) null;
|
||||
}
|
||||
|
||||
// A docked subject leaves the zone parented to its dock and rejoins on undock: the observer's decision about it, hostile or
|
||||
// neutral, outlives the lapse of the bearing, and is still there when it rejoins.
|
||||
[Theory]
|
||||
|
|
@ -1731,8 +1739,13 @@ public sealed partial class ContactRevealTests : IDisposable
|
|||
var e = Add(float3(0, 0, 100), new[] { "Gun" });
|
||||
var dock = Add(float3(200, 0, 0));
|
||||
var o = Add(float3(0, 0, 0), new[] { "Eye" });
|
||||
// The observer's faction owns the zone and the subject's presence is not permitted, so the stance derived for an
|
||||
// undecided subject is hostile: a neutral decision (false) that is dropped would read hostile, and is seen to be kept.
|
||||
_zone.GalaxyZone.Owner = o.Faction;
|
||||
e.CurrentSecurityLevel.Value = SecurityLevel.Critical;
|
||||
Assert.True(o.IsHostileTo(e), "fixture: an undecided subject derives hostile");
|
||||
o.SetIff(e, hostile);
|
||||
Assert.Equal(hostile, StoredStance(o, e)); // fixture: the decision is stored
|
||||
Assert.Equal(hostile, o.IsHostileTo(e)); // fixture: the decision is stored
|
||||
var gun = Guns(e)[0];
|
||||
Fire(e, gun);
|
||||
ShowOnly(e, 1f, gun);
|
||||
|
|
@ -1741,11 +1754,9 @@ public sealed partial class ContactRevealTests : IDisposable
|
|||
_zone.Remove(e);
|
||||
for (var i = 0; i < 100 && o.RevealedSubjects.Contains(e); i++) Tick(.1f);
|
||||
Assert.False(o.RevealedSubjects.Contains(e), "fixture: the book dropped the docked subject");
|
||||
Assert.Equal(hostile, StoredStance(o, e)); // a docked subject keeps the observer's decision
|
||||
Assert.Equal(hostile, o.IsHostileTo(e));
|
||||
e.Parent = null;
|
||||
_zone.Admit(e, piloted: false);
|
||||
Assert.Equal(hostile, StoredStance(o, e)); // and it is still there when the subject rejoins
|
||||
Assert.Equal(hostile, o.IsHostileTo(e));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -562,7 +562,7 @@ public sealed class FireControlCut123Tests : IDisposable
|
|||
var outcome = FireUntilHit(e, damageOverride: 5f);
|
||||
Assert.NotNull(outcome);
|
||||
Assert.True(outcome.Hit);
|
||||
Assert.Same(e.Shooter, source);
|
||||
Assert.True(ReferenceEquals(e.Shooter, source), "the hit names the shooter");
|
||||
}
|
||||
|
||||
// Stryker survivors: FireControl.Apply's two `walked[li] > 0` checks (metalLanes counting, and the
|
||||
|
|
|
|||
|
|
@ -266,8 +266,8 @@ public sealed class FireControlCut8Tests : IDisposable
|
|||
|
||||
s.Locker.Deactivate();
|
||||
Assert.False(s.Locker.Active);
|
||||
Assert.DoesNotContain(s.Bystander, s.Locker.EntityInfoGathered.Keys);
|
||||
Assert.Same(s.Bystander, s.Locker.Target.Value.Entity); // Deactivate does not null the entity's own Target
|
||||
Assert.False(s.Locker.EntityInfoGathered.Keys.Any(k => ReferenceEquals(k, s.Bystander)), "the bystander is dropped from the locker's info");
|
||||
Assert.True(ReferenceEquals(s.Bystander, s.Locker.Target.Value.Entity), "the target is kept"); // Deactivate does not null the entity's own Target
|
||||
|
||||
var ex = Record.Exception(() => s.Locker.Update(.1f));
|
||||
Assert.Null(ex);
|
||||
|
|
|
|||
|
|
@ -21,11 +21,25 @@ public sealed partial class ContactRevealTests
|
|||
|
||||
// A Bay at `position`: its reactor on the reactor mount, a gun on the weapon mount when asked (with the targeting system
|
||||
// and reactor tool it needs to fire), and `tools` on free cells.
|
||||
private Ship NewShip(string hull)
|
||||
{
|
||||
var hullRef = _items.ItemData.RefOf<ItemData>(_items.ItemData.GetByName<HullData>(hull));
|
||||
return new Ship(_items, _zone, new EquippableItem { Data = hullRef, Durability = 1000000, Lot = _nextLot++ }, new EntitySettings())
|
||||
{ Faction = new Faction { Name = "F" + Guid.NewGuid().ToString("N") } };
|
||||
}
|
||||
|
||||
private Ship Place(Ship ship, float3 position)
|
||||
{
|
||||
_zone.Admit(ship, piloted: false);
|
||||
ship.Position = position;
|
||||
ship.Aim = float3(0, 0, 1);
|
||||
_zone.Update(0f);
|
||||
return ship;
|
||||
}
|
||||
|
||||
private Ship AddBay(float3 position, bool gun = false, string[] tools = null)
|
||||
{
|
||||
var hullRef = _items.ItemData.RefOf<ItemData>(_items.ItemData.GetByName<HullData>("Bay"));
|
||||
var ship = new Ship(_items, _zone, new EquippableItem { Data = hullRef, Durability = 1000000, Lot = _nextLot++ }, new EntitySettings())
|
||||
{ Faction = new Faction { Name = "F" + Guid.NewGuid().ToString("N") } };
|
||||
var ship = NewShip("Bay");
|
||||
Assert.True(ship.TryEquip(Make("BayReactor"), new int2(3, 3)));
|
||||
if (gun) Assert.True(ship.TryEquip(Make("Gun"), new int2(0, 3)));
|
||||
foreach (var name in (tools ?? new string[0]).Concat(gun ? new[] { "Targeting", "Reactor" } : new string[0]))
|
||||
|
|
@ -34,11 +48,20 @@ public sealed partial class ContactRevealTests
|
|||
Assert.True(ship.TryFindSpace(item, out var at));
|
||||
Assert.True(ship.TryEquip(item, at));
|
||||
}
|
||||
_zone.Admit(ship, piloted: false);
|
||||
ship.Position = position;
|
||||
ship.Aim = float3(0, 0, 1);
|
||||
_zone.Update(0f);
|
||||
return ship;
|
||||
return Place(ship, position);
|
||||
}
|
||||
|
||||
// A Wing at `position` with a gun on each of its two Sensors mounts: gunA on the mount at (0,0), gunB at (2,3).
|
||||
private (Ship wing, EquippedItem gunA, EquippedItem gunB) AddWing(float3 position)
|
||||
{
|
||||
var ship = NewShip("Wing");
|
||||
Assert.True(ship.TryEquip(Make("Gun"), new int2(0, 0)));
|
||||
Assert.True(ship.TryEquip(Make("Gun"), new int2(2, 3)));
|
||||
Place(ship, position);
|
||||
var gunA = ship.Equipment.Single(i => i.Position.x == 0);
|
||||
var gunB = ship.Equipment.Single(i => i.Position.x == 2);
|
||||
Assert.True(gunA.Hardpoint != null && gunB.Hardpoint != null && !ReferenceEquals(gunA.Hardpoint, gunB.Hardpoint), "fixture: each gun sits on its own mount");
|
||||
return (ship, gunA, gunB);
|
||||
}
|
||||
|
||||
private static EquippedItem Fitted(Entity e, string design) => e.Equipment.Single(i => i.Data.Name == design);
|
||||
|
|
@ -134,6 +157,36 @@ public sealed partial class ContactRevealTests
|
|||
Assert.True(observer.TrySelectTargetItem(reactor));
|
||||
}
|
||||
|
||||
// The lie is a lookalike: the claimed hull is of the true hull's type and size, with a reactor mount of the same type,
|
||||
// position and shape as the true one, and a shield mount elsewhere. What the observer knows is the claimed layout, mount
|
||||
// object by mount object; nothing of the true hull's layout leaks in because the two are alike.
|
||||
[Fact]
|
||||
public void UnderALookalikeLieNoGearIsLocated()
|
||||
{
|
||||
var (bay, observer, reactor) = Identified();
|
||||
var truth = bay.HullData;
|
||||
var lookalike = new HullData
|
||||
{
|
||||
Name = "Lookalike", HullType = truth.HullType, Shape = new Shape(truth.Shape.Width, truth.Shape.Height),
|
||||
Hardpoints =
|
||||
{
|
||||
new HardpointData { Type = HardpointType.Reactor, Position = truth.Hardpoints[0].Position, Shape = truth.Hardpoints[0].Shape },
|
||||
new HardpointData { Type = HardpointType.Shield, Position = new int2(3, 1), Shape = new Shape() }
|
||||
}
|
||||
};
|
||||
bay.Claim(lookalike);
|
||||
Assert.True(truth.Shape.Width == lookalike.Shape.Width && truth.Shape.Height == lookalike.Shape.Height && truth.HullType == lookalike.HullType, "fixture: the two hulls are one size and type");
|
||||
Assert.True(lookalike.Hardpoints[0].Type == reactor.Hardpoint.Type && lookalike.Hardpoints[0].Position.Equals(reactor.Hardpoint.Position), "fixture: the claimed reactor mount looks like the true one");
|
||||
Assert.False(ReferenceEquals(lookalike.Hardpoints[0], reactor.Hardpoint), "fixture: and is not it");
|
||||
|
||||
Assert.True(lookalike.Hardpoints.SequenceEqual(observer.LayoutOf(bay)), "the layout is the claimed hull's, not the true hull's");
|
||||
Target(observer, bay);
|
||||
Assert.False(FireControl.IsLocated(observer, reactor));
|
||||
Assert.False(observer.TrySelectTargetItem(reactor));
|
||||
Assert.True(ReferenceEquals(bay, observer.Target.Value.Entity), "the ship stays targetable");
|
||||
Assert.Equal(2, observer.SchematicRows(bay).Count(r => r.Mount != null));
|
||||
}
|
||||
|
||||
// ---- ToolGearIsNotLocatedByLayout ----
|
||||
|
||||
// Tool gear sits on no mount, so the layout locates nothing of it; an act does.
|
||||
|
|
@ -160,14 +213,19 @@ public sealed partial class ContactRevealTests
|
|||
private (Ship shooter, Ship bay, EquippedItem reactor, Weapon weapon, EquippedItem gun) Gunner()
|
||||
{
|
||||
var bay = AddBay(float3(0, 0, 30));
|
||||
return GunnerOn(bay, Fitted(bay, "BayReactor"));
|
||||
}
|
||||
|
||||
// A shooter that holds a valid solution on `bay`, with `aimed` (fitted to it) selected as the aim point.
|
||||
private (Ship shooter, Ship bay, EquippedItem reactor, Weapon weapon, EquippedItem gun) GunnerOn(Ship bay, EquippedItem aimed)
|
||||
{
|
||||
var shooter = Add(float3(0, 0, 0), new[] { "Eye", "Gun" });
|
||||
shooter.SetTarget(bay);
|
||||
for (var i = 0; i < 3; i++) Hold(shooter, 1, 1f, bay);
|
||||
Assert.True(shooter.VisibleEntities.Contains(bay), "fixture: the shooter tracks the ship");
|
||||
var gun = Guns(shooter).Single();
|
||||
var reactor = Fitted(bay, "BayReactor");
|
||||
Assert.True(shooter.TrySelectTargetItem(reactor), "fixture: the located reactor is aimed at");
|
||||
return (shooter, bay, reactor, (Weapon) gun.Behaviors.Single(b => b is Weapon), gun);
|
||||
Assert.True(shooter.TrySelectTargetItem(aimed), "fixture: the located item is aimed at");
|
||||
return (shooter, bay, aimed, (Weapon) gun.Behaviors.Single(b => b is Weapon), gun);
|
||||
}
|
||||
|
||||
private PendingShot FireAt(Weapon weapon, EquippedItem gun, Entity shooter)
|
||||
|
|
@ -206,6 +264,20 @@ public sealed partial class ContactRevealTests
|
|||
Assert.True(Math.Abs(mountSil.POnHull - itemSil.POnHull) > 1e-3f, "fixture: the two aim points are priced apart");
|
||||
}
|
||||
|
||||
// The mount is a rectangle off the diagonal: the second Sensors mount covers (2,3) and (3,3), centre (2.5, 3). A position
|
||||
// read with its axes swapped would centre the shot on (3.5, 2).
|
||||
[Fact]
|
||||
public void AMountAimReadsThePositionAxesInOrder()
|
||||
{
|
||||
var (wing, _, gunB) = AddWing(float3(0, 0, 30));
|
||||
var (shooter, _, _, weapon, gun) = GunnerOn(wing, gunB);
|
||||
var shot = FireAt(weapon, gun, shooter);
|
||||
Assert.True(shot.AimAtMount && ReferenceEquals(gunB, shot.Aimed), "fixture: the shot is aimed at the located gun's mount");
|
||||
Assert.True(FireControl.CommitProbability(shot, _zone.Time, out var sil) > 0f, "fixture: the shot has a price");
|
||||
Assert.Equal(2.5f, sil.AimPoint.x, 4);
|
||||
Assert.Equal(3f, sil.AimPoint.y, 4);
|
||||
}
|
||||
|
||||
// ---- TheShotKeepsTheAimItWasFiredWith ----
|
||||
|
||||
[Fact]
|
||||
|
|
@ -259,6 +331,32 @@ public sealed partial class ContactRevealTests
|
|||
Assert.Equal(new[] { true, false, false }, rows.Select(r => r.Marks(observer.ResolvedTargetItem)).ToArray());
|
||||
}
|
||||
|
||||
// Two mounts of one type: the rows tell them apart by mount, not by type. Aiming at one marks its row alone, and revealing
|
||||
// the other gives way only that mount's row to its gear row, leaving this one a mount row.
|
||||
[Fact]
|
||||
public void TwoMountsOfOneTypeAreTwoRows()
|
||||
{
|
||||
var (wing, gunA, gunB) = AddWing(float3(0, 0, 100));
|
||||
var observer = Add(float3(0, 0, 0));
|
||||
observer.EntityInfoGathered[wing] = 1f;
|
||||
var mountA = gunA.Hardpoint;
|
||||
var mountB = gunB.Hardpoint;
|
||||
Target(observer, wing);
|
||||
Assert.True(observer.TrySelectTargetItem(gunB));
|
||||
|
||||
var rows = observer.SchematicRows(wing).ToArray();
|
||||
Assert.Equal(2, rows.Length);
|
||||
Assert.True(ReferenceEquals(mountA, rows[0].Mount) && ReferenceEquals(mountB, rows[1].Mount), "one mount row per mount, in order");
|
||||
Assert.Equal(new[] { false, true }, rows.Select(r => r.Marks(observer.ResolvedTargetItem)).ToArray());
|
||||
|
||||
observer.RecordReveal(wing, gunA, SignatureChannel.LaunchFlash, gunA.Data);
|
||||
rows = observer.SchematicRows(wing).ToArray();
|
||||
Assert.Equal(2, rows.Length);
|
||||
Assert.True(ReferenceEquals(rows[0].Item, gunA) && rows[0].Mount == null, "the revealed gun's row leads");
|
||||
Assert.True(ReferenceEquals(rows[1].Mount, mountB) && rows[1].Item == null, "the other mount of the same type stays a mount row");
|
||||
Assert.Equal(new[] { false, true }, rows.Select(r => r.Marks(observer.ResolvedTargetItem)).ToArray());
|
||||
}
|
||||
|
||||
// ---- TheAIStillChoosesOnlyRevealedWeapons ----
|
||||
|
||||
// An AI ship that identifies a Bay with a gun on its weapon mount aims at no item: Combat chooses among the weapons it has
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ public sealed partial class RunStartTests
|
|||
// The first real sensing tick: the hostile holds no track of the player, and the sim's own passive sensing, asked for
|
||||
// the player's signature at the staged place, settles below the detection threshold: the staging is out of its sight.
|
||||
arena.Update(.1f);
|
||||
Assert.DoesNotContain(player, hostile.VisibleEnemies);
|
||||
Assert.False(hostile.VisibleEnemies.Any(x => ReferenceEquals(x, player)), "the player is out of sight");
|
||||
Assert.DoesNotContain(player.IncomingPaints, paint => paint.Mode == PaintMode.Track);
|
||||
Assert.True(player.Visibility > 0f, "the player shows a signature to be seen by");
|
||||
var threshold = player.ItemManager.GameplaySettings.TargetDetectionInfoThreshold;
|
||||
|
|
|
|||
|
|
@ -224,14 +224,14 @@ public sealed class LockWarningTests : IDisposable
|
|||
var locker = NewShip(float3(0, 0, 0), "Launcher");
|
||||
var b = NewShip(float3(0, 0, 9));
|
||||
b.EntityInfoGathered[locker] = 0;
|
||||
Assert.DoesNotContain(locker, b.VisibleEnemies);
|
||||
Assert.False(b.VisibleEnemies.Any(x => ReferenceEquals(x, locker)), "the locker is not a visible enemy");
|
||||
|
||||
Aim(locker, b);
|
||||
Tick(.5f);
|
||||
Tick(0f);
|
||||
|
||||
Assert.Single(b.IncomingPaints);
|
||||
Assert.DoesNotContain(locker, b.VisibleEnemies);
|
||||
Assert.False(b.VisibleEnemies.Any(x => ReferenceEquals(x, locker)), "the locker is not a visible enemy");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -431,7 +431,7 @@ public sealed class LootTests : IDisposable
|
|||
Kill(ship);
|
||||
var count = lab.Zone.FloatingItems.Count;
|
||||
Assert.NotEmpty(lab.Zone.FloatingItems);
|
||||
Assert.DoesNotContain(ship, lab.Zone.Entities);
|
||||
Assert.False(lab.Zone.Entities.Any(x => ReferenceEquals(x, ship)), "the ship is removed from the zone");
|
||||
// A second death signal from the removed entity reaches no subscription.
|
||||
Kill(ship);
|
||||
Assert.Equal(count, lab.Zone.FloatingItems.Count);
|
||||
|
|
|
|||
|
|
@ -651,7 +651,7 @@ public sealed class MineTests : IDisposable
|
|||
lab.Layer.Position = float3(5000, 0, 5000);
|
||||
foreach (var step in Steps(1f, .1f)) lab.Zone.Update(step);
|
||||
lab.Layer.DamageHull(5000000f);
|
||||
Assert.DoesNotContain(lab.Layer, lab.Zone.Entities);
|
||||
Assert.False(lab.Zone.Entities.Any(x => ReferenceEquals(x, lab.Layer)), "the layer is removed from the zone");
|
||||
foreach (var step in Steps(1.5f, .1f)) lab.Zone.Update(step);
|
||||
|
||||
Assert.Equal(0, removed);
|
||||
|
|
|
|||
|
|
@ -290,7 +290,7 @@ public sealed partial class MiningCut3Tests
|
|||
Assert.Empty(candidates);
|
||||
s.Zone.Targets.Within(new TargetSearch(here, 500f), candidates);
|
||||
Assert.Empty(candidates);
|
||||
Assert.DoesNotContain(unseen, observer.VisibleEntities);
|
||||
Assert.False(observer.VisibleEntities.Any(x => ReferenceEquals(x, unseen)), "the unseen entity is not visible");
|
||||
}
|
||||
|
||||
private readonly struct NearestChunk : ITargetKey
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ public sealed partial class RunStartTests
|
|||
|
||||
// Death removes the boss from the zone (the one death path), and that alone opens the gate.
|
||||
boss.HeatstrokeDeath.OnNext(Unit.Default);
|
||||
Assert.DoesNotContain(boss, exit.Entities);
|
||||
Assert.False(exit.Entities.Any(x => ReferenceEquals(x, boss)), "the boss is removed from the exit");
|
||||
Assert.False(RunGoal.BossAlive(exit));
|
||||
Assert.True(RunGoal.ExitOpen(exit));
|
||||
|
||||
|
|
|
|||
|
|
@ -201,7 +201,7 @@ public sealed partial class FireControlCut124Tests
|
|||
var other = FireControl.Solution(e.Weapon, e.Shooter, decoy).Direction;
|
||||
Assert.True(FireControl.Bears(e.Weapon, e.Shooter, decoy, out _));
|
||||
Assert.True(length(designated - other) > .3f, "fixture: the two intercepts must differ");
|
||||
Assert.Same(e.Target, e.Shooter.Target.Value.Entity);
|
||||
Assert.True(ReferenceEquals(e.Target, e.Shooter.Target.Value.Entity), "the designated target is kept");
|
||||
|
||||
Near(other, FireControl.Solve(e.Weapon, e.WeaponItem, e.Shooter, decoy).TravelDirection);
|
||||
Near(designated, FireControl.Solve(e.Weapon, e.WeaponItem, e.Shooter, e.Target).TravelDirection);
|
||||
|
|
|
|||
Loading…
Reference in a new issue