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>
179 lines
5.6 KiB
C#
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),
|
|
};
|
|
}
|
|
}
|