Brunhild (held_brunhild): Heldenjägerin (+25 % gegen Helden/Champions), Speerwurf markiert (+15 % erlittener Schaden), Walkürensturz (Sprung, 200 % in 3 m, 1 s Betäubung, Bosse immun, Walkürenschild 30 %), Talente, Auslöser, Skin-Eintrag Eiswalküre, Bot-Profil walkuere (nur erweitert). Neue Effektarten vulnerable und stun wirken nur durch Brunhild; Golden 0. Probe-Vergleich 50 % wie die übrigen Helden; h2 22/22. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
145 lines
8.6 KiB
C#
145 lines
8.6 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using Luneraria.Core;
|
|
|
|
/// <summary>
|
|
/// 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]
|
|
/// </summary>
|
|
public static class Program
|
|
{
|
|
static readonly string[] Heroes = { "held_ringborke", "held_quellgeflecht", "held_funkenlanze", "held_sylvara", "held_brunhild" };
|
|
|
|
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<string> { "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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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).
|
|
/// </summary>
|
|
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<string>();
|
|
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;
|
|
}
|
|
}
|