using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using Microsoft.CodeAnalysis; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.0", FrameworkDisplayName = ".NET Standard 2.0")] [assembly: AssemblyCompany("AitServices")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0")] [assembly: AssemblyProduct("Medkit Scatter")] [assembly: AssemblyTitle("MedkitScatter.Planning")] [assembly: AssemblyVersion("1.0.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] internal sealed class IsReadOnlyAttribute : Attribute { } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace MedkitScatter.Planning { public interface IRandomSource { int Next(int minInclusive, int maxExclusive); double NextDouble(); } public readonly struct MedkitPlacement { public RoomKey Room { get; } public MedkitTier Tier { get; } public MedkitPlacement(RoomKey room, MedkitTier tier) { Room = room; Tier = tier; } public override string ToString() { return Room.ToString() + " " + Tier; } } public static class MedkitPlanner { private readonly struct Weights { public double Small { get; } public double Medium { get; } public double Large { get; } public double Total => Small + Medium + Large; public Weights(double small, double medium, double large) { Small = small; Medium = medium; Large = large; } } public const int MaxSupportedMedkits = 128; public static PlanResult Plan(PlanInput? input, IRandomSource? rng) { PlanInput planInput = input ?? new PlanInput(); IRandomSource rng2 = rng ?? new SystemRandomSource(0); List list = new List(); IReadOnlyList readOnlyList = planInput.Rooms ?? Array.Empty(); int num = 0; for (int i = 0; i < readOnlyList.Count; i++) { if (readOnlyList[i].CountsToTotal) { num++; } } List list2 = new List(); HashSet hashSet = new HashSet(); foreach (RoomDescriptor item in from r in readOnlyList where r.CanHostMedkit && r.CandidateSpots > 0 orderby r.Key select r) { if (hashSet.Add(item.Key)) { list2.Add(item); } else { list.Add("duplicate RoomKey " + item.Key.ToString() + " ignored"); } } int num2 = Math.Max(1, planInput.RoomsPerMedkit); double num3 = (double)num / (double)num2; int num4 = planInput.Rounding switch { RoundingMode.Ceil => (int)Math.Ceiling(num3), RoundingMode.Round => (int)Math.Round(num3, MidpointRounding.AwayFromZero), _ => (int)Math.Floor(num3), }; int num5 = Clamp(planInput.MinMedkits, 0, 128); int val = ((planInput.MaxMedkits == 0) ? int.MaxValue : planInput.MaxMedkits); val = Math.Max(num5, val); int num6 = Clamp(num4, num5, val); List list3 = OrderRooms(list2, rng2, planInput.PreferDistantRooms); int num7 = Math.Min(num6, list3.Count); List list4 = new List(num7); List list5 = new List(Math.Max(0, list3.Count - num7)); Weights weights = EffectiveWeights(planInput, list); List list6 = new List(num7); if (planInput.GuaranteeVariety && num7 >= 3) { List list7 = new List { MedkitTier.Small, MedkitTier.Medium, MedkitTier.Large }; Shuffle(list7, rng2); list6.AddRange(list7); } while (list6.Count < num7) { list6.Add(WeightedPick(weights, rng2)); } for (int num8 = 0; num8 < list3.Count; num8++) { if (num8 < num7) { list4.Add(new MedkitPlacement(list3[num8].Key, list6[num8])); } else { list5.Add(list3[num8].Key); } } string explanation = BuildExplanation(planInput, num, num2, num3, num4, num5, planInput.MaxMedkits, num6, list2.Count, num7, list5.Count, weights, list); return new PlanResult(num, num6, list4, list5, explanation, list); } private static Weights EffectiveWeights(PlanInput cfg, List notes) { double num = Math.Max(0.0, cfg.WeightSmall); double num2 = Math.Max(0.0, cfg.WeightMedium); double num3 = Math.Max(0.0, cfg.WeightLarge); if (cfg.DifficultyScaling) { int num4 = Math.Max(0, cfg.LevelsCompleted); double num5 = 1.0 + cfg.DifficultyFactor * (double)num4; if (double.IsNaN(num5) || double.IsInfinity(num5) || num5 <= 0.0) { notes.Add("difficulty factor " + Fmt(num5) + " invalid, using 1.0"); num5 = 1.0; } num3 *= num5; num /= num5; } if (num + num2 + num3 <= 0.0) { notes.Add("all tier weights <= 0, degraded to Small-only"); return new Weights(1.0, 0.0, 0.0); } return new Weights(num, num2, num3); } private static MedkitTier WeightedPick(Weights weights, IRandomSource rng) { double total = weights.Total; if (total <= 0.0) { return MedkitTier.Small; } double num = rng.NextDouble(); if (double.IsNaN(num) || num < 0.0) { num = 0.0; } else if (num >= 1.0) { num = 0.999999999; } double num2 = num * total; if (num2 < weights.Small) { return MedkitTier.Small; } if (num2 < weights.Small + weights.Medium) { return MedkitTier.Medium; } return MedkitTier.Large; } private static List OrderRooms(List sortedByKey, IRandomSource rng, bool preferDistant) { List list = new List(sortedByKey); if (!preferDistant) { Shuffle(list, rng); return list; } List list2 = new List(list); List list3 = new List(list2.Count); foreach (RoomDescriptor item in list2) { list3.Add(1.0 + (double)Math.Max(0, item.DistanceFromStart)); } List list4 = new List(list2.Count); while (list2.Count > 0) { double num = 0.0; for (int i = 0; i < list3.Count; i++) { num += list3[i]; } double num2 = rng.NextDouble(); if (double.IsNaN(num2) || num2 < 0.0) { num2 = 0.0; } else if (num2 >= 1.0) { num2 = 0.999999999; } double num3 = num2 * num; int index = list2.Count - 1; double num4 = 0.0; for (int j = 0; j < list2.Count; j++) { num4 += list3[j]; if (num3 < num4) { index = j; break; } } list4.Add(list2[index]); list2.RemoveAt(index); list3.RemoveAt(index); } return list4; } private static void Shuffle(IList list, IRandomSource rng) { for (int num = list.Count - 1; num > 0; num--) { int num2 = rng.Next(0, num + 1); if (num2 < 0 || num2 > num) { num2 = 0; } T value = list[num]; list[num] = list[num2]; list[num2] = value; } } private static int Clamp(int value, int min, int max) { if (value < min) { return min; } if (value <= max) { return value; } return max; } private static string Fmt(double value) { return value.ToString("0.##", CultureInfo.InvariantCulture); } private static string BuildExplanation(PlanInput cfg, int roomCount, int divisor, double raw, int rounded, int effMin, int rawMax, int targetCount, int eligibleCount, int plannedCount, int reserveCount, Weights weights, List notes) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append("RoomCount(counted)=").Append(roomCount).Append(" -> ") .Append(roomCount) .Append(" / ") .Append(divisor) .Append(" = ") .Append(raw.ToString("0.00", CultureInfo.InvariantCulture)) .Append(" -> ") .Append(cfg.Rounding) .Append(" -> ") .Append(rounded) .Append(" -> clamp[") .Append(effMin) .Append("..") .Append((rawMax == 0) ? "inf" : rawMax.ToString(CultureInfo.InvariantCulture)) .Append("] -> TargetCount=") .Append(targetCount); stringBuilder.Append(" | eligible=").Append(eligibleCount).Append(" -> PlannedCount=") .Append(plannedCount) .Append(", Reserve=") .Append(reserveCount); stringBuilder.Append(" | weights S/M/L=").Append(Fmt(weights.Small)).Append('/') .Append(Fmt(weights.Medium)) .Append('/') .Append(Fmt(weights.Large)) .Append(" variety=") .Append(cfg.GuaranteeVariety ? "on" : "off") .Append(" distant=") .Append(cfg.PreferDistantRooms ? "on" : "off"); if (notes.Count > 0) { stringBuilder.Append(" | notes: ").Append(string.Join("; ", notes.ToArray())); } return stringBuilder.ToString(); } } public enum MedkitTier { Small, Medium, Large } public sealed class PlanInput { public IReadOnlyList Rooms { get; set; } = Array.Empty(); public int RoomsPerMedkit { get; set; } = 4; public RoundingMode Rounding { get; set; } public int MinMedkits { get; set; } = 1; public int MaxMedkits { get; set; } public int WeightSmall { get; set; } = 60; public int WeightMedium { get; set; } = 30; public int WeightLarge { get; set; } = 10; public bool GuaranteeVariety { get; set; } = true; public bool PreferDistantRooms { get; set; } public bool DifficultyScaling { get; set; } public double DifficultyFactor { get; set; } = 0.15; public int LevelsCompleted { get; set; } } public sealed class PlanResult { public int RoomCount { get; } public int TargetCount { get; } public int PlannedCount => Placements.Count; public IReadOnlyList Placements { get; } public IReadOnlyList Reserve { get; } public string Explanation { get; } public IReadOnlyList Notes { get; } public PlanResult(int roomCount, int targetCount, IReadOnlyList? placements, IReadOnlyList? reserve, string? explanation, IReadOnlyList? notes) { RoomCount = roomCount; TargetCount = targetCount; Placements = placements ?? Array.Empty(); Reserve = reserve ?? Array.Empty(); Explanation = explanation ?? string.Empty; Notes = notes ?? Array.Empty(); } } public readonly struct RoomDescriptor { public RoomKey Key { get; } public RoomKind Kind { get; } public bool CountsToTotal { get; } public bool CanHostMedkit { get; } public int CandidateSpots { get; } public int DistanceFromStart { get; } public RoomDescriptor(RoomKey key, RoomKind kind, bool countsToTotal, bool canHostMedkit, int candidateSpots, int distanceFromStart) { Key = key; Kind = kind; CountsToTotal = countsToTotal; CanHostMedkit = canHostMedkit; CandidateSpots = candidateSpots; DistanceFromStart = distanceFromStart; } public override string ToString() { return Key.ToString() + " " + Kind.ToString() + " spots=" + CandidateSpots + " dist=" + DistanceFromStart; } } public readonly struct RoomKey : IEquatable, IComparable { public int GridX { get; } public int GridY { get; } public RoomKey(int gridX, int gridY) { GridX = gridX; GridY = gridY; } public int CompareTo(RoomKey other) { int num = GridY.CompareTo(other.GridY); if (num == 0) { return GridX.CompareTo(other.GridX); } return num; } public bool Equals(RoomKey other) { if (GridX == other.GridX) { return GridY == other.GridY; } return false; } public override bool Equals(object? obj) { if (obj is RoomKey other) { return Equals(other); } return false; } public override int GetHashCode() { return (GridX * 397) ^ GridY; } public static bool operator ==(RoomKey left, RoomKey right) { return left.Equals(right); } public static bool operator !=(RoomKey left, RoomKey right) { return !left.Equals(right); } public static bool operator <(RoomKey left, RoomKey right) { return left.CompareTo(right) < 0; } public static bool operator >(RoomKey left, RoomKey right) { return left.CompareTo(right) > 0; } public static bool operator <=(RoomKey left, RoomKey right) { return left.CompareTo(right) <= 0; } public static bool operator >=(RoomKey left, RoomKey right) { return left.CompareTo(right) >= 0; } public override string ToString() { return "(" + GridX + "," + GridY + ")"; } } public enum RoomKind { Normal, Passage, DeadEnd, Extraction, StartRoom, Unknown, Special } public enum RoundingMode { Floor, Round, Ceil } public sealed class SystemRandomSource : IRandomSource { private readonly Random _random; public int Seed { get; } public SystemRandomSource(int seed) { Seed = seed; _random = new Random(seed); } public int Next(int minInclusive, int maxExclusive) { if (maxExclusive <= minInclusive) { return minInclusive; } return _random.Next(minInclusive, maxExclusive); } public double NextDouble() { return _random.NextDouble(); } } }