Files
Leonid PershinandCursor f7d5e0c042 Add lesson whispers, teacher catch chance, and after-bell teacher talk.
Whispering cuts lesson skill gain from BehaviorDef; a teacher in the room may interrupt into a discipline circle, and pedagogy scales the pupil's opinion of the teacher.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-20 10:35:31 +03:00

179 lines
5.6 KiB
C#

using HSchool.Content;
using HSchool.People;
namespace HSchool.Ai.Tests;
public class WhisperTests
{
[Fact]
public void BlocksOnLesson_AllowsWhisper_NotChat()
{
Assert.True(TalkCircles.BlocksOnLesson(TalkActions.Chat));
Assert.True(TalkCircles.BlocksOnLesson(TalkActions.StaffChat));
Assert.True(TalkCircles.BlocksOnLesson(TalkActions.PhoneChat));
Assert.True(TalkCircles.BlocksOnLesson(TalkActions.TeacherTalk));
Assert.False(TalkCircles.BlocksOnLesson(TalkActions.Whisper));
}
[Fact]
public void FixedSeed_DoesNotCatchEveryWhisperWithTeacherChance()
{
var (catalog, _, _) = World();
var rules = catalog.BehaviorRules!;
var caught = 0;
const int attempts = 40;
for (var i = 0; i < attempts; i++)
{
var roll = TalkCircles.Roll01(Seed.Mix(1, i, Seed.WhisperSalt));
if (TalkCircles.WhisperCaught(roll, rules.WhisperCatchChance, rules.WhisperCatchMax, 1f))
{
caught++;
}
}
Assert.True(caught > 0);
Assert.True(caught < attempts);
Assert.True(rules.WhisperCatchMax < 1f);
}
[Fact]
public void CatchChance_NeverReachesOne_EvenWithLoudTraits()
{
var (catalog, _, _) = World();
var multiplier = TalkCircles.WhisperCatchMultiplier(["Outgoing", "Leader"], catalog);
Assert.True(multiplier > 1f);
var rules = catalog.BehaviorRules!;
const int attempts = 80;
var missed = 0;
for (var i = 0; i < attempts; i++)
{
var roll = TalkCircles.Roll01(Seed.Mix(2, i, Seed.WhisperSalt));
if (!TalkCircles.WhisperCaught(roll, rules.WhisperCatchChance, rules.WhisperCatchMax, multiplier))
{
missed++;
}
}
Assert.True(missed > 0);
}
[Fact]
public void HighPedagogy_AfterPraise_PlusesOpinionMoreThanLow()
{
var (catalog, _, _) = World();
var rules = catalog.BehaviorRules!;
var topic = catalog.Topics[TeacherTopics.Praise];
var from = Person("pupil", ["RussianLanguage"]);
var to = Person("teacher", ["RussianLanguage"]);
var raw = TalkCircles.OpinionDelta(
from,
to,
topic,
50f,
sharedLanguage: true,
TalkActions.TeacherTalk,
ApparelIssue.None,
rules,
catalog);
Assert.True(raw > 0);
var high = TalkCircles.ScaleByPedagogy(raw, 90f, rules);
var low = TalkCircles.ScaleByPedagogy(raw, 20f, rules);
Assert.True(high > low);
}
[Fact]
public void BoundToLesson_SocialZero_StartsWhisperNotChat()
{
var (catalog, map, walks) = World();
var classroom = map.Rooms.First(room => room.Def == "Classroom").Id;
var decision = DecisionPlanner.Decide(
catalog,
map,
walks,
new ActorState(
classroom,
classroom,
false,
false,
true,
false,
false,
true,
classroom,
Needs(social: 0f),
Intent.None,
Talk: new TalkPlannerContext(
"p1",
new Dictionary<string, int>(),
new Dictionary<string, string>(),
"c1",
false,
false,
catalog.BehaviorRules)),
(_, _) => 0);
Assert.Equal(TalkActions.Whisper, decision.StartAction);
Assert.NotEqual(TalkActions.Chat, decision.StartAction);
}
[Fact]
public void PickTopic_OnLesson_OnlyWhisperTopics()
{
var (catalog, _, _) = World();
var picker = Person("a", ["RussianLanguage"]);
for (var seed = 0; seed < 30; seed++)
{
var id = TalkCircles.PickTopic(catalog, picker, age: 14, seed, whisperOnLesson: true);
Assert.NotNull(id);
Assert.True(catalog.Topics[id!].WhisperOnLesson);
}
}
private static Dictionary<string, float> Needs(float social = 1f) => new(StringComparer.Ordinal)
{
["Social"] = social,
["Hunger"] = 1f,
["Sleep"] = 1f,
["Toilet"] = 1f,
};
private static (DefCatalog Catalog, MapLayout Map, WalkGraph Walks) World()
{
var (catalog, map) = Fixtures.Vanilla();
return (catalog, map, WalkGraph.Build(catalog, map));
}
private static Person Person(string id, string[] languages)
{
var skills = languages.ToDictionary(skill => skill, _ => 60, StringComparer.Ordinal);
skills["Communication"] = 50;
var cases = new CaseTable
{
Nom = "A",
Gen = "A",
Dat = "A",
Acc = "A",
Ins = "A",
Pre = "A",
};
return new Person
{
Id = id,
FamilyId = "f1",
Female = false,
BirthDate = new DateTime(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc),
Name = new PersonName("A", "B", "C", cases, cases, cases),
IsStudent = true,
IsStaff = false,
IsParent = false,
Numbers = new Dictionary<string, int>(StringComparer.Ordinal),
Choices = new Dictionary<string, string>(StringComparer.Ordinal),
Skills = skills,
Traits = [],
Needs = new Dictionary<string, float>(StringComparer.Ordinal),
Opinions = new Dictionary<string, int>(StringComparer.Ordinal),
};
}
}