wichtel_generator/generator.py
2026-01-09 16:53:37 +01:00

121 lines
4.1 KiB
Python

import random
from db import SecretSantaDB
import hashlib
class SecretSantaGenerator:
def __init__(self, imp_size: int, db: SecretSantaDB):
self.imp_size = imp_size
self.db = db
def constellation_key(self, imps: dict) -> str:
emails = sorted([data["email"].strip().lower() for data in imps.values()])
raw = "|".join(emails)
return hashlib.sha256(raw.encode("utf-8")).hexdigest()
def derangements(self, n: int) -> int:
if n == 0: return 1
if n == 1: return 0
a, b = 1, 0
for k in range(2, n + 1):
a, b = b, (k - 1) * (a + b)
return b
def pair_exists(self, cycle_id: int, imp_pairs: dict, giver: str, receiver: str, imps: dict) -> bool:
if giver == receiver:
return True
if giver in imp_pairs:
return True
if receiver in imp_pairs.values():
return True
giver_email = imps[giver]["email"]
receiver_email = imps[receiver]["email"]
if self.db.pair_used_in_cycle(cycle_id, giver_email, receiver_email):
return True
return False
def generate_imp_pairs(self, round_id: int, imps: dict, cycle_id: int):
names = list(imps.keys())
n = len(names)
emails = {name: imps[name]["email"] for name in names}
def is_allowed(giver: str, receiver: str, current: dict) -> bool:
if giver == receiver:
return False
if giver in current:
return False
if receiver in current.values():
return False
return not self.db.pair_used_in_cycle(cycle_id, emails[giver], emails[receiver])
givers = names[:]
def backtrack(i: int, current: dict) -> dict | None:
if i == n:
return current
giver = givers[i]
candidates = names[:]
random.shuffle(candidates)
for receiver in candidates:
if not is_allowed(giver, receiver, current):
continue
current[giver] = receiver
result = backtrack(i + 1, current)
if result is not None:
return result
del current[giver]
return None
pairs = backtrack(0, {})
if pairs is None:
raise RuntimeError("Keine gültige Zuordnung im aktuellen Cycle möglich. Neuer Cycle nötig.")
for giver, receiver in pairs.items():
giver_id = self.db.get_imp_id_from_name(round_id, giver)
receiver_id = self.db.get_imp_id_from_name(round_id, receiver)
self.db.add_new_pair(round_id, giver_id, receiver_id)
def create_new_round(self,round_name: str, participants: dict[str, dict]) -> tuple[int, str]:
try:
key = self.constellation_key(participants)
latest_cycle = self.db.get_latest_cycle_id(key)
max_permutations = self.derangements(len(participants))
if latest_cycle is None:
cycle_id = self.db.add_new_cycle(key)
else:
used_permutations = self.db.count_rounds_in_cycle(latest_cycle)
if used_permutations < max_permutations:
cycle_id = latest_cycle
else:
cycle_id = self.db.add_new_cycle(key)
round_id, created_at = self.db.add_new_round(round_name, key, cycle_id)
self.db.add_participants(round_id, participants)
try:
self.generate_imp_pairs(round_id, participants, cycle_id)
except RuntimeError:
cycle_id = self.db.add_new_cycle(key)
round_id, created_at = self.db.add_new_round(round_name, key, cycle_id)
self.db.add_participants(round_id, participants)
self.generate_imp_pairs(round_id, participants, cycle_id)
self.db.commit_to_db()
return round_id, created_at
except Exception as e:
self.db.connection.rollback()
raise RuntimeError(f"Fehler beim Erstellen der Runde: {e}")