using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using Luneraria.Core; /// /// H1 balance runs. Every hero gets the same probe bot (shares of "ausgewogen", same items, /// first talent choices, recruited in round 3) on slot 0 or slot 3; the other five slots are /// fixed reference profiles with Ringborke's probe on the opposite seat. Reports the win rate /// of the probe seat per hero over many seeds and both sides, plus how often Sylvara leaps and /// releases her ultimate. Usage: dotnet run -c Release --project tools/dotnet/Luneraria.Balance [seeds] /// public static class Program { static readonly string[] Heroes = { "held_ringborke", "held_quellgeflecht", "held_funkenlanze", "held_sylvara" }; public static int Main(string[] args) { if (args.Length > 0 && args[0] == "s1") return RulesComparison(args.Length > 1 ? int.Parse(args[1], CultureInfo.InvariantCulture) : 60); int seeds = args.Length > 0 ? int.Parse(args[0], CultureInfo.InvariantCulture) : 40; BotProfile reference = ContentData.BotProfiles["ausgewogen"]; foreach (string hero in Heroes) { ContentData.ExtraBotProfiles["probe_" + hero] = new BotProfile { Name = "probe_" + hero, Front = reference.Front, Melee = reference.Melee, Ranged = reference.Ranged, Splash = reference.Splash, Support = reference.Support, UpgradeFrom = reference.UpgradeFrom, SupportLane = 0.0, Hero = hero, HeroRound = 3, Items = new[] { "glutklinge", "basaltschild", "funkenfokus" }, TalentChoices = new[] { 0, 0 }, Champion = "", }; } Console.WriteLine("hero; seat wins; losses; draws; open; win rate; mean rounds; leaps/combat; ultimates/combat"); foreach (string hero in Heroes) { int wins = 0, losses = 0, draws = 0, open = 0, rounds = 0, combats = 0, leaps = 0, ults = 0; for (int seed = 1; seed <= seeds; seed++) { for (int side = 0; side < 2; side++) { var profiles = new List { "probe_held_ringborke", "druck", "fernkampf", "probe_held_ringborke", "druck", "fernkampf" }; profiles[side * 3] = "probe_" + hero; var match = new LuneMatch(); match.Setup(new MatchOptions { Seed = seed, HumanSlot = -1, Profiles = profiles }); while (match.Phase != Phase.Over && match.RoundNumber <= 80) { for (int slot = 0; slot < 6; slot++) LuneBot.Plan(match, slot); match.BeginCombat(); match.FinishCombat(); combats++; foreach (LuneUnit unit in match.Combat.Units) { if (unit.Kind == "hero" && unit.OwnerSlot == side * 3) { leaps += unit.LeapSerial; ults += unit.UltimateSerial; } } } rounds += match.RoundNumber; if (match.Phase != Phase.Over) open++; else if (match.Winner == side) wins++; else if (match.Winner == LuneMatch.Draw) draws++; else losses++; } } int games = seeds * 2; Console.WriteLine(string.Format(CultureInfo.InvariantCulture, "{0}; {1}; {2}; {3}; {4}; {5:P0}; {6:F1}; {7:F2}; {8:F2}", hero, wins, losses, draws, open, (double)wins / Math.Max(1, wins + losses + draws), (double)rounds / games, (double)leaps / combats, (double)ults / combats)); } return 0; } /// /// S1: plain bot matches (seed rotation of profiles) under reference and extended rules: /// match length, winners, how often each extended mechanic acts, and how often the team that /// first took a tower also won (snowball). /// static int RulesComparison(int seeds) { Console.WriteLine("rules; matches; blue wins; red wins; draws; open; mean rounds; min; max; first tower wins; champion income/seat; barracks waves; exhausted heroes; buybacks; blessings; sylvara seats; sylvara wins; glyphs fired; Ancient levels; recipe items"); foreach (bool extended in new[] { false, true }) { int blue = 0, red = 0, draws = 0, open = 0, rounds = 0, min = 999, max = 0, firstTowerWins = 0, firstTowerGames = 0; long income = 0; int barracks = 0, exhausted = 0, buybacks = 0, blessings = 0, sylvaraSeats = 0, sylvaraWins = 0, glyphs = 0, cores = 0, recipes = 0; // S3: every seed twice, the second time with the teams' profiles swapped, so a strong // profile combination counts for both sides and only a real side advantage remains. for (int game = 0; game < seeds * 2; game++) { int seed = game / 2 + 1; var probe = new LuneMatch(); probe.Setup(new MatchOptions { Seed = seed, HumanSlot = -1, Extended = extended }); var rotation = new List(); for (int slot = 0; slot < 6; slot++) rotation.Add(LuneBot.ProfileFor(probe, slot)); if (game % 2 == 1) rotation = rotation.Skip(3).Concat(rotation.Take(3)).ToList(); var match = new LuneMatch(); match.Setup(new MatchOptions { Seed = seed, HumanSlot = -1, Extended = extended, Profiles = rotation }); int firstTower = -1; while (match.Phase != Phase.Over && match.RoundNumber <= 80) { for (int slot = 0; slot < 6; slot++) { bool wasExhausted = match.Players[slot].Hero != null && match.Players[slot].Hero.Exhausted; LuneBot.Plan(match, slot); if (wasExhausted && match.Players[slot].Draft.Hero != null && !match.Players[slot].Draft.Hero.Exhausted) buybacks++; } for (int team = 0; team < 2; team++) foreach (string lane in LuneMap.Lanes) if (extended && match.BarracksBroken(team, lane)) barracks++; match.BeginCombat(); match.FinishCombat(); glyphs += (match.Combat.GlyphUsed[0] ? 1 : 0) + (match.Combat.GlyphUsed[1] ? 1 : 0); foreach (Player player in match.Players) if (player.Hero != null && player.Hero.Exhausted) exhausted++; if (firstTower < 0 && match.LastSettlement != null && match.LastSettlement.BuildingsLost.Count > 0) firstTower = match.LastSettlement.BuildingsLost[0].StartsWith("rot") ? 0 : 1; } int length = match.RoundNumber; rounds += length; min = Math.Min(min, length); max = Math.Max(max, length); if (match.Phase != Phase.Over) open++; else if (match.Winner == 0) blue++; else if (match.Winner == 1) red++; else draws++; if (firstTower >= 0 && match.Phase == Phase.Over) { firstTowerGames++; if (match.Winner == firstTower) firstTowerWins++; } foreach (Player player in match.Players) { income += player.IncomeBonus; if (player.Hero != null && player.Hero.HeroId == "held_sylvara") { sylvaraSeats++; if (match.Winner == player.Team) sylvaraWins++; } } blessings += match.Boss.Blessing[0] + match.Boss.Blessing[1]; cores += match.Boss.CoreLevel[0] + match.Boss.CoreLevel[1]; foreach (Player player in match.Players) if (player.Hero != null) recipes += player.Hero.Items.Count(id => ContentData.Items[id].IsRecipe); } Console.WriteLine(string.Format(CultureInfo.InvariantCulture, "{0}; {1}; {2}; {3}; {4}; {5}; {6:F1}; {7}; {8}; {9}/{10}; {11:F1}; {12}; {13}; {14}; {15}; {16}; {17}; {18}; {19}; {20}", extended ? "erweitert" : "referenz", seeds * 2, blue, red, draws, open, (double)rounds / (seeds * 2), min, max, firstTowerWins, firstTowerGames, (double)income / (seeds * 12), barracks, exhausted, buybacks, blessings, sylvaraSeats, sylvaraWins, glyphs, cores, recipes)); } return 0; } }