Feature: 100-point scoring, question memory & spaced repetition, random wizard name generator, cache-busting v2.2, gold footer pills

This commit is contained in:
kobold-ai
2026-09-21 13:53:33 +02:00
parent 26ff0660d3
commit a938123271
10 changed files with 257 additions and 33 deletions
+67 -8
View File
@@ -49,6 +49,7 @@ def format_question_for_client(q: Question, number: int, total: int, lang: str =
random.shuffle(options_list)
return {
"id": q.id,
"question_number": number,
"total_questions": total,
"category": q.category,
@@ -59,10 +60,15 @@ def format_question_for_client(q: Question, number: int, total: int, lang: str =
}
def get_tier_evaluation(score: int, total: int = 10, lang: str = "de") -> dict:
pct = (score / total) if total > 0 else 0
if total <= 10 and score <= 10:
pct = (score / total) if total > 0 else 0
else:
max_possible = (total * 100) if total <= 10 else total
pct = (score / max_possible) if max_possible > 0 else 0
is_de = (lang != "en")
if score >= 10 or pct >= 1.0:
if pct >= 0.90:
grade = "O"
title = "🧙‍♂️ Großmeister der Zauberei" if is_de else "🧙‍♂️ Grandmaster of Wizardry"
subtitle = "Note: O (Ohnegleichen)" if is_de else "Grade: O (Outstanding)"
@@ -76,7 +82,7 @@ def get_tier_evaluation(score: int, total: int = 10, lang: str = "de") -> dict:
"The Ministry of Magic bows in deep awe!"
)
badge_class = "tier-outstanding"
elif score >= 8 or pct >= 0.8:
elif pct >= 0.70:
grade = "E"
title = "⚡ Meister-Hexe / Zauberer" if is_de else "⚡ Master Witch / Wizard"
subtitle = "Note: E (Erwartungen übertroffen)" if is_de else "Grade: E (Exceeds Expectations)"
@@ -88,7 +94,7 @@ def get_tier_evaluation(score: int, total: int = 10, lang: str = "de") -> dict:
"Your wandwork is razor-sharp and your knowledge makes even Ravenclaws jealous."
)
badge_class = "tier-exceeds"
elif score >= 5 or pct >= 0.5:
elif pct >= 0.50:
grade = "A"
title = "📜 Solider Hogwarts-Schüler" if is_de else "📜 Solid Hogwarts Student"
subtitle = "Note: A (Annehmbar)" if is_de else "Grade: A (Acceptable)"
@@ -100,7 +106,7 @@ def get_tier_evaluation(score: int, total: int = 10, lang: str = "de") -> dict:
"but you bravely fought your way through. A solid magical performance!"
)
badge_class = "tier-acceptable"
elif score >= 2 or pct >= 0.2:
elif pct >= 0.20:
grade = "M"
title = "🧹 Quaffel-Fänger in Not" if is_de else "🧹 Quaffle Catcher in Distress"
subtitle = "Note: M (Mies)" if is_de else "Grade: P (Poor)"
@@ -163,8 +169,43 @@ async def start_game(payload: GameStartRequest = None, db: Session = Depends(get
if not pool:
raise HTTPException(status_code=400, detail="Keine aktiven Fragen vorhanden.")
sample_size = min(10, len(pool))
selected_questions = random.sample(pool, sample_size)
recent_set = set(payload.recent_ids or []) if payload else set()
wrong_set = set(payload.wrong_ids or []) if payload else set()
# Priority pool 1: Questions user previously failed (learning pool)
wrong_pool = [q for q in pool if q.id in wrong_set]
# Priority pool 2: Fresh questions not seen recently and not in wrong pool
fresh_pool = [q for q in pool if q.id not in recent_set and q.id not in wrong_set]
# Priority pool 3: Recent questions fallback
recent_pool = [q for q in pool if q.id in recent_set and q.id not in wrong_set]
selected_questions = []
# Up to 3 questions from wrong_pool for spaced repetition
if wrong_pool:
k_wrong = min(3, len(wrong_pool))
selected_questions.extend(random.sample(wrong_pool, k_wrong))
# Fill remaining from fresh_pool
needed = min(10, len(pool)) - len(selected_questions)
if needed > 0 and fresh_pool:
k_fresh = min(needed, len(fresh_pool))
selected_questions.extend(random.sample(fresh_pool, k_fresh))
# If still needed, fill from recent_pool
needed = min(10, len(pool)) - len(selected_questions)
if needed > 0 and recent_pool:
k_recent = min(needed, len(recent_pool))
selected_questions.extend(random.sample(recent_pool, k_recent))
# Fallback to any remaining from pool
needed = min(10, len(pool)) - len(selected_questions)
if needed > 0:
remaining = [q for q in pool if q not in selected_questions]
if remaining:
selected_questions.extend(random.sample(remaining, min(needed, len(remaining))))
# Shuffle to mix learning and fresh questions
random.shuffle(selected_questions)
question_ids = [q.id for q in selected_questions]
session_id = str(uuid.uuid4())
@@ -211,6 +252,8 @@ async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db))
return {
"correct": False,
"score": session.score,
"points_earned": 0,
"question_id": None,
"attempts_used": session.attempts_current,
"attempts_left": 0,
"is_finished": True,
@@ -226,7 +269,15 @@ async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db))
correct = is_answer_correct(payload.answer, current_q.canonical_answer, current_q.accepted_answers, current_q.answer_en)
if correct:
session.score += 1
# Tiered points based on attempt
if session.attempts_current == 0:
points_earned = 100
elif session.attempts_current == 1:
points_earned = 70
else:
points_earned = 40
session.score += points_earned
session.current_question_idx += 1
session.attempts_current = 0
@@ -248,6 +299,8 @@ async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db))
return {
"correct": True,
"score": session.score,
"points_earned": points_earned,
"question_id": current_q_id,
"attempts_used": 0,
"attempts_left": 3,
"is_finished": is_finished,
@@ -286,6 +339,8 @@ async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db))
return {
"correct": False,
"score": session.score,
"points_earned": 0,
"question_id": current_q_id,
"attempts_used": 3,
"attempts_left": 0,
"is_finished": is_finished,
@@ -298,6 +353,8 @@ async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db))
return {
"correct": False,
"score": session.score,
"points_earned": 0,
"question_id": current_q_id,
"attempts_used": session.attempts_current,
"attempts_left": attempts_left,
"is_finished": False,
@@ -330,6 +387,7 @@ async def get_hint(session_id: str, db: Session = Depends(get_db)):
@app.get("/finale", response_class=HTMLResponse)
async def finale(request: Request, score: int = 0, total: int = 10, time: int = 0, lang: str = "de"):
max_score = 1000 if (total <= 10 and score > 10) else (total * 100 if total <= 10 else total)
tier = get_tier_evaluation(score, total, lang)
if time >= 60:
minutes = time // 60
@@ -344,6 +402,7 @@ async def finale(request: Request, score: int = 0, total: int = 10, time: int =
context={
"score": score,
"total": total,
"max_score": max_score,
"time_seconds": time,
"time_formatted": time_str,
"lang": lang,