[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
+130 -49
View File
@@ -53,7 +53,6 @@ public class CourseCrafter
var inst = new CourseInstance(); var inst = new CourseInstance();
inst.Sport = item.Item1; inst.Sport = item.Item1;
inst.Students = new List<string>(); inst.Students = new List<string>();
// int dist = 1;
for (int i = item.Item2.Count - 1; i >= 0; i--) for (int i = item.Item2.Count - 1; i >= 0; i--)
{ {
if (inst.Students.Count >= inst.Sport.MaxStudents) if (inst.Students.Count >= inst.Sport.MaxStudents)
@@ -68,7 +67,7 @@ public class CourseCrafter
item.Item2.RemoveAt(i); item.Item2.RemoveAt(i);
} }
} }
if (inst.Students.Count < inst.Sport.MinStudents) if (inst.Students.Count < getEffectiveMinStudents(inst.Sport, semester))
{ {
// Rückgängig machen // Rückgängig machen
foreach (var s in inst.Students) foreach (var s in inst.Students)
@@ -79,20 +78,14 @@ public class CourseCrafter
continue; // Kurs nicht erstellen continue; // Kurs nicht erstellen
} }
GeneratedCourses.Add((semester, inst)); 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: ; semeq0: ;
} }
} }
FillExistingCourses(); FillExistingCourses();
// Kurs umdisponieren (besser verteilen)
// Kurs umdisponieren (besser verteilen) // Kurs umdisponieren (besser verteilen)
bool changed; bool changed;
int maxIterations = 20; int maxIterations = 20;
@@ -103,7 +96,6 @@ public class CourseCrafter
changed = false; changed = false;
iteration++; iteration++;
// nach Sport gruppieren
var sports = GeneratedCourses var sports = GeneratedCourses
.GroupBy(c => c.Instance.Sport.Name); .GroupBy(c => c.Instance.Sport.Name);
@@ -111,7 +103,6 @@ public class CourseCrafter
{ {
var courses = sportGroup.ToList(); var courses = sportGroup.ToList();
// paarweise vergleichen
for (int i = 0; i < courses.Count; i++) for (int i = 0; i < courses.Count; i++)
{ {
for (int j = 0; j < courses.Count; j++) for (int j = 0; j < courses.Count; j++)
@@ -121,28 +112,23 @@ public class CourseCrafter
var cA = courses[i]; var cA = courses[i];
var cB = courses[j]; var cB = courses[j];
// nur sinnvoll, wenn Unterschied
if (cA.Instance.Students.Count <= cB.Instance.Students.Count + 1) if (cA.Instance.Students.Count <= cB.Instance.Students.Count + 1)
continue; continue;
// Kandidaten aus A nach B verschieben
for (int k = cA.Instance.Students.Count - 1; k >= 0; k--) for (int k = cA.Instance.Students.Count - 1; k >= 0; k--)
{ {
string stud = cA.Instance.Students[k]; string stud = cA.Instance.Students[k];
// 1. Zielsemester frei?
if (!isStudentFree(cB.Semester, stud)) if (!isStudentFree(cB.Semester, stud))
continue; continue;
// 2. Zielkurs hat noch Platz?
if (cB.Instance.Students.Count >= cB.Instance.Sport.MaxStudents) if (cB.Instance.Students.Count >= cB.Instance.Sport.MaxStudents)
continue; continue;
// 3. Quellkurs darf nicht unter Min fallen // Quellkurs darf nicht unter effektives Min fallen
if (cA.Instance.Students.Count - 1 < cA.Instance.Sport.MinStudents) if (cA.Instance.Students.Count - 1 < getEffectiveMinStudents(cA.Instance.Sport, cA.Semester))
continue; continue;
// --- MOVE durchführen ---
cA.Instance.Students.RemoveAt(k); cA.Instance.Students.RemoveAt(k);
students_in_semester[cA.Semester - 1].Remove(stud); students_in_semester[cA.Semester - 1].Remove(stud);
@@ -150,7 +136,7 @@ public class CourseCrafter
students_in_semester[cB.Semester - 1].Add(stud); students_in_semester[cB.Semester - 1].Add(stud);
changed = true; changed = true;
break; // nach jedem Move neu bewerten break;
} }
} }
} }
@@ -166,10 +152,10 @@ public class CourseCrafter
{ {
cancel++; cancel++;
if (cancel >= 20) break; if (cancel >= 20) break;
// Kandidaten suchen: splittbare Kurse, deren Sport noch Kapazität hat
var candidate = GeneratedCourses var candidate = GeneratedCourses
.Where(c => c.Semester == semester) .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 => .Where(c =>
{ {
int sportCount = GeneratedCourses.Count(g => int sportCount = GeneratedCourses.Count(g =>
@@ -190,8 +176,8 @@ public class CourseCrafter
int movedCount = totalStudents / 2; int movedCount = totalStudents / 2;
int remainingCount = totalStudents - movedCount; int remainingCount = totalStudents - movedCount;
if (movedCount < candidate.Instance.Sport.MinStudents || if (movedCount < getEffectiveMinStudents(candidate.Instance.Sport, candidate.Semester) ||
remainingCount < candidate.Instance.Sport.MinStudents) remainingCount < getEffectiveMinStudents(candidate.Instance.Sport, candidate.Semester))
{ {
break; break;
} }
@@ -233,6 +219,120 @@ public class CourseCrafter
OptimizeStudentWishCoverage(); 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) bool isStudentFree(int semester, string studentID)
{ {
foreach (var inst in GeneratedCourses) foreach (var inst in GeneratedCourses)
@@ -240,22 +340,17 @@ public class CourseCrafter
if (semester != inst.Semester) continue; if (semester != inst.Semester) continue;
foreach (string stud in inst.Instance.Students) 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; return true;
} }
bool requestExit() bool requestExit()
{ {
globalCount++; globalCount++;
// max Kursanzahl
if (GeneratedCourses.Count >= Settings.Instance.NumCoursesPerSemester * 4) return true; if (GeneratedCourses.Count >= Settings.Instance.NumCoursesPerSemester * 4) return true;
int low = 0; int low = 0;
foreach (var item in initial_sportlist) foreach (var item in initial_sportlist)
{ {
@@ -263,7 +358,6 @@ public class CourseCrafter
} }
if (low >= initial_sportlist.Count) return true; if (low >= initial_sportlist.Count) return true;
if (globalCount >= 20) return true; if (globalCount >= 20) return true;
return false; return false;
} }
@@ -286,7 +380,6 @@ public class CourseCrafter
MainWindow.Instance.TbResultTextout.Text += MainWindow.Instance.TbResultTextout.Text +=
$"{tuple.Item1}: {tuple.Item2.Count} remaining ({missingPerSemester[0]},{missingPerSemester[1]},{missingPerSemester[2]},{missingPerSemester[3]})\n"; $"{tuple.Item1}: {tuple.Item2.Count} remaining ({missingPerSemester[0]},{missingPerSemester[1]},{missingPerSemester[2]},{missingPerSemester[3]})\n";
} }
MainWindow.Instance.TbResultTextout.Text += $"\n total remaining: {total_missing}"; 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) .Where(c => c.Instance.Sport.Name == sport.Name && c.Semester != semester)
.OrderByDescending(c => c.Instance.Students.Count)) .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) if (movableCount <= 0)
continue; continue;
@@ -377,7 +470,7 @@ public class CourseCrafter
} }
} }
if (directlyPlaceable.Count + donorMoves.Count < sport.MinStudents) if (directlyPlaceable.Count + donorMoves.Count < getEffectiveMinStudents(sport, semester))
continue; continue;
var newCourse = new CourseInstance var newCourse = new CourseInstance
@@ -407,7 +500,7 @@ public class CourseCrafter
newCourse.Students.Add(move.StudentId); newCourse.Students.Add(move.StudentId);
} }
if (newCourse.Students.Count < sport.MinStudents) if (newCourse.Students.Count < getEffectiveMinStudents(sport, semester))
continue; continue;
foreach (var studentId in newCourse.Students) foreach (var studentId in newCourse.Students)
@@ -527,7 +620,7 @@ public class CourseCrafter
if (sourceCourse.Semester == targetCourse.Semester) if (sourceCourse.Semester == targetCourse.Semester)
return false; 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; return false;
if (targetCourse.Instance.Students.Count >= targetCourse.Instance.Sport.MaxStudents) if (targetCourse.Instance.Students.Count >= targetCourse.Instance.Sport.MaxStudents)
@@ -580,7 +673,6 @@ public class CourseCrafter
{ {
if (sp.Semester[i] == 0) continue; 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 int sportCoursesInSemester = GeneratedCourses
.Count(g => g.Semester == i + 1 && g.Instance.Sport.Name == sp.Name); .Count(g => g.Semester == i + 1 && g.Instance.Sport.Name == sp.Name);
@@ -591,7 +683,7 @@ public class CourseCrafter
.Distinct() .Distinct()
.Count(studentId => !students_in_semester[i].Contains(studentId)); .Count(studentId => !students_in_semester[i].Contains(studentId));
if (freeInterestedStudents < sp.MinStudents) if (freeInterestedStudents < getEffectiveMinStudents(sp, i + 1))
continue; continue;
if (semcount[i] < Settings.Instance.NumCoursesPerSemester && if (semcount[i] < Settings.Instance.NumCoursesPerSemester &&
@@ -607,7 +699,6 @@ public class CourseCrafter
int getSemesterForSport(Sport sp, List<string> interestedStudents) int getSemesterForSport(Sport sp, List<string> interestedStudents)
{ {
// 1. Zähle alle Kurse pro Semester (egal welche Sportart)
int[] totalCoursesPerSemester = new int[4]; int[] totalCoursesPerSemester = new int[4];
foreach (var inst in GeneratedCourses) foreach (var inst in GeneratedCourses)
totalCoursesPerSemester[inst.Semester - 1]++; totalCoursesPerSemester[inst.Semester - 1]++;
@@ -616,25 +707,20 @@ public class CourseCrafter
int minCourses = int.MaxValue; int minCourses = int.MaxValue;
int maxFreeInterestedStudents = -1; int maxFreeInterestedStudents = -1;
// 2. Kandidaten-Semester durchgehen
for (int i = 0; i < 4; i++) for (int i = 0; i < 4; i++)
{ {
// a) Sport darf in diesem Semester gar nicht stattfinden?
if (sp.Semester[i] == 0) if (sp.Semester[i] == 0)
continue; continue;
int semesterNumber = i + 1; int semesterNumber = i + 1;
// b) Wie viele Kurse DIESES Sports gibt es schon in diesem Semester?
int sportCoursesInThisSemester = GeneratedCourses int sportCoursesInThisSemester = GeneratedCourses
.Count(g => g.Semester == semesterNumber && .Count(g => g.Semester == semesterNumber &&
g.Instance.Sport.Name == sp.Name); g.Instance.Sport.Name == sp.Name);
// c) Pro-Sport-Limit erreicht?
if (sportCoursesInThisSemester >= sp.Semester[i]) if (sportCoursesInThisSemester >= sp.Semester[i])
continue; continue;
// d) Globales Limit pro Semester erreicht?
if (totalCoursesPerSemester[i] >= Settings.Instance.NumCoursesPerSemester) if (totalCoursesPerSemester[i] >= Settings.Instance.NumCoursesPerSemester)
continue; continue;
@@ -642,12 +728,9 @@ public class CourseCrafter
.Distinct() .Distinct()
.Count(studentId => !students_in_semester[i].Contains(studentId)); .Count(studentId => !students_in_semester[i].Contains(studentId));
// e) Ohne genügend freie Interessenten kann in diesem Semester kein Kurs entstehen if (freeInterestedStudents < getEffectiveMinStudents(sp, semesterNumber))
if (freeInterestedStudents < sp.MinStudents)
continue; 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 || if (freeInterestedStudents > maxFreeInterestedStudents ||
(freeInterestedStudents == maxFreeInterestedStudents && (freeInterestedStudents == maxFreeInterestedStudents &&
totalCoursesPerSemester[i] < minCourses)) 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); var errors = ValidateCourses(GeneratedCourses);
@@ -689,9 +772,7 @@ public class CourseCrafter
} }
} }
} }
} }
public static string GenerateStatistics() public static string GenerateStatistics()
{ {
GeneratedCourses.Sort((x,y) => x.Semester.CompareTo(y.Semester) ); GeneratedCourses.Sort((x,y) => x.Semester.CompareTo(y.Semester) );