Wichtel-Werkstatt: Secret-Santa-App mit Räumen, Ausschlüssen und Recovery-Links
- FastAPI + SQLite, serverseitig gerenderte Templates, mobil-zuerst - Admin-Passwort, einseitige Ausschlüsse, eingefrorene Auslosung - Teilnehmer sehen Ergebnis per Cookie; Einmal-Recovery-Links bei Verlust - Docker/podman-tauglich (Entrypoint mit Privilegien-Drop, SELinux-:z) - Unit-Tests für Auslosung, E2E-Testskript (30 Checks)
This commit is contained in:
+41
@@ -0,0 +1,41 @@
|
||||
"""Auslosung: zufällige Zuordnung, bei der niemand sich selbst zieht
|
||||
und einseitige Ausschlüsse respektiert werden.
|
||||
|
||||
Ansatz: Rejection-Sampling über zufällige Permutationen. Für typische
|
||||
Gruppen (Familien/Freunde, < 30 Personen) findet sich eine gültige
|
||||
Permutation fast immer in wenigen Versuchen (nur die Derangement-
|
||||
Bedingung allein hat bereits Erfolgswahrscheinlichkeit ~1/e).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
|
||||
MAX_ATTEMPTS = 2000
|
||||
|
||||
|
||||
def draw_assignment(
|
||||
participant_ids: list[str],
|
||||
exclusions: set[tuple[str, str]],
|
||||
*,
|
||||
rng: random.Random | None = None,
|
||||
max_attempts: int = MAX_ATTEMPTS,
|
||||
) -> dict[str, str] | None:
|
||||
"""Liefert {giver_id: receiver_id} oder None, wenn keine gültige
|
||||
Zuordnung gefunden wurde (z. B. unerfüllbare Ausschlüsse)."""
|
||||
rng = rng or random.Random()
|
||||
ids = list(participant_ids)
|
||||
n = len(ids)
|
||||
if n < 2:
|
||||
return None
|
||||
|
||||
for _ in range(max_attempts):
|
||||
receivers = ids[:]
|
||||
rng.shuffle(receivers)
|
||||
if any(receivers[i] == ids[i] for i in range(n)):
|
||||
continue
|
||||
mapping = dict(zip(ids, receivers))
|
||||
if any((giver, receiver) in exclusions for giver, receiver in mapping.items()):
|
||||
continue
|
||||
return mapping
|
||||
return None
|
||||
Reference in New Issue
Block a user