using XFEExtension.NetCore.XUnit.Attributes; using XFEEDToolBox.Core.Data; using XFEEDToolBox.Core.Models; using XFEEDToolBox.Core.Planning; namespace XFEEDToolBox.Test; [TestFixture] public class CargoTests { [Test] public void MatchesIndependentExhaustiveSearch() { var random = new Random(71903); for (var trial = 0; trial < 160; trial++) { var goods = Enumerable.Range(0, 4).Select(i => { var buy = random.Next(1, 40); return new CargoCandidate("g" + i, "g" + i, buy, buy + random.Next(1, 25), random.Next(1, 6)); }).ToArray(); var capacity = random.Next(1, 10); var cash = random.Next(1, 180); decimal optimum = 0; for (var a = 0; a <= goods[0].Limit; a++) for (var b = 0; b <= goods[1].Limit; b++) for (var c = 0; c <= goods[2].Limit; c++) for (var d = 0; d <= goods[3].Limit; d++) { var quantities = new[] { a, b, c, d }; if (quantities.Sum() <= capacity && quantities.Select((q, i) => (decimal)q * goods[i].BuyPrice).Sum() <= cash) optimum = Math.Max(optimum, quantities.Select((q, i) => (decimal)q * goods[i].ProfitPerTon).Sum()); } var result = CargoOptimizer.Optimize(goods, capacity, cash, 20000); Check.Equal(optimum, result.Profit); Check.That(result.ProvenOptimal, "Small problem should finish within budget"); Check.That(result.Orders.Sum(o => o.Cost) <= cash && result.Orders.Sum(o => o.Quantity) <= capacity); } } [Test] public void MixesCheapAndExpensiveCargoUnderCashLimit() { var result = CargoOptimizer.Optimize([new("a", "A", 90, 190, 10), new("b", "B", 10, 80, 10)], 10, 180); Check.Equal(730m, result.Profit); Check.Equal(2, result.Orders.Count); } [Test] public void RespectsSupplyAndDemandLimit() => Check.Equal(2, CargoOptimizer.Optimize([new("a", "A", 1, 100, 2)], 100, 100).Orders.Single().Quantity); [Test] public void UsesDecimalForLargeTransactionValues() => Check.Equal(1000000000000m, CargoOptimizer.Optimize([new("a", "A", 10000000000, 20000000000, 100)], 100, 1000000000000m).Profit); [Test] public void MarksBudgetLimitedResult() => Check.That(!CargoOptimizer.Optimize([new("a", "A", 9, 15, 20), new("b", "B", 2, 5, 20)], 20, 90, 1).ProvenOptimal); [Test] public void EmptyBudgetHasNoCargo() => Check.Equal(0, CargoOptimizer.Optimize([new("a", "A", 1, 2, 100)], 100, 0).Orders.Count); [Test] public void CancellationIsObserved() { using var cancel = new CancellationTokenSource(); cancel.Cancel(); Check.Throws(() => CargoOptimizer.Optimize([new("a", "A", 1, 2, 10)], 10, 10, token: cancel.Token)); } } [TestFixture] public class PlannerTests { private static RoutePlan Run(MarketBook? book = null, PlannerRequest? request = null) => new TradeRoutePlanner().Calculate(book ?? Check.TwoStations, request ?? Check.Request); [Test] public void RollsProfitIntoNextStationCash() { var result = Run(request: Check.Request with { Loop = true }); Check.Equal(2, result.Legs.Count); Check.That(result.IsLoop); Check.Equal(1600m, result.NetProfit); Check.Equal(11600m, result.Legs[^1].CashAfter); } [Test] public void ReserveIsNeverSpent() { var result = Run(request: Check.Request with { Credits = 1500, ReserveCredits = 1000 }); Check.That(result.Legs.Count > 0); Check.That(result.Legs[0].Orders.Sum(o => o.Cost) <= 500); } [Test] public void StaleQuotesDoNotBecomeProfitableRoutes() { var b = Check.TwoStations; b = b with { Quotes = b.Quotes.Select(q => q with { CollectedAt = DateTimeOffset.UtcNow.AddDays(-2) }).ToArray() }; Check.Equal(0, Run(b).Legs.Count); } [Test] public void FutureQuotesAreRejected() { var b = Check.TwoStations with { Quotes = Check.TwoStations.Quotes.Select(q => q with { CollectedAt = DateTimeOffset.UtcNow.AddHours(2) }).ToArray() }; Check.Equal(0, Run(b).Legs.Count); } [Test] public void IllegalGoodsAreExcluded() { var b = Check.TwoStations with { Quotes = Check.TwoStations.Quotes.Select(q => q with { Illegal = true }).ToArray() }; Check.Equal(0, Run(b).Legs.Count); } [Test] public void LargeShipsCannotUseMediumPads() { var b = Check.TwoStations with { Stations = [Check.Station(1), Check.Station(2, 10) with { MaxLandingPad = LandingPad.Medium }] }; Check.Equal(0, Run(b).Legs.Count); } [Test] public void ConservativeRiskExcludesAnarchy() { var b = Check.TwoStations with { Stations = [Check.Station(1), Check.Station(2, 10) with { Security = SecurityLevel.Anarchy }] }; Check.Equal(0, Run(b, Check.Request with { Risk = RiskTolerance.Conservative }).Legs.Count); } [Test] public void UnknownMetadataIsExcludedByDefault() { var b = Check.TwoStations with { Stations = [Check.Station(1), Check.Station(2, 10) with { DistanceToArrivalLs = null }] }; Check.Equal(0, Run(b).Legs.Count); } [Test] public void ExcludesPermitRestrictedStations() { var b = Check.TwoStations with { Stations = [Check.Station(1), Check.Station(2, 10) with { RequiresPermit = true }] }; Check.Equal(0, Run(b).Legs.Count); } [Test] public void CountsSupplyAcrossRepeatedLoopVisits() { var b = Check.TwoStations with { Quotes = Check.TwoStations.Quotes.Select(q => q with { Supply = 2, Demand = 3 }).ToArray() }; var result = Run(b, Check.Request with { Loop = true, MaxLegs = 6, Objective = RouteObjective.TotalProfit }); foreach (var group in result.Legs.SelectMany(l => l.Orders.Select(o => (l.From.MarketId, o))).GroupBy(x => (x.MarketId, x.o.Commodity))) Check.That(group.Sum(x => x.o.Quantity) <= 2); } [Test] public void DeductsOperatingCosts() { var result = Run(request: Check.Request with { MaxLegs = 1, CostPerJump = 100 }); Check.Equal(700m, result.NetProfit); } [Test] public void RejectsZeroJumpRange() => Check.Throws(() => Run(request: Check.Request with { LadenJumpRangeLy = 0 })); [Test] public void RejectsAllCashReserved() => Check.Throws(() => Run(request: Check.Request with { ReserveCredits = 10000 })); [Test] public void DemoLoopContainsMixedCargo() { var p = DemoData.Player; var result = Run(DemoData.CreateBook(), Check.Request with { StartMarketId = p.MarketId!.Value, Credits = p.Credits!.Value, ReserveCredits = p.Rebuy, CargoCapacity = p.FreeCargo, Loop = true, MaxLegs = 4 }); Check.That(result.NetProfit > 0 && result.IsLoop); Check.That(result.Legs.Any(l => l.Orders.Count > 1)); } [Test] public void ReportsInputTruncation() => Check.That(Run(Check.TwoStations with { Truncated = true }).Warnings.Any(w => w.Contains("截断"))); }