XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
using XFEExtension.NetCore.XUnit.Attributes;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Planning;

namespace XFEEDToolBox.Test;

[TestFixture]
public sealed class PlannerCandidateTests
{
    private static MarketBook Alternatives()
        => new([Check.Station(1) with { DistanceToArrivalLs = 0 }, Check.Station(2, 10) with { DistanceToArrivalLs = 0 }, Check.Station(3, 50) with { DistanceToArrivalLs = 10000 }],
            [Check.Quote(1, "gold", 100, 90), Check.Quote(2, "gold", 0, 180), Check.Quote(3, "gold", 0, 200)]);

    private static PlannerRequest Request => Check.Request with { Loop = false, MaxLegs = 1, Objective = RouteObjective.TotalProfit };
    private static string PathOf(RoutePlan plan) => string.Join("/", plan.Legs.Select(leg => leg.From.MarketId)) + "/" + plan.Legs[^1].To.MarketId;

    [Test] public void ShorterDeliveryOutranksLargerAbsoluteProfitRegardlessOfLegacyObjective()
    {
        var planner = new TradeRoutePlanner();
        var plans = planner.CalculateCandidates(Alternatives(), Request);
        Check.Equal(2, plans.Count);
        Check.Equal(2L, plans[0].Legs.Single().To.MarketId);
        Check.That(plans[0].NetProfit < plans[1].NetProfit, "Fixture must have lower absolute profit on the faster route.");
        Check.That(plans[0].CreditsPerHour > plans[1].CreditsPerHour);
        Check.Equal(RouteObjective.CreditsPerHour, plans[0].Request.Objective);
        Check.Equal(3L, planner.Calculate(Alternatives(), Request).Legs.Single().To.MarketId);
    }

    [Test] public void RiskIsAFilterAndVisibleTierNotAnInventedExpectedLoss()
    {
        var book = new MarketBook([Check.Station(1), Check.Station(2, 10) with { Security = SecurityLevel.Low }, Check.Station(3, 10)],
            [Check.Quote(1, "gold", 100, 90), Check.Quote(2, "gold", 0, 200), Check.Quote(3, "gold", 0, 195)]);
        var plans = new TradeRoutePlanner().CalculateCandidates(book, Request);
        Check.Equal(2L, plans[0].Legs.Single().To.MarketId);
        Check.That(plans[0].RiskTier > plans[1].RiskTier);
        Check.That(!plans[0].Legs.Single().Metrics.Contains('%') && !plans[0].RiskDisplay.Contains('%'));
        Check.That(plans[0].RiskMethod.Contains("不是概率"));
    }

    [Test] public void OccupiedCargoReducesEveryLegWithoutInventingSaleRevenue()
    {
        var plans = new TradeRoutePlanner().CalculateCandidates(Check.TwoStations, Request with { Loop = true, MaxLegs = 4, CargoCapacity = 10, OccupiedCargo = 7 });
        var plan = plans[0];
        Check.Equal(3, plan.Request.UsableCargoCapacity);
        Check.That(plan.Legs.All(leg => leg.Quantity == 3));
        Check.Equal(plan.Request.Credits + plan.NetProfit, plan.Legs[^1].CashAfter);
        Check.Equal(480m, plan.NetProfit);
    }

    [Test] public void TravelToOriginConsumesTimeFuelBudgetAndWorkingCapital()
    {
        var request = Request with { Credits = 1500, ReserveCredits = 500, CostPerJump = 100, CurrentPosition = new(-40, 0, 0), CurrentSystemName = "Remote", CurrentMarketId = 9 };
        var book = Check.TwoStations;
        var plan = new TradeRoutePlanner().CalculateCandidates(book, request).Single(plan=>plan.Request.StartMarketId==request.StartMarketId);
        Check.Equal(40d, plan.DeadheadDistanceLy);
        Check.Equal(2, plan.DeadheadJumps);
        Check.Equal(200m, plan.DeadheadOperatingCost);
        Check.That(plan.DeadheadMinutes > 0);
        Check.Equal(7, plan.Legs.Single().Quantity);
        Check.Equal(plan.Legs.Sum(leg => leg.NetProfit) - 200, plan.NetProfit);
        Check.Equal(plan.Legs.Sum(leg => leg.EstimatedMinutes) + plan.DeadheadMinutes, plan.Minutes);
        Check.Equal(request.Credits + plan.NetProfit, plan.Legs[^1].CashAfter);
        var alreadyAtOrigin = new TradeRoutePlanner().CalculateCandidates(book, request with { CurrentMarketId = request.StartMarketId }).Single(plan=>plan.Request.StartMarketId==request.StartMarketId);
        Check.Equal(0d, alreadyAtOrigin.DeadheadMinutes);
        Check.Equal(9, alreadyAtOrigin.Legs.Single().Quantity);
        Check.That(alreadyAtOrigin.CreditsPerHour > plan.CreditsPerHour);
    }

    [Test] public void JournalMaximumJumpRangeUsesExplicitConservativeLadenEstimate()
    {
        var book = Check.TwoStations with { Stations = [Check.Station(1), Check.Station(2, 22)] };
        var planner = new TradeRoutePlanner();
        var verified = planner.CalculateCandidates(book, Request with { LadenJumpRangeLy = 25 }).Single(plan=>plan.Request.StartMarketId==Request.StartMarketId);
        var estimate = planner.CalculateCandidates(book, Request with { LadenJumpRangeLy = 25, JumpRangeIsEstimate = true }).Single(plan=>plan.Request.StartMarketId==Request.StartMarketId);
        Check.Equal(1, verified.Legs.Single().EstimatedJumps);
        Check.Equal(2, estimate.Legs.Single().EstimatedJumps);
        Check.Equal(20d, estimate.Request.EffectiveLadenJumpRangeLy);
        Check.That(estimate.Minutes > verified.Minutes && estimate.CreditsPerHour < verified.CreditsPerHour);
        Check.That(estimate.Warnings.Any(warning => warning.Contains("不是经核实的满载跳距")));
    }

    [Test] public void RepeatedLoopCountsDoNotCrowdAlternativeCandidates()
    {
        var plans = new TradeRoutePlanner().CalculateCandidates(Check.TwoStations, Request with { Loop = true, MaxLegs = 8 });
        Check.That(plans.Count > 0);
        Check.That(plans.All(plan => plan.Legs.Count == 2 && plan.IsLoop));
        Check.Equal(plans.Count, plans.Select(plan => string.Join("/", plan.Legs.SelectMany(leg => leg.Orders).Select(order => order.Commodity + ":" + order.Quantity))).Distinct().Count());
    }

    [Test] public void CandidateOrderingIsIndependentOfInputEnumerationOrder()
    {
        var book = Alternatives();
        var planner = new TradeRoutePlanner();
        var first = planner.CalculateCandidates(book, Request).Select(PathOf).ToArray();
        var reversed = planner.CalculateCandidates(book with { Stations = book.Stations.Reverse().ToArray(), Quotes = book.Quotes.Reverse().ToArray() }, Request).Select(PathOf).ToArray();
        Check.That(first.SequenceEqual(reversed));
    }

    [Test] public void TruncationAndUnverifiedAssumptionsRemainVisible()
    {
        var plan = new TradeRoutePlanner().CalculateCandidates(Check.TwoStations with { Truncated = true }, Request).First();
        Check.That(plan.IsEstimate && plan.SearchBounded && plan.SearchPruned);
        Check.That(plan.Warnings.Any(warning => warning.Contains("不保证")));
        Check.That(plan.Warnings.Any(warning => warning.Contains("截断")));
        Check.That(plan.CargoOptimal);
    }

    [Test] public void EmptyAndInvalidInputsDoNotProduceInventedCandidates()
    {
        var planner = new TradeRoutePlanner();
        var stale = Check.TwoStations with { Quotes = Check.TwoStations.Quotes.Select(quote => quote with { CollectedAt = Request.AsOf.AddDays(-2) }).ToArray() };
        Check.Equal(0, planner.CalculateCandidates(stale, Request).Count);
        Check.Throws<ArgumentException>(() => planner.CalculateCandidates(Check.TwoStations, Request with { OccupiedCargo = 10 }));
        Check.Throws<ArgumentException>(() => planner.CalculateCandidates(Check.TwoStations, Request with { EstimatedJumpRangeFactor = 0 }));
        Check.Throws<ArgumentOutOfRangeException>(() => planner.CalculateCandidates(Check.TwoStations, Request, 0));
    }

