Sample outdoor weather from the climate preset so people can freeze and the clock can show it.
Protocol v8 adds tenths of a °C and precipitation to the clock frame; warmth drains from insulation versus place temperature. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -428,6 +428,22 @@ public class GameSocketTests(AppHostFixture fixture)
|
||||
Assert.Equal(ClientTime, pong.ClientTimeMs);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OldProtocolVersion_IsRejected()
|
||||
{
|
||||
using var socket = await ConnectRawAsync();
|
||||
|
||||
await SendAsync(socket, buffer => ProtocolCodec.WriteHello(
|
||||
buffer,
|
||||
new ClientHelloMessage((byte)(ProtocolConstants.Version - 1), ProtocolConstants.LocaleRussian)));
|
||||
|
||||
var buffer = new byte[ProtocolConstants.MaxMessageSize];
|
||||
var result = await socket.ReceiveAsync(buffer, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(WebSocketMessageType.Close, result.MessageType);
|
||||
Assert.Equal(WebSocketCloseStatus.ProtocolError, socket.CloseStatus);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task VersionMismatch_IsRejected()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
namespace HSchool.Content.Tests;
|
||||
|
||||
public class ClimatePresetTests
|
||||
{
|
||||
private readonly CatalogLoader _loader = new();
|
||||
|
||||
[Fact]
|
||||
public void VanillaPresets_HaveWorkingWeatherNumbers()
|
||||
{
|
||||
var catalog = Vanilla();
|
||||
var temperate = catalog.ClimatePresets["TemperateContinental"];
|
||||
Assert.Equal(12, temperate.MonthlyNorms.Count);
|
||||
Assert.True(temperate.MonthlyNorms[0] < temperate.MonthlyNorms[6]);
|
||||
Assert.InRange(temperate.PrecipitationChance, 0f, 1f);
|
||||
Assert.True(temperate.IndoorOffset > 0);
|
||||
|
||||
var cold = catalog.ClimatePresets["ContinentalCold"];
|
||||
Assert.True(cold.MonthlyNorms[0] < temperate.MonthlyNorms[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HeatAndColdLoving_AreMutuallyIncompatible_AndOffsetComfort()
|
||||
{
|
||||
var catalog = Vanilla();
|
||||
var heat = catalog.Traits["HeatLoving"];
|
||||
var cold = catalog.Traits["ColdLoving"];
|
||||
Assert.Contains("ColdLoving", heat.Incompatible);
|
||||
Assert.Contains("HeatLoving", cold.Incompatible);
|
||||
Assert.Contains("ColdLoving", catalog.TraitIncompatibilities("HeatLoving"));
|
||||
Assert.Contains("HeatLoving", catalog.TraitIncompatibilities("ColdLoving"));
|
||||
Assert.True(heat.ComfortTemperatureOffset > 0);
|
||||
Assert.True(cold.ComfortTemperatureOffset < 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WarmthNeed_RestoresOffCampus_AndIsEnvironmental()
|
||||
{
|
||||
var warmth = Vanilla().Needs["Warmth"];
|
||||
Assert.True(warmth.RestoredOffCampus);
|
||||
Assert.True(warmth.Environmental);
|
||||
Assert.True(warmth.DecayPerHour > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Porch_IsOutdoor()
|
||||
{
|
||||
Assert.True(Vanilla().Rooms["EntranceHall"].Outdoor);
|
||||
Assert.False(Vanilla().Rooms["Classroom"].Outdoor);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConcretePresetWithoutMonthlyNorms_FailsTheCatalog()
|
||||
{
|
||||
var ex = Assert.Throws<ContentLoadException>(() => _loader.Load(
|
||||
[CatalogLoader.CorePackId],
|
||||
[
|
||||
PackDocuments.Def(
|
||||
CatalogLoader.CorePackId,
|
||||
"climates",
|
||||
"empty",
|
||||
"""{ "defName": "EmptyClimate" }"""),
|
||||
]));
|
||||
|
||||
Assert.Contains("monthlyNorms", ex.Message, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private DefCatalog Vanilla()
|
||||
{
|
||||
var root = Path.Combine(AppContext.BaseDirectory, "vanilla");
|
||||
return _loader.Load([CatalogLoader.CorePackId], PackDocuments.FromDirectory(CatalogLoader.CorePackId, root));
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@ public class CountryDefTests
|
||||
CatalogLoader.CorePackId,
|
||||
"climates",
|
||||
"temperate",
|
||||
"""{ "defName": "TemperateContinental" }"""),
|
||||
"""{ "defName": "TemperateContinental", "monthlyNorms": [0,0,0,0,0,0,0,0,0,0,0,0] }"""),
|
||||
PackDocuments.Def(
|
||||
CatalogLoader.CorePackId,
|
||||
"countries",
|
||||
@@ -58,7 +58,7 @@ public class CountryDefTests
|
||||
CatalogLoader.CorePackId,
|
||||
"climates",
|
||||
"temperate",
|
||||
"""{ "defName": "TemperateContinental" }"""),
|
||||
"""{ "defName": "TemperateContinental", "monthlyNorms": [0,0,0,0,0,0,0,0,0,0,0,0] }"""),
|
||||
PackDocuments.Def(
|
||||
CatalogLoader.CorePackId,
|
||||
"countries",
|
||||
|
||||
@@ -11,13 +11,15 @@ public class PeopleDefTests
|
||||
var catalog = _loader.Load([CatalogLoader.CorePackId], PackDocuments.FromDirectory(CatalogLoader.CorePackId, root));
|
||||
|
||||
Assert.True(catalog.Skills.Count >= 10);
|
||||
Assert.Equal(10, catalog.Traits.Count);
|
||||
Assert.Equal(4, catalog.Needs.Count);
|
||||
Assert.Equal(12, catalog.Traits.Count);
|
||||
Assert.Equal(5, catalog.Needs.Count);
|
||||
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.True(need.DecayPerHour > 0));
|
||||
Assert.True(catalog.Needs["Sleep"].RestoredOffCampus);
|
||||
Assert.True(catalog.Needs["Warmth"].RestoredOffCampus);
|
||||
Assert.True(catalog.Needs["Warmth"].Environmental);
|
||||
Assert.Equal(0.2f, catalog.Needs["Hunger"].DecayPerHour);
|
||||
Assert.True(catalog.Skills["Communication"].Always);
|
||||
Assert.True(catalog.Skills["Agility"].Always);
|
||||
@@ -223,7 +225,7 @@ public class PeopleDefTests
|
||||
CatalogLoader.CorePackId,
|
||||
"climates",
|
||||
"temperate",
|
||||
"""{ "defName": "TemperateContinental" }"""),
|
||||
"""{ "defName": "TemperateContinental", "monthlyNorms": [0,0,0,0,0,0,0,0,0,0,0,0] }"""),
|
||||
PackDocuments.Def(
|
||||
CatalogLoader.CorePackId,
|
||||
"countries",
|
||||
|
||||
@@ -121,7 +121,7 @@ public class ProtocolCodecTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Clock_RoundTripsAndIsTwentyFourBytes()
|
||||
public void Clock_RoundTripsAndIsTwentySevenBytes()
|
||||
{
|
||||
var message = new ServerClockMessage(
|
||||
7,
|
||||
@@ -129,12 +129,14 @@ public class ProtocolCodecTests
|
||||
Running: true,
|
||||
SpeedIndex: 2,
|
||||
SkipAllowed: true,
|
||||
SkipTargetUnixMs: 1_333_516_800_000);
|
||||
SkipTargetUnixMs: 1_333_516_800_000,
|
||||
TemperatureTenths: -50,
|
||||
Precipitation: 2);
|
||||
Span<byte> buffer = stackalloc byte[ProtocolCodec.MaxFrameSize];
|
||||
|
||||
var length = ProtocolCodec.WriteClock(buffer, message);
|
||||
|
||||
Assert.Equal(24, length);
|
||||
Assert.Equal(27, length);
|
||||
Assert.Equal((byte)MessageType.ServerClock, buffer[0]);
|
||||
Assert.Equal(7, BitConverter.ToInt32(buffer[1..5]));
|
||||
Assert.Equal(1_333_432_800_000, BitConverter.ToInt64(buffer[5..13]));
|
||||
@@ -142,6 +144,8 @@ public class ProtocolCodecTests
|
||||
Assert.Equal(2, buffer[14]);
|
||||
Assert.Equal(1, buffer[15]);
|
||||
Assert.Equal(1_333_516_800_000, BitConverter.ToInt64(buffer[16..24]));
|
||||
Assert.Equal(-50, BitConverter.ToInt16(buffer[24..26]));
|
||||
Assert.Equal(2, buffer[26]);
|
||||
Assert.Equal(message, ProtocolCodec.ReadClock(buffer[..length]));
|
||||
}
|
||||
|
||||
@@ -154,9 +158,11 @@ public class ProtocolCodecTests
|
||||
var length = ProtocolCodec.WriteClock(buffer, message);
|
||||
var read = ProtocolCodec.ReadClock(buffer[..length]);
|
||||
|
||||
Assert.Equal(24, length);
|
||||
Assert.Equal(27, length);
|
||||
Assert.False(read.SkipAllowed);
|
||||
Assert.Equal(0, read.SkipTargetUnixMs);
|
||||
Assert.Equal(0, read.TemperatureTenths);
|
||||
Assert.Equal(PrecipitationKind.None, read.Precipitation);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
using Arch.Core;
|
||||
using HSchool.Ai;
|
||||
using HSchool.Content;
|
||||
using HSchool.People;
|
||||
|
||||
namespace HSchool.Simulation.Tests;
|
||||
|
||||
public class WarmthTests
|
||||
{
|
||||
private static readonly DateTime JanuaryMorning = new(2012, 1, 15, 6, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
[Fact]
|
||||
public void NakedOnFrost_LosesWarmth_AndHomeRestoresItOvernight()
|
||||
{
|
||||
using var school = Open(JanuaryMorning);
|
||||
PutFirstPerson(school, "yard", PersonInsulation.Naked, warmth: 1f);
|
||||
school.SyncWeather(force: true);
|
||||
Assert.True(school.Weather.TemperatureC < 0f);
|
||||
|
||||
WarmthDecay.Apply(school, gameMinutes: 60);
|
||||
var afterHour = WarmthOf(school);
|
||||
Assert.True(afterHour < 1f, $"warmth stayed {afterHour} on the frost");
|
||||
|
||||
PutFirstPerson(school, nodeId: null, PersonInsulation.Naked, afterHour);
|
||||
NeedDecay.Apply(school.World, school.Catalog!, gameMinutes: 60);
|
||||
Assert.Equal(school.Catalog!.Needs["Warmth"].Max, WarmthOf(school));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JacketOnFrost_LosesLessWarmthThanNaked()
|
||||
{
|
||||
using var school = Open(JanuaryMorning);
|
||||
var catalog = school.Catalog!;
|
||||
school.SyncWeather(force: true);
|
||||
|
||||
PutFirstPerson(school, "yard", PersonInsulation.Naked, warmth: 1f);
|
||||
WarmthDecay.Apply(school, gameMinutes: 60);
|
||||
var naked = WarmthOf(school);
|
||||
|
||||
PutFirstPerson(school, "yard", PersonInsulation.FromWorn(catalog, "Jacket"), warmth: 1f);
|
||||
WarmthDecay.Apply(school, gameMinutes: 60);
|
||||
var coated = WarmthOf(school);
|
||||
|
||||
Assert.True(naked < 1f);
|
||||
Assert.True(coated > naked, $"jacket {coated} should beat naked {naked}");
|
||||
}
|
||||
|
||||
private static void PutFirstPerson(School school, string? nodeId, PersonInsulation insulation, float warmth)
|
||||
{
|
||||
var query = new QueryDescription().WithAll<PersonNeeds, PersonInsulation, Presence>();
|
||||
var first = true;
|
||||
school.World.Query(
|
||||
in query,
|
||||
(ref PersonNeeds needs, ref PersonInsulation worn, ref Presence presence) =>
|
||||
{
|
||||
if (!first)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
first = false;
|
||||
worn = insulation;
|
||||
needs.Values["Warmth"] = warmth;
|
||||
presence = nodeId is null
|
||||
? Presence.OffCampus
|
||||
: new Presence(nodeId, 0f, nodeId, false, []);
|
||||
});
|
||||
}
|
||||
|
||||
private static float WarmthOf(School school)
|
||||
{
|
||||
var value = float.NaN;
|
||||
var query = new QueryDescription().WithAll<PersonNeeds>();
|
||||
school.World.Query(in query, (ref PersonNeeds needs) =>
|
||||
{
|
||||
if (float.IsNaN(value) && needs.Values.TryGetValue("Warmth", out var warmth))
|
||||
{
|
||||
value = warmth;
|
||||
}
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
private static School Open(DateTime start)
|
||||
{
|
||||
var (catalog, map) = Vanilla();
|
||||
var roster = RosterGenerator.Generate(catalog, map, schoolSeed: 1, "Russia", start);
|
||||
var pool = ApplicantPool.Create(catalog, roster, schoolSeed: 1, "Russia", start);
|
||||
var school = School.Create(1, "Тепло", start, catalog, map);
|
||||
school.InstallPeople(roster, seed: 1, "Russia", pool, climatePresetId: "TemperateContinental");
|
||||
school.ConfigurePresence();
|
||||
return school;
|
||||
}
|
||||
|
||||
private static (DefCatalog Catalog, MapLayout Map) Vanilla()
|
||||
{
|
||||
var root = Path.Combine(AppContext.BaseDirectory, "vanilla");
|
||||
var documents = new List<ContentDocument>();
|
||||
foreach (var path in Directory.EnumerateFiles(root, "*.*", SearchOption.AllDirectories))
|
||||
{
|
||||
if (!path.EndsWith(".jsonc", StringComparison.OrdinalIgnoreCase)
|
||||
&& !path.EndsWith(".json", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var relative = Path.GetRelativePath(root, path).Replace('\\', '/');
|
||||
documents.Add(new ContentDocument(CatalogLoader.CorePackId, relative, File.ReadAllText(path)));
|
||||
}
|
||||
|
||||
var catalog = new CatalogLoader().Load([CatalogLoader.CorePackId], documents);
|
||||
var map = CatalogLoader.LastDefaultMap([CatalogLoader.CorePackId], documents);
|
||||
Assert.NotNull(map);
|
||||
return (catalog, map);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
using HSchool.Content;
|
||||
using HSchool.People;
|
||||
|
||||
namespace HSchool.Simulation.Tests;
|
||||
|
||||
public class WeatherTests
|
||||
{
|
||||
private static readonly DateTime JanuaryMorning = new(2012, 1, 15, 6, 0, 0, DateTimeKind.Utc);
|
||||
private static readonly DateTime JulyAfternoon = new(2012, 7, 15, 15, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
[Fact]
|
||||
public void SamePresetSeedAndTimestamp_YieldTheSameStreet()
|
||||
{
|
||||
var preset = Vanilla().ClimatePresets["TemperateContinental"];
|
||||
var first = WeatherSampler.Sample(preset, schoolSeed: 7, JanuaryMorning);
|
||||
var second = WeatherSampler.Sample(preset, schoolSeed: 7, JanuaryMorning);
|
||||
Assert.Equal(first, second);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void January_IsColderThanJuly_OnTheVanillaPreset()
|
||||
{
|
||||
var preset = Vanilla().ClimatePresets["TemperateContinental"];
|
||||
var january = WeatherSampler.Sample(preset, schoolSeed: 3, JanuaryMorning);
|
||||
var july = WeatherSampler.Sample(preset, schoolSeed: 3, JulyAfternoon);
|
||||
Assert.True(january.TemperatureC < july.TemperatureC, $"{january.TemperatureC} vs {july.TemperatureC}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PrecipitationBelowZero_IsSnowNotRain()
|
||||
{
|
||||
var wet = new ClimatePresetDef
|
||||
{
|
||||
DefName = "AlwaysWet",
|
||||
MonthlyNorms = [-12, -10, -4, 5, 12, 17, 20, 18, 12, 5, -1, -8],
|
||||
DaySpread = 0,
|
||||
HourSpread = 0,
|
||||
PrecipitationChance = 1f,
|
||||
};
|
||||
|
||||
var january = WeatherSampler.Sample(wet, schoolSeed: 1, JanuaryMorning);
|
||||
var july = WeatherSampler.Sample(wet, schoolSeed: 1, JulyAfternoon);
|
||||
Assert.True(january.TemperatureC < 0);
|
||||
Assert.Equal(Precipitation.Snow, january.Precipitation);
|
||||
Assert.True(july.TemperatureC > 0);
|
||||
Assert.Equal(Precipitation.Rain, july.Precipitation);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VanillaJanuary_SnowIsNeverRain()
|
||||
{
|
||||
var preset = Vanilla().ClimatePresets["TemperateContinental"];
|
||||
for (var seed = 1; seed <= 40; seed++)
|
||||
{
|
||||
var weather = WeatherSampler.Sample(preset, seed, JanuaryMorning);
|
||||
if (weather.Precipitation == Precipitation.None)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Assert.True(weather.TemperatureC < 0);
|
||||
Assert.Equal(Precipitation.Snow, weather.Precipitation);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SkipEmpty_SetsMondayMorningWeather_NotSaturdays()
|
||||
{
|
||||
var saturday = new DateTime(2012, 1, 14, 22, 0, 0, DateTimeKind.Utc);
|
||||
var monday = new DateTime(2012, 1, 16, 6, 0, 0, DateTimeKind.Utc);
|
||||
using var school = Open(saturday);
|
||||
var evening = school.Weather;
|
||||
Assert.True(school.TrySkipEmpty().Succeeded);
|
||||
Assert.Equal(monday, school.Clock.Time);
|
||||
|
||||
var expected = WeatherSampler.Sample(
|
||||
school.Catalog!.ClimatePresets["TemperateContinental"],
|
||||
school.PeopleSeed,
|
||||
monday);
|
||||
Assert.Equal(expected, school.Weather);
|
||||
Assert.NotEqual(evening, school.Weather);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Indoor_IsWarmerThanTheYard_ButNotComfortableInJanuary()
|
||||
{
|
||||
using var school = Open(JanuaryMorning);
|
||||
var outdoor = PlaceClimate.TemperatureC(school, "yard");
|
||||
var porch = PlaceClimate.TemperatureC(school, "porch");
|
||||
var room = PlaceClimate.TemperatureC(school, "classroom-101");
|
||||
Assert.Equal(outdoor, porch);
|
||||
Assert.True(room > outdoor);
|
||||
Assert.True(room < 18f, $"walls only, got {room}");
|
||||
}
|
||||
|
||||
private static School Open(DateTime start)
|
||||
{
|
||||
var (catalog, map) = VanillaMap();
|
||||
var roster = RosterGenerator.Generate(catalog, map, schoolSeed: 1, "Russia", start);
|
||||
var pool = ApplicantPool.Create(catalog, roster, schoolSeed: 1, "Russia", start);
|
||||
var school = School.Create(1, "Погода", start, catalog, map);
|
||||
school.InstallPeople(roster, seed: 1, "Russia", pool, climatePresetId: "TemperateContinental");
|
||||
school.ConfigurePresence();
|
||||
return school;
|
||||
}
|
||||
|
||||
private static DefCatalog Vanilla()
|
||||
{
|
||||
var (catalog, _) = VanillaMap();
|
||||
return catalog;
|
||||
}
|
||||
|
||||
private static (DefCatalog Catalog, MapLayout Map) VanillaMap()
|
||||
{
|
||||
var root = Path.Combine(AppContext.BaseDirectory, "vanilla");
|
||||
var documents = new List<ContentDocument>();
|
||||
foreach (var path in Directory.EnumerateFiles(root, "*.*", SearchOption.AllDirectories))
|
||||
{
|
||||
if (!path.EndsWith(".jsonc", StringComparison.OrdinalIgnoreCase)
|
||||
&& !path.EndsWith(".json", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var relative = Path.GetRelativePath(root, path).Replace('\\', '/');
|
||||
documents.Add(new ContentDocument(CatalogLoader.CorePackId, relative, File.ReadAllText(path)));
|
||||
}
|
||||
|
||||
var catalog = new CatalogLoader().Load([CatalogLoader.CorePackId], documents);
|
||||
var map = CatalogLoader.LastDefaultMap([CatalogLoader.CorePackId], documents);
|
||||
Assert.NotNull(map);
|
||||
return (catalog, map);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user