Feature: 100-point scoring, question memory & spaced repetition, random wizard name generator, cache-busting v2.2, gold footer pills
This commit is contained in:
+67
-8
@@ -49,6 +49,7 @@ def format_question_for_client(q: Question, number: int, total: int, lang: str =
|
|||||||
random.shuffle(options_list)
|
random.shuffle(options_list)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
"id": q.id,
|
||||||
"question_number": number,
|
"question_number": number,
|
||||||
"total_questions": total,
|
"total_questions": total,
|
||||||
"category": q.category,
|
"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:
|
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")
|
is_de = (lang != "en")
|
||||||
|
|
||||||
if score >= 10 or pct >= 1.0:
|
if pct >= 0.90:
|
||||||
grade = "O"
|
grade = "O"
|
||||||
title = "🧙♂️ Großmeister der Zauberei" if is_de else "🧙♂️ Grandmaster of Wizardry"
|
title = "🧙♂️ Großmeister der Zauberei" if is_de else "🧙♂️ Grandmaster of Wizardry"
|
||||||
subtitle = "Note: O (Ohnegleichen)" if is_de else "Grade: O (Outstanding)"
|
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!"
|
"The Ministry of Magic bows in deep awe!"
|
||||||
)
|
)
|
||||||
badge_class = "tier-outstanding"
|
badge_class = "tier-outstanding"
|
||||||
elif score >= 8 or pct >= 0.8:
|
elif pct >= 0.70:
|
||||||
grade = "E"
|
grade = "E"
|
||||||
title = "⚡ Meister-Hexe / Zauberer" if is_de else "⚡ Master Witch / Wizard"
|
title = "⚡ Meister-Hexe / Zauberer" if is_de else "⚡ Master Witch / Wizard"
|
||||||
subtitle = "Note: E (Erwartungen übertroffen)" if is_de else "Grade: E (Exceeds Expectations)"
|
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."
|
"Your wandwork is razor-sharp and your knowledge makes even Ravenclaws jealous."
|
||||||
)
|
)
|
||||||
badge_class = "tier-exceeds"
|
badge_class = "tier-exceeds"
|
||||||
elif score >= 5 or pct >= 0.5:
|
elif pct >= 0.50:
|
||||||
grade = "A"
|
grade = "A"
|
||||||
title = "📜 Solider Hogwarts-Schüler" if is_de else "📜 Solid Hogwarts Student"
|
title = "📜 Solider Hogwarts-Schüler" if is_de else "📜 Solid Hogwarts Student"
|
||||||
subtitle = "Note: A (Annehmbar)" if is_de else "Grade: A (Acceptable)"
|
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!"
|
"but you bravely fought your way through. A solid magical performance!"
|
||||||
)
|
)
|
||||||
badge_class = "tier-acceptable"
|
badge_class = "tier-acceptable"
|
||||||
elif score >= 2 or pct >= 0.2:
|
elif pct >= 0.20:
|
||||||
grade = "M"
|
grade = "M"
|
||||||
title = "🧹 Quaffel-Fänger in Not" if is_de else "🧹 Quaffle Catcher in Distress"
|
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)"
|
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:
|
if not pool:
|
||||||
raise HTTPException(status_code=400, detail="Keine aktiven Fragen vorhanden.")
|
raise HTTPException(status_code=400, detail="Keine aktiven Fragen vorhanden.")
|
||||||
|
|
||||||
sample_size = min(10, len(pool))
|
recent_set = set(payload.recent_ids or []) if payload else set()
|
||||||
selected_questions = random.sample(pool, sample_size)
|
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]
|
question_ids = [q.id for q in selected_questions]
|
||||||
session_id = str(uuid.uuid4())
|
session_id = str(uuid.uuid4())
|
||||||
|
|
||||||
@@ -211,6 +252,8 @@ async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db))
|
|||||||
return {
|
return {
|
||||||
"correct": False,
|
"correct": False,
|
||||||
"score": session.score,
|
"score": session.score,
|
||||||
|
"points_earned": 0,
|
||||||
|
"question_id": None,
|
||||||
"attempts_used": session.attempts_current,
|
"attempts_used": session.attempts_current,
|
||||||
"attempts_left": 0,
|
"attempts_left": 0,
|
||||||
"is_finished": True,
|
"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)
|
correct = is_answer_correct(payload.answer, current_q.canonical_answer, current_q.accepted_answers, current_q.answer_en)
|
||||||
|
|
||||||
if correct:
|
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.current_question_idx += 1
|
||||||
session.attempts_current = 0
|
session.attempts_current = 0
|
||||||
|
|
||||||
@@ -248,6 +299,8 @@ async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db))
|
|||||||
return {
|
return {
|
||||||
"correct": True,
|
"correct": True,
|
||||||
"score": session.score,
|
"score": session.score,
|
||||||
|
"points_earned": points_earned,
|
||||||
|
"question_id": current_q_id,
|
||||||
"attempts_used": 0,
|
"attempts_used": 0,
|
||||||
"attempts_left": 3,
|
"attempts_left": 3,
|
||||||
"is_finished": is_finished,
|
"is_finished": is_finished,
|
||||||
@@ -286,6 +339,8 @@ async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db))
|
|||||||
return {
|
return {
|
||||||
"correct": False,
|
"correct": False,
|
||||||
"score": session.score,
|
"score": session.score,
|
||||||
|
"points_earned": 0,
|
||||||
|
"question_id": current_q_id,
|
||||||
"attempts_used": 3,
|
"attempts_used": 3,
|
||||||
"attempts_left": 0,
|
"attempts_left": 0,
|
||||||
"is_finished": is_finished,
|
"is_finished": is_finished,
|
||||||
@@ -298,6 +353,8 @@ async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db))
|
|||||||
return {
|
return {
|
||||||
"correct": False,
|
"correct": False,
|
||||||
"score": session.score,
|
"score": session.score,
|
||||||
|
"points_earned": 0,
|
||||||
|
"question_id": current_q_id,
|
||||||
"attempts_used": session.attempts_current,
|
"attempts_used": session.attempts_current,
|
||||||
"attempts_left": attempts_left,
|
"attempts_left": attempts_left,
|
||||||
"is_finished": False,
|
"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)
|
@app.get("/finale", response_class=HTMLResponse)
|
||||||
async def finale(request: Request, score: int = 0, total: int = 10, time: int = 0, lang: str = "de"):
|
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)
|
tier = get_tier_evaluation(score, total, lang)
|
||||||
if time >= 60:
|
if time >= 60:
|
||||||
minutes = time // 60
|
minutes = time // 60
|
||||||
@@ -344,6 +402,7 @@ async def finale(request: Request, score: int = 0, total: int = 10, time: int =
|
|||||||
context={
|
context={
|
||||||
"score": score,
|
"score": score,
|
||||||
"total": total,
|
"total": total,
|
||||||
|
"max_score": max_score,
|
||||||
"time_seconds": time,
|
"time_seconds": time,
|
||||||
"time_formatted": time_str,
|
"time_formatted": time_str,
|
||||||
"lang": lang,
|
"lang": lang,
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ from typing import Optional, List
|
|||||||
class GameStartRequest(BaseModel):
|
class GameStartRequest(BaseModel):
|
||||||
lang: Optional[str] = "de"
|
lang: Optional[str] = "de"
|
||||||
difficulty: Optional[str] = "all"
|
difficulty: Optional[str] = "all"
|
||||||
|
recent_ids: Optional[List[int]] = None
|
||||||
|
wrong_ids: Optional[List[int]] = None
|
||||||
|
|
||||||
class GameStartResponse(BaseModel):
|
class GameStartResponse(BaseModel):
|
||||||
session_id: str
|
session_id: str
|
||||||
@@ -24,6 +26,8 @@ class GameCheckRequest(BaseModel):
|
|||||||
class GameCheckResponse(BaseModel):
|
class GameCheckResponse(BaseModel):
|
||||||
correct: bool
|
correct: bool
|
||||||
score: int
|
score: int
|
||||||
|
points_earned: Optional[int] = 0
|
||||||
|
question_id: Optional[int] = None
|
||||||
attempts_used: int
|
attempts_used: int
|
||||||
attempts_left: int
|
attempts_left: int
|
||||||
max_attempts: int = 3
|
max_attempts: int = 3
|
||||||
|
|||||||
@@ -957,6 +957,32 @@ body {
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-dice {
|
||||||
|
background: rgba(201, 168, 106, 0.18);
|
||||||
|
border: 1px solid var(--gold);
|
||||||
|
color: var(--gold-bright);
|
||||||
|
padding: 0 14px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-family: inherit;
|
||||||
|
font-weight: bold;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
min-height: 46px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-dice:hover {
|
||||||
|
background: var(--gold);
|
||||||
|
color: #0b0b16;
|
||||||
|
box-shadow: 0 0 12px rgba(201, 168, 106, 0.5);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
/* Leaderboard tables & filters */
|
/* Leaderboard tables & filters */
|
||||||
.diff-filter-tabs {
|
.diff-filter-tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -1245,27 +1271,40 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.footer-links {
|
.footer-links {
|
||||||
margin-top: 8px;
|
margin-top: 12px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-link {
|
.footer-link {
|
||||||
color: var(--text-muted);
|
color: var(--gold-soft);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-size: 0.8rem;
|
font-size: 0.82rem;
|
||||||
transition: color 0.2s ease;
|
font-weight: 600;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: rgba(201, 168, 106, 0.12);
|
||||||
|
border: 1px solid rgba(201, 168, 106, 0.35);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-link:hover {
|
.footer-link:hover {
|
||||||
color: var(--gold-bright);
|
color: #0b0b16;
|
||||||
text-decoration: underline;
|
background: var(--gold);
|
||||||
|
border-color: var(--gold-bright);
|
||||||
|
box-shadow: 0 0 12px rgba(201, 168, 106, 0.5);
|
||||||
|
text-decoration: none;
|
||||||
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-sep {
|
.footer-sep {
|
||||||
color: rgba(255, 255, 255, 0.2);
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.legal-card {
|
.legal-card {
|
||||||
|
|||||||
+79
-3
@@ -159,6 +159,58 @@ function applyQuizTranslations(lang) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let sessionQuestionIds = [];
|
||||||
|
|
||||||
|
function getRecentQuestionIds() {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem("quiz_recent_qids");
|
||||||
|
return stored ? JSON.parse(stored) : [];
|
||||||
|
} catch(e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWrongQuestionIds() {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem("quiz_wrong_qids");
|
||||||
|
return stored ? JSON.parse(stored) : [];
|
||||||
|
} catch(e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function recordQuestionWrong(qid) {
|
||||||
|
if (!qid) return;
|
||||||
|
try {
|
||||||
|
const wrong = getWrongQuestionIds();
|
||||||
|
if (!wrong.includes(qid)) {
|
||||||
|
wrong.push(qid);
|
||||||
|
if (wrong.length > 30) wrong.shift();
|
||||||
|
localStorage.setItem("quiz_wrong_qids", JSON.stringify(wrong));
|
||||||
|
}
|
||||||
|
} catch(e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function recordQuestionMastered(qid) {
|
||||||
|
if (!qid) return;
|
||||||
|
try {
|
||||||
|
let wrong = getWrongQuestionIds();
|
||||||
|
if (wrong.includes(qid)) {
|
||||||
|
wrong = wrong.filter(id => id !== qid);
|
||||||
|
localStorage.setItem("quiz_wrong_qids", JSON.stringify(wrong));
|
||||||
|
}
|
||||||
|
} catch(e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveRecentQuestions(ids) {
|
||||||
|
if (!ids || ids.length === 0) return;
|
||||||
|
try {
|
||||||
|
let recent = getRecentQuestionIds();
|
||||||
|
recent = Array.from(new Set([...ids, ...recent])).slice(0, 20);
|
||||||
|
localStorage.setItem("quiz_recent_qids", JSON.stringify(recent));
|
||||||
|
} catch(e) {}
|
||||||
|
}
|
||||||
|
|
||||||
async function startGame() {
|
async function startGame() {
|
||||||
const envelope = document.getElementById("envelope");
|
const envelope = document.getElementById("envelope");
|
||||||
const startBtn = document.getElementById("startBtn");
|
const startBtn = document.getElementById("startBtn");
|
||||||
@@ -167,6 +219,7 @@ async function startGame() {
|
|||||||
startBtn.disabled = true;
|
startBtn.disabled = true;
|
||||||
startBtn.innerText = t.startBtnCasting;
|
startBtn.innerText = t.startBtnCasting;
|
||||||
envelope.classList.add("envelope-opened");
|
envelope.classList.add("envelope-opened");
|
||||||
|
sessionQuestionIds = [];
|
||||||
|
|
||||||
// Start background music if enabled or first click
|
// Start background music if enabled or first click
|
||||||
const bgm = document.getElementById("bgm");
|
const bgm = document.getElementById("bgm");
|
||||||
@@ -183,12 +236,17 @@ async function startGame() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const recentIds = getRecentQuestionIds();
|
||||||
|
const wrongIds = getWrongQuestionIds();
|
||||||
|
|
||||||
const res = await fetch("/api/game/start", {
|
const res = await fetch("/api/game/start", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
lang: currentLang,
|
lang: currentLang,
|
||||||
difficulty: selectedDifficulty
|
difficulty: selectedDifficulty,
|
||||||
|
recent_ids: recentIds,
|
||||||
|
wrong_ids: wrongIds
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -239,6 +297,9 @@ function updateTimer() {
|
|||||||
|
|
||||||
function renderQuestion(qData) {
|
function renderQuestion(qData) {
|
||||||
lastQuestionData = qData;
|
lastQuestionData = qData;
|
||||||
|
if (qData.id && !sessionQuestionIds.includes(qData.id)) {
|
||||||
|
sessionQuestionIds.push(qData.id);
|
||||||
|
}
|
||||||
const t = UI_TEXT[currentLang] || UI_TEXT.de;
|
const t = UI_TEXT[currentLang] || UI_TEXT.de;
|
||||||
|
|
||||||
document.getElementById("questionCounter").innerText = `${t.questionLabel} ${qData.question_number} ${t.ofLabel} ${totalQuestions}`;
|
document.getElementById("questionCounter").innerText = `${t.questionLabel} ${qData.question_number} ${t.ofLabel} ${totalQuestions}`;
|
||||||
@@ -364,7 +425,17 @@ async function submitAnswer(answer, clickedBtn = null) {
|
|||||||
clickedBtn.classList.add("choice-correct");
|
clickedBtn.classList.add("choice-correct");
|
||||||
}
|
}
|
||||||
frageBox.classList.add("sparkle-effect");
|
frageBox.classList.add("sparkle-effect");
|
||||||
feedbackBox.innerHTML = `<span class="feedback-correct">${t.correctFeedback}</span>`;
|
|
||||||
|
// Mark question as mastered in history
|
||||||
|
if (result.question_id) {
|
||||||
|
recordQuestionMastered(result.question_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pts = result.points_earned || 100;
|
||||||
|
const ptsText = (currentLang === "en")
|
||||||
|
? `✨ Correct! +${pts} House Points! ✨`
|
||||||
|
: `✨ Richtig! +${pts} Hauspunkte für dein Haus! ✨`;
|
||||||
|
feedbackBox.innerHTML = `<span class="feedback-correct">${ptsText}</span>`;
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
frageBox.classList.remove("sparkle-effect");
|
frageBox.classList.remove("sparkle-effect");
|
||||||
@@ -391,7 +462,11 @@ async function submitAnswer(answer, clickedBtn = null) {
|
|||||||
document.getElementById("attemptBadge").innerText = `${t.attemptLabel} ${Math.min(attemptsUsed + 1, maxAttempts)} ${t.ofLabel} ${maxAttempts}`;
|
document.getElementById("attemptBadge").innerText = `${t.attemptLabel} ${Math.min(attemptsUsed + 1, maxAttempts)} ${t.ofLabel} ${maxAttempts}`;
|
||||||
|
|
||||||
if (result.attempts_left === 0) {
|
if (result.attempts_left === 0) {
|
||||||
// Exhausted all attempts
|
// Exhausted all attempts -> record for spaced repetition learning
|
||||||
|
if (result.question_id) {
|
||||||
|
recordQuestionWrong(result.question_id);
|
||||||
|
}
|
||||||
|
|
||||||
feedbackBox.innerHTML = `
|
feedbackBox.innerHTML = `
|
||||||
<div class="feedback-wrong">${t.exhaustedWrong}</div>
|
<div class="feedback-wrong">${t.exhaustedWrong}</div>
|
||||||
<div class="feedback-revealed">${t.solutionLabel} <strong>${result.revealed_answer}</strong></div>
|
<div class="feedback-revealed">${t.solutionLabel} <strong>${result.revealed_answer}</strong></div>
|
||||||
@@ -448,6 +523,7 @@ async function requestHint() {
|
|||||||
|
|
||||||
function finishGame() {
|
function finishGame() {
|
||||||
if (timerInterval) clearInterval(timerInterval);
|
if (timerInterval) clearInterval(timerInterval);
|
||||||
|
saveRecentQuestions(sessionQuestionIds);
|
||||||
document.getElementById("progressBar").style.width = "100%";
|
document.getElementById("progressBar").style.width = "100%";
|
||||||
window.location.href = `/finale?score=${currentScore}&total=${totalQuestions}&time=${elapsedSeconds}&lang=${currentLang}`;
|
window.location.href = `/finale?score=${currentScore}&total=${totalQuestions}&time=${elapsedSeconds}&lang=${currentLang}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -309,5 +309,5 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script src="/static/js/admin.js"></script>
|
<script src="/static/js/admin.js?v=2.2"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0">
|
||||||
<title>{% block title %}Magisches Harry-Potter-Escape-Quiz{% endblock %}</title>
|
<title>{% block title %}Magisches Harry-Potter-Escape-Quiz{% endblock %}</title>
|
||||||
<link rel="icon" href="/static/favicon.ico">
|
<link rel="icon" href="/static/favicon.ico">
|
||||||
<link rel="stylesheet" href="/static/css/style.css">
|
<link rel="stylesheet" href="/static/css/style.css?v=2.2">
|
||||||
{% block head %}{% endblock %}
|
{% block head %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -181,7 +181,7 @@
|
|||||||
tr.innerHTML = `
|
tr.innerHTML = `
|
||||||
<td><strong>${rankDisplay}</strong></td>
|
<td><strong>${rankDisplay}</strong></td>
|
||||||
<td><strong>${escapeHtml(e.nickname)}</strong></td>
|
<td><strong>${escapeHtml(e.nickname)}</strong></td>
|
||||||
<td><span class="gold-text font-weight-bold">${e.score}/${e.total_questions}</span></td>
|
<td><span class="gold-text font-weight-bold">${e.score} Pkt.</span></td>
|
||||||
<td>${e.time_formatted}</td>
|
<td>${e.time_formatted}</td>
|
||||||
<td><span class="badge badge-diff ${diffBadgeClass}">${diffLabel}</span></td>
|
<td><span class="badge badge-diff ${diffBadgeClass}">${diffLabel}</span></td>
|
||||||
<td class="text-muted" style="font-size: 0.85rem;">${e.date}</td>
|
<td class="text-muted" style="font-size: 0.85rem;">${e.date}</td>
|
||||||
|
|||||||
@@ -17,9 +17,9 @@
|
|||||||
<div class="score-banner">
|
<div class="score-banner">
|
||||||
<p class="score-highlight" id="finaleScoreText">
|
<p class="score-highlight" id="finaleScoreText">
|
||||||
{% if lang == 'en' %}
|
{% if lang == 'en' %}
|
||||||
You achieved <span class="gold-text">{{ score }}</span> out of <span class="gold-text">{{ total }}</span> House Points!
|
You achieved <span class="gold-text">{{ score }}</span> out of <span class="gold-text">{{ max_score }}</span> House Points!
|
||||||
{% else %}
|
{% else %}
|
||||||
Du hast <span class="gold-text">{{ score }}</span> von <span class="gold-text">{{ total }}</span> Hauspunkten erzielt!
|
Du hast <span class="gold-text">{{ score }}</span> von <span class="gold-text">{{ max_score }}</span> Hauspunkten erzielt!
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
<p class="time-highlight" id="finaleTimeText">
|
<p class="time-highlight" id="finaleTimeText">
|
||||||
@@ -45,6 +45,9 @@
|
|||||||
<form id="leaderboardForm" class="magic-form" onsubmit="submitScoreToLeaderboard(event)" style="margin-top: 12px;">
|
<form id="leaderboardForm" class="magic-form" onsubmit="submitScoreToLeaderboard(event)" style="margin-top: 12px;">
|
||||||
<div class="input-with-btn">
|
<div class="input-with-btn">
|
||||||
<input type="text" id="nicknameInput" class="input-text" placeholder="z. B. Fliegender_Besen oder Lord_Voldi" maxlength="30" required autocomplete="off">
|
<input type="text" id="nicknameInput" class="input-text" placeholder="z. B. Fliegender_Besen oder Lord_Voldi" maxlength="30" required autocomplete="off">
|
||||||
|
<button type="button" id="diceBtn" class="btn btn-secondary btn-dice" onclick="rollRandomName()" title="Zufälliger Zauberer-Name">
|
||||||
|
🎲 <span class="dice-label" id="diceLabel">Zufall</span>
|
||||||
|
</button>
|
||||||
<button type="submit" id="lbSubmitBtn" class="btn btn-primary">
|
<button type="submit" id="lbSubmitBtn" class="btn btn-primary">
|
||||||
📜 Verewigen
|
📜 Verewigen
|
||||||
</button>
|
</button>
|
||||||
@@ -91,17 +94,55 @@
|
|||||||
<script>
|
<script>
|
||||||
const score = {{ score }};
|
const score = {{ score }};
|
||||||
const total = {{ total }};
|
const total = {{ total }};
|
||||||
|
const maxScore = {{ max_score }};
|
||||||
const timeSec = {{ time_seconds }};
|
const timeSec = {{ time_seconds }};
|
||||||
let scoreAlreadySubmitted = false;
|
let scoreAlreadySubmitted = false;
|
||||||
|
|
||||||
|
const WIZARD_NAMES = {
|
||||||
|
de: [
|
||||||
|
"Fliegender_Besen", "Goldener_Schnatz", "Butterbier_Genießer",
|
||||||
|
"Severus_Snape_Fan", "Dumbledores_Phaenix", "Hogwarts_Spion",
|
||||||
|
"Peeves_Geist", "Niffler_Raeuber", "Gryffindor_Loewe",
|
||||||
|
"Ravenclaw_Adler", "Hufflepuff_Dachs", "Slytherin_Schlange",
|
||||||
|
"Mondkalb_Freund", "Zauberschach_Meister", "Kessel_Explodierer",
|
||||||
|
"Alraunen_Zaehmer", "Krummbein_Fan", "Tarnumhang_Traeger",
|
||||||
|
"Zeitumkehrer_Reisender", "Kuerbissaft_Trinker", "Patronus_Beschwoerer",
|
||||||
|
"Erlesener_Zauberstab", "Kopfloser_Reiter", "Schokofrosch_Sammler",
|
||||||
|
"Verbotene_Abteilung", "Hippogreif_Reiter", "Hauselfen_Freund"
|
||||||
|
],
|
||||||
|
en: [
|
||||||
|
"Flying_Broom", "Golden_Snitch", "Butterbeer_Lover",
|
||||||
|
"Severus_Fan", "Dumbledores_Phoenix", "Hogwarts_Spy",
|
||||||
|
"Peeves_Poltergeist", "Niffler_Bandit", "Gryffindor_Lion",
|
||||||
|
"Ravenclaw_Eagle", "Hufflepuff_Badger", "Slytherin_Snake",
|
||||||
|
"Mooncalf_Buddy", "Wizard_Chess_Master", "Cauldron_Exploder",
|
||||||
|
"Mandrake_Whisperer", "Crookshanks_Fan", "Invisibility_Bearer",
|
||||||
|
"Time_Turner_Voyager", "Pumpkin_Juice_Drinker", "Patronus_Caster",
|
||||||
|
"Elder_Wand_Seeker", "Headless_Hunt_Guest", "Chocolate_Frog_Collector",
|
||||||
|
"Restricted_Section", "Hippogriff_Rider", "House_Elf_Defender"
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
function rollRandomName() {
|
||||||
|
const isDe = (currentLang !== "en");
|
||||||
|
const list = isDe ? WIZARD_NAMES.de : WIZARD_NAMES.en;
|
||||||
|
const name = list[Math.floor(Math.random() * list.length)];
|
||||||
|
const input = document.getElementById("nicknameInput");
|
||||||
|
if (input) {
|
||||||
|
input.value = name;
|
||||||
|
input.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const FINALE_I18N = {
|
const FINALE_I18N = {
|
||||||
de: {
|
de: {
|
||||||
title: "⚡ Das große Hogwarts-Zeugnis ⚡",
|
title: "⚡ Das große Hogwarts-Zeugnis ⚡",
|
||||||
scoreText: `Du hast <span class="gold-text">${score}</span> von <span class="gold-text">${total}</span> Hauspunkten erzielt!`,
|
scoreText: `Du hast <span class="gold-text">${score}</span> von <span class="gold-text">${maxScore}</span> Hauspunkten erzielt!`,
|
||||||
timeText: `⏱️ Benötigte Zeit: <strong class="gold-text">${formatSecondsDe(timeSec)}</strong>`,
|
timeText: `⏱️ Benötigte Zeit: <strong class="gold-text">${formatSecondsDe(timeSec)}</strong>`,
|
||||||
lbTitle: "🏆 Trage dich in die Ruhmeshalle ein!",
|
lbTitle: "🏆 Trage dich in die Ruhmeshalle ein!",
|
||||||
lbSub: "Völlig anonym – wähle deinen Zauberer-Spitznamen:",
|
lbSub: "Völlig anonym – wähle deinen Zauberer-Spitznamen:",
|
||||||
lbPlaceholder: "z. B. Fliegender_Besen oder Lord_Voldi",
|
lbPlaceholder: "z. B. Fliegender_Besen oder Lord_Voldi",
|
||||||
|
diceLabel: "Zufall",
|
||||||
lbBtn: "📜 Verewigen",
|
lbBtn: "📜 Verewigen",
|
||||||
lbBtnSending: "✨ Wird verewigt...",
|
lbBtnSending: "✨ Wird verewigt...",
|
||||||
miniLbTitle: "Top Zauberer:",
|
miniLbTitle: "Top Zauberer:",
|
||||||
@@ -115,11 +156,12 @@
|
|||||||
},
|
},
|
||||||
en: {
|
en: {
|
||||||
title: "⚡ The Great Hogwarts Report Card ⚡",
|
title: "⚡ The Great Hogwarts Report Card ⚡",
|
||||||
scoreText: `You achieved <span class="gold-text">${score}</span> out of <span class="gold-text">${total}</span> House Points!`,
|
scoreText: `You achieved <span class="gold-text">${score}</span> out of <span class="gold-text">${maxScore}</span> House Points!`,
|
||||||
timeText: `⏱️ Time needed: <strong class="gold-text">${formatSecondsEn(timeSec)}</strong>`,
|
timeText: `⏱️ Time needed: <strong class="gold-text">${formatSecondsEn(timeSec)}</strong>`,
|
||||||
lbTitle: "🏆 Enter the Hogwarts Hall of Fame!",
|
lbTitle: "🏆 Enter the Hogwarts Hall of Fame!",
|
||||||
lbSub: "Completely anonymous – choose your wizard nickname:",
|
lbSub: "Completely anonymous – choose your wizard nickname:",
|
||||||
lbPlaceholder: "e.g. Flying_Broom or Lord_Voldi",
|
lbPlaceholder: "e.g. Flying_Broom or Lord_Voldi",
|
||||||
|
diceLabel: "Random",
|
||||||
lbBtn: "📜 Enter Score",
|
lbBtn: "📜 Enter Score",
|
||||||
lbBtnSending: "✨ Entering score...",
|
lbBtnSending: "✨ Entering score...",
|
||||||
miniLbTitle: "Top Wizards:",
|
miniLbTitle: "Top Wizards:",
|
||||||
@@ -225,10 +267,11 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
function getTierForScore(s) {
|
function getTierForScore(s) {
|
||||||
if (s >= 10) return TIERS_DATA[10];
|
const pct = (s <= 10 && total <= 10) ? (s / total) : (s / maxScore);
|
||||||
if (s >= 8) return TIERS_DATA[8];
|
if (pct >= 0.90) return TIERS_DATA[10];
|
||||||
if (s >= 5) return TIERS_DATA[5];
|
if (pct >= 0.70) return TIERS_DATA[8];
|
||||||
if (s >= 2) return TIERS_DATA[2];
|
if (pct >= 0.50) return TIERS_DATA[5];
|
||||||
|
if (pct >= 0.20) return TIERS_DATA[2];
|
||||||
return TIERS_DATA[0];
|
return TIERS_DATA[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,6 +285,7 @@
|
|||||||
const lbTitle = document.getElementById("lbCardTitle");
|
const lbTitle = document.getElementById("lbCardTitle");
|
||||||
const lbSub = document.getElementById("lbCardSub");
|
const lbSub = document.getElementById("lbCardSub");
|
||||||
const nicknameInput = document.getElementById("nicknameInput");
|
const nicknameInput = document.getElementById("nicknameInput");
|
||||||
|
const diceLabel = document.getElementById("diceLabel");
|
||||||
const lbSubmitBtn = document.getElementById("lbSubmitBtn");
|
const lbSubmitBtn = document.getElementById("lbSubmitBtn");
|
||||||
const miniLbTitle = document.getElementById("miniLbTitle");
|
const miniLbTitle = document.getElementById("miniLbTitle");
|
||||||
const thNick = document.getElementById("thNick");
|
const thNick = document.getElementById("thNick");
|
||||||
@@ -256,6 +300,7 @@
|
|||||||
if (timeText) timeText.innerHTML = t.timeText;
|
if (timeText) timeText.innerHTML = t.timeText;
|
||||||
if (lbTitle) lbTitle.innerText = t.lbTitle;
|
if (lbTitle) lbTitle.innerText = t.lbTitle;
|
||||||
if (lbSub) lbSub.innerText = t.lbSub;
|
if (lbSub) lbSub.innerText = t.lbSub;
|
||||||
|
if (diceLabel) diceLabel.innerText = t.diceLabel;
|
||||||
if (nicknameInput) nicknameInput.placeholder = t.lbPlaceholder;
|
if (nicknameInput) nicknameInput.placeholder = t.lbPlaceholder;
|
||||||
if (lbSubmitBtn && !lbSubmitBtn.disabled) lbSubmitBtn.innerText = t.lbBtn;
|
if (lbSubmitBtn && !lbSubmitBtn.disabled) lbSubmitBtn.innerText = t.lbBtn;
|
||||||
if (miniLbTitle) miniLbTitle.innerText = t.miniLbTitle;
|
if (miniLbTitle) miniLbTitle.innerText = t.miniLbTitle;
|
||||||
@@ -354,7 +399,7 @@
|
|||||||
tr.innerHTML = `
|
tr.innerHTML = `
|
||||||
<td><strong>${rankDisplay}</strong></td>
|
<td><strong>${rankDisplay}</strong></td>
|
||||||
<td>${e.nickname}</td>
|
<td>${e.nickname}</td>
|
||||||
<td><span class="gold-text">${e.score}/${e.total_questions}</span></td>
|
<td><span class="gold-text">${e.score} Pkt.</span></td>
|
||||||
<td>${e.time_formatted}</td>
|
<td>${e.time_formatted}</td>
|
||||||
`;
|
`;
|
||||||
body.appendChild(tr);
|
body.appendChild(tr);
|
||||||
@@ -368,6 +413,7 @@
|
|||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
onLanguageChange(currentLang);
|
onLanguageChange(currentLang);
|
||||||
loadMiniLeaderboard();
|
loadMiniLeaderboard();
|
||||||
|
rollRandomName();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -113,5 +113,5 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script src="/static/js/quiz.js"></script>
|
<script src="/static/js/quiz.js?v=2.2"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -70,5 +70,5 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script src="/static/js/vorschlag.js"></script>
|
<script src="/static/js/vorschlag.js?v=2.2"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user