From e756a90f4c317781d1c19d1d042d23eacd507b94 Mon Sep 17 00:00:00 2001 From: Martin Stickelbroeck Date: Sun, 11 Oct 2026 16:03:05 +0200 Subject: [PATCH] =?UTF-8?q?S3A=20Bots:=20fr=C3=BChere=20Helden,=20Rotation?= =?UTF-8?q?sfehler=20behoben,=20Champion-Kopfgeld=20halbiert?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Erweiterte Regeln: Bots sparen ab ihrer Heldenrunde für den Helden (Helden in Runde 4-6 statt 11-12). Rotation mit sieben Profilen gab allen Plätzen dasselbe Profil (Schritt 7); jetzt Schritt 5. Angriffschampions geben dem Verteidiger das halbe Kopfgeld (Legion TD 2); Assassinen-Profil ohne Champion. Balance-Werkzeug spielt jeden Seed gespiegelt: Blau/Rot 59/61, Sylvara 41 %. Referenz unverändert (Golden 0). Co-Authored-By: Claude Opus 5.5 --- Assets/Luneraria/Core/Content/ContentData.cs | 2 +- Assets/Luneraria/Core/Content/LuneBalance.cs | 2 ++ Assets/Luneraria/Core/Simulation/LuneBot.cs | 19 ++++++++++++++++--- Assets/Luneraria/Core/Simulation/LuneMatch.cs | 5 +++++ Assets/Luneraria/UI/Hud/LuneArmyPanels.cs | 2 +- docs/port/logs/s3/balance-s3a-bots.txt | 3 +++ tools/dotnet/Luneraria.Balance/Program.cs | 18 ++++++++++++++---- 7 files changed, 42 insertions(+), 9 deletions(-) create mode 100644 docs/port/logs/s3/balance-s3a-bots.txt diff --git a/Assets/Luneraria/Core/Content/ContentData.cs b/Assets/Luneraria/Core/Content/ContentData.cs index 4d4d2a5..78b0133 100644 --- a/Assets/Luneraria/Core/Content/ContentData.cs +++ b/Assets/Luneraria/Core/Content/ContentData.cs @@ -168,7 +168,7 @@ namespace Luneraria.Core /// bot match). Usable as a fixed profile per slot (MatchOptions.Profiles, balance runs). /// public static readonly OrderedMap ExtraBotProfiles = Ordered( - new BotProfile { Name = "assassine", Front = 0.3, Melee = 0.25, Ranged = 0.25, Splash = 0.1, Support = 0.1, UpgradeFrom = 6, SupportLane = 0.0, Hero = "held_sylvara", HeroRound = 3, Items = new[] { "glutklinge", "funkenfokus", "basaltschild" }, TalentChoices = new[] { 1, 0 }, Champion = "ch_druck" } + new BotProfile { Name = "assassine", Front = 0.3, Melee = 0.25, Ranged = 0.25, Splash = 0.1, Support = 0.1, UpgradeFrom = 6, SupportLane = 0.0, Hero = "held_sylvara", HeroRound = 3, Items = new[] { "glutklinge", "funkenfokus", "basaltschild" }, TalentChoices = new[] { 1, 0 }, Champion = "" } ); } } diff --git a/Assets/Luneraria/Core/Content/LuneBalance.cs b/Assets/Luneraria/Core/Content/LuneBalance.cs index d458da4..35ea288 100644 --- a/Assets/Luneraria/Core/Content/LuneBalance.cs +++ b/Assets/Luneraria/Core/Content/LuneBalance.cs @@ -63,6 +63,8 @@ namespace Luneraria.Core // S1: extended rules (MatchOptions.Extended; the reference rules ignore all of these). /// Legion TD 2: a booked attack champion raises the booker's income by cost / this, for good. public int ChampionIncomeDivisor = 12; + /// S3: share of a champion's bounty the defenders get under the extended rules (Legion TD 2: less than the send costs). + public double ChampionBountyShare = 0.5; /// Dota 2 barracks: waves of a team on a lane whose enemy inner tower fell get stronger. public double BarracksBoost = 0.25; public int BarracksExtraMonsters = 1; diff --git a/Assets/Luneraria/Core/Simulation/LuneBot.cs b/Assets/Luneraria/Core/Simulation/LuneBot.cs index 74e6cbe..b07ae97 100644 --- a/Assets/Luneraria/Core/Simulation/LuneBot.cs +++ b/Assets/Luneraria/Core/Simulation/LuneBot.cs @@ -19,10 +19,15 @@ namespace Luneraria.Core if (match.BotProfiles.Count == 6) return match.BotProfiles[slot]; // S1: with extended rules the rotation also holds the profiles of later heroes (Sylvara). + if (!match.Extended) + { + IReadOnlyList reference = ContentData.BotProfiles.Keys; + return reference[(slot * 7 + match.SeedValue) % reference.Count]; + } var names = new List(ContentData.BotProfiles.Keys); - if (match.Extended) - names.AddRange(ContentData.ExtraBotProfiles.Keys); - return names[(slot * 7 + match.SeedValue) % names.Count]; + names.AddRange(ContentData.ExtraBotProfiles.Keys); + // S3 fix: with seven profiles the reference step 7 gave every seat the same profile. + return names[(slot * 5 + match.SeedValue) % names.Count]; } public static void Plan(LuneMatch match, int slot, string profileName = "") @@ -43,6 +48,14 @@ namespace Luneraria.Core // S2 team actions come before the army too, so a threatened base gets its Ancient level. if (match.Extended) PlanTeam(match, slot); + // S3 (extended rules): from its hero round a bot saves for the hero instead of buying troops + // (the reference bots spend everything and recruit around round 11). + if (match.Extended && player.Draft.Hero == null && match.RoundNumber >= profile.HeroRound && player.Draft.Roster.Count >= 3) + { + PlanBoss(match, slot, profile); + match.SetReady(slot); + return; + } for (int guard = 0; guard < 40; guard++) { List roster = player.Draft.Roster; diff --git a/Assets/Luneraria/Core/Simulation/LuneMatch.cs b/Assets/Luneraria/Core/Simulation/LuneMatch.cs index 91abc40..745bf50 100644 --- a/Assets/Luneraria/Core/Simulation/LuneMatch.cs +++ b/Assets/Luneraria/Core/Simulation/LuneMatch.cs @@ -644,6 +644,9 @@ namespace Luneraria.Core /// Legion TD 2: income a booked champion adds for good. public int ChampionIncome(int cost) { return cost / GdMath.Max(1, Balance.ChampionIncomeDivisor); } + /// S3: the defenders' bounty of a champion (halved under the extended rules). + public int ChampionBountyFor(int bounty) { return Extended ? (int)Math.Floor(bounty * Balance.ChampionBountyShare) : bounty; } + public int BuybackCost(HeroState hero) { return Balance.BuybackCost(LuneContent.LevelFor(hero.Xp)); } /// Dota 2 barracks: the enemy inner tower of this lane has fallen, so this team's waves there are reinforced. @@ -711,6 +714,8 @@ namespace Luneraria.Core if (booking.DeployRound == RoundNumber && booking.Lane == lane) { UnitDef definition = LuneContent.ChampionDefinition(booking.ChampionId, RoundNumber, Balance); + if (Extended) + definition.Bounty = ChampionBountyFor(definition.Bounty); Combat.Spawn("champion", team, lane, definition, 13.0, ((double)(index % 3) - 1.0) * 1.4, slot, booking.Uid); index += 1; } diff --git a/Assets/Luneraria/UI/Hud/LuneArmyPanels.cs b/Assets/Luneraria/UI/Hud/LuneArmyPanels.cs index 1fb3b6e..e296018 100644 --- a/Assets/Luneraria/UI/Hud/LuneArmyPanels.cs +++ b/Assets/Luneraria/UI/Hud/LuneArmyPanels.cs @@ -137,7 +137,7 @@ namespace Luneraria.UI Subtitle = spec.RoleText + " · " + spec.Cost + " Gold · Einsatz in Welle " + round + " – " + LuneUi.Upper(LuneMap.LaneNames[TargetLane]), // S1 (Legion TD 2): sending is also an investment in income. Text = spec.Text + (_model != null && _model.State.Extended ? "\nSöldner-Einkommen: +" + _model.State.ChampionIncome(spec.Cost) + " Gold in jeder folgenden Runde, dauerhaft." : ""), - Note = "Einmaliger Einsatz: kämpft eine Welle lang und verschwindet danach. Die Ankündigung ist für alle sichtbar; sein Tod bringt dem Gegner " + definition.Bounty + " Gold.", + Note = "Einmaliger Einsatz: kämpft eine Welle lang und verschwindet danach. Die Ankündigung ist für alle sichtbar; sein Tod bringt dem Gegner " + (_model != null ? _model.State.ChampionBountyFor(definition.Bounty) : definition.Bounty) + " Gold.", Warn = Cards[championId].Reason, }; tip.Stats.AddRange(LuneStats.Rows(definition)); diff --git a/docs/port/logs/s3/balance-s3a-bots.txt b/docs/port/logs/s3/balance-s3a-bots.txt new file mode 100644 index 0000000..9813b4f --- /dev/null +++ b/docs/port/logs/s3/balance-s3a-bots.txt @@ -0,0 +1,3 @@ +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 +referenz; 120; 60; 60; 0; 0; 10.3; 9; 12; 100/120; 0.0; 0; 0; 0; 0; 0; 0; 0; 0; 0 +erweitert; 120; 59; 61; 0; 0; 10.1; 9; 11; 104/120; 14.6; 287; 4035; 490; 60; 102; 42; 231; 333; 0 diff --git a/tools/dotnet/Luneraria.Balance/Program.cs b/tools/dotnet/Luneraria.Balance/Program.cs index 573c59b..af11108 100644 --- a/tools/dotnet/Luneraria.Balance/Program.cs +++ b/tools/dotnet/Luneraria.Balance/Program.cs @@ -84,10 +84,20 @@ public static class Program { 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; - for (int seed = 1; seed <= seeds; seed++) + // 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 }); + match.Setup(new MatchOptions { Seed = seed, HumanSlot = -1, Extended = extended, Profiles = rotation }); int firstTower = -1; while (match.Phase != Phase.Over && match.RoundNumber <= 80) { @@ -127,8 +137,8 @@ public static class Program 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, blue, red, draws, open, (double)rounds / seeds, min, max, firstTowerWins, firstTowerGames, - (double)income / (seeds * 6), barracks, exhausted, buybacks, blessings, sylvaraSeats, sylvaraWins, glyphs, cores, recipes)); + 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; }