diff --git a/Assets/Scripts/ServerShared/ZoneData.cs b/Assets/Scripts/ServerShared/ZoneData.cs index 67419e00a..a08895aaa 100644 --- a/Assets/Scripts/ServerShared/ZoneData.cs +++ b/Assets/Scripts/ServerShared/ZoneData.cs @@ -299,17 +299,22 @@ public static class Provinces } // The crossing links to prune so each boss zone keeps one exit: all of them but the link to the boss zone's successor on - // exitPath (the zones from the entrance to the exit), which is the link the run goes through. A boss zone that is not on - // exitPath, or whose successor is not one of its crossing links, keeps all of its links: it is never left with none. - public static (int Boss, int Far)[] ExtraExits(int[][] adjacency, IReadOnlyCollection bossZones, int exit, int[] exitPath) => - CrossingLinks(adjacency, bossZones, exit).GroupBy(link => link.Boss) - .SelectMany(links => - { - var at = Array.IndexOf(exitPath, links.Key); - var kept = at >= 0 && at + 1 < exitPath.Length ? exitPath[at + 1] : -1; - return links.Any(link => link.Far == kept) ? links.Where(link => link.Far != kept) : Enumerable.Empty<(int Boss, int Far)>(); - }) - .ToArray(); + // exitPath (the zones from the entrance to the exit), which is the link the run goes through. The generator's exit path + // passes through every boss zone by one of its crossing links; a boss zone off exitPath, last on it, or followed by a zone + // it has no crossing link to breaks that, and throws naming the boss zone rather than guessing which link to keep. + public static (int Boss, int Far)[] ExtraExits(int[][] adjacency, IReadOnlyCollection bossZones, int exit, int[] exitPath) + { + var extra = new List<(int Boss, int Far)>(); + foreach (var links in CrossingLinks(adjacency, bossZones, exit).GroupBy(link => link.Boss)) + { + var at = Array.IndexOf(exitPath, links.Key); + var kept = at >= 0 && at + 1 < exitPath.Length ? exitPath[at + 1] : -1; + if (!links.Any(link => link.Far == kept)) + throw new InvalidOperationException($"boss zone {links.Key} has no crossing link to its successor on the exit path, so no exit to keep"); + extra.AddRange(links.Where(link => link.Far != kept)); + } + return extra.ToArray(); + } // Each zone's region, named by the region's least zone index. Cutting every boss zone's crossing links (CrossingLinks) // leaves the regions as the connected components. Every zone, boss zones included, is in exactly one. diff --git a/tests/Aetheria.Shared.Tests/OreGeographyTests.cs b/tests/Aetheria.Shared.Tests/OreGeographyTests.cs index 5e950d34d..bc497e88e 100644 --- a/tests/Aetheria.Shared.Tests/OreGeographyTests.cs +++ b/tests/Aetheria.Shared.Tests/OreGeographyTests.cs @@ -543,10 +543,9 @@ public sealed class OreGeographyTests : IDisposable Assert.Empty(Provinces.ExtraExits(pruned, new[] {4, 1}, 7, path)); Assert.Equal(Provinces.Regions(chain, new[] {4, 1}, 7), Provinces.Regions(pruned, new[] {4, 1}, 7)); - // A boss zone off the path, last on it, or followed by a zone it has no crossing link to is never left with fewer links. - Assert.Empty(Provinces.ExtraExits(diamond, new[] {2}, 6, new[] {0, 1, 5, 6})); - Assert.Empty(Provinces.ExtraExits(diamond, new[] {2}, 6, new[] {0, 1, 2})); - Assert.Empty(Provinces.ExtraExits(diamond, new[] {2}, 6, new[] {0, 1, 2, 5, 6})); + // A boss zone off the path, last on it, or followed by a zone it has no crossing link to has no exit to keep: it throws, naming the boss zone. + foreach (var broken in new[] {new[] {0, 1, 5, 6}, new[] {0, 1, 2}, new[] {0, 1, 2, 5, 6}}) + Assert.Contains("boss zone 2", Assert.Throws(() => Provinces.ExtraExits(diamond, new[] {2}, 6, broken)).Message); } [Fact]