Enhance protocol and simulation features with activity tracking and behavior definitions

- Updated protocol documentation to include new `activity` and `activityLabel` fields in the person card response, reflecting real-time activity status.
- Introduced `BehaviorDef` to define behavior rules, including need thresholds and lesson skill gains, enhancing AI decision-making.
- Revised the `DefCatalog` to incorporate behavior definitions and updated validation logic to ensure proper behavior handling.
- Enhanced the simulation to manage presence and activity states, allowing for more dynamic interactions within the school environment.
- Updated tests to validate the new activity tracking and behavior functionalities, ensuring robust performance and reliability.
- Improved localization strings to support new activity and behavior features, enhancing user experience.
This commit is contained in:
Leonid Pershin
2026-08-19 19:49:44 +03:00
parent 3c54f981b7
commit b135a9caad
42 changed files with 1134 additions and 59 deletions
@@ -0,0 +1,87 @@
namespace HSchool.Content.Tests;
public class ActionDefTests
{
private readonly CatalogLoader _loader = new();
[Fact]
public void ConcreteAction_WithoutRoom_FailsLoad()
{
var ex = Assert.Throws<ContentLoadException>(() => _loader.Load(
[CatalogLoader.CorePackId],
[
PackDocuments.Def(
CatalogLoader.CorePackId,
"actions",
"eat",
"""{ "defName": "Eat", "minutes": 15, "need": "Hunger", "needGain": 0.5 }"""),
PackDocuments.Def(
CatalogLoader.CorePackId,
"needs",
"hunger",
"""{ "defName": "Hunger", "initial": 1, "decayPerHour": 0, "min": 0, "max": 1 }"""),
]));
Assert.Contains("Eat", ex.Message, StringComparison.Ordinal);
Assert.Contains("room", ex.Message, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void ConcreteAction_UnknownNeed_FailsLoad()
{
var ex = Assert.Throws<ContentLoadException>(() => _loader.Load(
[CatalogLoader.CorePackId],
[
PackDocuments.Def(
CatalogLoader.CorePackId,
"rooms",
"cafeteria",
"""{ "defName": "Cafeteria", "travelMinutes": 1 }"""),
PackDocuments.Def(
CatalogLoader.CorePackId,
"actions",
"eat",
"""
{ "defName": "EatLunch", "room": "Cafeteria", "minutes": 15, "need": "Snacks", "needGain": 0.5 }
"""),
]));
Assert.Contains("EatLunch", ex.Message, StringComparison.Ordinal);
Assert.Contains("Snacks", ex.Message, StringComparison.Ordinal);
}
[Fact]
public void ConcreteAction_UnknownThing_FailsLoad()
{
var ex = Assert.Throws<ContentLoadException>(() => _loader.Load(
[CatalogLoader.CorePackId],
[
PackDocuments.Def(
CatalogLoader.CorePackId,
"rooms",
"cafeteria",
"""{ "defName": "Cafeteria", "travelMinutes": 1 }"""),
PackDocuments.Def(
CatalogLoader.CorePackId,
"actions",
"eat",
"""
{ "defName": "EatLunch", "room": "Cafeteria", "thing": "Stool", "minutes": 15 }
"""),
]));
Assert.Contains("EatLunch", ex.Message, StringComparison.Ordinal);
Assert.Contains("Stool", ex.Message, StringComparison.Ordinal);
}
[Fact]
public void AbstractAction_DoesNotNeedARoom()
{
var catalog = _loader.Load(
[CatalogLoader.CorePackId],
[PackDocuments.Def(CatalogLoader.CorePackId, "actions", "sit", """{ "defName": "Sit", "abstract": true }""")]);
Assert.True(catalog.Actions["Sit"].Abstract);
Assert.Null(catalog.Actions["Sit"].Room);
}
}
@@ -16,7 +16,7 @@ public class CatalogLoaderTests
"sit",
"""
// a verb the Sit system already knows
{ "defName": "Sit", }
{ "defName": "Sit", "abstract": true, }
"""),
]);
@@ -32,7 +32,7 @@ public class CatalogLoaderTests
[
PackDocuments.Def(CatalogLoader.CorePackId, "things", "chair", """{ "defName": "Chair", "actions": [] }"""),
PackDocuments.Def("addon", "things", "chair", """{ "defName": "Chair", "actions": ["Sit"] }"""),
PackDocuments.Def(CatalogLoader.CorePackId, "actions", "sit", """{ "defName": "Sit" }"""),
PackDocuments.Def(CatalogLoader.CorePackId, "actions", "sit", """{ "defName": "Sit", "abstract": true }"""),
],
log);
@@ -47,7 +47,7 @@ public class CatalogLoaderTests
var catalog = _loader.Load(
[CatalogLoader.CorePackId, "addon"],
[
PackDocuments.Def(CatalogLoader.CorePackId, "actions", "sit", """{ "defName": "Sit" }"""),
PackDocuments.Def(CatalogLoader.CorePackId, "actions", "sit", """{ "defName": "Sit", "abstract": true }"""),
PackDocuments.Locale(CatalogLoader.CorePackId, "ru", """{ "Sit": "Сесть" }"""),
PackDocuments.Locale("addon", "ru", """{ "Sit": "Присесть" }"""),
],
@@ -63,7 +63,7 @@ public class CatalogLoaderTests
var catalog = _loader.Load(
[CatalogLoader.CorePackId],
[
PackDocuments.Def(CatalogLoader.CorePackId, "actions", "sit", """{ "defName": "Sit" }"""),
PackDocuments.Def(CatalogLoader.CorePackId, "actions", "sit", """{ "defName": "Sit", "abstract": true }"""),
PackDocuments.Locale(CatalogLoader.CorePackId, "ru", """{ "Sit": "Сесть" }"""),
]);
@@ -77,8 +77,8 @@ public class CatalogLoaderTests
var catalog = _loader.Load(
["addon"],
[
PackDocuments.Def(CatalogLoader.CorePackId, "actions", "sit", """{ "defName": "Sit" }"""),
PackDocuments.Def("addon", "actions", "wave", """{ "defName": "Wave" }"""),
PackDocuments.Def(CatalogLoader.CorePackId, "actions", "sit", """{ "defName": "Sit", "abstract": true }"""),
PackDocuments.Def("addon", "actions", "wave", """{ "defName": "Wave", "abstract": true }"""),
]);
Assert.Equal([CatalogLoader.CorePackId, "addon"], catalog.PackIds);
@@ -10,8 +10,8 @@ public class InheritanceTests
var catalog = _loader.Load(
[CatalogLoader.CorePackId],
[
PackDocuments.Def(CatalogLoader.CorePackId, "actions", "sit", """{ "defName": "Sit" }"""),
PackDocuments.Def(CatalogLoader.CorePackId, "actions", "inspect", """{ "defName": "Inspect" }"""),
PackDocuments.Def(CatalogLoader.CorePackId, "actions", "sit", """{ "defName": "Sit", "abstract": true }"""),
PackDocuments.Def(CatalogLoader.CorePackId, "actions", "inspect", """{ "defName": "Inspect", "abstract": true }"""),
PackDocuments.Def(
CatalogLoader.CorePackId,
"things",
@@ -118,7 +118,7 @@ public class MapValidationTests
private static List<ContentDocument> MiniDefs(bool abstractYard = false) =>
[
PackDocuments.Def(CatalogLoader.CorePackId, "actions", "sit", """{ "defName": "Sit" }"""),
PackDocuments.Def(CatalogLoader.CorePackId, "actions", "sit", """{ "defName": "Sit", "abstract": true }"""),
PackDocuments.Def(CatalogLoader.CorePackId, "things", "chair", """{ "defName": "Chair", "actions": ["Sit"] }"""),
PackDocuments.Def(CatalogLoader.CorePackId, "positions", "principal", """{ "defName": "Principal" }"""),
PackDocuments.Def(
+2 -2
View File
@@ -10,8 +10,8 @@ public class PatchTests
var catalog = _loader.Load(
[CatalogLoader.CorePackId, "addon"],
[
PackDocuments.Def(CatalogLoader.CorePackId, "actions", "sit", """{ "defName": "Sit" }"""),
PackDocuments.Def(CatalogLoader.CorePackId, "actions", "inspect", """{ "defName": "Inspect" }"""),
PackDocuments.Def(CatalogLoader.CorePackId, "actions", "sit", """{ "defName": "Sit", "abstract": true }"""),
PackDocuments.Def(CatalogLoader.CorePackId, "actions", "inspect", """{ "defName": "Inspect", "abstract": true }"""),
PackDocuments.Def(CatalogLoader.CorePackId, "things", "chair", """{ "defName": "Chair", "actions": ["Sit"] }"""),
PackDocuments.Patch(
"addon",
@@ -16,7 +16,9 @@ public class PeopleDefTests
Assert.True(catalog.BodyAttributes.ContainsKey("Height"));
Assert.Equal(BodyAttributeKind.Number, catalog.BodyAttributes["Height"].Kind);
Assert.Equal(BodyAttributeKind.Choice, catalog.BodyAttributes["HairColor"].Kind);
Assert.All(catalog.Needs.Values, need => Assert.Equal(0, need.DecayPerHour));
Assert.All(catalog.Needs.Values, need => Assert.True(need.DecayPerHour > 0));
Assert.True(catalog.Needs["Sleep"].RestoredOffCampus);
Assert.Equal(0.1f, catalog.Needs["Hunger"].DecayPerHour);
Assert.Contains(catalog.Skills["Agility"].BodyLimits, limit => limit.Attribute == "Build" && limit.Value == "Obese");
Assert.True(catalog.NameSets.ContainsKey("Slavic"));
Assert.True(catalog.NameSets["Slavic"].MaleGiven.Count >= 20);
@@ -64,6 +64,19 @@ public class VanillaCoreTests
Assert.Equal(3f, catalog.Territories["SchoolYard"].TravelMinutes);
Assert.Equal(4, catalog.Traits["Diligent"].CommuteMinutes);
Assert.Equal(-4, catalog.Traits["Lazy"].CommuteMinutes);
Assert.True(catalog.Actions["Sit"].Abstract);
Assert.Equal("Cafeteria", catalog.Actions["EatLunch"].Room);
Assert.Equal("Chair", catalog.Actions["EatLunch"].Thing);
Assert.Equal(15f, catalog.Actions["EatLunch"].Minutes);
Assert.Equal("Hunger", catalog.Actions["EatLunch"].Need);
Assert.Equal(0.5f, catalog.Actions["EatLunch"].NeedGain);
Assert.Equal("SchoolYard", catalog.Actions["WalkYard"].Room);
Assert.Equal(0.1f, catalog.Needs["Hunger"].DecayPerHour);
Assert.True(catalog.Needs["Sleep"].RestoredOffCampus);
Assert.NotNull(catalog.BehaviorRules);
Assert.Equal(0, catalog.BehaviorRules.CommuteSlackMin);
Assert.Equal(6, catalog.BehaviorRules.CommuteSlackMax);
Assert.Equal(0.35f, catalog.BehaviorRules.NeedThreshold);
}
/// <summary>
@@ -97,6 +110,7 @@ public class VanillaCoreTests
keys.AddRange(Names(catalog.Staffing.Values));
keys.AddRange(Names(catalog.DayFrames.Values));
keys.AddRange(Names(catalog.Holidays.Values));
keys.AddRange(Names(catalog.Behavior.Values));
// Derived in code, so no def carries them.
keys.Add(BodyBuilds.Attribute);