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:
@@ -16,6 +16,26 @@ public sealed class DayFrameDef : Def
|
||||
public int LongBreakAfter { get; init; }
|
||||
|
||||
public int LongBreakMinutes { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Which break each parallel eats at. Empty means one sitting for the whole school, at
|
||||
/// <see cref="LongBreakAfter"/>. Every listed break is a long one, so a school that feeds its
|
||||
/// juniors and seniors separately gets two wide breaks rather than one crowded canteen.
|
||||
/// </summary>
|
||||
public IReadOnlyList<LunchBreakDef> LunchBreaks { get; init; } = [];
|
||||
}
|
||||
|
||||
/// <summary>One lunch sitting: the break it happens at and the parallels it feeds.</summary>
|
||||
public sealed class LunchBreakDef
|
||||
{
|
||||
/// <summary>The sitting fills the break after this 1-based lesson.</summary>
|
||||
public int AfterLesson { get; init; }
|
||||
|
||||
public int GradeMin { get; init; }
|
||||
|
||||
public int GradeMax { get; init; }
|
||||
|
||||
public bool Covers(int year) => year >= GradeMin && year <= GradeMax;
|
||||
}
|
||||
|
||||
public sealed class MonthDay
|
||||
|
||||
@@ -54,6 +54,13 @@ public sealed class ActionDef : Def
|
||||
|
||||
/// <summary>Leisure weight. Zero means phase 21 will not pick this for fun — only for a need.</summary>
|
||||
public float Weight { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// A sitting rather than something done whenever the need bites: only offered during the
|
||||
/// eater's own lunch break (<see cref="DayFrameDef.LunchBreaks"/>). Without it a hungry class
|
||||
/// would walk out of a lesson to the canteen, and the whole school would arrive at once.
|
||||
/// </summary>
|
||||
public bool Lunch { get; init; }
|
||||
}
|
||||
|
||||
public sealed class ThingDef : Def
|
||||
|
||||
@@ -391,6 +391,31 @@ internal static class PeopleDefValidator
|
||||
{
|
||||
throw new ContentLoadException($"DayFrameDef '{frame.DefName}' longBreakAfter must be 0 or a lesson before the last.");
|
||||
}
|
||||
|
||||
var fed = new HashSet<int>();
|
||||
foreach (var sitting in frame.LunchBreaks)
|
||||
{
|
||||
if (sitting.AfterLesson < 1 || sitting.AfterLesson >= frame.LessonCount)
|
||||
{
|
||||
throw new ContentLoadException(
|
||||
$"DayFrameDef '{frame.DefName}' has a lunch break after lesson {sitting.AfterLesson}; it must be a lesson before the last.");
|
||||
}
|
||||
|
||||
if (sitting.GradeMin < 1 || sitting.GradeMax < sitting.GradeMin)
|
||||
{
|
||||
throw new ContentLoadException(
|
||||
$"DayFrameDef '{frame.DefName}' has a lunch break with grades {sitting.GradeMin}–{sitting.GradeMax}.");
|
||||
}
|
||||
|
||||
for (var year = sitting.GradeMin; year <= sitting.GradeMax; year++)
|
||||
{
|
||||
if (!fed.Add(year))
|
||||
{
|
||||
throw new ContentLoadException(
|
||||
$"DayFrameDef '{frame.DefName}' feeds grade {year} at two lunch breaks.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateAction(ActionDef action, DefCatalog catalog)
|
||||
|
||||
@@ -65,7 +65,7 @@ public static class SchoolDay
|
||||
break;
|
||||
}
|
||||
|
||||
var gap = TimeSpan.FromMinutes(i == frame.LongBreakAfter ? frame.LongBreakMinutes : frame.BreakMinutes);
|
||||
var gap = TimeSpan.FromMinutes(IsLongBreakAfter(frame, i) ? frame.LongBreakMinutes : frame.BreakMinutes);
|
||||
var breakEnd = cursor + gap;
|
||||
if (clock >= cursor && clock < breakEnd)
|
||||
{
|
||||
@@ -152,7 +152,7 @@ public static class SchoolDay
|
||||
for (var i = 1; i < period; i++)
|
||||
{
|
||||
cursor += lesson;
|
||||
cursor += TimeSpan.FromMinutes(i == frame.LongBreakAfter ? frame.LongBreakMinutes : frame.BreakMinutes);
|
||||
cursor += TimeSpan.FromMinutes(IsLongBreakAfter(frame, i) ? frame.LongBreakMinutes : frame.BreakMinutes);
|
||||
}
|
||||
|
||||
return TimeOnly.FromTimeSpan(cursor);
|
||||
@@ -225,4 +225,77 @@ public static class SchoolDay
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A break is long when it is the school-wide one or when somebody eats in it. Sittings are
|
||||
/// what make a second break wide: fifteen minutes of lunch do not fit into ten.
|
||||
/// </summary>
|
||||
public static bool IsLongBreakAfter(DayFrameDef frame, int lesson)
|
||||
{
|
||||
if (lesson == frame.LongBreakAfter)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach (var sitting in frame.LunchBreaks)
|
||||
{
|
||||
if (sitting.AfterLesson == lesson)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The break this parallel eats in. Falls back to the school-wide long break when no sitting
|
||||
/// lists the year — a staff member (<paramref name="year"/> null) eats at any of them.
|
||||
/// </summary>
|
||||
public static int LunchBreakAfter(DayFrameDef frame, int? year)
|
||||
{
|
||||
if (frame.LunchBreaks.Count == 0 || year is null)
|
||||
{
|
||||
return frame.LongBreakAfter;
|
||||
}
|
||||
|
||||
foreach (var sitting in frame.LunchBreaks)
|
||||
{
|
||||
if (sitting.Covers(year.Value))
|
||||
{
|
||||
return sitting.AfterLesson;
|
||||
}
|
||||
}
|
||||
|
||||
return frame.LongBreakAfter;
|
||||
}
|
||||
|
||||
/// <summary>True when <paramref name="slot"/> is the sitting that feeds this parallel.</summary>
|
||||
public static bool IsLunchWindow(DayFrameDef frame, DaySlot slot, int? year)
|
||||
{
|
||||
if (slot.Kind != DaySlotKind.Break)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (frame.LunchBreaks.Count == 0)
|
||||
{
|
||||
return slot.Index == frame.LongBreakAfter;
|
||||
}
|
||||
|
||||
if (year is null)
|
||||
{
|
||||
foreach (var sitting in frame.LunchBreaks)
|
||||
{
|
||||
if (sitting.AfterLesson == slot.Index)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return slot.Index == LunchBreakAfter(frame, year);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user