311 lines
13 KiB
C#
311 lines
13 KiB
C#
using HSchool.Content;
|
|
using HSchool.Server.Api;
|
|
using HSchool.Server.Game;
|
|
using HSchool.Simulation;
|
|
|
|
namespace HSchool.Server.Tests;
|
|
|
|
public class PortraitSceneCollectorTests
|
|
{
|
|
private static readonly SimulationOptions Budget = new();
|
|
private readonly CatalogLoader _loader = new();
|
|
|
|
[Fact]
|
|
public void Classroom_IncludesBlackboardPhrase_OfficeWithoutBoardDoesNot()
|
|
{
|
|
var catalog = LoadCatalog();
|
|
var map = SampleMap();
|
|
|
|
var withBoard = PortraitSceneCollector.Collect(
|
|
catalog, map, "class-1", Precipitation.None, "TestClimate", [], [], Budget);
|
|
var withoutBoard = PortraitSceneCollector.Collect(
|
|
catalog, map, "chairs", Precipitation.None, "TestClimate", [], [], Budget);
|
|
|
|
Assert.Contains(withBoard.PositiveFragments, fragment => fragment.Contains("blackboard", StringComparison.OrdinalIgnoreCase));
|
|
Assert.DoesNotContain(withoutBoard.PositiveFragments, fragment => fragment.Contains("blackboard", StringComparison.OrdinalIgnoreCase));
|
|
Assert.DoesNotContain(withoutBoard.PositiveFragments, fragment => fragment.Contains("chair", StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
[Fact]
|
|
public void FillBlackboard_IsIncluded_ChairFillIsSilent()
|
|
{
|
|
var catalog = LoadCatalog();
|
|
var map = SampleMap();
|
|
|
|
var office = PortraitSceneCollector.Collect(
|
|
catalog, map, "office", Precipitation.None, "TestClimate", [], [], Budget);
|
|
|
|
Assert.Contains(office.PositiveFragments, fragment => fragment.Contains("blackboard", StringComparison.OrdinalIgnoreCase));
|
|
Assert.DoesNotContain(office.PositiveFragments, fragment => fragment.Contains("chair", StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
[Fact]
|
|
public void Snow_OnlyOutdoors()
|
|
{
|
|
var catalog = LoadCatalog();
|
|
var map = SampleMap();
|
|
|
|
var yard = PortraitSceneCollector.Collect(
|
|
catalog, map, "yard", Precipitation.Snow, "TestClimate", [], [], Budget);
|
|
var indoor = PortraitSceneCollector.Collect(
|
|
catalog, map, "class-1", Precipitation.Snow, "TestClimate", [], [], Budget);
|
|
|
|
Assert.Contains(yard.PositiveFragments, fragment => fragment.Contains("snow", StringComparison.OrdinalIgnoreCase));
|
|
Assert.DoesNotContain(indoor.PositiveFragments, fragment => fragment.Contains("snow", StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
[Fact]
|
|
public void MoreThanSixCandidates_KeepsTopWeightsThenDefName()
|
|
{
|
|
var catalog = LoadCatalog();
|
|
var map = SampleMap();
|
|
var options = new SimulationOptions { PortraitSceneFragmentLimit = 6 };
|
|
|
|
var scene = PortraitSceneCollector.Collect(
|
|
catalog, map, "ranked", Precipitation.None, "TestClimate", [], [], options);
|
|
|
|
Assert.Equal(6, scene.PositiveFragments.Count);
|
|
Assert.Contains("frag-w7", scene.PositiveFragments);
|
|
Assert.Contains("frag-w2", scene.PositiveFragments);
|
|
Assert.DoesNotContain("frag-w1", scene.PositiveFragments);
|
|
Assert.Equal(
|
|
["frag-w7", "frag-w6", "frag-w5", "frag-w4", "frag-w3", "frag-w2"],
|
|
scene.PositiveFragments);
|
|
}
|
|
|
|
[Fact]
|
|
public void EqualWeight_BreaksTiesByDefName()
|
|
{
|
|
var catalog = LoadCatalog();
|
|
var map = SampleMap();
|
|
var options = new SimulationOptions { PortraitSceneFragmentLimit = 1 };
|
|
|
|
var scene = PortraitSceneCollector.Collect(
|
|
catalog, map, "tie", Precipitation.None, "TestClimate", [], [], options);
|
|
|
|
Assert.Equal(["alpha-tie"], scene.PositiveFragments);
|
|
Assert.DoesNotContain("zeta-tie", scene.PositiveFragments);
|
|
}
|
|
|
|
[Fact]
|
|
public void SceneLora_GoesToLoras_EmbeddingGoesToTextTags()
|
|
{
|
|
var catalog = LoadCatalog();
|
|
var map = SampleMap();
|
|
|
|
var scene = PortraitSceneCollector.Collect(
|
|
catalog, map, "props", Precipitation.None, "TestClimate", [], [], Budget);
|
|
|
|
Assert.Equal(["snow.safetensors"], scene.PositiveLoras.Select(entry => entry.Name));
|
|
Assert.Equal(["winter-embed"], scene.PositiveEmbeddings.Select(entry => entry.Name));
|
|
Assert.DoesNotContain(scene.PositiveFragments, fragment => fragment.Contains("snow.safetensors", StringComparison.Ordinal));
|
|
Assert.DoesNotContain(scene.PositiveFragments, fragment => fragment.Contains("<embed:", StringComparison.Ordinal));
|
|
}
|
|
|
|
[Fact]
|
|
public void CollectThenBuild_PutsBlackboardAfterClothing_ChairRoomDoesNot()
|
|
{
|
|
var catalog = LoadCatalog();
|
|
var map = SampleMap();
|
|
var profile = SwarmUiPresetDefinition.CreateDefault().ToProfile(PortraitKind.Avatar);
|
|
var card = SampleWornCard();
|
|
|
|
var withBoard = PortraitSceneCollector.Collect(
|
|
catalog, map, "class-1", Precipitation.None, "TestClimate", [], [], Budget);
|
|
var (positive, _) = PortraitPromptBuilder.Build(card, profile, PortraitKind.Avatar, scene: withBoard);
|
|
var clothesAt = positive.IndexOf("wearing a white shirt", StringComparison.OrdinalIgnoreCase);
|
|
var sceneAt = positive.IndexOf("blackboard", StringComparison.OrdinalIgnoreCase);
|
|
Assert.True(clothesAt >= 0 && sceneAt > clothesAt);
|
|
|
|
var chairs = PortraitSceneCollector.Collect(
|
|
catalog, map, "chairs", Precipitation.None, "TestClimate", [], [], Budget);
|
|
var (without, _) = PortraitPromptBuilder.Build(card, profile, PortraitKind.Avatar, scene: chairs);
|
|
Assert.DoesNotContain("blackboard", without, StringComparison.OrdinalIgnoreCase);
|
|
Assert.DoesNotContain("chair", without, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
private static PersonCardResponse SampleWornCard() =>
|
|
new(
|
|
"f0.c0",
|
|
"Maria Ivanova",
|
|
"Ivanova",
|
|
"Maria",
|
|
"",
|
|
true,
|
|
12,
|
|
new DateTime(2000, 3, 14, 0, 0, 0, DateTimeKind.Utc),
|
|
["student"],
|
|
5,
|
|
"A",
|
|
"class-1",
|
|
null,
|
|
null,
|
|
[new LabeledStatResponse("HairColor", "Hair colour", "Black")],
|
|
[],
|
|
[],
|
|
[],
|
|
null,
|
|
null,
|
|
new PersonFamilyResponse([], [], [], []),
|
|
[new WornItemResponse("Shirt", "Shirt", "White", "White", [new DefLabelResponse(ApparelLayers.Top, "Top")], 1f, "new", [ApparelLayers.Underwear])],
|
|
[],
|
|
0f,
|
|
0f);
|
|
|
|
private DefCatalog LoadCatalog()
|
|
{
|
|
return _loader.Load(
|
|
[CatalogLoader.CorePackId],
|
|
[
|
|
new ContentDocument(
|
|
CatalogLoader.CorePackId,
|
|
"defs/things/scene.jsonc",
|
|
"""
|
|
[
|
|
{ "defName": "Blackboard", "prompt": { "positive": "a classroom blackboard with chalk notes", "weight": 1 } },
|
|
{ "defName": "Chair" },
|
|
{ "defName": "StudentDesk", "pupilSlots": 1 },
|
|
{ "defName": "W7", "prompt": { "positive": "frag-w7", "weight": 7 } },
|
|
{ "defName": "W6", "prompt": { "positive": "frag-w6", "weight": 6 } },
|
|
{ "defName": "W5", "prompt": { "positive": "frag-w5", "weight": 5 } },
|
|
{ "defName": "W4", "prompt": { "positive": "frag-w4", "weight": 4 } },
|
|
{ "defName": "W3", "prompt": { "positive": "frag-w3", "weight": 3 } },
|
|
{ "defName": "W2", "prompt": { "positive": "frag-w2", "weight": 2 } },
|
|
{ "defName": "W1", "prompt": { "positive": "frag-w1", "weight": 1 } },
|
|
{ "defName": "AlphaTie", "prompt": { "positive": "alpha-tie", "weight": 1 } },
|
|
{ "defName": "ZetaTie", "prompt": { "positive": "zeta-tie", "weight": 1 } },
|
|
{
|
|
"defName": "Prop",
|
|
"prompt": {
|
|
"positive": "a winter prop",
|
|
"weight": 1,
|
|
"positiveLoras": [{ "name": "snow.safetensors", "weight": 0.7 }],
|
|
"positiveEmbeddings": [{ "name": "winter-embed", "weight": 1 }]
|
|
}
|
|
}
|
|
]
|
|
"""),
|
|
new ContentDocument(
|
|
CatalogLoader.CorePackId,
|
|
"defs/rooms/scene.jsonc",
|
|
"""
|
|
[
|
|
{
|
|
"defName": "Classroom",
|
|
"homeroom": true,
|
|
"seatThing": "StudentDesk",
|
|
"defaultSeats": 16,
|
|
"travelMinutes": 0.5,
|
|
"prompt": { "positive": "a classroom blackboard with chalk notes", "weight": 1 }
|
|
},
|
|
{
|
|
"defName": "Office",
|
|
"travelMinutes": 0.5,
|
|
"slots": [
|
|
{ "key": "board", "thing": "Blackboard" },
|
|
{ "key": "seat", "thing": "Chair" },
|
|
{ "key": "a", "thing": "W7" },
|
|
{ "key": "b", "thing": "W6" },
|
|
{ "key": "c", "thing": "W5" },
|
|
{ "key": "d", "thing": "W4" },
|
|
{ "key": "e", "thing": "W3" },
|
|
{ "key": "f", "thing": "W2" },
|
|
{ "key": "g", "thing": "W1" },
|
|
{ "key": "alpha", "thing": "AlphaTie" },
|
|
{ "key": "zeta", "thing": "ZetaTie" },
|
|
{ "key": "prop", "thing": "Prop" }
|
|
]
|
|
}
|
|
]
|
|
"""),
|
|
new ContentDocument(
|
|
CatalogLoader.CorePackId,
|
|
"defs/territories/yard.jsonc",
|
|
"""{ "defName": "SchoolYard", "travelMinutes": 3 }"""),
|
|
new ContentDocument(
|
|
CatalogLoader.CorePackId,
|
|
"defs/climates/test.jsonc",
|
|
"""
|
|
{
|
|
"defName": "TestClimate",
|
|
"monthlyNorms": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
"daySpread": 0,
|
|
"hourSpread": 0,
|
|
"precipitationChance": 0,
|
|
"prompt": { "snow": { "positive": "falling snow, winter overcast schoolyard", "weight": 1 } }
|
|
}
|
|
"""),
|
|
]);
|
|
}
|
|
|
|
private static MapLayout SampleMap() =>
|
|
new()
|
|
{
|
|
Territory = new TerritoryNode { Id = "yard", Def = "SchoolYard" },
|
|
Rooms =
|
|
[
|
|
new RoomNode
|
|
{
|
|
Id = "class-1",
|
|
Def = "Classroom",
|
|
Building = "main",
|
|
Floor = "f1",
|
|
Seats = 16,
|
|
},
|
|
new RoomNode
|
|
{
|
|
Id = "office",
|
|
Def = "Office",
|
|
Building = "main",
|
|
Floor = "f1",
|
|
Slots = [new SlotFill { Key = "board", Thing = "Blackboard" }],
|
|
},
|
|
new RoomNode
|
|
{
|
|
Id = "chairs",
|
|
Def = "Office",
|
|
Building = "main",
|
|
Floor = "f1",
|
|
Slots = [new SlotFill { Key = "seat", Thing = "Chair" }],
|
|
},
|
|
new RoomNode
|
|
{
|
|
Id = "ranked",
|
|
Def = "Office",
|
|
Building = "main",
|
|
Floor = "f1",
|
|
Slots =
|
|
[
|
|
new SlotFill { Key = "a", Thing = "W7" },
|
|
new SlotFill { Key = "b", Thing = "W6" },
|
|
new SlotFill { Key = "c", Thing = "W5" },
|
|
new SlotFill { Key = "d", Thing = "W4" },
|
|
new SlotFill { Key = "e", Thing = "W3" },
|
|
new SlotFill { Key = "f", Thing = "W2" },
|
|
new SlotFill { Key = "g", Thing = "W1" },
|
|
],
|
|
},
|
|
new RoomNode
|
|
{
|
|
Id = "tie",
|
|
Def = "Office",
|
|
Building = "main",
|
|
Floor = "f1",
|
|
Slots =
|
|
[
|
|
new SlotFill { Key = "zeta", Thing = "ZetaTie" },
|
|
new SlotFill { Key = "alpha", Thing = "AlphaTie" },
|
|
],
|
|
},
|
|
new RoomNode
|
|
{
|
|
Id = "props",
|
|
Def = "Office",
|
|
Building = "main",
|
|
Floor = "f1",
|
|
Slots = [new SlotFill { Key = "prop", Thing = "Prop" }],
|
|
},
|
|
],
|
|
};
|
|
}
|