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
44 changes: 43 additions & 1 deletion Assets/_Project/Scripts/AI.Data/AiBehaviorId.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,47 @@ public static class AiBehaviorId
/// behaviour journal V003.
/// </para>
/// <para>
/// NOT bumped either for naming those branches <see cref="GoalKind"/>.
/// Same four conditions, same order, same orders out; the canonical
/// match decides on the same tick with the same end state and a lab
/// run's artifacts came out byte-identical bar the measured runtime.
/// The refactor deliberately adds NO profile field — priorities in the
/// profile would move <see cref="ProfileHash"/>, and with it this very
/// identifier, which would have made the neutrality unprovable in the
/// artifacts it is printed into. A goal module that earns an off switch
/// brings one in the pull request that gives it a rule.
/// </para>
/// <para>
/// r8 IS THAT PULL REQUEST, and the identifier moves twice over: the
/// revision here because decisions change, and
/// <see cref="ProfileHash"/> because the rule ships with the off switch
/// that makes it measurable one-sided at all (finding M001).
/// <c>DefendHome</c> is the first goal to carry a rule of its own — the
/// units still waiting in the staging ring break off and walk home when
/// a visible armed enemy comes within
/// <see cref="AiProfile.DefendHomeCells"/> of their headquarters. What
/// it fixes is a defect as old as the staging cell itself (r3), not one
/// r6 introduced: a unit that has arrived is deliberately given no
/// order, so it hangs entirely on an auto-acquisition that reaches six
/// or seven cells while the staging cell sits twelve from the base.
/// </para>
/// <para>
/// TWO CORRECTIONS WENT INTO r8 BEFORE IT SHIPPED, both found by
/// reviewing the claim rather than the code, and neither moves
/// <see cref="ProfileHash"/> because neither adds a number. First: the
/// argument "the destination is static, so the suppression swallows
/// every repeat" only holds WHILE the defender walks — arriving clears
/// the standing order the suppression compares against, so the
/// headquarters cell went out again every cadence, one intent per
/// cadence for the whole siege, with the standing defenders flipped back
/// into movement each time. <c>DefendHome</c> now falls silent once
/// home, the way <c>Hold</c> does at the staging cell. Second: the goal
/// asked not to be retreating, which could only ever exclude a wounded
/// unit that had ALREADY ARRIVED — and arriving ends the retreat by the
/// rule right above it. Those units fell to <c>Hold</c> and stood twelve
/// cells out while the base they had run to burned.
/// </para>
/// <para>
/// r5 fixes two defects found in the review of r3/r4, both of which
/// change decisions and therefore the end state: the wave now waits for
/// what production can still deliver instead of a fixed cap (a single
Expand Down Expand Up @@ -116,7 +157,7 @@ public static class AiBehaviorId
/// retried an illegal placement forever once the all-of gate shipped.
/// </para>
/// </summary>
public const int Revision = 7;
public const int Revision = 8;

/// <summary>
/// Hash over every value of the shipped profile. Domain-separated like
Expand Down Expand Up @@ -167,6 +208,7 @@ public static ulong ComputeProfileHash(AiProfile profile)
writer.WriteInt32(profile.RetreatHealthPercent);
writer.WriteInt32(profile.RetreatDangerCells);
writer.WriteInt32(profile.WaveStrengthPoints);
writer.WriteInt32(profile.DefendHomeCells);
return writer.Digest();
}
}
Expand Down
54 changes: 52 additions & 2 deletions Assets/_Project/Scripts/AI.Data/AiProfile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,52 @@ namespace Nova.AI.Data
/// </summary>
public int RetreatDangerCells { get; }

