Enhance school day structure and decision-making for lunch breaks
ci / server (push) Failing after 3m43s
ci / client (push) Successful in 15s

- Updated `ai.md` to clarify the mechanics of hunger restoration and the importance of lunch breaks in the school schedule.
- Revised `schedule.md` to detail the new lunch break structure, allowing for separate sittings for different grade levels.
- Enhanced `Decision.cs` and `DecisionPlanner.cs` to incorporate logic for lunch breaks, ensuring that students only leave lessons during their designated lunch windows.
- Updated `DayFrameDef` and related classes to support multiple lunch breaks and validate their configurations.
- Adjusted tests to validate the new decision-making logic regarding lunch breaks and hunger management, ensuring robust functionality.
- Improved localization strings to reflect changes in the school day structure and lunch functionalities.
This commit is contained in:
Leonid Pershin
2026-08-19 23:17:16 +03:00
parent a441ed9763
commit 5eabc90d53
33 changed files with 831 additions and 404 deletions
+5 -1
View File
@@ -375,6 +375,9 @@ internal static class PresenceSystem
var bound = Duty.IsOtherStaff(person)
? slot.Kind != DaySlotKind.Outside
: slot.Kind == DaySlotKind.Lesson && lessons.Any(lesson => lesson.Period == slot.Index);
var frame = school.Catalog!.DayFrame;
var lunchOpen = frame is not null
&& SchoolDay.IsLunchWindow(frame, slot, ClassOf(school, person)?.Year);
var state = new ActorState(
presence.NodeId,
presence.DestinationId,
@@ -386,7 +389,8 @@ internal static class PresenceSystem
bound,
duty,
needs.Values,
intent);
intent,
lunchOpen);
var decision = DecisionPlanner.Decide(
school.Catalog!,
school.Map!,
@@ -0,0 +1,18 @@
namespace HSchool.Simulation;
/// <summary>
/// Why a school could not be created. Lives here rather than in the server because the checks
/// that produce it — the name rules, the clock range — belong to the simulation.
/// </summary>
public enum SchoolCreationError
{
None = 0,
LimitReached,
InvalidName,
InvalidStartDate,
InvalidMap,
UnknownMod,
InvalidCatalog,
UnknownNameSet,
UnknownNativeLanguage,
}
+28
View File
@@ -0,0 +1,28 @@
namespace HSchool.Simulation;
/// <summary>
/// The rules a school name has to pass. A name arrives from a browser, so it is trimmed and
/// stripped before anything stores it.
/// </summary>
public static class SchoolNames
{
/// <summary>Trims, strips control characters and enforces the length limit.</summary>
public static bool TryNormalize(string? name, out string normalized)
{
normalized = string.Empty;
if (string.IsNullOrWhiteSpace(name))
{
return false;
}
var cleaned = new string(name.Where(character => !char.IsControl(character)).ToArray()).Trim();
if (cleaned.Length == 0 || cleaned.Length > School.MaxNameLength)
{
return false;
}
normalized = cleaned;
return true;
}
}
-147
View File
@@ -1,147 +0,0 @@
namespace HSchool.Simulation;
/// <summary>Why a school could not be created.</summary>
public enum SchoolCreationError
{
None = 0,
LimitReached,
InvalidName,
InvalidStartDate,
InvalidMap,
UnknownMod,
InvalidCatalog,
UnknownNameSet,
UnknownNativeLanguage,
}
/// <summary>Outcome of <see cref="SchoolRegistry.Create"/>: either the school or the reason there is none.</summary>
public readonly record struct SchoolCreationResult(School? School, SchoolCreationError Error)
{
public bool Succeeded => Error == SchoolCreationError.None && School is not null;
public static SchoolCreationResult Failed(SchoolCreationError error) => new(null, error);
}
/// <summary>
/// In-memory set of schools plus the cap from configuration. Not thread-safe — unit tests and
/// name/limit checks use it; the live server gives each school its own worker instead.
/// </summary>
public sealed class SchoolRegistry : IDisposable
{
private readonly SimulationOptions _options;
private readonly List<School> _schools = [];
private int _nextId = 1;
private bool _disposed;
public SchoolRegistry(SimulationOptions options)
{
_options = options;
NameGenerator = new SchoolNameGenerator();
}
public SchoolNameGenerator NameGenerator { get; }
public int MaxSchools => _options.MaxSchools;
public int Count => _schools.Count;
public bool IsFull => _schools.Count >= _options.MaxSchools;
/// <summary>Schools in creation order — the order the menu lists them in.</summary>
public IReadOnlyList<School> Schools => _schools;
public School? Find(int id) => _schools.Find(school => school.Id == id);
public SchoolCreationResult Create(string name, DateTime startDate)
{
ObjectDisposedException.ThrowIf(_disposed, this);
if (IsFull)
{
return SchoolCreationResult.Failed(SchoolCreationError.LimitReached);
}
if (!TryNormalizeName(name, out var normalized))
{
return SchoolCreationResult.Failed(SchoolCreationError.InvalidName);
}
if (!GameClock.IsValidStartDate(startDate))
{
return SchoolCreationResult.Failed(SchoolCreationError.InvalidStartDate);
}
var school = School.Create(_nextId++, normalized, startDate);
_schools.Add(school);
return new SchoolCreationResult(school, SchoolCreationError.None);
}
public bool Delete(int id)
{
ObjectDisposedException.ThrowIf(_disposed, this);
var school = Find(id);
if (school is null)
{
return false;
}
_schools.Remove(school);
school.Dispose();
return true;
}
/// <summary>Advances every running school by one fixed step.</summary>
public void Tick()
{
ObjectDisposedException.ThrowIf(_disposed, this);
foreach (var school in _schools)
{
school.Tick(_options.FixedDeltaTime, _options.GameMinutesPerRealSecond);
}
}
/// <summary>A name the player has not used yet, for the "random" button in the creation form.</summary>
public string SuggestName(SchoolNameLanguage language = SchoolNameLanguage.Russian) =>
NameGenerator.Next(_schools.Select(school => school.Name), language);
/// <summary>Trims, strips control characters and enforces the length limit.</summary>
public static bool TryNormalizeName(string? name, out string normalized)
{
normalized = string.Empty;
if (string.IsNullOrWhiteSpace(name))
{
return false;
}
var cleaned = new string(name.Where(character => !char.IsControl(character)).ToArray()).Trim();
if (cleaned.Length == 0 || cleaned.Length > School.MaxNameLength)
{
return false;
}
normalized = cleaned;
return true;
}
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
foreach (var school in _schools)
{
school.Dispose();
}
_schools.Clear();
}
}