champions-of-luneraria/Assets/Luneraria/View/LuneTrace.cs
Martin Stickelbroeck 3871b3e2e6 U2C Simulation/View Binding
Fester Simulationstakt und Ansichten, die nur lesen; Nachweis, dass die Partie
bei jeder Bildrate dieselbe ist.

- LuneClock: Akkumulator, ganze Schritte zu 0,05 s, Alpha zum Interpolieren.
- LuneMatchRunner: Host-Ablauf einer Bot-Partie (Planung über die
  Befehlsschnittstelle, Kampf im festen Takt, Abrechnung, nächste Runde);
  LuneMatchDriver reicht in Update nur Bildzeit weiter.
- LuneTrace: Fingerabdruck der Partie (jeder Schritt, jede Abrechnung) und der
  reine Kernlauf zum Vergleich.
- Ansichten: LuneMatchView, LuneUnitView (interpolierte Position, Blickrichtung,
  Clips aus Kampfereignissen, Teamring, Lebensbalken), LuneBuildingView mit
  Einsturz und Ruine aus den autoritativen HP, Turmblitze, LuneCameraRig wie
  lune_camera.gd, vorläufige Debug-Anzeige (kein HUD; U3 portiert das G3-HUD).
- Einheitenbibliothek: Kohlenbeißer aus 131 Teilen zu einem Mesh, Animator-
  Controller mit den Blender-Clips, Ring- und Balkenmeshes, Markermaterialien.
- Tests: EditMode (30/60/120 fps, unbegrenzt, Ruckler, 8x Tempo; Seed 2 und
  Seed 9 ab Runde 21) und PlayMode in der Szene bei echten Bildraten 30, 60,
  120 und unbegrenzt: überall derselbe Fingerabdruck wie der Kern.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 17:31:18 +02:00

86 lines
3.3 KiB
C#

using System;
using System.Collections.Generic;
using Luneraria.Core;
namespace Luneraria.View
{
/// <summary>
/// Fingerprint of a match (U2C): FNV-1a over every combat step (step, time, and per unit id,
/// alive, hit points, position, target) and every settlement (gains, lost buildings, winner).
/// Two runs are the same match exactly when their fingerprints are equal; the Unity match and
/// the plain core run are compared this way at any frame rate.
/// </summary>
public sealed class LuneTrace
{
const ulong Offset = 14695981039346656037UL;
const ulong Prime = 1099511628211UL;
ulong _hash = Offset;
public int Steps;
public readonly List<ulong> RoundHashes = new List<ulong>();
public readonly List<string> Rounds = new List<string>();
public ulong Hash { get { return _hash; } }
void Mix(ulong value)
{
for (int i = 0; i < 8; i++)
{
_hash ^= (value >> (i * 8)) & 0xFF;
_hash *= Prime;
}
}
void Mix(double value) { Mix((ulong)BitConverter.DoubleToInt64Bits(value)); }
void Mix(float value) { Mix((ulong)(uint)BitConverter.ToInt32(BitConverter.GetBytes(value), 0)); }
public void RecordStep(LuneCombat combat)
{
Steps++;
Mix((ulong)combat.Steps);
Mix(combat.Elapsed);
foreach (LuneUnit unit in combat.Units)
{
Mix((ulong)unit.Id);
Mix(unit.Alive ? 1UL : 0UL);
Mix(unit.Hp);
Mix(unit.Position.X);
Mix(unit.Position.Y);
Mix((ulong)(unit.Target != null ? unit.Target.Id : 0));
}
}
public void RecordSettlement(LuneMatch match)
{
Settlement settlement = match.LastSettlement;
foreach (int gain in settlement.Gains)
Mix((ulong)(uint)gain);
foreach (string building in settlement.BuildingsLost)
foreach (char c in building)
Mix((ulong)c);
Mix((ulong)(uint)match.Winner);
RoundHashes.Add(_hash);
Rounds.Add(string.Format("round {0}: {1} steps, lost [{2}], winner {3}", settlement.Round, match.Combat != null ? match.Combat.Steps : 0,
string.Join(",", settlement.BuildingsLost), match.Winner));
}
/// <summary>The same match without Unity: plan, begin, step to the end, round after round.</summary>
public static LuneTrace RunCore(MatchOptions options, int startRound, int extraGold, string profile, int maxRound = 60)
{
var match = LuneMatchRunner.NewMatch(options, startRound, extraGold);
var trace = new LuneTrace();
while (match.Phase != Phase.Over && match.RoundNumber <= maxRound)
{
for (int slot = 0; slot < 6; slot++)
LuneBot.Plan(match, slot, profile);
match.BeginCombat();
while (match.Phase == Phase.Combat)
{
match.StepCombat();
trace.RecordStep(match.Combat);
}
trace.RecordSettlement(match);
}
return trace;
}
}
}