/// <summary>
/// How near a visible ARMED enemy has to come to the own headquarters
/// before the units still waiting in the staging ring break off and
/// walk home to fight. <b>0 means off.</b>
/// <para>
/// WHY THE RULE HAS TO EXIST. A unit that has ARRIVED at the staging
/// cell is given no order at all — deliberately, because an order per
/// cadence to a standing unit is intent churn without a change of
/// behaviour. It therefore depends entirely on the D-087
/// auto-acquisition, and that reaches exactly as far as its weapon:
/// six cells for Legion infantry, seven for an Alliance rifleman. The
/// staging cell sits <see cref="StagingDistanceCells"/> — twelve — from
/// the headquarters. An attacker at the base is outside every one of
/// those ranges, so the waiting units do not ignore it: they cannot see
/// it. Measured in the canonical match: the Legion headquarters takes
/// 327 hits over 766 ticks while its own units stand a median of
/// thirteen cells away under Hold, and not one of them is attacking.
/// </para>
/// <para>
/// WHY TEN. It is the radius B003 was measured with, and it sits
/// between <see cref="RetreatDangerCells"/> (eight, so inside the base)
/// and the staging ring (sixteen, so a defender is not summoned by a
/// skirmish at the gathering point).
/// </para>
/// <para>
/// THERE IS NO SECOND RADIUS. A hysteresis value — "stay home until the
/// enemy is fourteen cells out" — is the obvious second number and is
/// deliberately absent: both destinations are STATIC cells, a defender
/// that has arrived stops being ordered at all, and the re-issue
/// suppression swallows the rest, so whether the trigger flutters is a
/// question for the intent column and not for a precaution. That is the
/// correction over the discarded <c>DefendBase</c> (journal V002), which
/// handed the WHOLE army a new destination every cadence and paid 23 %
/// more intents for it.
/// </para>
/// <para>
/// THE STATIC CELL IS NOT ENOUGH ON ITS OWN, and the first version of
/// this rule assumed it was. The suppression compares the STANDING
/// order, and arriving clears it — so the silence a defender needs once
/// it is home is written out in <c>SkirmishAiSystem.HasArrivedAtHome</c>
/// rather than falling out of the geometry. Measured before that
/// existed: one move intent per cadence for as long as the siege ran.
/// </para>
/// </summary>
public int DefendHomeCells { get; }

// ---- target scoring ----
//
// Four weights over ONE integer score, no scalar quality function:
Expand Down Expand Up @@ -219,7 +265,8 @@ public AiProfile(
int stagingToleranceCells,
int retreatHealthPercent,
int retreatDangerCells,
int waveStrengthPoints)
int waveStrengthPoints,
int defendHomeCells)
{
ProfileId = profileId ?? string.Empty;
DecisionTickInterval = decisionTickInterval;
Expand All @@ -240,6 +287,7 @@ public AiProfile(
RetreatHealthPercent = retreatHealthPercent;
RetreatDangerCells = retreatDangerCells;
WaveStrengthPoints = waveStrengthPoints;
DefendHomeCells = defendHomeCells;
}

/// <summary>
Expand Down Expand Up @@ -272,7 +320,8 @@ public bool Equals(AiProfile other) =>
&& StagingToleranceCells == other.StagingToleranceCells
&& RetreatHealthPercent == other.RetreatHealthPercent
&& RetreatDangerCells == other.RetreatDangerCells
&& WaveStrengthPoints == other.WaveStrengthPoints;
&& WaveStrengthPoints == other.WaveStrengthPoints
&& DefendHomeCells == other.DefendHomeCells;

public override bool Equals(object obj) => obj is AiProfile other && Equals(other);

Expand All @@ -299,6 +348,7 @@ public override int GetHashCode()
hash = (hash * 397) ^ RetreatHealthPercent;
hash = (hash * 397) ^ RetreatDangerCells;
hash = (hash * 397) ^ WaveStrengthPoints;
hash = (hash * 397) ^ DefendHomeCells;
return hash;
}
}
Expand Down
33 changes: 31 additions & 2 deletions Assets/_Project/Scripts/AI.Data/AiProfiles.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,34 @@ public static class AiProfiles
// 0 switches the rule off and restores the head count exactly
// (finding M001: a behaviour without an off setting cannot be
// measured one-sided).
waveStrengthPoints: 1200);
waveStrengthPoints: 1200,
// Defend home (r8). Ten cells around the own headquarters: inside
// the base, and short of the staging ring at sixteen, so a skirmish
// at the gathering point does not summon anybody.
//
// THE RULE EXISTS BECAUSE OF A MEASURED HOLE, not a hunch. A unit
// that has arrived at the staging cell is given no order at all —
// on purpose, an order per cadence to a standing unit is churn — so
// it depends entirely on the D-087 auto-acquisition, and that
// reaches six cells for Legion infantry and seven for an Alliance
// rifleman. The staging cell sits twelve cells from the base. An
// attacker at the headquarters is outside all of it. In the
// canonical match the Legion headquarters takes 327 hits over 766
// ticks while its own units stand a median of thirteen cells away
// under Hold, and not one of them attacks.
//
// The wave is INTERRUPTED, not released: the destination is the own
// headquarters, a static cell, so the defenders walk toward the
// fight rather than away from it, the re-issue suppression swallows
// the repeats on the way, and a defender that has arrived is not
// ordered any more at all (the suppression alone cannot carry that
// — arriving clears the standing order it compares against). That is
// the correction over the discarded DefendBase (V002), which handed
// the whole army a moving target every cadence and paid 23 % more
// intents for it.
//
// 0 switches the rule off and restores r7 exactly (M001).
defendHomeCells: 10);

/// <summary>
/// The old <c>AiFactionProfile</c> constructor defaults (power margin
Expand Down Expand Up @@ -173,6 +200,8 @@ public static class AiProfiles
retreatDangerCells: 8,
// Same reasoning again: there is no "legacy" strength value, the
// wave counted heads. 0 is the off setting.
waveStrengthPoints: 0);
waveStrengthPoints: 0,
// And no legacy defence value either — there was no defence rule.
defendHomeCells: 0);
}
}
127 changes: 127 additions & 0 deletions Assets/_Project/Scripts/AI.Data/GoalKind.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
namespace Nova.AI.Data
{
/// <summary>
/// What one unit is trying to do in THIS decision — a name for a condition
/// and its effect, and nothing else.
/// <para>
/// A GOAL IS NOT STATE. It is worked out fresh on every decision cadence
/// from the committed world and thrown away again; nothing stores it, and
/// therefore nothing has to serialize it. That is what keeps the skirmish AI
/// a pure function of the tick and the committed state after the goals were
/// named — the names describe the decision, they do not survive it.
/// </para>
/// <para>
/// It lives in this assembly rather than beside the rules because THREE
/// SURFACES HAVE TO AGREE ON THE WORDS: the simulation that picks a goal,
/// the lab that records which one was picked, and the panel that draws it.
/// Nova.AI.Data references Nova.Core and nothing else, so every one of them
/// can name a goal without pulling the simulation in behind it.
/// </para>
/// <para>
/// THE PRIORITY IS FIXED AND IT IS NOT THE NUMBER. When more than one
/// condition holds, the winner is decided by the order the tests are
/// written in <c>SkirmishAiSystem.ResolveGoal</c>, which is:
/// </para>
/// <list type="number">
/// <item><see cref="Retreat"/> — a wounded unit STILL RUNNING outranks
/// everything, or the pull-back could never reach the one unit it exists
/// for; one that has arrived is an ordinary waiting unit again</item>
/// <item><see cref="DefendHome"/> — the base burning outranks gathering for
/// a wave that will leave it burning</item>
/// <item><see cref="Attack"/></item>
/// <item><see cref="Hold"/></item>
/// <item><see cref="Advance"/></item>
/// </list>
/// <para>
/// THE NUMBERS ARE APPENDED, NEVER RENUMBERED, and that is why the two came
/// apart. A value is written into <c>goals.ndjson</c> as a bare integer;
/// renumbering to keep value order equal to priority order would silently
/// re-label every recorded run — the panel would print <c>attack</c> where
/// the file means <c>hold</c>, which is precisely the display error a
/// diagnostic tool must not have. Columns are appended in the artifacts for
/// the same reason, and a goal is a column.
/// </para>
/// <para>
/// The priority is fixed in code and not a profile value: priorities in the
/// profile would have moved <c>AiProfile.ProfileHash</c> while the first
/// step of this strand still had to be provably behaviour-neutral
/// (ROADMAP.md point 2). A module that needs an off setting brings one with
/// it, in the pull request that gives it a rule worth switching off —
/// <see cref="DefendHome"/> is the first that did.
/// </para>
/// </summary>
public enum GoalKind : byte
{
/// <summary>
/// No goal — the unit was not judged at all this decision, and for the
/// goal mask (<c>Nova.AI.IAiGoalOverride</c>) it means "leave this one to
/// the AI". Never the answer of the resolver: every combat unit the army
/// step looks at gets one of the five below.
/// </summary>
None = 0,

/// <summary>
/// Wounded, in danger, and pulling out: walk to the staging cell and
/// shoot at whoever is chasing. Outranks everything, because a rule that
/// cannot beat "you are out with the wave, keep going" can never pull
/// anybody back.
/// </summary>
Retreat = 1,

/// <summary>
/// Marching on the army's target: the shared attack target and the
/// shared destination. The wave is out, or this unit already is.
/// </summary>
Attack = 2,

/// <summary>
/// Standing at the staging cell with nothing to say. THE EFFECT IS
/// SILENCE, and that is the goal's whole content: a unit that is where
/// it belongs and gets told so again every cadence turns 23 actions per
/// minute into 40 without changing anything (behaviour journal V002).
/// </summary>
Hold = 3,

/// <summary>
/// Reinforcement on its way to the staging cell. No attack target while
/// it walks — an explicit order is released only by its target's death,
/// so aiming while not closing the distance silences the unit instead of
/// arming it (finding F001, journal V003).
/// </summary>
Advance = 4,

/// <summary>
/// The base is under attack and this unit was waiting to leave it:
/// break off, walk to the own headquarters, shoot at whoever is nearest.
/// Outranks <see cref="Attack"/> — gathering for a wave that marches
/// away from a burning base is the defect this goal exists for — and
/// gives way to <see cref="Retreat"/>, because a unit too wounded to
/// fight and still running is no defender. One that has already made it
/// home is: arriving ends the retreat, so it defends like anybody else
/// standing in the ring.
/// <para>
/// AND ONCE HOME IT SAYS NOTHING FURTHER. The destination is a static
/// cell, but that alone does not stop the order repeating — the re-issue
/// suppression reads the standing order, and arriving CLEARS the
/// standing order. A defender that has arrived therefore falls silent
/// explicitly, exactly the way <see cref="Hold"/> is silent at the
/// staging cell; without it the headquarters cell went out again every
/// cadence for the whole siege.
/// </para>
/// <para>
/// ONLY UNITS STILL INSIDE THE STAGING RING. A wave that is already out
/// keeps marching: "units that are out are never called back" is the r3
/// rule that made a wave a wave, and recalling them is the V002 failure
/// mode with a new name.
/// </para>
/// <para>
/// THE DESTINATION IS A STATIC CELL — the headquarters, which does not
/// move all match — and not the enemy. That is the whole difference to
/// the discarded <c>DefendBase</c>: a moving destination meant a fresh
/// order every cadence for every unit, 23 % more intents and a worse
/// match (journal V002).
/// </para>
/// </summary>
DefendHome = 5,
}
}
2 changes: 2 additions & 0 deletions Assets/_Project/Scripts/AI.Data/GoalKind.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Assets/_Project/Scripts/AI/AiFactionProfile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ public AiFactionProfile(string factionName, int targetPowerMargin = 30, int targ
stagingToleranceCells: shipped.StagingToleranceCells,
retreatHealthPercent: shipped.RetreatHealthPercent,
retreatDangerCells: shipped.RetreatDangerCells,
waveStrengthPoints: shipped.WaveStrengthPoints);
waveStrengthPoints: shipped.WaveStrengthPoints,
defendHomeCells: shipped.DefendHomeCells);
}

/// <summary>Binds a faction name to a fully specified profile — the tuning path.</summary>
Expand Down
Loading
Loading