Enhance school simulation and management by integrating roster functionality, allowing for the installation and persistence of student and staff data. Update the school architecture to include a roster alongside existing components, ensuring proper validation against map layouts. Revise room definitions to support homeroom designations and update related tests to validate new functionalities and ensure robustness in roster handling.
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
namespace HSchool.People;
|
||||
|
||||
/// <summary>
|
||||
/// Whether a generated or loaded roster still fills this map. A mismatch means the file is stale
|
||||
/// relative to the layout — the school must not start and the file must stay untouched.
|
||||
/// </summary>
|
||||
public static class RosterFit
|
||||
{
|
||||
public static bool Matches(Roster roster, SchoolDemand demand)
|
||||
{
|
||||
if (roster.Classes.Count != demand.Classes.Count || roster.People.Count(person => person.IsStudent) != demand.Seats.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var classesByRoom = roster.Classes.ToDictionary(schoolClass => schoolClass.RoomId, StringComparer.Ordinal);
|
||||
foreach (var expected in demand.Classes)
|
||||
{
|
||||
if (!classesByRoom.TryGetValue(expected.RoomId, out var actual)
|
||||
|| actual.Capacity != expected.Capacity
|
||||
|| actual.PupilIds.Count != expected.Capacity)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var staffed = roster.People
|
||||
.Where(person => person.IsStaff && person.Position is not null && person.WorkplaceRoomId is not null)
|
||||
.Select(person => new StaffOpening(person.WorkplaceRoomId!, person.Position!))
|
||||
.ToHashSet();
|
||||
|
||||
if (staffed.Count != demand.Staff.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var opening in demand.Staff)
|
||||
{
|
||||
if (!staffed.Contains(opening))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace HSchool.People;
|
||||
|
||||
/// <summary>On-disk shape of <c>saves/{id}.people.json</c>. Composition only — needs are not live values.</summary>
|
||||
public sealed class RosterDocument
|
||||
{
|
||||
public const int CurrentFormat = 1;
|
||||
|
||||
public int Format { get; init; } = CurrentFormat;
|
||||
|
||||
public int Seed { get; init; }
|
||||
|
||||
public required IReadOnlyList<Person> People { get; init; }
|
||||
|
||||
public required IReadOnlyList<Family> Families { get; init; }
|
||||
|
||||
public required IReadOnlyList<SchoolClass> Classes { get; init; }
|
||||
|
||||
public Roster ToRoster() => new(People, Families, Classes);
|
||||
|
||||
public static RosterDocument From(int seed, Roster roster) =>
|
||||
new()
|
||||
{
|
||||
Format = CurrentFormat,
|
||||
Seed = seed,
|
||||
People = roster.People,
|
||||
Families = roster.Families,
|
||||
Classes = roster.Classes,
|
||||
};
|
||||
}
|
||||
|
||||
public static class RosterJson
|
||||
{
|
||||
public static JsonSerializerOptions Options { get; } = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
PropertyNameCaseInsensitive = true,
|
||||
WriteIndented = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
};
|
||||
|
||||
public static string Serialize(RosterDocument document) =>
|
||||
JsonSerializer.Serialize(document, Options);
|
||||
|
||||
public static RosterDocument Parse(string json)
|
||||
{
|
||||
var document = JsonSerializer.Deserialize<RosterDocument>(json, Options);
|
||||
if (document is null || document.People is null || document.Families is null || document.Classes is null)
|
||||
{
|
||||
throw new InvalidOperationException("People save deserialized to nothing.");
|
||||
}
|
||||
|
||||
return document;
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,8 @@ public readonly record struct StaffOpening(string RoomId, string Position);
|
||||
public readonly record struct PupilSeat(string ClassId, string RoomId, int Year, string Letter);
|
||||
|
||||
/// <summary>
|
||||
/// How many pupils and staff a map asks for. Classrooms are rooms with pupil slots; each one
|
||||
/// becomes a roster class. Positions come from <see cref="RoomDef.Positions"/>, not the wire labels.
|
||||
/// How many pupils and staff a map asks for. Homeroom rooms with pupil slots become roster
|
||||
/// classes; every <see cref="RoomDef.Positions"/> entry is a staff opening.
|
||||
/// </summary>
|
||||
public sealed class SchoolDemand
|
||||
{
|
||||
@@ -34,17 +34,17 @@ public sealed class SchoolDemand
|
||||
foreach (var room in map.Rooms)
|
||||
{
|
||||
var slots = PupilSlotsOf(catalog, room);
|
||||
if (slots > 0)
|
||||
{
|
||||
classrooms.Add((room, slots));
|
||||
}
|
||||
|
||||
if (catalog.Rooms.TryGetValue(room.Def, out var def))
|
||||
{
|
||||
foreach (var position in def.Positions)
|
||||
{
|
||||
staff.Add(new StaffOpening(room.Id, position));
|
||||
}
|
||||
|
||||
if (slots > 0 && def.Homeroom)
|
||||
{
|
||||
classrooms.Add((room, slots));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user