    [Test] public void CandidateSearchObservesCancellation()
    {
        using var cancelled = new CancellationTokenSource();
        cancelled.Cancel();
        Check.Throws<OperationCanceledException>(() => new TradeRoutePlanner().CalculateCandidates(Check.TwoStations, Request, token: cancelled.Token));
    }

    [Test] public void ExplicitInvestmentCapsEveryPurchaseWhileAllFundsCanReinvestProfit()
    {
        var request = Request with { Credits = 2000, ReserveCredits = 1500, CargoCapacity = 10, Loop = true, MaxLegs = 2, InvestmentCredits = 500 };
        var planner = new TradeRoutePlanner();
        var fixedAmount = planner.CalculateCandidates(Check.TwoStations, request)[0];
        Check.That(fixedAmount.Legs.All(leg => leg.Orders.Sum(order => order.Cost) <= 500));
        Check.That(fixedAmount.Legs.All(leg => leg.Quantity == 5));
        var reinvest = planner.CalculateCandidates(Check.TwoStations, request with { InvestmentCredits = null })[0];
        Check.Equal(5, reinvest.Legs[0].Quantity);
        Check.Equal(9, reinvest.Legs[1].Quantity);
        Check.That(reinvest.NetProfit > fixedAmount.NetProfit);
        Check.Throws<ArgumentException>(() => planner.CalculateCandidates(Check.TwoStations, request with { InvestmentCredits = 0 }));
    }

    [Test] public void ThreeProfitableGoodsProduceRealDistinctCargoAlternativesOnOneStationPair()
    {
        var book = new MarketBook([Check.Station(1), Check.Station(2, 10)],
            [Check.Quote(1, "gold", 100, 90), Check.Quote(2, "gold", 0, 200),
             Check.Quote(1, "silver", 100, 90), Check.Quote(2, "silver", 0, 180),
             Check.Quote(1, "uranium", 100, 90), Check.Quote(2, "uranium", 0, 160)]);
        var request = Request with { Loop = true, MaxLegs = 2, OccupiedCargo = 6, InvestmentCredits = 250 };
        var planner = new TradeRoutePlanner();
        var originalBest = planner.CalculateCandidates(book, request, maxCandidates: 1).Single();
        var plans = planner.CalculateCandidates(book, request);
        Check.Equal(3, plans.Count);
        Check.Equal(1, plans.Select(PathOf).Distinct().Count());
        Check.Equal(3, plans.Select(plan => plan.Legs.SelectMany(leg => leg.Orders).Single().Commodity).Distinct().Count());
        Check.Equal(originalBest.CreditsPerHour, plans[0].CreditsPerHour);
        Check.Equal(originalBest.NetProfit, plans[0].NetProfit);
        Check.That(!plans[0].IsCargoAlternative && plans.Skip(1).All(plan => plan.IsCargoAlternative));
        Check.That(plans.Select(plan => plan.CreditsPerHour).SequenceEqual(plans.OrderByDescending(plan => plan.CreditsPerHour).Select(plan => plan.CreditsPerHour)));
        foreach (var plan in plans)
        {
            var order = plan.Legs.SelectMany(leg => leg.Orders).Single();
            Check.Equal(2, order.Quantity); // Real cash cap, not cosmetic one-tonne variants.
            Check.That(order.Cost <= 250 && plan.Legs.All(leg => leg.Quantity <= request.UsableCargoCapacity));
            Check.That(plan.CargoOptimal && plan.IsEstimate && plan.SearchBounded);
            Check.Equal(request.Credits + plan.NetProfit, plan.Legs[^1].CashAfter);
        }
    }

    [Test] public void AOneCommodityBookDoesNotFabricateExtraCargoSchemes()
    {
        var book = new MarketBook([Check.Station(1), Check.Station(2, 10)], [Check.Quote(1, "gold", 100, 90), Check.Quote(2, "gold", 0, 200)]);
        var plans = new TradeRoutePlanner().CalculateCandidates(book, Request with { Loop = true, MaxLegs = 2 });
        Check.Equal(1, plans.Count);
        Check.That(!plans[0].IsCargoAlternative);
    }
}