289 lines
15 KiB
C#
289 lines
15 KiB
C#
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using CultMath;
|
|
using Xunit;
|
|
using static CultMath.math;
|
|
|
|
// An equipped item knows the one hardpoint it occupies (EquippedItem.Hardpoint), decided once by the placement rule at
|
|
// equip. Its origin cell need not lie in that hardpoint: these are Soul's scenarios batch-4 P30 fixtures, where reading
|
|
// the hardpoint from the origin cell found nothing, or found a neighbour.
|
|
public sealed partial class RunStartTests
|
|
{
|
|
// A 2x2 L whose origin cell (0, 0) is empty.
|
|
private static Shape LShape()
|
|
{
|
|
var l = new Shape(2, 2);
|
|
l[int2(1, 0)] = true;
|
|
l[int2(0, 1)] = true;
|
|
l[int2(1, 1)] = true;
|
|
return l;
|
|
}
|
|
|
|
// A 2x1 design whose only cell is (1, 0): its origin lies one cell left of wherever it is placed.
|
|
private static Shape EdgeShape()
|
|
{
|
|
var edge = new Shape(2, 1);
|
|
edge[int2(1, 0)] = true;
|
|
return edge;
|
|
}
|
|
|
|
private static HardpointData Mount(HardpointType type, int x, int y, Shape shape = null, float arc = 0,
|
|
ItemRotation rotation = ItemRotation.None) =>
|
|
new HardpointData { Type = type, Position = int2(x, y), Shape = shape ?? new Shape(), FiringArc = arc, Rotation = rotation };
|
|
|
|
private GearData ScratchGear(string name, HardpointType type, Shape shape)
|
|
{
|
|
var design = ScratchGear(name, type, 1, 1);
|
|
design.Shape = shape;
|
|
return design;
|
|
}
|
|
|
|
private static bool InHardpoint(HardpointData hardpoint, int2 hullCell) => hardpoint.Shape[hullCell - hardpoint.Position];
|
|
|
|
// P30 (a): an L gun in an L hardpoint sits at origin (1, 1), a cell outside the hardpoint, yet knows its hardpoint, and
|
|
// FireControl reads that hardpoint's authored arc rather than the default.
|
|
[Fact]
|
|
public void AnLShapedGunInAnLHardpointKnowsItsHardpoint()
|
|
{
|
|
var hull = ScratchHull("P30 L Mount", 9, Mount(HardpointType.Ballistic, 1, 1, LShape(), arc: 360));
|
|
var gun = ScratchGear("P30 L Gun", HardpointType.Ballistic, LShape());
|
|
var entity = BareHull(hull);
|
|
Assert.True(entity.TryEquip(Instance(gun)));
|
|
var item = entity.Equipment.Single(i => i.Data == gun);
|
|
|
|
Assert.False(InHardpoint(hull.Hardpoints[0], item.Position), $"the fixture's origin {item.Position} lies outside the hardpoint");
|
|
Assert.Same(hull.Hardpoints[0], item.Hardpoint);
|
|
Assert.NotEqual(360f, _items.GameplaySettings.FiringArc);
|
|
Assert.Equal(360f, FireControl.ArcFor(item));
|
|
}
|
|
|
|
// P30 (c) shape: an edge gun whose origin cell lies in a radiator hardpoint reports its ballistic hardpoint.
|
|
[Fact]
|
|
public void AnItemWhoseOriginIsInAnotherHardpointKnowsItsOwn()
|
|
{
|
|
var radiator = Mount(HardpointType.Radiator, 0, 4);
|
|
var ballistic = Mount(HardpointType.Ballistic, 1, 4, arc: 360);
|
|
var hull = ScratchHull("P30 Neighbours", 9, radiator, ballistic);
|
|
var gun = ScratchGear("P30 Edge Gun", HardpointType.Ballistic, EdgeShape());
|
|
var entity = BareHull(hull);
|
|
Assert.True(entity.TryEquip(Instance(gun), int2(0, 4)));
|
|
var item = entity.Equipment.Single(i => i.Data == gun);
|
|
|
|
Assert.True(InHardpoint(radiator, item.Position), "the fixture's origin lies in the radiator hardpoint");
|
|
Assert.Same(ballistic, item.Hardpoint);
|
|
Assert.Equal(360f, FireControl.ArcFor(item));
|
|
}
|
|
|
|
// P30 (b): for a design lacking its origin cell, TryFindSpace finds a placement wherever ItemFits accepts one, and the
|
|
// origin it returns passes ItemFits and equips into that hardpoint. Unrotated, and turned so the item's extent swaps
|
|
// axes (a counter-clockwise edge gun lies one cell below its origin).
|
|
[Fact]
|
|
public void TryFindSpaceFindsEveryPlacementTheFitRuleAccepts()
|
|
{
|
|
var gun = ScratchGear("P30 Edge Search Gun", HardpointType.Ballistic, EdgeShape());
|
|
var cases = new[]
|
|
{
|
|
(hull: ScratchHull("P30 Edge Mount", 9, Mount(HardpointType.Ballistic, 0, 4)), fits: int2(-1, 4)),
|
|
(hull: ScratchHull("P30 Turned Mount", 9, Mount(HardpointType.Ballistic, 4, 0, rotation: ItemRotation.CounterClockwise)), fits: int2(4, -1)),
|
|
};
|
|
foreach (var (hull, fits) in cases)
|
|
{
|
|
var entity = BareHull(hull);
|
|
var unit = Instance(gun);
|
|
Assert.True(entity.ItemFits(unit, fits), $"{hull.Name}: the fit rule accepts {fits}");
|
|
Assert.True(entity.TryFindSpace(unit, out var found), $"{hull.Name}: TryFindSpace finds what ItemFits accepts");
|
|
Assert.True(entity.ItemFits(unit, found), $"{hull.Name}: the found origin {found} passes ItemFits");
|
|
Assert.True(entity.TryEquip(unit, found));
|
|
Assert.Same(hull.Hardpoints[0], entity.Equipment.Single(i => i.Data == gun).Hardpoint);
|
|
}
|
|
}
|
|
|
|
// A saved entity reloads each item into the same hardpoint, the L gun and the edge gun at a negative origin alike.
|
|
[Fact]
|
|
public void AnEquippedItemReloadsWithTheSameHardpoint()
|
|
{
|
|
var hull = ScratchHull("P30 Reload Mounts", 9, Mount(HardpointType.Ballistic, 1, 1, LShape()), Mount(HardpointType.Ballistic, 0, 6));
|
|
var lGun = ScratchGear("P30 Reload L Gun", HardpointType.Ballistic, LShape());
|
|
var edgeGun = ScratchGear("P30 Reload Edge Gun", HardpointType.Ballistic, EdgeShape());
|
|
var entity = BareHull(hull);
|
|
Assert.True(entity.TryEquip(Instance(lGun), int2(1, 1)));
|
|
Assert.True(entity.TryEquip(Instance(edgeGun), int2(-1, 6)));
|
|
|
|
var reloaded = EntitySerializer.Unpack(_items, null, EntitySerializer.Pack(entity));
|
|
foreach (var design in new[] { lGun, edgeGun })
|
|
{
|
|
var before = hull.Hardpoints.IndexOf(entity.Equipment.Single(i => i.Data == design).Hardpoint);
|
|
var after = reloaded.HullData.Hardpoints.IndexOf(reloaded.Equipment.Single(i => i.Data == design).Hardpoint);
|
|
Assert.True(before >= 0, $"{design.Name} holds a hardpoint");
|
|
Assert.Equal(before, after);
|
|
}
|
|
}
|
|
|
|
// Tool gear occupies no hardpoint, even in a hardpoint's leftover cells; the hardpoint item beside it keeps its own.
|
|
[Fact]
|
|
public void ToolGearHasNoHardpoint()
|
|
{
|
|
var shape = new Shape(2, 1);
|
|
foreach (var cell in shape.AllCoordinates) shape[cell] = true;
|
|
var hull = ScratchHull("P30 Shared Mount", 9, Mount(HardpointType.Ballistic, 1, 1, shape));
|
|
var gun = ScratchGear("P30 Small Gun", HardpointType.Ballistic, 1, 1);
|
|
var tool = ScratchGear("P30 Tool", HardpointType.Tool, 1, 1);
|
|
var entity = BareHull(hull);
|
|
Assert.True(entity.TryEquip(Instance(gun), int2(1, 1)));
|
|
Assert.True(entity.TryEquip(Instance(tool), int2(2, 1)), "tool gear in the hardpoint's leftover cell");
|
|
|
|
Assert.Null(entity.Equipment.Single(i => i.Data == tool).Hardpoint);
|
|
Assert.Same(hull.Hardpoints[0], entity.Equipment.Single(i => i.Data == gun).Hardpoint);
|
|
Assert.Null(entity.EquippedHull.Hardpoint);
|
|
}
|
|
|
|
// The design's cells, turned to a rotation, moved so the lowest x and y are zero. Written here from the cells alone
|
|
// (the game's turn directions, up to translation), so it is not Shape.Rotate checking itself.
|
|
private static HashSet<(int x, int y)> Normalized(IEnumerable<int2> cells, ItemRotation rotation)
|
|
{
|
|
var turned = cells.Select(c => rotation switch
|
|
{
|
|
ItemRotation.Clockwise => (x: c.y, y: -c.x),
|
|
ItemRotation.Reversed => (x: -c.x, y: -c.y),
|
|
ItemRotation.CounterClockwise => (x: -c.y, y: c.x),
|
|
_ => (x: c.x, y: c.y)
|
|
}).ToList();
|
|
if (turned.Count == 0) return new HashSet<(int, int)>();
|
|
var minX = turned.Min(c => c.x);
|
|
var minY = turned.Min(c => c.y);
|
|
return turned.Select(c => (c.x - minX, c.y - minY)).ToHashSet();
|
|
}
|
|
|
|
// IsFilledBy against an independent oracle: a design fills a hardpoint when it is of the hardpoint's type and its
|
|
// cells, turned to the hardpoint's rotation, are exactly the hardpoint's cells. Every catalog design against every
|
|
// catalog hardpoint of its type, plus the L fixtures, whose rotations a rectangle-only catalog would not tell apart.
|
|
[Fact]
|
|
public void IsFilledByAgreesWithACellOracle()
|
|
{
|
|
var l = LShape();
|
|
var hardpoints = _cache.GetAll<HullData>().SelectMany(hull => hull.Hardpoints).ToList();
|
|
hardpoints.AddRange(Enum.GetValues(typeof(ItemRotation)).Cast<ItemRotation>()
|
|
.Select(rotation => Mount(HardpointType.Ballistic, 0, 0, l, rotation: rotation)));
|
|
var designs = _cache.GetAll<EquippableItemData>().ToList();
|
|
designs.Add(new GearData { Name = "Oracle L", Hardpoint = HardpointType.Ballistic, Shape = l });
|
|
designs.Add(new GearData { Name = "Oracle Edge", Hardpoint = HardpointType.Ballistic, Shape = EdgeShape() });
|
|
|
|
int compared = 0, filled = 0;
|
|
var mismatches = new List<string>();
|
|
foreach (var hardpoint in hardpoints)
|
|
foreach (var design in designs.Where(d => d.HardpointType == hardpoint.Type))
|
|
{
|
|
var oracle = Normalized(design.Shape.Coordinates, hardpoint.Rotation).SetEquals(Normalized(hardpoint.Shape.Coordinates, ItemRotation.None));
|
|
compared++;
|
|
if (oracle) filled++;
|
|
if (hardpoint.IsFilledBy(design) != oracle && mismatches.Count < 10)
|
|
mismatches.Add($"{design.Name} in a {hardpoint.Shape.Width}x{hardpoint.Shape.Height} {hardpoint.Type} hardpoint turned {hardpoint.Rotation}: oracle {oracle}");
|
|
}
|
|
Assert.True(mismatches.Count == 0, string.Join("; ", mismatches));
|
|
Assert.True(filled > 0 && filled < compared, $"the oracle separates: {filled} of {compared} fill");
|
|
}
|
|
|
|
// Placement searches the hardpoint's last row and column: in a 3x3 hardpoint whose other cells hold tool gear, a
|
|
// one-cell gun is placed in the bottom-right cell, and once it is, the search finds no space.
|
|
[Fact]
|
|
public void PlacementSearchesTheLastRow()
|
|
{
|
|
var mount = new Shape(3, 3);
|
|
foreach (var cell in mount.AllCoordinates) mount[cell] = true;
|
|
var hull = ScratchHull("P30 Crowded Mount", 9, Mount(HardpointType.Ballistic, 1, 1, mount));
|
|
var gun = ScratchGear("P30 Corner Gun", HardpointType.Ballistic, 1, 1);
|
|
var tool = ScratchGear("P30 Filler", HardpointType.Tool, 1, 1);
|
|
var entity = BareHull(hull);
|
|
var last = int2(3, 3);
|
|
foreach (var cell in mount.Coordinates.Select(c => int2(1, 1) + c).Where(c => !c.Equals(last)))
|
|
Assert.True(entity.TryEquip(Instance(tool), cell), $"filler at {cell}");
|
|
|
|
var corner = Instance(gun);
|
|
Assert.True(entity.TryFindSpace(corner, out var found));
|
|
Assert.Equal(last, found);
|
|
Assert.True(entity.TryEquip(corner, found));
|
|
Assert.False(entity.TryFindSpace(Instance(gun), out _), "a full hardpoint offers no space");
|
|
}
|
|
|
|
// Billing does not depend on equip order: two heaters with different targets and draws, equipped in either order,
|
|
// bill the bus the same at every step of a 60 s temperature ramp that crosses both targets.
|
|
[Fact]
|
|
public void ThermostatOrderDoesNotChangeBilling()
|
|
{
|
|
GearData Heater(string name, float target, float draw)
|
|
{
|
|
var design = ScratchGear(name, HardpointType.Tool, 1, 1);
|
|
design.MinimumTemperature = 100;
|
|
design.MaximumTemperature = 500;
|
|
design.OptimalTemperature = 280;
|
|
design.PlateauWidth = 200; // online at every temperature of the ramp
|
|
design.Behaviors.Add(new ThermotoggleData { Group = 0, TargetTemperature = target });
|
|
design.Behaviors.Add(new EnergyDrawData { Group = 0, EnergyDraw = new PerformanceStat { Min = draw, Max = draw }, PerSecond = true });
|
|
return design;
|
|
}
|
|
var low = Heater("P30 Low Heater", 260, 10);
|
|
var high = Heater("P30 High Heater", 320, 7);
|
|
var hull = Hull("Djinni");
|
|
var zone = Arena(false);
|
|
|
|
List<float> Bills(params GearData[] order)
|
|
{
|
|
var ship = BareHull(hull);
|
|
ship.Zone = zone;
|
|
foreach (var design in order) Assert.True(ship.TryEquip(Instance(design)), $"{design.Name} equips");
|
|
zone.Admit(ship, piloted: false);
|
|
for (var tick = 0; tick < 5; tick++) zone.Update(.1f);
|
|
var bills = new List<float>();
|
|
for (var tick = 0; tick < 600; tick++)
|
|
{
|
|
var temperature = 220 + 140 * tick / 599f;
|
|
foreach (var v in ship.HullData.Shape.Coordinates) ship.Temperature[v.x, v.y] = temperature;
|
|
ship.PowerBus.Step(.1f);
|
|
bills.Add(ship.PowerBus.TotalDemand);
|
|
}
|
|
return bills;
|
|
}
|
|
var lowFirst = Bills(low, high);
|
|
var highFirst = Bills(high, low);
|
|
|
|
Assert.Equal(lowFirst, highFirst);
|
|
Assert.True(lowFirst.Distinct().Count() >= 3, $"the ramp crosses both thermostats: bills {string.Join(", ", lowFirst.Distinct())}");
|
|
}
|
|
|
|
// The readers that bind muzzles, arcs, thruster emitters and radiators dereference item.Hardpoint: on generated
|
|
// ships, stations and turrets every weapon, thruster and radiator holds a hardpoint containing all its cells, and
|
|
// Tool gear and the hull hold none (Soul's scenarios-adopt probe S4, finding headless-reader-input).
|
|
[Fact]
|
|
public void GeneratedLoadoutsGiveEveryReaderItsHardpoint()
|
|
{
|
|
var shipHulls = LoadoutGenerator.SoldHulls(_cache).Where(h => h.HullType == HullType.Ship).Select(h => h.Name).ToList();
|
|
var generator = new LoadoutGenerator(ref _items.Random, _items, _galaxy, _galaxy.Entrance, _protagonist, .5f);
|
|
var packs = shipHulls.SelectMany(name => Enumerable.Range(0, 2).Select(_ => generator.GenerateShipLoadout(h => h.Name == name)))
|
|
.Concat(Enumerable.Range(0, 4).Select(_ => generator.GenerateStationLoadout(TestStationKinds.TradeHub)))
|
|
.Concat(Enumerable.Range(0, 4).Select(_ => generator.GenerateTurretLoadout()))
|
|
.ToList();
|
|
var readers = 0;
|
|
foreach (var pack in packs)
|
|
{
|
|
var entity = EntitySerializer.Unpack(_items, null, pack);
|
|
Assert.Null(entity.EquippedHull.Hardpoint);
|
|
foreach (var item in entity.Equipment.Where(i => i != entity.EquippedHull))
|
|
{
|
|
var label = $"{entity.HullData.Name}/{item.Data.Name}";
|
|
if (item.Data.HardpointType == HardpointType.Tool) Assert.True(item.Hardpoint == null, $"{label}: Tool gear holds no hardpoint");
|
|
if (!item.Behaviors.Any(b => b is Weapon || b is Thruster || b is Radiator)) continue;
|
|
readers++;
|
|
var hp = item.Hardpoint;
|
|
Assert.True(hp != null, $"{label}: a reader item holds a hardpoint");
|
|
var cells = item.Data.Shape.Coordinates.Select(c => item.Position + item.Data.Shape.Rotate(c, item.EquippableItem.Rotation));
|
|
Assert.True(cells.All(c => hp.Shape[c - hp.Position]), $"{label}: its hardpoint contains every cell");
|
|
}
|
|
}
|
|
Assert.True(readers > 0, "the generated loadouts carry weapons, thrusters or radiators");
|
|
}
|
|
}
|