Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion Assets/_Project/Scripts/AI.Data/AiBehaviorId.cs
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,19 @@ public static class AiBehaviorId
/// plays a non-neutral personality is still "r9" — the personality
/// is printed next to this identifier, not folded into it.
/// </para>
/// <para>
/// r10 fixes two defects of r9 found in the review of PR #144. The
/// flank pushed the staging cell perpendicular to the diagonal, to
/// Chebyshev 22 from the HQ and outside the staging ring at 16, so
/// every unit walking there counted as committed and marched alone; the
/// flank now turns the cell and keeps it at the staging distance. And
/// the Turtle's home radius (14) reached past the staging cell; it now
/// stops one cell short. The neutral personality and the canonical
/// outcome are untouched — the revision moves because a flanked or
/// Turtle personality decides differently than it did under r9.
/// </para>
/// </summary>
public const int Revision = 9;
public const int Revision = 10;

/// <summary>
/// Hash over every value of the shipped profile. Domain-separated like
Expand Down
23 changes: 19 additions & 4 deletions Assets/_Project/Scripts/AI.Data/AiPersonality.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ public static AiPersonality Create(
army += 4; harvesters = 1; retreat += 5;
break;
case AiOpening.Turtle:
// home 4 is what the Turtle asks for; Apply caps the result
// under the staging distance (11 on the shipped profile).
army += 2; retreat += 5; home = 4; platforms = 1 + (extraDefensePlatform & 1);
break;
}
Expand All @@ -133,9 +135,15 @@ public static AiPersonality Create(
/// on every machine, which is what lets both peers of a networked match
/// agree without a message.
/// <para>
/// One hash, independent bit ranges: opening from the low bits, flank
/// from the next byte, and so on, so that no field is a function of
/// another and 512 seeds cover every combination of the small ones.
/// One hash, one shift per field: opening <c>h % 4</c>, flank
/// <c>(h &gt;&gt; 8) % 3</c>, layout <c>(h &gt;&gt; 16) % 8</c>, army jitter
/// <c>(h &gt;&gt; 24) % 3</c>, retreat jitter <c>(h &gt;&gt; 32) % 3</c>, extra
/// platform bit 40. A modulo by 3 reads every bit above its shift, not
/// one byte, so the ranges overlap; the shared high bits are mixed with
/// bits only that field reads, which keeps the fields as good as
/// independent. 4·3·8·3·3·2 = 1728 combinations exist;
/// <c>Derive_IsPure_AndCoversEveryBandOver512Seeds</c> checks that every
/// band of every field is reached, not that every combination is.
/// </para>
/// </summary>
public static AiPersonality Derive(ulong matchSeed, byte slot)
Expand Down Expand Up @@ -174,6 +182,13 @@ public AiProfile Apply(AiProfile baseProfile)
if (retreat > 0) retreat = Math.Min(95, Math.Max(0, retreat + RetreatPercentDelta));
int home = baseProfile.DefendHomeCells;
if (home > 0) home = Math.Max(0, home + DefendHomeCellsDelta);
// A wider home guard stops one cell short of the gathering point:
// the radius stays under the staging distance, so a skirmish at the
// staging cell does not call the waiting wave home (the invariant
// on AiProfiles.Ms1Canonical.DefendHomeCells; review of PR #144,
// B2). Only what the personality ADDS is capped — a base profile
// that set a wider radius itself keeps it.
home = Math.Min(home, Math.Max(baseProfile.DefendHomeCells, baseProfile.StagingDistanceCells - 1));

return new AiProfile(
profileId: baseProfile.ProfileId + "+" + Opening.ToString().ToLowerInvariant(),
Expand All @@ -198,7 +213,7 @@ public AiProfile Apply(AiProfile baseProfile)
defendHomeCells: home);
}

