champions-of-luneraria/Assets/Luneraria/Core/Simulation/MatchModel.cs
Martin Stickelbroeck 4d6ed17dca U1: Regelkern in C#, verhaltensgleich zur Godot-Referenz
Vollständiger, engineunabhängiger Regelkern (Luneraria.Core, ohne
UnityEngine): Partie mit sechs Plätzen und zwei Teams, private Planung mit
atomarer Übernahme, Gold, Käufe, Aufwertungen, Formation, drei Lanes mit
zwei Laneturmen und zwei Basistürmen je Team, Ancients, unbegrenzte Wellen,
Helden mit Stufen, Items und Talenten, Angriffschampions, Bossgrotte mit
Anspruch, Entsendung, Kopfgeld und dreirundiger Sperre, Bots und
Siegbedingung (nur Ancient-Fall). Serverautoritative Befehlsschnittstelle.

- Deterministisch: fester Takt, geordnete Maps, fortlaufende IDs, kein
  Zufall, keine Uhr, keine Threads
- Tabellen einmal aus Godot erzeugt (tools/port/gen_content.py)
- Golden-Referenzen aus Godot (tools/godot-golden): 19 Szenarien,
  120 Snapshots, 51.221 Werte, Abweichung 0 (Toleranzen dokumentiert)
- Tests: content 75, core 91, m2 73, golden 157, determinism 6 = 402/402
- Leistung (M5): Simulationsschritt 0,259 ms statt 1,587 ms in GDScript
- Referenzfehler R1 im Bot-Gedächtnis bewusst übernommen und dokumentiert
- Geplante Helden (Lucy, Ghaliga, Siegfried, Feena) im Datenmodell, inaktiv
- Doku unter docs/port (U1-RULES, PORT-STATUS, PORT-QA, U2 bis U5)

