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>
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
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),
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user