Enhance school day structure and decision-making for lunch breaks
- 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:
@@ -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,
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user