Unity-Editor-Lauf offen: Die Unity-Lizenz ist noch nicht aktiviert; gebaut
und getestet mit dem .NET 8 SDK auf Unitys Sprachstand (C# 9, .NET
Standard 2.1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 14:31:18 +02:00

236 lines
8.8 KiB
C#

using System.Collections.Generic;
namespace Luneraria.Core
{
public enum Phase { Planning, Combat, Over }
/// <summary>One persistent troop of a player (committed roster or planning draft).</summary>
public sealed class RosterEntry
{
/// <summary>"p{slot}-{seq}": unique per match; ownership is the slot in the uid.</summary>
public string Uid = "";
public int Seq;
public string TypeId = "";
/// <summary>top, mid, bot or grotto (one round).</summary>
public string Lane = "";
public int Invested;
/// <summary>Bought in the current planning (full refund on sale).</summary>
public bool Fresh;
public RosterEntry Clone() { return (RosterEntry)MemberwiseClone(); }
}
/// <summary>The single hero of a player.</summary>
public sealed class HeroState
{
public string Uid = "";
public int Seq;
public string HeroId = "";
public string Lane = "";
public int Xp;
public List<string> Items = new List<string>();
public List<string> Talents = new List<string>();
public int Invested;
public bool Fresh;
/// <summary>Items bought in the current planning (full refund on sale).</summary>
public List<string> FreshItems = new List<string>();
public HeroState Clone()
{
var copy = (HeroState)MemberwiseClone();
copy.Items = new List<string>(Items);
copy.Talents = new List<string>(Talents);
copy.FreshItems = new List<string>(FreshItems);
return copy;
}
}
/// <summary>An attack champion booking: bought in round n, fights once in round n + 1.</summary>
public sealed class Booking
{
public string Uid = "";
public int Seq;
public string ChampionId = "";
public string Lane = "";
public int Cost;
/// <summary>0 while uncommitted (draft); the round it fights in once committed.</summary>
public int DeployRound;
public Booking Clone() { return (Booking)MemberwiseClone(); }
}
/// <summary>Private planning of one player; validated and committed atomically at combat start.</summary>
public sealed class Draft
{
public int Gold;
public List<RosterEntry> Roster = new List<RosterEntry>();
public int NextSeq;
public HeroState Hero;
public List<Booking> NewBookings = new List<Booking>();
/// <summary>Lane chosen by the boss chooser in this planning, "" if unchanged.</summary>
public string BossLane = "";
public Draft Clone()
{
var copy = new Draft { Gold = Gold, NextSeq = NextSeq, BossLane = BossLane, Hero = Hero != null ? Hero.Clone() : null };
foreach (RosterEntry entry in Roster)
copy.Roster.Add(entry.Clone());
foreach (Booking booking in NewBookings)
copy.NewBookings.Add(booking.Clone());
return copy;
}
}
/// <summary>One of six seats. Team = slot / 3, lane = slot % 3 (top, mid, bot).</summary>
public sealed class Player
{
public int Slot;
public int Team;
public string Lane = "";
public bool IsHuman;
public string Name = "";
public string Bund = "";
public int Gold;
public List<RosterEntry> Roster = new List<RosterEntry>();
public HeroState Hero;
public List<Booking> Bookings = new List<Booking>();
public bool Ready;
public int Earned;
public int Spent;
public int NextSeq;
public Draft Draft;
public List<Draft> Undo = new List<Draft>();
}
public sealed class BossClaim
{
public int Team;
public int WonRound;
public int DeployRound;
public int ChooserSlot;
public string Lane = "";
public BossClaim Clone() { return (BossClaim)MemberwiseClone(); }
}
/// <summary>Public boss state: the grotto opens from NextAvailableRound; Claim is null or the conquest.</summary>
public sealed class BossState
{
public int NextAvailableRound = 1;
public BossClaim Claim;
/// <summary>Round whose settlement was already applied (idempotence), 0 = none.</summary>
public int SettledRound;
}
public sealed class BossLogEntry
{
public int Round;
/// <summary>deployed, won or failed.</summary>
public string Event = "";
public int Team = -1;
public string Lane = "";
public int Chooser = -1;
}
/// <summary>Public announcement of the current round: a champion or the deployed boss.</summary>
public sealed class Announcement
{
/// <summary>champion or boss.</summary>
public string Kind = "";
public int Team;
public int Slot;
public string Lane = "";
public string ChampionId = "";
public int Bounty;
}
public sealed class Settlement
{
public int Round;
public int[] Gains = new int[6];
public int Income;
public List<string> BuildingsLost = new List<string>();
public string EndReason = "";
public double CombatTime;
public OrderedMap<string, int> LaneKills = new OrderedMap<string, int>();
public OrderedMap<string, int> Xp = new OrderedMap<string, int>();
/// <summary>none, won, failed, deployed or repeat.</summary>
public string Boss = "none";
}
public sealed class HistoryEntry
{
public int Round;
public string EndReason = "";
public double CombatTime;
public int[] Buildings = new int[2];
public int[] Gold = new int[6];
public List<string> Lost = new List<string>();
public string Boss = "";
public int[] HeroLevels = new int[6];
}
public sealed class MatchOptions
{
public int Seed = 1;
/// <summary>-1 for six bots.</summary>
public int HumanSlot = 1;
/// <summary>Six Bünde; missing entries follow the seed.</summary>
public List<string> Bunds = new List<string>();
/// <summary>Optional fixed bot profile per slot (balance runs).</summary>
public List<string> Profiles = new List<string>();
}
/// <summary>Bot-private notes per slot, derived only from its own actions and public outcomes.</summary>
public sealed class BotMemory
{
public bool HasChampionRound;
public int ChampionRound;
public bool HasBossFields;
public double Bar;
public int Sent;
}
public struct CommandResult
{
public bool Ok;
public string Message;
public CommandResult(bool ok, string message)
{
Ok = ok;
Message = message;
}
}
/// <summary>
/// A player's intent during planning. Clients only ever send these; the authority applies
/// them through LuneMatch.Submit. Null fields mean "not given" (e.g. buy without lane uses
/// the player's own lane).
/// </summary>
public sealed class Command
{
public string Type = "";
public string TypeId;
public string Lane;
public string Uid;
public string HeroId;
public string ItemId;
public string ChampionId;
public int? Tier;
public int? Choice;
public static Command Buy(string typeId, string lane = null) { return new Command { Type = "buy", TypeId = typeId, Lane = lane }; }
public static Command Upgrade(string uid) { return new Command { Type = "upgrade", Uid = uid }; }
public static Command Sell(string uid) { return new Command { Type = "sell", Uid = uid }; }
public static Command Assign(string uid, string lane) { return new Command { Type = "assign", Uid = uid, Lane = lane }; }
public static Command HeroBuy(string heroId) { return new Command { Type = "hero_buy", HeroId = heroId }; }
public static Command HeroCancel() { return new Command { Type = "hero_cancel" }; }
public static Command ItemBuy(string itemId, string uid = null) { return new Command { Type = "item_buy", ItemId = itemId, Uid = uid }; }
public static Command ItemSell(string itemId) { return new Command { Type = "item_sell", ItemId = itemId }; }
public static Command Talent(int tier, int choice) { return new Command { Type = "talent", Tier = tier, Choice = choice }; }
public static Command ChampionBook(string championId, string lane = null) { return new Command { Type = "champion_book", ChampionId = championId, Lane = lane }; }
public static Command ChampionCancel(string uid) { return new Command { Type = "champion_cancel", Uid = uid }; }
public static Command BossLaneChoice(string lane) { return new Command { Type = "boss_lane", Lane = lane }; }
}
}