[chore:] better crafting #5

Merged
fierke merged 1 commits from bettercrafting into main 2026-05-30 11:33:11 +00:00
Showing only changes of commit 22e9377062 - Show all commits
+134 -53
View File
@@ -16,7 +16,7 @@ public class CourseCrafter
= new();
public static void Craft()
{
{
GeneratedCourses = new();
int globalCount = 0;
List<(Sport, List<string>)> initial_sportlist = new();
@@ -53,7 +53,6 @@ public class CourseCrafter
var inst = new CourseInstance();
inst.Sport = item.Item1;
inst.Students = new List<string>();
// int dist = 1;
for (int i = item.Item2.Count - 1; i >= 0; i--)
{
if (inst.Students.Count >= inst.Sport.MaxStudents)
@@ -68,31 +67,25 @@ public class CourseCrafter
item.Item2.RemoveAt(i);
}
}
if (inst.Students.Count < inst.Sport.MinStudents)
if (inst.Students.Count < getEffectiveMinStudents(inst.Sport, semester))
{
// Rückgängig machen
foreach (var s in inst.Students)
{
students_in_semester[semester-1].Remove(s);
students_in_semester[semester - 1].Remove(s);
item.Item2.Add(s);
}
continue; // Kurs nicht erstellen
}
GeneratedCourses.Add((semester, inst));
//MainWindow.Instance.TbResultLog.Text += ($"{semester} -> {inst.Students.Count}\n");
//MainWindow.Instance.TbResultLog.Text += ($"{students_in_semester[0].Count} - {students_in_semester[1].Count} - {students_in_semester[2].Count} - {students_in_semester[3].Count}\n\n");
}
semeq0: ;
}
}
FillExistingCourses();
// Kurs umdisponieren (besser verteilen)
// Kurs umdisponieren (besser verteilen)
bool changed;
int maxIterations = 20;
@@ -103,7 +96,6 @@ public class CourseCrafter
changed = false;
iteration++;
// nach Sport gruppieren
var sports = GeneratedCourses
.GroupBy(c => c.Instance.Sport.Name);
@@ -111,7 +103,6 @@ public class CourseCrafter
{
var courses = sportGroup.ToList();
// paarweise vergleichen
for (int i = 0; i < courses.Count; i++)
{
for (int j = 0; j < courses.Count; j++)
@@ -121,28 +112,23 @@ public class CourseCrafter
var cA = courses[i];
var cB = courses[j];
// nur sinnvoll, wenn Unterschied
if (cA.Instance.Students.Count <= cB.Instance.Students.Count + 1)
continue;
// Kandidaten aus A nach B verschieben
for (int k = cA.Instance.Students.Count - 1; k >= 0; k--)
{
string stud = cA.Instance.Students[k];
// 1. Zielsemester frei?
if (!isStudentFree(cB.Semester, stud))
continue;
// 2. Zielkurs hat noch Platz?
if (cB.Instance.Students.Count >= cB.Instance.Sport.MaxStudents)
continue;
// 3. Quellkurs darf nicht unter Min fallen
if (cA.Instance.Students.Count - 1 < cA.Instance.Sport.MinStudents)
// Quellkurs darf nicht unter effektives Min fallen
if (cA.Instance.Students.Count - 1 < getEffectiveMinStudents(cA.Instance.Sport, cA.Semester))
continue;
// --- MOVE durchführen ---
cA.Instance.Students.RemoveAt(k);
students_in_semester[cA.Semester - 1].Remove(stud);
@@ -150,7 +136,7 @@ public class CourseCrafter
students_in_semester[cB.Semester - 1].Add(stud);
changed = true;
break; // nach jedem Move neu bewerten
break;
}
}
}
@@ -166,10 +152,10 @@ public class CourseCrafter
{
cancel++;
if (cancel >= 20) break;
// Kandidaten suchen: splittbare Kurse, deren Sport noch Kapazität hat
var candidate = GeneratedCourses
.Where(c => c.Semester == semester)
.Where(c => c.Instance.Students.Count >= c.Instance.Sport.MinStudents * 2)
.Where(c => c.Instance.Students.Count >= getEffectiveMinStudents(c.Instance.Sport, c.Semester) * 2)
.Where(c =>
{
int sportCount = GeneratedCourses.Count(g =>
@@ -190,8 +176,8 @@ public class CourseCrafter
int movedCount = totalStudents / 2;
int remainingCount = totalStudents - movedCount;
if (movedCount < candidate.Instance.Sport.MinStudents ||
remainingCount < candidate.Instance.Sport.MinStudents)
if (movedCount < getEffectiveMinStudents(candidate.Instance.Sport, candidate.Semester) ||
remainingCount < getEffectiveMinStudents(candidate.Instance.Sport, candidate.Semester))
{
break;
}
@@ -233,6 +219,120 @@ public class CourseCrafter
OptimizeStudentWishCoverage();
// --- Ringtausch: Schüler mit 4 Wünschen aber < 4 Zuteilungen neu zuordnen ---
foreach (var student in Settings.Instance.Students)
{
if (student.SelectedCourseNames.Count < 4)
continue;
var currentAssignments = GetAssignmentsBySemester(student.ID);
int currentCount = currentAssignments.Count(a => a != null);
if (currentCount >= 4)
continue;
// Alle aktuellen Zuteilungen entfernen (temporär)
foreach (var assignment in currentAssignments)
{
if (assignment == null) continue;
assignment.Value.Instance.Students.Remove(student.ID);
students_in_semester[assignment.Value.Semester - 1].Remove(student.ID);
}
// Alle gewünschten Sports ermitteln
var desiredSports = student.SelectedCourseNames
.Select(ResolveSportFromSelection)
.Where(s => s != null)
.DistinctBy(s => s!.Name)
.Select(s => s!)
.ToList();
// Kandidatenkurse pro Semester: Kurse mit freiem Platz, die einem Wunschsport entsprechen
var candidatesPerSemester = new List<(int Semester, CourseInstance Instance)>[4];
for (int si = 0; si < 4; si++)
{
candidatesPerSemester[si] = GeneratedCourses
.Where(c => c.Semester == si + 1)
.Where(c => c.Instance.Students.Count < c.Instance.Sport.MaxStudents)
.Where(c => desiredSports.Any(ds => ds.Name == c.Instance.Sport.Name))
.ToList();
candidatesPerSemester[si].Insert(0, default); // default = kein Kurs
}
(int Semester, CourseInstance Instance)?[] bestAssignment = currentAssignments;
int bestScore = currentCount;
int s0 = candidatesPerSemester[0].Count;
int s1 = candidatesPerSemester[1].Count;
int s2 = candidatesPerSemester[2].Count;
int s3 = candidatesPerSemester[3].Count;
var sportSelectedCounts = student.SelectedCourseNames
.Select(ResolveSportFromSelection)
.Where(sp => sp != null)
.GroupBy(sp => sp!.Name)
.ToDictionary(g => g.Key, g => g.Count());
for (int i0 = 0; i0 < s0; i0++)
for (int i1 = 0; i1 < s1; i1++)
for (int i2 = 0; i2 < s2; i2++)
for (int i3 = 0; i3 < s3; i3++)
{
var c0 = candidatesPerSemester[0][i0];
var c1 = candidatesPerSemester[1][i1];
var c2 = candidatesPerSemester[2][i2];
var c3 = candidatesPerSemester[3][i3];
var chosen = new (int Semester, CourseInstance Instance)?[]
{
c0.Instance != null ? (c0.Semester, c0.Instance) : null,
c1.Instance != null ? (c1.Semester, c1.Instance) : null,
c2.Instance != null ? (c2.Semester, c2.Instance) : null,
c3.Instance != null ? (c3.Semester, c3.Instance) : null
};
bool valid = true;
var sportUsageCounts = new Dictionary<string, int>();
for (int si = 0; si < 4; si++)
{
if (chosen[si] == null) continue;
string sportName = chosen[si]!.Value.Instance.Sport.Name;
sportUsageCounts.TryGetValue(sportName, out int used);
int allowed = sportSelectedCounts.GetValueOrDefault(sportName, 0);
if (used >= allowed) { valid = false; break; }
sportUsageCounts[sportName] = used + 1;
}
if (!valid) continue;
int score = chosen.Count(c => c != null);
if (score > bestScore)
{
bestScore = score;
bestAssignment = chosen;
}
}
// Beste Zuteilung anwenden
foreach (var assignment in bestAssignment)
{
if (assignment == null) continue;
assignment.Value.Instance.Students.Add(student.ID);
students_in_semester[assignment.Value.Semester - 1].Add(student.ID);
}
}
// ---------------------------------------------------------------------------
// Lokale Hilfsfunktionen
// ---------------------------------------------------------------------------
int getEffectiveMinStudents(Sport sport, int semester)
{
int reduction = (semester >= 3) ? 2 : 0;
return Math.Max(1, sport.MinStudents - reduction);
}
bool isStudentFree(int semester, string studentID)
{
foreach (var inst in GeneratedCourses)
@@ -240,22 +340,17 @@ public class CourseCrafter
if (semester != inst.Semester) continue;
foreach (string stud in inst.Instance.Students)
{
if (stud == studentID) return false; // Schüler in genanntem Semester bereits gefunden
if (stud == studentID) return false;
}
}
// Schüler nicht gefunden:
return true;
}
bool requestExit()
{
globalCount++;
// max Kursanzahl
if (GeneratedCourses.Count >= Settings.Instance.NumCoursesPerSemester * 4) return true;
int low = 0;
foreach (var item in initial_sportlist)
{
@@ -263,7 +358,6 @@ public class CourseCrafter
}
if (low >= initial_sportlist.Count) return true;
if (globalCount >= 20) return true;
return false;
}
@@ -286,7 +380,6 @@ public class CourseCrafter
MainWindow.Instance.TbResultTextout.Text +=
$"{tuple.Item1}: {tuple.Item2.Count} remaining ({missingPerSemester[0]},{missingPerSemester[1]},{missingPerSemester[2]},{missingPerSemester[3]})\n";
}
MainWindow.Instance.TbResultTextout.Text += $"\n total remaining: {total_missing}";
@@ -360,7 +453,7 @@ public class CourseCrafter
.Where(c => c.Instance.Sport.Name == sport.Name && c.Semester != semester)
.OrderByDescending(c => c.Instance.Students.Count))
{
int movableCount = course.Instance.Students.Count - sport.MinStudents;
int movableCount = course.Instance.Students.Count - getEffectiveMinStudents(sport, course.Semester);
if (movableCount <= 0)
continue;
@@ -377,7 +470,7 @@ public class CourseCrafter
}
}
if (directlyPlaceable.Count + donorMoves.Count < sport.MinStudents)
if (directlyPlaceable.Count + donorMoves.Count < getEffectiveMinStudents(sport, semester))
continue;
var newCourse = new CourseInstance
@@ -407,7 +500,7 @@ public class CourseCrafter
newCourse.Students.Add(move.StudentId);
}
if (newCourse.Students.Count < sport.MinStudents)
if (newCourse.Students.Count < getEffectiveMinStudents(sport, semester))
continue;
foreach (var studentId in newCourse.Students)
@@ -527,7 +620,7 @@ public class CourseCrafter
if (sourceCourse.Semester == targetCourse.Semester)
return false;
if (sourceCourse.Instance.Students.Count - 1 < sourceCourse.Instance.Sport.MinStudents)
if (sourceCourse.Instance.Students.Count - 1 < getEffectiveMinStudents(sourceCourse.Instance.Sport, sourceCourse.Semester))
return false;
if (targetCourse.Instance.Students.Count >= targetCourse.Instance.Sport.MaxStudents)
@@ -580,7 +673,6 @@ public class CourseCrafter
{
if (sp.Semester[i] == 0) continue;
// prüfen, ob für diesen Sport im Semester i schon die maximale Anzahl erreicht ist
int sportCoursesInSemester = GeneratedCourses
.Count(g => g.Semester == i + 1 && g.Instance.Sport.Name == sp.Name);
@@ -591,7 +683,7 @@ public class CourseCrafter
.Distinct()
.Count(studentId => !students_in_semester[i].Contains(studentId));
if (freeInterestedStudents < sp.MinStudents)
if (freeInterestedStudents < getEffectiveMinStudents(sp, i + 1))
continue;
if (semcount[i] < Settings.Instance.NumCoursesPerSemester &&
@@ -607,7 +699,6 @@ public class CourseCrafter
int getSemesterForSport(Sport sp, List<string> interestedStudents)
{
// 1. Zähle alle Kurse pro Semester (egal welche Sportart)
int[] totalCoursesPerSemester = new int[4];
foreach (var inst in GeneratedCourses)
totalCoursesPerSemester[inst.Semester - 1]++;
@@ -616,25 +707,20 @@ public class CourseCrafter
int minCourses = int.MaxValue;
int maxFreeInterestedStudents = -1;
// 2. Kandidaten-Semester durchgehen
for (int i = 0; i < 4; i++)
{
// a) Sport darf in diesem Semester gar nicht stattfinden?
if (sp.Semester[i] == 0)
continue;
int semesterNumber = i + 1;
// b) Wie viele Kurse DIESES Sports gibt es schon in diesem Semester?
int sportCoursesInThisSemester = GeneratedCourses
.Count(g => g.Semester == semesterNumber &&
g.Instance.Sport.Name == sp.Name);
// c) Pro-Sport-Limit erreicht?
if (sportCoursesInThisSemester >= sp.Semester[i])
continue;
// d) Globales Limit pro Semester erreicht?
if (totalCoursesPerSemester[i] >= Settings.Instance.NumCoursesPerSemester)
continue;
@@ -642,12 +728,9 @@ public class CourseCrafter
.Distinct()
.Count(studentId => !students_in_semester[i].Contains(studentId));
// e) Ohne genügend freie Interessenten kann in diesem Semester kein Kurs entstehen
if (freeInterestedStudents < sp.MinStudents)
if (freeInterestedStudents < getEffectiveMinStudents(sp, semesterNumber))
continue;
// f) Primär das Semester mit den meisten tatsächlich freien Interessenten wählen,
// sekundär das mit den wenigsten Kursen insgesamt.
if (freeInterestedStudents > maxFreeInterestedStudents ||
(freeInterestedStudents == maxFreeInterestedStudents &&
totalCoursesPerSemester[i] < minCourses))
@@ -658,7 +741,7 @@ public class CourseCrafter
}
}
return bestSem; // 0, falls kein zulässiges Semester gefunden
return bestSem;
}
var errors = ValidateCourses(GeneratedCourses);
@@ -685,13 +768,11 @@ public class CourseCrafter
{
if (course.Instance.Students.Contains(student.ID))
{
student.Result[course.Semester-1] = course.Instance.Sport.Name;
student.Result[course.Semester - 1] = course.Instance.Sport.Name;
}
}
}
}
}
public static string GenerateStatistics()
{
GeneratedCourses.Sort((x,y) => x.Semester.CompareTo(y.Semester) );