diff --git a/Rotationator/GambitStageInfo.cs b/Rotationator/GambitStageInfo.cs new file mode 100644 index 0000000..a4044cd --- /dev/null +++ b/Rotationator/GambitStageInfo.cs @@ -0,0 +1,37 @@ +namespace Rotationator; + +public class GambitStageInfo +{ + public VersusRule Rule + { + get; + set; + } + + public List Stages + { + get; + set; + } + + public GambitStageInfo() + { + Rule = VersusRule.None; + Stages = new List(); + } + + public dynamic ToByamlStagesList() + { + List byamlStages = new List(); + + foreach (int id in Stages) + { + byamlStages.Add(new Dictionary() + { + { "MapID", id } + }); + } + + return byamlStages; + } +} \ No newline at end of file diff --git a/Rotationator/GambitVersusPhase.cs b/Rotationator/GambitVersusPhase.cs new file mode 100644 index 0000000..e1c2b1d --- /dev/null +++ b/Rotationator/GambitVersusPhase.cs @@ -0,0 +1,73 @@ +namespace Rotationator; + +public class GambitVersusPhase +{ + public int Length + { + get; + set; + } + + public GambitStageInfo RegularInfo + { + get; + set; + } + + public GambitStageInfo GachiInfo + { + get; + set; + } + + public GambitVersusPhase() + { + Length = 0; + RegularInfo = new GambitStageInfo(); + GachiInfo = new GambitStageInfo(); + } + + public GambitVersusPhase(dynamic byamlPhase) + { + Length = byamlPhase["Time"]; + + dynamic byamlRegularStages = byamlPhase["RegularStages"]; + + RegularInfo = new GambitStageInfo() + { + Rule = VersusRuleUtil.FromEnumString(byamlPhase["RegularRule"]), + Stages = new List() + { + byamlRegularStages[0]["MapID"], + byamlRegularStages[1]["MapID"] + } + }; + + dynamic byamlGachiStages = byamlPhase["GachiStages"]; + + GachiInfo = new GambitStageInfo() + { + Rule = VersusRuleUtil.FromEnumString(byamlPhase["GachiRule"]), + Stages = new List() + { + byamlGachiStages[0]["MapID"], + byamlGachiStages[1]["MapID"] + } + }; + } + + public dynamic ToByamlPhase() + { + dynamic byamlPhase = new Dictionary(); + + byamlPhase["Time"] = Length; + + byamlPhase["GachiRule"] = GachiInfo.Rule.ToEnumString(); + byamlPhase["GachiStages"] = GachiInfo.ToByamlStagesList(); + + byamlPhase["RegularRule"] = RegularInfo.Rule.ToEnumString(); + byamlPhase["RegularStages"] = RegularInfo.ToByamlStagesList(); + + return byamlPhase; + } +} \ No newline at end of file diff --git a/Rotationator/Program.cs b/Rotationator/Program.cs index e5dff12..9520f2b 100644 --- a/Rotationator/Program.cs +++ b/Rotationator/Program.cs @@ -1,3 +1,273 @@ -// See https://aka.ms/new-console-template for more information +using System.CommandLine; +using System.Text.Json; +using OatmealDome.BinaryData; +using OatmealDome.NinLib.Byaml; +using OatmealDome.NinLib.Byaml.Dynamic; +using Rotationator; -Console.WriteLine("Hello, World!"); \ No newline at end of file +// +// Constants +// + +const int maximumPhases = 192; // TODO correct? + +Dictionary> bannedStages = new Dictionary>() +{ + { + VersusRule.Paint, + new List() // nothing banned + }, + { + VersusRule.Goal, + new List() + { + 2, // Saltspray Rig + 4, // Blackbelly Skatepark + 14 // Piranha Pit + } + }, + { + VersusRule.Area, + new List() // nothing banned + }, + { + VersusRule.Lift, + new List() + { + 2, // Saltspray Rig + 6, // Port Mackerel + } + } +}; + +List defaultStagePool = new List() +{ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 +}; + +List defaultGachiRulePool = new List() +{ + VersusRule.Goal, + VersusRule.Area, + VersusRule.Lift +}; + +Random random = new Random(); + +// +// Command handling +// + +Argument lastByamlArg = new Argument("lastByaml", "The last VSSetting BYAML file."); + +Argument outputByamlArg = new Argument("lastByaml", "The output VSSetting BYAML file."); + +Command command = new RootCommand("Generates a new VSSetting BYAMl file.") +{ + lastByamlArg, + outputByamlArg +}; + +command.SetHandler((lastPath, outPath) => Run(lastPath, outPath), lastByamlArg, outputByamlArg); + +command.Invoke(args); + +// +// Entrypoint +// + +void Run(string lastByamlPath, string outputByamlPath) +{ + Console.WriteLine("run"); + + dynamic lastByaml = ByamlFile.Load(lastByamlPath); + + DateTime lastBaseTime = DateTime.Parse(lastByaml["DateTime"]).ToUniversalTime(); + List lastPhases = lastByaml["Phases"]; + + // + // Find phase start point + // + + DateTime loopTime = lastBaseTime; + DateTime referenceNow = DateTime.UtcNow; + + int lastPhasesStartIdx = -1; + + for (int i = 0; i < lastPhases.Count; i++) + { + Dictionary phase = lastPhases[i]; + + DateTime phaseEndTime = loopTime.AddHours((int)phase["Time"]); + + if (referenceNow >= loopTime && phaseEndTime > referenceNow) + { + lastPhasesStartIdx = i; + break; + } + + loopTime = phaseEndTime; + } + + DateTime baseTime; + List currentPhases; + + if (lastPhasesStartIdx != -1) + { + baseTime = loopTime; + currentPhases = lastPhases.Skip(lastPhasesStartIdx).Select(p => new GambitVersusPhase(p)).ToList(); + } + else + { + throw new NotImplementedException("not supported yet"); + } + + // The last phase is set to 10 years, so correct this to the correct phase length. + currentPhases.Last().Length = 4; + + // + // Generate new phases to fill out the schedule + // + + List gachiRulePool = new List(); + Dictionary> stagePools = new Dictionary>() + { + { VersusRule.Paint, new List() }, + { VersusRule.Goal, new List() }, + { VersusRule.Area, new List() }, + { VersusRule.Lift, new List() } + }; + + for (int i = currentPhases.Count; i < maximumPhases; i++) + { + GambitVersusPhase currentPhase = new GambitVersusPhase(); + GambitVersusPhase lastPhase = i != 0 ? currentPhases[i - 1] : new GambitVersusPhase(); + + VersusRule gachiRule = PickGachiRule(currentPhase.GachiInfo, lastPhase.GachiInfo, gachiRulePool); + + currentPhase.RegularInfo.Rule = VersusRule.Paint; + currentPhase.RegularInfo.Stages.Add(PickStage(currentPhase, lastPhase, VersusRule.Paint, + stagePools[VersusRule.Paint])); + currentPhase.RegularInfo.Stages.Add(PickStage(currentPhase, lastPhase, VersusRule.Paint, + stagePools[VersusRule.Paint])); + currentPhase.RegularInfo.Stages.Sort(); + + currentPhase.GachiInfo.Rule = gachiRule; + currentPhase.GachiInfo.Stages.Add(PickStage(currentPhase, lastPhase, gachiRule, stagePools[gachiRule])); + currentPhase.GachiInfo.Stages.Add(PickStage(currentPhase, lastPhase, gachiRule, stagePools[gachiRule])); + currentPhase.GachiInfo.Stages.Sort(); + + currentPhase.Length = 4; + + currentPhases.Add(currentPhase); + } + + // + // Write BYAML + // + + // As a fallback in case the schedule isn't updated in time, make the last phase 10 years long. + currentPhases.Last().Length = 24 * 365 * 10; + + // Set the new base DateTime (this is usually in the JST time zone, but it accepts UTC time as well). + lastByaml["DateTime"] = baseTime.ToString("yyyy-MM-dd'T'HH:mm:ssK"); + + // Set the new phases. + lastByaml["Phases"] = currentPhases.Select(p => p.ToByamlPhase()); + + // Add some metadata about this BYAML file and how it was built. + lastByaml["ByamlInfo"] = new Dictionary() + { + { "Generator", "Rotationator 1" }, + { "GenerationTime", referenceNow.ToString("O") }, + { "BaseByamlStartTime", baseTime.ToString("O") }, + }; + + ByamlFile.Save(outputByamlPath, lastByaml, new ByamlSerializerSettings() + { + ByteOrder = ByteOrder.BigEndian, + SupportsBinaryData = false, + Version = ByamlVersion.One + }); + + File.WriteAllText(outputByamlPath + ".json", JsonSerializer.Serialize(lastByaml, new JsonSerializerOptions() + { + WriteIndented = true + })); +} + +// +// Utility function to pick a random element from a pool. +// + +T GetRandomElementFromPool(List pool, Func validityChecker) +{ + T element; + + do + { + element = pool[random.Next(0, pool.Count)]; + } while (!validityChecker(element)); + + pool.Remove(element); + + return element; +} + +// +// Random stage + rule pickers. +// + +VersusRule PickGachiRule(GambitStageInfo stageInfo, GambitStageInfo lastStageInfo, List pool) +{ + if (pool.Count == 0) + { + pool.AddRange(defaultGachiRulePool); + } + + return GetRandomElementFromPool(pool, rule => rule != lastStageInfo.Rule); +} + +int PickStage(GambitVersusPhase phase, GambitVersusPhase lastPhase, VersusRule rule, List pool) +{ + List bannedStagesForRule = bannedStages[rule]; + + if (pool.Count == 0) + { + pool.AddRange(defaultStagePool.Except(bannedStagesForRule)); + } + + bool IsStageValid(int stageId) + { + // Don't pick this stage if it's already used in this phase. + if (phase.RegularInfo.Stages.Contains(stageId) || phase.GachiInfo.Stages.Contains(stageId)) + { + return false; + } + + // Don't pick this stage if it's present in the last phase. + if (lastPhase.RegularInfo.Stages.Contains(stageId) || lastPhase.GachiInfo.Stages.Contains(stageId)) + { + return false; + } + + return true; + } + + // Check if all of our options are invalid. + if (pool.All(i => !IsStageValid(i))) + { + // If so, pick a random stage from the default pool, excluding: + // - the current phase's stages (in both Regular and Gachi) + // - the last phase's stages (in both Regular and Gachi) + // - all banned stages for this rule + pool = defaultStagePool.Except(phase.RegularInfo.Stages) + .Except(phase.GachiInfo.Stages) + .Except(lastPhase.RegularInfo.Stages) + .Except(lastPhase.GachiInfo.Stages) + .Except(bannedStagesForRule) + .ToList(); + } + + return GetRandomElementFromPool(pool, IsStageValid); +} \ No newline at end of file diff --git a/Rotationator/VersusRule.cs b/Rotationator/VersusRule.cs new file mode 100644 index 0000000..f574560 --- /dev/null +++ b/Rotationator/VersusRule.cs @@ -0,0 +1,26 @@ +namespace Rotationator; + +public enum VersusRule +{ + None, + + /// + /// Turf War + /// + Paint, + + /// + /// Rainmaker + /// + Goal, + + /// + /// Splat Zones + /// + Area, + + /// + /// Tower Control + /// + Lift +} \ No newline at end of file diff --git a/Rotationator/VersusRuleUtil.cs b/Rotationator/VersusRuleUtil.cs new file mode 100644 index 0000000..718be5d --- /dev/null +++ b/Rotationator/VersusRuleUtil.cs @@ -0,0 +1,40 @@ +namespace Rotationator; + +public static class VersusRuleUtil +{ + public static VersusRule FromEnumString(string str) + { + switch (str) + { + case "cPnt": + return VersusRule.Paint; + case "cVgl": + return VersusRule.Goal; + case "cVar": + return VersusRule.Area; + case "cVlf": + return VersusRule.Lift; + default: // "cNone" + return VersusRule.None; + } + } + + public static string ToEnumString(this VersusRule rule) + { + switch (rule) + { + case VersusRule.None: + return "cNone"; + case VersusRule.Paint: + return "cPnt"; + case VersusRule.Goal: + return "cVgl"; + case VersusRule.Area: + return "cVar"; + case VersusRule.Lift: + return "cVlf"; + default: + throw new ArgumentOutOfRangeException(nameof(rule), "VersusRule not supported"); + } + } +} \ No newline at end of file