diff --git a/IntelOrca.Biohazard.BioRand.Tests/TestRouting.cs b/IntelOrca.Biohazard.BioRand.Tests/TestRouting.cs index 24375349..605a474c 100644 --- a/IntelOrca.Biohazard.BioRand.Tests/TestRouting.cs +++ b/IntelOrca.Biohazard.BioRand.Tests/TestRouting.cs @@ -57,6 +57,7 @@ public void Basic() AssertKeyOnce(route, key0, item0a, item0b); AssertKeyOnce(route, key1, item0a, item0b, item1a); + Assert.Equal((RouteSolverResult)0, route.Solve()); } } @@ -319,8 +320,8 @@ public void SingleUseKey_RouteOrderMatters_Flexible() var route = builder.GenerateRoute(i); - AssertKeyOnce(route, key1, item3a, item3b); Assert.True(route.AllNodesVisited); + AssertKeyOnce(route, key1, item3a, item3b); } } diff --git a/IntelOrca.Biohazard.BioRand/Routing/Edge.cs b/IntelOrca.Biohazard.BioRand/Routing/Edge.cs index 220a6158..d08896b3 100644 --- a/IntelOrca.Biohazard.BioRand/Routing/Edge.cs +++ b/IntelOrca.Biohazard.BioRand/Routing/Edge.cs @@ -14,5 +14,7 @@ public Edge(Node node, EdgeFlags flags) Node = node; Flags = flags; } + + public override string ToString() => $"{Node} Flags = {Flags}"; } } diff --git a/IntelOrca.Biohazard.BioRand/Routing/OneToManyDictionary.cs b/IntelOrca.Biohazard.BioRand/Routing/OneToManyDictionary.cs index cc0adcdc..8c8679b4 100644 --- a/IntelOrca.Biohazard.BioRand/Routing/OneToManyDictionary.cs +++ b/IntelOrca.Biohazard.BioRand/Routing/OneToManyDictionary.cs @@ -92,7 +92,7 @@ public ImmutableOneToManyDictionary Add(TOne key, TMany value) } else { - newValueToKeys = _valueToKeys.Add(value, ImmutableHashSet.Empty); + newValueToKeys = _valueToKeys.Add(value, ImmutableHashSet.Create(key)); } return new ImmutableOneToManyDictionary(newKeyToValue, newValueToKeys); } diff --git a/IntelOrca.Biohazard.BioRand/Routing/Route.cs b/IntelOrca.Biohazard.BioRand/Routing/Route.cs index a493b8e5..b85e1641 100644 --- a/IntelOrca.Biohazard.BioRand/Routing/Route.cs +++ b/IntelOrca.Biohazard.BioRand/Routing/Route.cs @@ -101,5 +101,7 @@ void Visit(Node n) } } } + + public RouteSolverResult Solve() => RouteSolver.Default.Solve(this); } } diff --git a/IntelOrca.Biohazard.BioRand/Routing/RouteFinder.cs b/IntelOrca.Biohazard.BioRand/Routing/RouteFinder.cs index 566b8a53..32931d47 100644 --- a/IntelOrca.Biohazard.BioRand/Routing/RouteFinder.cs +++ b/IntelOrca.Biohazard.BioRand/Routing/RouteFinder.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using System.Runtime.InteropServices; namespace IntelOrca.Biohazard.BioRand.Routing { @@ -17,13 +18,15 @@ public RouteFinder(int? seed = null) public Route Find(Graph input) { - var m = input.ToMermaid(); - var state = new State(input); state = DoSubgraph(state, input.Start, first: true, _rng); + return GetRoute(state); + } + private static Route GetRoute(State state) + { return new Route( - input, + state.Input, state.Next.Count == 0, state.ItemToKey, string.Join("\n", state.Log)); @@ -50,35 +53,78 @@ private static State DoSubgraph(State state, IEnumerable start, bool first foreach (var v in toVisit) state = state.VisitNode(v); - state = DoPass(state, rng); + return Fulfill(state, rng); + } + + private static State Fulfill(State state, Random rng) + { + state = Expand(state); + if (!ValidateState(state)) + return state; - var subGraphs = state.OneWay.ToArray(); - foreach (var n in subGraphs) + var checklist = GetChecklist(state); + var requiredKeys = Shuffle(rng, checklist + .SelectMany(x => x.Need) + .Select(x => x.Node) + .Distinct()); + var states = new List(); + foreach (var key in requiredKeys) { - state = DoSubgraph(state, new[] { n }, first: false, rng); + var allEdges = checklist + .Where(x => x.Need.All(x => x.Node == key)) + .SelectMany(x => x.Need) + .ToArray(); + var multipleRequired = allEdges.Any(x => (x.Flags & EdgeFlags.Consume) != 0); + var need = multipleRequired ? allEdges.Length : 1; + var available = Shuffle(rng, state.SpareItems.Where(x => x.Group == key.Group)); + if (need == 1) + { + foreach (var a in available) + { + states.Add(state.PlaceKey(a, key)); + } + } + else if (available.Length >= need) + { + var newState = state; + for (var i = 0; i < need; i++) + { + newState = newState.PlaceKey(available[i], key); + } + states.Add(newState); + } } - return state; - } - - private static State DoPass(State state, Random rng) - { - while (true) + if (states.Count == 0) { - state = Expand(state); - var newState = Fulfill(state, rng); - if (newState == state) - break; - state = newState; + var subGraphs = state.OneWay.ToArray(); + foreach (var n in subGraphs) + { + state = DoSubgraph(state, new[] { n }, first: false, rng); + } + return state; + } + else + { + State? firstState = null; + foreach (var s in states) + { + var finalState = Fulfill(s, rng); + if (finalState.Next.Count == 0 && finalState.OneWay.Count == 0) + { + return finalState; + } + firstState ??= finalState; + } + return firstState!; } - return state; } private static State Expand(State state) { while (true) { - var (newState, satisfied) = TakeNextNodes(state, IsSatisfied); + var (newState, satisfied) = TakeNextNodes(state); if (satisfied.Length == 0) break; @@ -98,29 +144,13 @@ private static State Expand(State state) return state; } - private static State Fulfill(State state, Random rng) - { - var checklist = GetChecklist(state); - var requiredKeys = Shuffle(rng, checklist.SelectMany(x => x.Need).Distinct()); - foreach (var key in requiredKeys) - { - // Find an item for this key - var available = Shuffle(rng, state.SpareItems.Where(x => x.Group == key.Group)); - if (available.Length != 0) - { - return state.PlaceKey(available[0], key); - } - } - return state; - } - - private static (State, Node[]) TakeNextNodes(State state, Func predicate) + private static (State, Node[]) TakeNextNodes(State state) { var result = new List(); while (true) { var next = state.Next.ToArray(); - var index = Array.FindIndex(next, x => predicate(state, x)); + var index = Array.FindIndex(next, x => IsSatisfied(state, x)); if (index == -1) break; @@ -175,19 +205,24 @@ private static ImmutableArray GetChecklist(State state) private static ChecklistItem GetChecklistItem(State state, Node node) { var haveList = new List(); - var missingList = new List(); + var missingList = new List(); var requiredKeys = GetRequiredKeys(state, node) - .GroupBy(x => x) - .Select(x => (x.Key, x.Count())) + .GroupBy(x => x.Node) .ToArray(); - foreach (var (key, need) in requiredKeys) + foreach (var edges in requiredKeys) { + var key = edges.Key; + EdgeFlags flags = 0; + foreach (var edge in edges) + flags |= edge.Flags; + + var need = edges.Count(); var have = state.Keys.GetCount(key); var missing = Math.Max(0, need - have); for (var i = 0; i < missing; i++) - missingList.Add(key); + missingList.Add(new Edge(key, flags)); var progress = Math.Min(have, need); for (var i = 0; i < progress; i++) @@ -197,13 +232,19 @@ private static ChecklistItem GetChecklistItem(State state, Node node) return new ChecklistItem(node, haveList.ToImmutableArray(), missingList.ToImmutableArray()); } + private static bool ValidateState(State state) + { + var flags = RouteSolver.Default.Solve(GetRoute(state)); + return (flags & RouteSolverResult.PotentialSoftlock) == 0; + } + private sealed class ChecklistItem { public Node Destination { get; } public ImmutableArray Have { get; } - public ImmutableArray Need { get; } + public ImmutableArray Need { get; } - public ChecklistItem(Node destination, ImmutableArray have, ImmutableArray need) + public ChecklistItem(Node destination, ImmutableArray have, ImmutableArray need) { Destination = destination; Have = have; @@ -246,9 +287,9 @@ private static bool IsSatisfied(State state, Node node) } } - private static Node[] GetRequiredKeys(State state, Node node) + private static Edge[] GetRequiredKeys(State state, Node node) { - var leaves = new List(); + var leaves = new List(); GetRequiredKeys(node); return leaves.ToArray(); @@ -257,12 +298,12 @@ void GetRequiredKeys(Node c) if (state.Visited.Contains(c)) return; - if (c.Kind == NodeKind.Key) - leaves.Add(c); - - foreach (var r in c.Requires.Select(x => x.Node)) + foreach (var r in c.Requires) { - GetRequiredKeys(r); + if (r.Node.Kind == NodeKind.Key) + { + leaves.Add(r); + } } } } diff --git a/IntelOrca.Biohazard.BioRand/Routing/RouteSolver.cs b/IntelOrca.Biohazard.BioRand/Routing/RouteSolver.cs new file mode 100644 index 00000000..1af4d3a0 --- /dev/null +++ b/IntelOrca.Biohazard.BioRand/Routing/RouteSolver.cs @@ -0,0 +1,177 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +namespace IntelOrca.Biohazard.BioRand.Routing +{ + public class RouteSolver + { + public static RouteSolver Default => new RouteSolver(); + + private RouteSolver() + { + } + + public RouteSolverResult Solve(Route route) + { + var state = Begin(route); + while (true) + { + state = Expand(state); + var newState = UseKey(state); + if (newState == null) + return RouteSolverResult.PotentialSoftlock | RouteSolverResult.NodesRemaining; + if (newState == state) + break; + state = newState; + } + + RouteSolverResult flags = 0; + if (state.Next.Count != 0) + flags |= RouteSolverResult.NodesRemaining; + return flags; + } + + private static State Begin(Route route) + { + return new State( + route, + ImmutableHashSet.CreateRange(route.Graph.Start), + ImmutableHashSet.CreateRange(route.Graph.Start.SelectMany(x => route.Graph.GetEdges(x))), + ImmutableMultiSet.Empty); + } + + private static State Expand(State state) + { + var graph = state.Route.Graph; + var newVisits = new List(); + do + { + newVisits.Clear(); + foreach (var node in state.Next) + { + if (!node.Requires.All(x => state.Visited.Contains(x.Node))) + continue; + + newVisits.Add(node); + } + state = state.Visit(newVisits); + } while (newVisits.Count != 0); + return state; + } + + private static State? UseKey(State state) + { + var graph = state.Route.Graph; + var possibleWays = state.Next + .Where(x => HasAllKeys(state, x)) + .ToArray(); + + // Lets first unlock anything that doesn't consume a key + var safeWays = possibleWays + .Where(x => x.Requires.All(x => (x.Flags & EdgeFlags.Consume) == 0)) + .ToArray(); + if (safeWays.Length != 0) + { + foreach (var way in safeWays) + { + state = state.Visit(way); + } + return state; + } + + // Only possible ways left consume a key, so lets detect we can + // do all of them + foreach (var way in possibleWays) + { + if (!HasAllKeys(state, way)) + return null; + + var consumeKeys = way.Requires + .Where(x => (x.Flags & EdgeFlags.Consume) != 0) + .Select(x => x.Node) + .ToArray(); + state = state.UseKeys(consumeKeys); + } + + // Now visit everything we unlocked + foreach (var way in possibleWays) + { + state = state.Visit(way); + } + + return state; + } + + private static bool HasAllKeys(State state, Node node) + { + var keys = state.Keys; + var requiredKeys = node.Requires + .Where(x => x.Node.Kind == NodeKind.Key) + .ToArray(); + foreach (var g in requiredKeys.GroupBy(x => x.Node)) + { + var have = keys.GetCount(g.Key); + var need = g.Count(); + if (have < need) + { + return false; + } + } + return true; + } + + private class State + { + public Route Route { get; } + public ImmutableHashSet Visited { get; } + public ImmutableHashSet Next { get; } + public ImmutableMultiSet Keys { get; } + + public State( + Route route, + ImmutableHashSet visited, + ImmutableHashSet next, + ImmutableMultiSet keys) + { + Route = route; + Visited = visited; + Next = next; + Keys = keys; + } + + public State Visit(params Node[] nodes) => Visit((IEnumerable)nodes); + public State Visit(IEnumerable nodes) + { + if (!nodes.Any()) + return this; + + var newNodes = nodes.SelectMany(x => Route.Graph.GetEdges(x)); + var newKeys = nodes + .Select(x => Route.GetItemContents(x)) + .Where(x => x != null) + .Select(x => x!.Value) + .ToArray(); + return new State( + Route, + Visited.Union(nodes), + Next.Except(nodes).Union(newNodes), + Keys.AddRange(newKeys)); + } + + public State AddKey(Node key) + { + return new State(Route, Visited, Next, Keys.Add(key)); + } + + public State UseKeys(IEnumerable keys) + { + if (!keys.Any()) + return this; + + return new State(Route, Visited, Next, Keys.RemoveMany(keys)); + } + } + } +} diff --git a/IntelOrca.Biohazard.BioRand/Routing/RouteSolverResult.cs b/IntelOrca.Biohazard.BioRand/Routing/RouteSolverResult.cs new file mode 100644 index 00000000..0b2298ad --- /dev/null +++ b/IntelOrca.Biohazard.BioRand/Routing/RouteSolverResult.cs @@ -0,0 +1,11 @@ +using System; + +namespace IntelOrca.Biohazard.BioRand.Routing +{ + [Flags] + public enum RouteSolverResult + { + NodesRemaining = 1 << 0, + PotentialSoftlock = 1 << 1, + } +}