/// <summary>Compact description for the debug HUD and logs, e.g. <c>Rush, flank +10, layout 5, army 8</c>.</summary>
/// <summary>Compact description for the debug HUD and logs, e.g. <c>Rush, flank +10, layout 5, army delta -3</c>.</summary>
public override string ToString()
{
string flank = FlankOffsetCells > 0 ? "+" + FlankOffsetCells : FlankOffsetCells.ToString();
Expand Down
46 changes: 36 additions & 10 deletions Assets/_Project/Scripts/AI/SkirmishAiSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -923,14 +923,22 @@ private UnitRole NextBuildingToPlan(bool hasRefinery, bool hasBarracks, int defe
/// cell is the SAME for the whole match — a unit ordered there is not
/// re-ordered on the next cadence.
/// <para>
/// A personality with a flank offset shifts the cell sideways,
/// perpendicular to the line toward the enemy. The offset is a match
/// constant and the shift reads no live state, so the cell stays a
/// pure function of map and personality — the same every cadence.
/// Deliberately no footprint test: one would let a building placed
/// later flip the cell mid-match, and the unshifted cell never had one
/// either. That the shifted cells stay off the rock is pinned for the
/// canonical map by <c>AiPersonalityTests.FlankedStagingCells_AreInsideTheGridAndWalkable</c>.
/// A personality with a flank offset turns the cell sideways: shifted
/// perpendicular to the line toward the enemy, then scaled back so it
/// stays within <see cref="AiProfile.StagingDistanceCells"/> (Chebyshev)
/// of the HQ. That bound is what the wave rules stand on —
/// <see cref="IsCommittedToTheWave"/> measures its ring from the HQ, and
/// a gathering point outside the ring turns every unit walking there
/// into a lone attacker. The offset is a match constant and the turn
/// reads no live state, so the cell stays a pure function of map and
/// personality — the same every cadence. Deliberately no footprint
/// test: one would let a building placed later flip the cell
/// mid-match, and the unflanked cell never had one either. Both
/// properties are pinned for the canonical map by
/// <c>AiPersonalityTests.FlankedStagingCells_StayInsideTheRingAndWalkable</c>
/// (both slots, mirrored) and
/// <c>AiPersonalityTests.FlankedStagingCell_IsTheOneTheAiReports</c>
/// (slot 1, the production value).
/// </para>
/// <para>
/// Integer division truncates, which is deterministic and identical on
Expand Down Expand Up @@ -960,8 +968,26 @@ private void GetStagingCell(int hqCellX, int hqCellY, out int cellX, out int cel
int flank = Personality.FlankOffsetCells;
if (flank != 0)
{
cellX = ClampToGrid(cellX + (-dy * flank / span));
cellY = ClampToGrid(cellY + (dx * flank / span));
// Shift perpendicular to the line, then scale the offset back
// onto the staging distance: the flank TURNS the cell around
// the HQ instead of pushing it outward. Without the second step
// a diagonal line moves the cell on both axes — on the
// Glutrinne to Chebyshev 22 from the HQ, outside the staging
// ring at 16 — and every unit walking there counted as
// committed and marched alone (review of PR #144, B1). The
// scale-back keeps the direction and leaves an offset that is
// already inside (an axis-aligned line) untouched. The HQ is
// inside the grid, so the clamp can only shorten the offset.
int offsetX = cellX - hqCellX + (-dy * flank / span);
int offsetY = cellY - hqCellY + (dx * flank / span);
int reach = Math.Max(Math.Abs(offsetX), Math.Abs(offsetY));
if (reach > distance)
{
offsetX = offsetX * distance / reach;
offsetY = offsetY * distance / reach;
}
cellX = ClampToGrid(hqCellX + offsetX);
cellY = ClampToGrid(hqCellY + offsetY);
}
}

Expand Down
28 changes: 26 additions & 2 deletions Assets/_Project/Scripts/Gameplay/Match/MatchBootstrap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,9 @@ public sealed class MatchBootstrap : MonoBehaviour
/// game. Off by default: the canonical seed stays the default for
/// tests and harnesses (the canonical opening-position hash depends on
/// it); the main menu switches this on. Network matches take their
/// seed from the relay offer and ignore it.
/// seed from the relay offer and ignore it. <c>NOVA_MATCH_SEED</c>
/// pins the seed of every local start whether this is on or off — see
/// <see cref="ApplyLocalMatchSeed"/>.
/// </summary>
public bool FreshSeedPerLocalMatch { get; set; }

Expand Down Expand Up @@ -822,6 +824,16 @@ private void BuildOpening(MatchConfig config)
/// otherwise a fresh non-zero seed is drawn when
/// <see cref="FreshSeedPerLocalMatch"/> is on. The seed is the only
/// input of the AI personality, so a logged seed reproduces the match.
/// <para>
/// THE PIN APPLIES TO EVERY LOCAL START, with or without
/// <see cref="FreshSeedPerLocalMatch"/> — on purpose: a match reported
/// from the menu has to be reproducible through AutoStart and the
/// harnesses too. The price is that a variable left in the shell moves
/// those starts off the canonical seed, so outside the menu the pin is
/// logged as a warning, and a value that does not parse is never
/// dropped silently. The value is always read as hex: a decimal
/// "12345" pins 0x12345, which the log line shows.
/// </para>
/// </summary>
private void ApplyLocalMatchSeed(MatchConfig config)
{
Expand All @@ -835,9 +847,21 @@ private void ApplyLocalMatchSeed(MatchConfig config)
if (ulong.TryParse(hex, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out ulong parsed))
{
config.Seed = parsed;
Debug.Log($"[MatchBootstrap] Match seed pinned by NOVA_MATCH_SEED: 0x{parsed:X16}.");
if (FreshSeedPerLocalMatch)
{
Debug.Log($"[MatchBootstrap] Match seed pinned by NOVA_MATCH_SEED: 0x{parsed:X16}.");
}
else
{
Debug.LogWarning(
$"[MatchBootstrap] Match seed pinned by NOVA_MATCH_SEED: 0x{parsed:X16}, " +
"replacing the seed of a start outside the menu (AutoStart, harness, PlayMode).");
}
return;
}
Debug.LogWarning(
$"[MatchBootstrap] NOVA_MATCH_SEED '{pinned}' is not a hex u64 (at most 16 hex digits, " +
"optional 0x) and was ignored; the match seed is not pinned.");
}

if (!FreshSeedPerLocalMatch) return;
Expand Down
41 changes: 39 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,15 +82,22 @@ die Versionierung folgt (in der aktuellen Doku-Phase) dem Dokumentationsstand de
- **Eröffnung:** ausgewogen; Ansturm (Kaserne vor Raffinerie, kleinere und
frühere Welle); Ökonom (dritter Sammler, größere Armee); Igel (ein bis zwei
Abwehrplattformen Richtung Front).
- **Sammelpunkt:** um zehn Felder zur Seite versetzt oder mittig.
- **Sammelpunkt:** mittig oder zur Seite gedreht (Flanke ±10 Felder), immer
im selben Abstand vom HQ wie der mittige.
- **Bauplätze:** eine von acht Suchrichtungen, damit die Basis jedes Mal
anders aussieht.
- **Profilwerte:** kleine Streuung in festen Bändern.
- Das Debug-HUD (F3) und das Startlog zeigen Persönlichkeit und Seed;
`NOVA_MATCH_SEED=<hex>` stellt eine gemeldete Partie nach.
`NOVA_MATCH_SEED=<hex>` stellt eine gemeldete Partie nach. Die Variable
greift bei jedem lokalen Start, auch außerhalb des Menüs (AutoStart,
Harness, PlayMode); dort schreibt sie eine Warnung ins Log. Ein ungültiger
Wert wird ebenfalls mit einer Warnung gemeldet, nicht still übergangen.
- Die neutrale Persönlichkeit spielt exakt wie bisher. Der gepinnte Ausgang
der kanonischen KI-Partie bleibt stehen, `AiBehaviorId` steigt auf r9.
Netzpartien sind unberührt.
- **Der kanonische Seed spielt nicht mehr neutral.** Er leitet einen
Ansturm-Gegner ab. Wer ohne Menü startet (AutoStart, Harness), spielt also
gegen Ansturm, nicht gegen das Verhalten von r8.
- **Bewusst klein:** Solange die KI nur Infanterie und drei Gebäude kennt,
variieren Bauordnung, Basisbild, Sammelpunkt und Angriffszeitpunkt, mehr
nicht. Panzer, Konter-Bau und Tech folgen als Stufe B.
Expand Down Expand Up @@ -424,6 +431,36 @@ die Versionierung folgt (in der aktuellen Doku-Phase) dem Dokumentationsstand de
bei 2 AE/Tick, bis eine gespielte Balance-Kalibrierung belastbare Werte gibt

### Behoben
- **Die KI-Flanke hebelt die Angriffswelle nicht mehr aus (D-114, Review von
#144).** In zwei Dritteln aller Persönlichkeiten griff die KI nicht in Wellen
an, sondern Einheit für Einheit.
- **Ursache:** Die Flanke verschob den Sammelpunkt senkrecht zur Linie zum
Gegner. Auf der diagonalen Glutrinne bewegte das den Punkt auf beiden
Achsen um zehn Felder, also 22 Felder vom HQ statt 12. Der Wellenring
reicht aber nur bis 16. Jede Einheit auf dem Weg zum Sammelpunkt galt
deshalb als „schon mit einer Welle draußen“ und marschierte allein los.
Welle, Stärkegate und Heimverteidigung liefen leer: gemessen 91 bis 222
Angriffsbefehle ohne fertige Welle je Partie, ohne Flanke null.
- **Behebung:** Die Flanke dreht den Sammelpunkt jetzt um das HQ, statt ihn
hinauszuschieben. Er bleibt im Abstand des mittigen Sammelpunkts (12
Felder), die Richtung der Verschiebung bleibt erhalten. Gemessen: null
Angriffsbefehle ohne fertige Welle, bei beiden Fraktionen und allen
Flanken.
- **Igel:** Sein erweiterter Heimradius (10 + 4 = 14) reichte über den
Sammelpunkt hinaus. Ein Gegner dort rief die ganze wartende Welle nach
Hause. Der Radius endet jetzt ein Feld vor dem Sammelpunkt, auf dem
ausgelieferten Profil also bei 11. Gedeckelt wird nur, was die
Persönlichkeit hinzufügt.
- Die neutrale Persönlichkeit und der Pfad ohne Flanke ändern sich nicht;
der gepinnte Ausgang der kanonischen KI-Partie bleibt stehen.
`AiBehaviorId` steigt auf r10, weil geflankte und Igel-Persönlichkeiten
anders entscheiden als unter r9. Eine neue D-ID gibt es nicht, D-114
trägt eine Berichtigung.
- **Tests:** Der Spiegeltest prüfte bisher den Footprint-Ursprung statt der
echten HQ-Zelle. Er liest das HQ jetzt aus der gebauten Partie und
vergleicht sich mit dem Sammelpunkt, den die KI selbst meldet. Neu sind
ein Regressionstest (keine Angriffsbefehle vor der ersten fertigen Welle)
und ein Test für den Igel-Radius.
- **Die Bauverweigerung nennt jetzt den Grund (#135).** Aus der Proberunde vom
31.08.2026 kamen zwei Beschwerden, die sich als **dieselbe** erwiesen: „Der
zweite Atlas kann keine Gebäude bauen" und „Ich konnte in der Kartenmitte kein
Expand Down
23 changes: 21 additions & 2 deletions docs/production/DecisionLog.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Decision Log

**Version:** 1.46.0 | **Status:** aktiv (laufend) | **Verantwortungsbereich:** Game Director / Lead Technical Director / Project Owner | **Sprint:** 21
**Version:** 1.46.1 | **Status:** aktiv (laufend) | **Verantwortungsbereich:** Game Director / Lead Technical Director / Project Owner | **Sprint:** 21

## Zweck

Expand Down Expand Up @@ -3624,7 +3624,9 @@ das Fähigkeiten-Set des Computergegners je Partie neu.
- **Ansturm**: Kaserne vor Raffinerie, kleinere Armee, die früher losschlägt;
- **Ökonom**: dritter Sammler und größere Armee;
- **Igel**: ein bis zwei Abwehrplattformen zwischen HQ und Sammelpunkt;
- einen seitlich versetzten Sammelpunkt (−10, 0 oder +10 Felder);
- einen zur Seite gedrehten Sammelpunkt (Flanke −10, 0 oder +10 Felder),
der im Abstand des mittigen Sammelpunkts vom HQ bleibt (siehe
Berichtigung);
- eine von acht Suchrichtungen für Bauplätze;
- kleine Streuungen der Profilwerte in festen Bändern.
- Die neutrale Persönlichkeit (`default`) spielt exakt wie r8. Der gepinnte
Expand Down Expand Up @@ -3662,6 +3664,22 @@ berührt.
Ansturm-Gegner ab. Wer ohne Menü startet (AutoStart, Harness), spielt diesen,
nicht r8.

**Berichtigung (2026-10-17):** Die Erstfassung nannte den Sammelpunkt
„seitlich versetzt“, und so war er auch umgesetzt: zehn Felder senkrecht zur
Linie zum Gegner. Auf der diagonalen Glutrinne verschob das den Punkt auf
beiden Achsen, 22 Felder vom HQ statt 12, und damit aus dem Wellenring (16)
hinaus. Jede Einheit auf dem Weg dorthin galt als Teil einer schon
abmarschierten Welle und griff allein an. Welle, Stärkegate und
Heimverteidigung liefen in zwei Dritteln aller Persönlichkeiten leer. Das
unabhängige Review von PR #144 hat das gefunden und gemessen. Die Flanke
**dreht** den Sammelpunkt jetzt um das HQ: verschoben wie bisher, dann auf
den Sammelpunktabstand zurückskaliert. Im selben Zug endet der erweiterte
Heimradius des Igels ein Feld vor dem Sammelpunkt (11 statt 14). Sonst ruft
ein Gefecht dort die wartende Welle nach Hause, entgegen der Invariante am
ausgelieferten Profil. Die neutrale Persönlichkeit und der kanonische
Ausgang sind unberührt. `AiBehaviorId` steigt auf r10; eine eigene D-ID gibt
es dafür nicht (Inhaberentscheidung vom 2026-10-17).

## Offene Punkte

- Alle Sprint-4-Review-Befunde (105, davon 9 kritisch): 7 entscheidungsbedürftige kritische Befunde sind durch D-043–D-052 entschieden.
Expand Down Expand Up @@ -3742,6 +3760,7 @@ berührt.

| Version | Datum | Änderung | Autor |
|---|---|---|---|
| 1.46.1 | 2026-10-17 | D-114 berichtigt: Die Sammelpunkt-Flanke dreht den Punkt um das HQ, statt ihn aus dem Wellenring (16) hinauszuschieben; der Igel-Heimradius endet ein Feld vor dem Sammelpunkt. Befunde B1 und B2 aus dem Review von PR #144; `AiBehaviorId` r10, keine neue D-ID | Dennis Westermann / Orchestrator |
| 1.46.0 | 2026-10-17 | D-114 aufgenommen: Die Skirmish-KI bekommt je Partie eine Persönlichkeit als reine Funktion aus Match-Seed und Slot (Eröffnung, Sammelpunkt-Flanke, Bauplatz-Suchrichtung, Profilstreuung); das Hauptmenü zieht je lokaler Partie einen frischen Seed. Die Versionen 1.42.0 bis 1.45.0 und D-110 bis D-113 trägt der Asset-Pass auf `feat/generals-look-and-feel` | Dennis Westermann / Orchestrator |
| 1.41.0 | 2026-08-18 | **D-108 in der Prämisse berichtigt und in der Sache neu gefasst.** Die Erstfassung behauptete, der Code lasse bereits jedes Gebäude als Bauanker gelten, und wollte das nur aussprechen — tatsächlich prüft `IsInsideBuildInfluence` seit D-104 auf HQ, Lager und Kraftwerk. Der ausführende Agent hat den Widerspruch beim Umsetzen von Paket 21.1 gefunden und angehalten. Der Inhaber hat daraufhin in Kenntnis der Lage neu entschieden: die Ankerliste wird bewusst geöffnet, **jedes eigene fertiggestellte Gebäude wird Anker**. Damit ist D-108 eine Verhaltensänderung (`RulesHash64`, Baselines, kanonischer KI-Pin), keine Festschreibung. `MinimumBuildingDistanceCells` bleibt bei 2 — die Messung aus 21.1 (15/23/23) weist die gemeldete Enge nicht als Abstandsproblem aus | Dennis Westermann / Orchestrator |
| 1.40.0 | 2026-08-17 | D-108 und D-109 aufgenommen (Sprint 21, aus Testbericht T-01): Territorium wächst **kriechend an jedem eigenen Bauanker** — die bestehende Regel wird festgeschrieben statt geändert, und die gemeldete Enge wird an `MinimumBuildingDistanceCells` gemessen, bevor ein Wert fällt. Die Kartenmitte wird ein Gebiet mit schmalen Zufahrten, mit der verbindlichen Auflage, dass Begehbarkeit und Optik aus **einer** Struktur stammen — heute streut `GlutrinneBlockoutView` begehbare Deko-Felsen, während niemand ausser `ConstructionSystem` in das `CostField` schreibt | Dennis Westermann / Orchestrator |
Expand Down
Loading
Loading