Move goal weights into BehaviorDef so a pack can rebalance decisions without a fork.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+49
-26
@@ -48,6 +48,9 @@ public readonly record struct ActorState(
|
||||
/// </summary>
|
||||
public static class DecisionPlanner
|
||||
{
|
||||
// Fallbacks when the catalog has no BehaviorDef. Vanilla writes the same numbers into the
|
||||
// def; a pack that omits the ruleset must keep today's decisions, not empty the classrooms.
|
||||
|
||||
/// <summary>Lesson or posted work. Beats leisure and a need that only just crossed the threshold.</summary>
|
||||
public const float DutyLessonWeight = 10f;
|
||||
|
||||
@@ -77,12 +80,10 @@ public static class DecisionPlanner
|
||||
ArgumentNullException.ThrowIfNull(walks);
|
||||
ArgumentNullException.ThrowIfNull(occupied);
|
||||
|
||||
var rules = catalog.BehaviorRules;
|
||||
var threshold = rules?.NeedThreshold ?? 0.35f;
|
||||
var margin = rules?.SwitchMargin ?? 0.15f;
|
||||
var best = PickGoal(catalog, map, walks, state, occupied, threshold);
|
||||
var held = HeldGoal(catalog, map, walks, state, occupied, threshold);
|
||||
if (held.IsSet && best.Weight <= held.Weight + margin && SameGoal(state.Intent, held))
|
||||
var rules = Rules.From(catalog);
|
||||
var best = PickGoal(catalog, map, walks, state, occupied, rules);
|
||||
var held = HeldGoal(catalog, map, walks, state, occupied, rules);
|
||||
if (held.IsSet && best.Weight <= held.Weight + rules.Margin && SameGoal(state.Intent, held))
|
||||
{
|
||||
return Continue(state with { Intent = held });
|
||||
}
|
||||
@@ -96,16 +97,16 @@ public static class DecisionPlanner
|
||||
WalkGraph walks,
|
||||
ActorState state,
|
||||
OccupiedCount occupied,
|
||||
float threshold)
|
||||
Rules rules)
|
||||
{
|
||||
var best = Intent.None;
|
||||
Consider(ref best, DutyGoal(state));
|
||||
Consider(ref best, DutyGoal(state, rules));
|
||||
foreach (var need in catalog.Needs.Values.OrderBy(def => def.DefName, StringComparer.Ordinal))
|
||||
{
|
||||
Consider(ref best, NeedGoal(catalog, map, walks, state, occupied, need, threshold));
|
||||
Consider(ref best, NeedGoal(catalog, map, walks, state, occupied, need, rules));
|
||||
}
|
||||
|
||||
if (best.Kind != GoalKind.Duty || best.Weight < DutyLessonWeight)
|
||||
if (best.Kind != GoalKind.Duty || best.Weight < rules.DutyLesson)
|
||||
{
|
||||
foreach (var action in catalog.Actions.Values.OrderBy(def => def.DefName, StringComparer.Ordinal))
|
||||
{
|
||||
@@ -118,7 +119,7 @@ public static class DecisionPlanner
|
||||
|
||||
/// <summary>
|
||||
/// The stored intent's weight under current inputs. A lesson that just ended is not still
|
||||
/// worth 10 — otherwise leisure can never beat a stale duty.
|
||||
/// worth the lesson weight — otherwise leisure can never beat a stale duty.
|
||||
/// </summary>
|
||||
private static Intent HeldGoal(
|
||||
DefCatalog catalog,
|
||||
@@ -126,7 +127,7 @@ public static class DecisionPlanner
|
||||
WalkGraph walks,
|
||||
ActorState state,
|
||||
OccupiedCount occupied,
|
||||
float threshold)
|
||||
Rules rules)
|
||||
{
|
||||
if (!state.Intent.IsSet)
|
||||
{
|
||||
@@ -136,14 +137,14 @@ public static class DecisionPlanner
|
||||
switch (state.Intent.Kind)
|
||||
{
|
||||
case GoalKind.Duty:
|
||||
return DutyGoal(state);
|
||||
return DutyGoal(state, rules);
|
||||
case GoalKind.Need:
|
||||
if (state.Intent.Id is null || !catalog.Needs.TryGetValue(state.Intent.Id, out var need))
|
||||
{
|
||||
return Intent.None;
|
||||
}
|
||||
|
||||
return NeedGoal(catalog, map, walks, state, occupied, need, threshold);
|
||||
return NeedGoal(catalog, map, walks, state, occupied, need, rules);
|
||||
case GoalKind.Leisure:
|
||||
if (state.Intent.ActionId is null || !catalog.Actions.TryGetValue(state.Intent.ActionId, out var action))
|
||||
{
|
||||
@@ -156,7 +157,7 @@ public static class DecisionPlanner
|
||||
}
|
||||
}
|
||||
|
||||
private static Intent DutyGoal(ActorState state)
|
||||
private static Intent DutyGoal(ActorState state, Rules rules)
|
||||
{
|
||||
if (state.DutyRoom is null)
|
||||
{
|
||||
@@ -165,7 +166,7 @@ public static class DecisionPlanner
|
||||
|
||||
if (state.BoundToLesson)
|
||||
{
|
||||
return new Intent(GoalKind.Duty, state.DutyRoom, DutyLessonWeight, null);
|
||||
return new Intent(GoalKind.Duty, state.DutyRoom, rules.DutyLesson, null);
|
||||
}
|
||||
|
||||
if (state.Intent.Kind == GoalKind.Leisure)
|
||||
@@ -178,19 +179,19 @@ public static class DecisionPlanner
|
||||
&& !state.IsWalking)
|
||||
{
|
||||
// Arrived this break (travel-weight duty). Stay put so a 3-weight chat does not
|
||||
// pull them out of the gym they just walked to. A leftover lesson intent (weight
|
||||
// 10) in the same room is the other case: the next lesson is here, leisure can win.
|
||||
// pull them out of the gym they just walked to. A leftover lesson intent in the
|
||||
// same room is the other case: the next lesson is here, leisure can win.
|
||||
if (state.Intent.Kind == GoalKind.Duty
|
||||
&& string.Equals(state.Intent.Id, state.DutyRoom, StringComparison.Ordinal)
|
||||
&& state.Intent.Weight <= DutyTravelWeight)
|
||||
&& state.Intent.Weight <= rules.DutyTravel)
|
||||
{
|
||||
return new Intent(GoalKind.Duty, state.DutyRoom, DutyTravelWeight, null);
|
||||
return new Intent(GoalKind.Duty, state.DutyRoom, rules.DutyTravel, null);
|
||||
}
|
||||
|
||||
return Intent.None;
|
||||
}
|
||||
|
||||
return new Intent(GoalKind.Duty, state.DutyRoom, DutyTravelWeight, null);
|
||||
return new Intent(GoalKind.Duty, state.DutyRoom, rules.DutyTravel, null);
|
||||
}
|
||||
|
||||
private static Intent NeedGoal(
|
||||
@@ -200,14 +201,14 @@ public static class DecisionPlanner
|
||||
ActorState state,
|
||||
OccupiedCount occupied,
|
||||
NeedDef need,
|
||||
float threshold)
|
||||
Rules rules)
|
||||
{
|
||||
if (need.Abstract || !state.Needs.TryGetValue(need.DefName, out var value))
|
||||
{
|
||||
return Intent.None;
|
||||
}
|
||||
|
||||
var urgent = value < threshold;
|
||||
var urgent = value < rules.Threshold;
|
||||
|
||||
// ActionForNeed already refuses a sitting outside its window, so an action that comes back
|
||||
// Lunch means this person's own break is open right now.
|
||||
@@ -222,11 +223,11 @@ public static class DecisionPlanner
|
||||
return Intent.None;
|
||||
}
|
||||
|
||||
var span = Math.Max(threshold, 0.0001f);
|
||||
var weight = urgent ? (threshold - value) / span * NeedWeightAtZero : 0f;
|
||||
var span = Math.Max(rules.Threshold, 0.0001f);
|
||||
var weight = urgent ? (rules.Threshold - value) / span * rules.NeedAtZero : 0f;
|
||||
if (action.Lunch)
|
||||
{
|
||||
weight = Math.Max(weight, LunchWeight);
|
||||
weight = Math.Max(weight, rules.Lunch);
|
||||
}
|
||||
|
||||
return new Intent(GoalKind.Need, need.DefName, weight, action.DefName);
|
||||
@@ -488,6 +489,28 @@ public static class DecisionPlanner
|
||||
GoalKind.Leisure => 2,
|
||||
_ => 3,
|
||||
};
|
||||
|
||||
/// <summary>Effective numbers: the catalog's BehaviorDef, or the constants when a pack has none.</summary>
|
||||
private readonly record struct Rules(
|
||||
float DutyLesson,
|
||||
float DutyTravel,
|
||||
float NeedAtZero,
|
||||
float Lunch,
|
||||
float Threshold,
|
||||
float Margin)
|
||||
{
|
||||
public static Rules From(DefCatalog catalog)
|
||||
{
|
||||
var def = catalog.BehaviorRules;
|
||||
return new(
|
||||
def?.DutyLessonWeight ?? DutyLessonWeight,
|
||||
def?.DutyTravelWeight ?? DutyTravelWeight,
|
||||
def?.NeedWeightAtZero ?? NeedWeightAtZero,
|
||||
def?.LunchWeight ?? LunchWeight,
|
||||
def?.NeedThreshold ?? 0.35f,
|
||||
def?.SwitchMargin ?? 0.15f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public delegate int OccupiedCount(string nodeId, string thing);
|
||||
|
||||
Reference in New Issue
Block a user