Introduce variety arc scoring penalties and enhance interest handling.
- Implement scoring penalties for consecutive same variety arc shows - Update InterestVariety and InterestScoring to track arc streaks - Add new commands for noting interest variety in AgentHarness - Expand HarnessScenarios to test new variety arc mechanics - Increment version to 0.28.34 in mod.json and scoring-model.json
This commit is contained in:
parent
7d23f590db
commit
394968a956
8 changed files with 269 additions and 7 deletions
|
|
@ -2732,6 +2732,44 @@ public static class AgentHarness
|
||||||
Emit(cmd, ok: true, detail: "variety_cleared");
|
Emit(cmd, ok: true, detail: "variety_cleared");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "interest_variety_note":
|
||||||
|
{
|
||||||
|
// Stamp the current (or pending needle in value) as the last shown variety arc.
|
||||||
|
InterestCandidate c = InterestDirector.CurrentCandidate;
|
||||||
|
string needle = (cmd.value ?? "").Trim();
|
||||||
|
if (!string.IsNullOrEmpty(needle))
|
||||||
|
{
|
||||||
|
var pending = new System.Collections.Generic.List<InterestCandidate>(64);
|
||||||
|
InterestRegistry.CopyPending(pending);
|
||||||
|
for (int i = 0; i < pending.Count; i++)
|
||||||
|
{
|
||||||
|
InterestCandidate p = pending[i];
|
||||||
|
if (p?.Key != null
|
||||||
|
&& p.Key.IndexOf(needle, StringComparison.OrdinalIgnoreCase) >= 0)
|
||||||
|
{
|
||||||
|
c = p;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c == null)
|
||||||
|
{
|
||||||
|
_cmdFail++;
|
||||||
|
Emit(cmd, ok: false, detail: "no candidate to note");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
InterestVariety.NoteSelection(c, updateMix: false);
|
||||||
|
InterestScoring.RecalcTotal(c);
|
||||||
|
_cmdOk++;
|
||||||
|
Emit(
|
||||||
|
cmd,
|
||||||
|
ok: true,
|
||||||
|
detail: $"arc={InterestVariety.LastArcKey} streak={InterestVariety.ArcStreak} key={c.Key}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case "scoring_reload":
|
case "scoring_reload":
|
||||||
{
|
{
|
||||||
bool ok = InterestScoringConfig.Reload();
|
bool ok = InterestScoringConfig.Reload();
|
||||||
|
|
@ -6996,8 +7034,35 @@ public static class AgentHarness
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (win != null)
|
||||||
|
{
|
||||||
|
InterestScoring.RecalcTotal(win);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lose != null)
|
||||||
|
{
|
||||||
|
InterestScoring.RecalcTotal(lose);
|
||||||
|
}
|
||||||
|
|
||||||
pass = win != null && lose != null && win.TotalScore >= lose.TotalScore;
|
pass = win != null && lose != null && win.TotalScore >= lose.TotalScore;
|
||||||
detail = $"win={win?.Key}({win?.TotalScore:0.#}) lose={lose?.Key}({lose?.TotalScore:0.#})";
|
detail = $"win={win?.Key}({win?.TotalScore:0.#}) lose={lose?.Key}({lose?.TotalScore:0.#})"
|
||||||
|
+ $" arc={InterestVariety.LastArcKey} streak={InterestVariety.ArcStreak}";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "variety_arc":
|
||||||
|
{
|
||||||
|
string want = (cmd.value ?? "").Trim();
|
||||||
|
string have = InterestVariety.LastArcKey ?? "";
|
||||||
|
pass = !string.IsNullOrEmpty(want)
|
||||||
|
&& have.IndexOf(want, StringComparison.OrdinalIgnoreCase) >= 0;
|
||||||
|
detail = $"arc='{have}' want='{want}' streak={InterestVariety.ArcStreak}";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "variety_arc_streak":
|
||||||
|
{
|
||||||
|
int want = ParseInt(cmd.value, 1);
|
||||||
|
pass = InterestVariety.ArcStreak == want;
|
||||||
|
detail = $"streak={InterestVariety.ArcStreak} want={want} arc={InterestVariety.LastArcKey}";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "civic_boost":
|
case "civic_boost":
|
||||||
|
|
|
||||||
|
|
@ -1939,6 +1939,19 @@ internal static class HarnessScenarios
|
||||||
Step("ap44", "assert", expect: "tip_contains", value: "RealFight"),
|
Step("ap44", "assert", expect: "tip_contains", value: "RealFight"),
|
||||||
Step("ap45", "assert", expect: "tip_not_contains", value: "KingStroll"),
|
Step("ap45", "assert", expect: "tip_not_contains", value: "KingStroll"),
|
||||||
|
|
||||||
|
// Consecutive same variety arc stacks a score penalty until a different arc shows.
|
||||||
|
Step("ap50", "interest_variety_clear"),
|
||||||
|
Step("ap50b", "interest_expire_pending", value: ""),
|
||||||
|
Step("ap51", "interest_inject", asset: "sheep", label: "FirstDuel", tier: "Action", expect: "first_duel",
|
||||||
|
value: "lead=event;evt=88;char=5;fighters=2;notables=0;force=true"),
|
||||||
|
Step("ap52", "assert", expect: "variety_arc", value: "combat:duel"),
|
||||||
|
Step("ap53", "assert", expect: "variety_arc_streak", value: "1"),
|
||||||
|
Step("ap54", "interest_inject", asset: "sheep", label: "RepeatDuel", tier: "Action", expect: "repeat_duel",
|
||||||
|
value: "lead=event;evt=100;char=5;fighters=2;notables=0"),
|
||||||
|
Step("ap55", "interest_inject", asset: "sheep", label: "FreshStory", tier: "Action", expect: "fresh_story",
|
||||||
|
value: "lead=event;evt=78;char=10"),
|
||||||
|
Step("ap56", "assert", expect: "interest_score_order", value: "fresh_story", label: "repeat_duel"),
|
||||||
|
|
||||||
// Cold-boot focus gaps from welcome/dismiss are unrelated to score order.
|
// Cold-boot focus gaps from welcome/dismiss are unrelated to score order.
|
||||||
Step("ap89", "reset_counters"),
|
Step("ap89", "reset_counters"),
|
||||||
Step("ap90", "assert", expect: "no_bad"),
|
Step("ap90", "assert", expect: "no_bad"),
|
||||||
|
|
|
||||||
|
|
@ -157,12 +157,15 @@ public static class InterestScoring
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float repeatPenalty = InterestVariety.RepeatArcPenalty(c);
|
||||||
c.TotalScore = c.EventStrength + scaleBonus
|
c.TotalScore = c.EventStrength + scaleBonus
|
||||||
+ c.CharacterSignificance * charWeight
|
+ c.CharacterSignificance * charWeight
|
||||||
+ c.VisualConfidence * w.visualMultiplier
|
+ c.VisualConfidence * w.visualMultiplier
|
||||||
+ c.Novelty * w.noveltyMultiplier;
|
+ c.Novelty * w.noveltyMultiplier
|
||||||
|
- repeatPenalty;
|
||||||
c.ScoreDetail =
|
c.ScoreDetail =
|
||||||
$"evt={c.EventStrength:0.#} char={c.CharacterSignificance:0.#} n={fighters}/{notables} vis={c.VisualConfidence:0.##}";
|
$"evt={c.EventStrength:0.#} char={c.CharacterSignificance:0.#} n={fighters}/{notables} vis={c.VisualConfidence:0.##}"
|
||||||
|
+ (repeatPenalty > 0.05f ? $" rpt=-{repeatPenalty:0.#}" : "");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -136,6 +136,13 @@ public class ScoringWeights
|
||||||
/// on the same subject so life milestones do not reclaim the camera every few seconds.
|
/// on the same subject so life milestones do not reclaim the camera every few seconds.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public float fixedDwellBeatCooldownSeconds = 45f;
|
public float fixedDwellBeatCooldownSeconds = 45f;
|
||||||
|
/// <summary>
|
||||||
|
/// Score subtracted per consecutive camera show of the same variety arc
|
||||||
|
/// (e.g. duel → duel). Resets when a different arc is shown.
|
||||||
|
/// </summary>
|
||||||
|
public float repeatArcPenaltyPer = 24f;
|
||||||
|
/// <summary>Cap on stacked repeat-arc penalty.</summary>
|
||||||
|
public float repeatArcPenaltyCap = 72f;
|
||||||
public int enrichTopK = 8;
|
public int enrichTopK = 8;
|
||||||
public float metaCacheTtl = 2f;
|
public float metaCacheTtl = 2f;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,9 @@ using UnityEngine;
|
||||||
namespace IdleSpectator;
|
namespace IdleSpectator;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Soft ~70/30 event-led vs character-led mix with decaying novelty penalties
|
/// Soft ~70/30 event-led vs character-led mix with decaying novelty penalties,
|
||||||
/// and hard FixedDwell beat cooldowns (same life beat / subject cannot reclaim immediately).
|
/// hard FixedDwell beat cooldowns, and consecutive variety-arc score drop-off
|
||||||
|
/// (same arc loses TotalScore until a different arc is shown).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class InterestVariety
|
public static class InterestVariety
|
||||||
{
|
{
|
||||||
|
|
@ -24,14 +25,25 @@ public static class InterestVariety
|
||||||
private static readonly List<InterestCandidate> CharPool = new List<InterestCandidate>(64);
|
private static readonly List<InterestCandidate> CharPool = new List<InterestCandidate>(64);
|
||||||
private static readonly List<InterestCandidate> Ranked = new List<InterestCandidate>(16);
|
private static readonly List<InterestCandidate> Ranked = new List<InterestCandidate>(16);
|
||||||
|
|
||||||
|
private static string _lastArcKey = "";
|
||||||
|
private static int _arcStreak;
|
||||||
|
|
||||||
/// <summary>Whether the last <see cref="Pick"/> saw both pools non-empty (for mix metering).</summary>
|
/// <summary>Whether the last <see cref="Pick"/> saw both pools non-empty (for mix metering).</summary>
|
||||||
public static bool LastPickHadBothPools { get; private set; }
|
public static bool LastPickHadBothPools { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>Harness/debug: last shown variety arc key.</summary>
|
||||||
|
public static string LastArcKey => _lastArcKey ?? "";
|
||||||
|
|
||||||
|
/// <summary>Harness/debug: consecutive shows of <see cref="LastArcKey"/>.</summary>
|
||||||
|
public static int ArcStreak => _arcStreak;
|
||||||
|
|
||||||
public static void Clear()
|
public static void Clear()
|
||||||
{
|
{
|
||||||
RecentMix.Clear();
|
RecentMix.Clear();
|
||||||
CooldownUntil.Clear();
|
CooldownUntil.Clear();
|
||||||
LastPickHadBothPools = false;
|
LastPickHadBothPools = false;
|
||||||
|
_lastArcKey = "";
|
||||||
|
_arcStreak = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Harness: seed the rolling mix meter without touching cooldowns.</summary>
|
/// <summary>Harness: seed the rolling mix meter without touching cooldowns.</summary>
|
||||||
|
|
@ -68,6 +80,8 @@ public static class InterestVariety
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NoteVarietyArc(chosen);
|
||||||
|
|
||||||
if (updateMix)
|
if (updateMix)
|
||||||
{
|
{
|
||||||
RecentMix.Add(chosen.LeadKind);
|
RecentMix.Add(chosen.LeadKind);
|
||||||
|
|
@ -120,6 +134,161 @@ public static class InterestVariety
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Score to subtract when this candidate matches the last shown variety arc.
|
||||||
|
/// After one duel show, the next duel pays <c>repeatArcPenaltyPer</c>; a lover resets the streak.
|
||||||
|
/// </summary>
|
||||||
|
public static float RepeatArcPenalty(InterestCandidate c)
|
||||||
|
{
|
||||||
|
if (c == null || _arcStreak <= 0 || string.IsNullOrEmpty(_lastArcKey))
|
||||||
|
{
|
||||||
|
return 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
string arc = VarietyArcKey(c);
|
||||||
|
if (string.IsNullOrEmpty(arc)
|
||||||
|
|| !string.Equals(arc, _lastArcKey, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
ScoringWeights w = InterestScoringConfig.W;
|
||||||
|
float per = w.repeatArcPenaltyPer > 0f ? w.repeatArcPenaltyPer : 24f;
|
||||||
|
float cap = w.repeatArcPenaltyCap > 0f ? w.repeatArcPenaltyCap : 72f;
|
||||||
|
return Mathf.Min(cap, _arcStreak * per);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Class-level arc for repeat drop-off (not per unit / pair id).
|
||||||
|
/// Duels share one arc so back-to-back scraps lose score; Mass/Battle share another.
|
||||||
|
/// </summary>
|
||||||
|
public static string VarietyArcKey(InterestCandidate c)
|
||||||
|
{
|
||||||
|
if (c == null)
|
||||||
|
{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c.Completion == InterestCompletionKind.WarFront
|
||||||
|
|| string.Equals(c.AssetId, "live_war", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return "arc:war";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c.Completion == InterestCompletionKind.PlotActive
|
||||||
|
|| string.Equals(c.Category, "Plot", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return "arc:plot";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c.Completion == InterestCompletionKind.StatusOutbreak
|
||||||
|
|| string.Equals(c.AssetId, "live_outbreak", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return "arc:outbreak";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c.Completion == InterestCompletionKind.CombatActive
|
||||||
|
|| string.Equals(c.Category, "Combat", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(c.AssetId, "live_battle", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(c.AssetId, "live_combat", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(c.Verb, "fighting", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (string.Equals(c.AssetId, "live_battle", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return "arc:combat:multi";
|
||||||
|
}
|
||||||
|
|
||||||
|
ScoringWeights w = InterestScoringConfig.W;
|
||||||
|
int sideSum = Mathf.Max(0, c.CombatSideACount) + Mathf.Max(0, c.CombatSideBCount);
|
||||||
|
int size = Mathf.Max(Mathf.Max(0, c.ParticipantCount), sideSum);
|
||||||
|
int duelMax = Mathf.Max(1, w.duelMaxFighters);
|
||||||
|
if (size > 0 && size <= duelMax)
|
||||||
|
{
|
||||||
|
return "arc:combat:duel";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "arc:combat:multi";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(c.HappinessEffectId))
|
||||||
|
{
|
||||||
|
string h = c.HappinessEffectId.Trim().ToLowerInvariant();
|
||||||
|
if (h.IndexOf("death_", StringComparison.Ordinal) >= 0
|
||||||
|
|| h.IndexOf("mourn", StringComparison.Ordinal) >= 0
|
||||||
|
|| h.IndexOf("despair", StringComparison.Ordinal) >= 0
|
||||||
|
|| h.IndexOf("grief", StringComparison.Ordinal) >= 0)
|
||||||
|
{
|
||||||
|
return "arc:life:grief";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (h.IndexOf("lover", StringComparison.Ordinal) >= 0
|
||||||
|
|| h.IndexOf("love", StringComparison.Ordinal) >= 0
|
||||||
|
|| h.IndexOf("married", StringComparison.Ordinal) >= 0
|
||||||
|
|| h.IndexOf("wedding", StringComparison.Ordinal) >= 0)
|
||||||
|
{
|
||||||
|
return "arc:life:love";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "arc:hap:" + h;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(c.StatusId))
|
||||||
|
{
|
||||||
|
return "arc:status:" + c.StatusId.Trim().ToLowerInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
string asset = (c.AssetId ?? "").Trim().ToLowerInvariant();
|
||||||
|
if (!string.IsNullOrEmpty(asset) && !asset.StartsWith("live_", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
if (string.Equals(c.Category, "Spectacle", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| asset.StartsWith("cast_", StringComparison.Ordinal)
|
||||||
|
|| asset.StartsWith("summon_", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return "arc:spectacle";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(c.Category, "Politics", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| (c.Label != null
|
||||||
|
&& c.Label.IndexOf("decides to", StringComparison.OrdinalIgnoreCase) >= 0))
|
||||||
|
{
|
||||||
|
return "arc:decision";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "arc:asset:" + asset;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(c.Category))
|
||||||
|
{
|
||||||
|
return "arc:cat:" + c.Category.Trim().ToLowerInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(c.Verb))
|
||||||
|
{
|
||||||
|
return "arc:verb:" + c.Verb.Trim().ToLowerInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void NoteVarietyArc(InterestCandidate chosen)
|
||||||
|
{
|
||||||
|
string arc = VarietyArcKey(chosen);
|
||||||
|
if (string.IsNullOrEmpty(arc))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(arc, _lastArcKey, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
_arcStreak = Mathf.Max(1, _arcStreak + 1);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_lastArcKey = arc;
|
||||||
|
_arcStreak = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// True when a FixedDwell / moment beat for this candidate is still hard-cooled.
|
/// True when a FixedDwell / moment beat for this candidate is still hard-cooled.
|
||||||
/// Sticky combat / war / plot / pack / outbreak scenes are never beat-blocked.
|
/// Sticky combat / war / plot / pack / outbreak scenes are never beat-blocked.
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "IdleSpectator",
|
"name": "IdleSpectator",
|
||||||
"author": "dazed",
|
"author": "dazed",
|
||||||
"version": "0.28.33",
|
"version": "0.28.34",
|
||||||
"description": "AFK Idle Spectator (I) + Lore (L). Killer button retargets dossier to the killer.",
|
"description": "AFK Idle Spectator (I) + Lore (L). Killer button retargets dossier to the killer.",
|
||||||
"GUID": "com.dazed.idlespectator"
|
"GUID": "com.dazed.idlespectator"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"modVersion": "0.28.32",
|
"modVersion": "0.28.34",
|
||||||
"title": "IdleSpectator interest scoring model",
|
"title": "IdleSpectator interest scoring model",
|
||||||
"updated": "2026-07-17",
|
"updated": "2026-07-17",
|
||||||
"note": "Edit weights below - InterestScoringConfig loads this file at mod start. Docs sections are human-facing only (ignored by Unity JsonUtility).",
|
"note": "Edit weights below - InterestScoringConfig loads this file at mod start. Docs sections are human-facing only (ignored by Unity JsonUtility).",
|
||||||
|
|
@ -98,6 +98,8 @@
|
||||||
"mixWindow": 20,
|
"mixWindow": 20,
|
||||||
"hardCooldownSeconds": 12,
|
"hardCooldownSeconds": 12,
|
||||||
"fixedDwellBeatCooldownSeconds": 45,
|
"fixedDwellBeatCooldownSeconds": 45,
|
||||||
|
"repeatArcPenaltyPer": 24,
|
||||||
|
"repeatArcPenaltyCap": 72,
|
||||||
"enrichTopK": 8,
|
"enrichTopK": 8,
|
||||||
"metaCacheTtl": 2
|
"metaCacheTtl": 2
|
||||||
},
|
},
|
||||||
|
|
@ -119,6 +121,7 @@
|
||||||
"Hard sticky combat is reserved for Skirmish+ or notable-pair Duels.",
|
"Hard sticky combat is reserved for Skirmish+ or notable-pair Duels.",
|
||||||
"Extremely notable 1v1s (kings/favorites) can beat nameless melees.",
|
"Extremely notable 1v1s (kings/favorites) can beat nameless melees.",
|
||||||
"Founding tips fire on meta outcomes (new_city / new_kingdom), not decision intent.",
|
"Founding tips fire on meta outcomes (new_city / new_kingdom), not decision intent.",
|
||||||
|
"Consecutive same variety-arc shows stack a score penalty until a different arc is shown.",
|
||||||
"TotalScore is the sole ranking signal; typed rules (combat vs vignette) gate interrupts."
|
"TotalScore is the sole ranking signal; typed rules (combat vs vignette) gate interrupts."
|
||||||
],
|
],
|
||||||
"pipeline": [
|
"pipeline": [
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ Anonymous / single-notable Duels are soft sticky: MaxWatch 24, variety valve at
|
||||||
|
|
||||||
Founding tips come from `new_city` / `new_kingdom` meta outcomes, not decision intent.
|
Founding tips come from `new_city` / `new_kingdom` meta outcomes, not decision intent.
|
||||||
|
|
||||||
|
Consecutive shows of the same variety arc (e.g. duel → duel) subtract `repeatArcPenaltyPer` from TotalScore (capped), resetting when a different arc is shown.
|
||||||
|
|
||||||
Edit the `weights` block in scoring-model.json to change live scoring formula knobs.
|
Edit the `weights` block in scoring-model.json to change live scoring formula knobs.
|
||||||
|
|
||||||
Edit event-catalog.json to change what each event is worth and what it says.
|
Edit event-catalog.json to change what each event is worth and what it says.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue