Feature: 40 bilingual questions, random 10 game run, difficulty levels, and full i18n support
This commit is contained in:
+87
-26
@@ -16,7 +16,7 @@ from app.models import Question, QuizSession, AdminUser
|
||||
from app.matcher import is_answer_correct, levenshtein_distance, normalize_text
|
||||
from app.auth import verify_password
|
||||
from app.seed import seed_db
|
||||
from app.schemas import GameCheckRequest, SuggestionCreateRequest
|
||||
from app.schemas import GameCheckRequest, SuggestionCreateRequest, GameStartRequest
|
||||
|
||||
load_dotenv()
|
||||
|
||||
@@ -29,25 +29,33 @@ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
app.mount("/static", StaticFiles(directory=os.path.join(BASE_DIR, "static")), name="static")
|
||||
templates = Jinja2Templates(directory=os.path.join(BASE_DIR, "templates"))
|
||||
|
||||
def format_question_for_client(q: Question, number: int, total: int) -> dict:
|
||||
def format_question_for_client(q: Question, number: int, total: int, lang: str = "de") -> dict:
|
||||
options_list = None
|
||||
if q.question_type == "multiple_choice" and q.options:
|
||||
if lang == "en" and q.question_en:
|
||||
q_text = q.question_en
|
||||
opts_raw = q.options_en or q.options
|
||||
else:
|
||||
q_text = q.question
|
||||
opts_raw = q.options
|
||||
|
||||
if q.question_type == "multiple_choice" and opts_raw:
|
||||
try:
|
||||
parsed = json.loads(q.options)
|
||||
parsed = json.loads(opts_raw)
|
||||
if isinstance(parsed, list):
|
||||
options_list = list(parsed)
|
||||
random.shuffle(options_list)
|
||||
except Exception:
|
||||
options_list = [opt.strip() for opt in q.options.split(",") if opt.strip()]
|
||||
options_list = [opt.strip() for opt in opts_raw.split(",") if opt.strip()]
|
||||
random.shuffle(options_list)
|
||||
|
||||
return {
|
||||
"question_number": number,
|
||||
"total_questions": total,
|
||||
"category": q.category,
|
||||
"question": q.question,
|
||||
"question": q_text,
|
||||
"question_type": q.question_type or "text",
|
||||
"options": options_list
|
||||
"options": options_list,
|
||||
"difficulty": q.difficulty or "medium"
|
||||
}
|
||||
|
||||
@app.on_event("startup")
|
||||
@@ -66,13 +74,26 @@ async def index(request: Request, db: Session = Depends(get_db)):
|
||||
)
|
||||
|
||||
@app.post("/api/game/start")
|
||||
async def start_game(db: Session = Depends(get_db)):
|
||||
# Fetch all active questions in order
|
||||
questions = db.query(Question).filter(Question.status == "active").order_by(Question.sort_order.asc(), Question.id.asc()).all()
|
||||
if not questions:
|
||||
raise HTTPException(status_code=400, detail="Keine aktiven Fragen vorhanden.")
|
||||
async def start_game(payload: GameStartRequest = None, db: Session = Depends(get_db)):
|
||||
lang = payload.lang if payload and payload.lang in ["de", "en"] else "de"
|
||||
difficulty = payload.difficulty if payload and payload.difficulty in ["easy", "medium", "hard"] else "all"
|
||||
|
||||
question_ids = [q.id for q in questions]
|
||||
# Fetch active questions matching difficulty (or all active)
|
||||
query = db.query(Question).filter(Question.status == "active")
|
||||
if difficulty != "all":
|
||||
query = query.filter(Question.difficulty == difficulty)
|
||||
|
||||
pool = query.all()
|
||||
if not pool:
|
||||
# Fallback to all active if filtered pool empty
|
||||
pool = db.query(Question).filter(Question.status == "active").all()
|
||||
if not pool:
|
||||
raise HTTPException(status_code=400, detail="Keine aktiven Fragen vorhanden.")
|
||||
|
||||
# Randomly sample 10 questions (or all if fewer)
|
||||
sample_size = min(10, len(pool))
|
||||
selected_questions = random.sample(pool, sample_size)
|
||||
question_ids = [q.id for q in selected_questions]
|
||||
session_id = str(uuid.uuid4())
|
||||
|
||||
new_session = QuizSession(
|
||||
@@ -81,18 +102,22 @@ async def start_game(db: Session = Depends(get_db)):
|
||||
current_question_idx=0,
|
||||
question_ids=json.dumps(question_ids),
|
||||
attempts_current=0,
|
||||
is_finished=False
|
||||
is_finished=False,
|
||||
lang=lang,
|
||||
difficulty=difficulty
|
||||
)
|
||||
db.add(new_session)
|
||||
db.commit()
|
||||
|
||||
first_q = questions[0]
|
||||
res = format_question_for_client(first_q, 1, len(question_ids))
|
||||
first_q = selected_questions[0]
|
||||
res = format_question_for_client(first_q, 1, len(question_ids), lang=lang)
|
||||
res.update({
|
||||
"session_id": session_id,
|
||||
"score": 0,
|
||||
"attempts_used": 0,
|
||||
"max_attempts": 3
|
||||
"max_attempts": 3,
|
||||
"lang": lang,
|
||||
"difficulty": difficulty
|
||||
})
|
||||
return res
|
||||
|
||||
@@ -102,6 +127,7 @@ async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db))
|
||||
if not session or session.is_finished:
|
||||
raise HTTPException(status_code=400, detail="Ungültige oder beendete Spiel-Session.")
|
||||
|
||||
session_lang = session.lang or "de"
|
||||
question_ids = json.loads(session.question_ids)
|
||||
if session.current_question_idx >= len(question_ids):
|
||||
session.is_finished = True
|
||||
@@ -120,7 +146,7 @@ async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db))
|
||||
if not current_q:
|
||||
raise HTTPException(status_code=500, detail="Frage nicht in Datenbank gefunden.")
|
||||
|
||||
correct = is_answer_correct(payload.answer, current_q.canonical_answer, current_q.accepted_answers)
|
||||
correct = is_answer_correct(payload.answer, current_q.canonical_answer, current_q.accepted_answers, current_q.answer_en)
|
||||
|
||||
if correct:
|
||||
session.score += 1
|
||||
@@ -138,7 +164,7 @@ async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db))
|
||||
if not is_finished:
|
||||
next_q = db.query(Question).filter(Question.id == question_ids[session.current_question_idx]).first()
|
||||
if next_q:
|
||||
next_q_data = format_question_for_client(next_q, session.current_question_idx + 1, len(question_ids))
|
||||
next_q_data = format_question_for_client(next_q, session.current_question_idx + 1, len(question_ids), lang=session_lang)
|
||||
|
||||
return {
|
||||
"correct": True,
|
||||
@@ -154,8 +180,12 @@ async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db))
|
||||
attempts_left = max(0, 3 - session.attempts_current)
|
||||
|
||||
if attempts_left == 0:
|
||||
# 3 failed attempts: move to next question, reveal answer so player learns it
|
||||
revealed = current_q.canonical_answer
|
||||
# 3 failed attempts: move to next question, reveal answer
|
||||
if session_lang == "en" and current_q.answer_en:
|
||||
revealed = current_q.answer_en
|
||||
else:
|
||||
revealed = current_q.canonical_answer
|
||||
|
||||
session.current_question_idx += 1
|
||||
session.attempts_current = 0
|
||||
|
||||
@@ -170,7 +200,7 @@ async def check_answer(payload: GameCheckRequest, db: Session = Depends(get_db))
|
||||
if not is_finished:
|
||||
next_q = db.query(Question).filter(Question.id == question_ids[session.current_question_idx]).first()
|
||||
if next_q:
|
||||
next_q_data = format_question_for_client(next_q, session.current_question_idx + 1, len(question_ids))
|
||||
next_q_data = format_question_for_client(next_q, session.current_question_idx + 1, len(question_ids), lang=session_lang)
|
||||
|
||||
return {
|
||||
"correct": False,
|
||||
@@ -199,20 +229,29 @@ async def get_hint(session_id: str, db: Session = Depends(get_db)):
|
||||
if not session or session.is_finished:
|
||||
raise HTTPException(status_code=400, detail="Ungültige Session.")
|
||||
|
||||
session_lang = session.lang or "de"
|
||||
question_ids = json.loads(session.question_ids)
|
||||
if session.current_question_idx >= len(question_ids):
|
||||
return {"hint": "Kein Hinweis verfügbar."}
|
||||
return {"hint": "No hint available." if session_lang == "en" else "Kein Hinweis verfügbar."}
|
||||
|
||||
current_q_id = question_ids[session.current_question_idx]
|
||||
current_q = db.query(Question).filter(Question.id == current_q_id).first()
|
||||
return {"hint": current_q.hint if current_q and current_q.hint else "Kein Tipp verfügbar."}
|
||||
if not current_q:
|
||||
return {"hint": "No hint available." if session_lang == "en" else "Kein Hinweis verfügbar."}
|
||||
|
||||
if session_lang == "en":
|
||||
hint_text = current_q.hint_en if current_q.hint_en else (current_q.hint or "No hint available.")
|
||||
else:
|
||||
hint_text = current_q.hint or "Kein Tipp verfügbar."
|
||||
|
||||
return {"hint": hint_text}
|
||||
|
||||
@app.get("/finale", response_class=HTMLResponse)
|
||||
async def finale(request: Request, score: int = 0, total: int = 15):
|
||||
async def finale(request: Request, score: int = 0, total: int = 10, lang: str = "de"):
|
||||
return templates.TemplateResponse(
|
||||
request=request,
|
||||
name="finale.html",
|
||||
context={"score": score, "total": total}
|
||||
context={"score": score, "total": total, "lang": lang}
|
||||
)
|
||||
|
||||
@app.get("/vorschlag", response_class=HTMLResponse)
|
||||
@@ -238,6 +277,8 @@ async def submit_suggestion(payload: SuggestionCreateRequest, db: Session = Depe
|
||||
opts.append(opt.strip())
|
||||
options_json = json.dumps(opts, ensure_ascii=False)
|
||||
|
||||
diff = payload.difficulty if payload.difficulty in ["easy", "medium", "hard"] else "medium"
|
||||
|
||||
new_suggestion = Question(
|
||||
category="community",
|
||||
question=payload.frage.strip(),
|
||||
@@ -245,6 +286,7 @@ async def submit_suggestion(payload: SuggestionCreateRequest, db: Session = Depe
|
||||
hint=payload.tipp.strip() if payload.tipp else None,
|
||||
question_type=q_type,
|
||||
options=options_json,
|
||||
difficulty=diff,
|
||||
status="needs_review", # User Question Need Test / Review
|
||||
submitted_by=payload.name.strip() if payload.name else "Gast",
|
||||
sort_order=999
|
||||
@@ -327,6 +369,10 @@ async def admin_add_question(
|
||||
hint: str = Form(None),
|
||||
category: str = Form("harry_potter"),
|
||||
question_type: str = Form("text"),
|
||||
difficulty: str = Form("medium"),
|
||||
question_en: str = Form(None),
|
||||
answer_en: str = Form(None),
|
||||
hint_en: str = Form(None),
|
||||
wrong_option_1: str = Form(None),
|
||||
wrong_option_2: str = Form(None),
|
||||
wrong_option_3: str = Form(None),
|
||||
@@ -344,6 +390,8 @@ async def admin_add_question(
|
||||
opts.append(w.strip())
|
||||
options_json = json.dumps(opts, ensure_ascii=False)
|
||||
|
||||
diff = difficulty.strip() if difficulty and difficulty.strip() in ["easy", "medium", "hard"] else "medium"
|
||||
|
||||
new_q = Question(
|
||||
category=category.strip(),
|
||||
question=question.strip(),
|
||||
@@ -351,6 +399,10 @@ async def admin_add_question(
|
||||
hint=hint.strip() if hint else None,
|
||||
question_type=question_type.strip(),
|
||||
options=options_json,
|
||||
difficulty=diff,
|
||||
question_en=question_en.strip() if question_en else None,
|
||||
answer_en=answer_en.strip() if answer_en else None,
|
||||
hint_en=hint_en.strip() if hint_en else None,
|
||||
status="active",
|
||||
sort_order=sort_order,
|
||||
submitted_by=f"Admin ({request.session['admin_user']})"
|
||||
@@ -402,6 +454,10 @@ async def admin_edit_question(
|
||||
canonical_answer: str = Form(...),
|
||||
hint: str = Form(None),
|
||||
question_type: str = Form("text"),
|
||||
difficulty: str = Form("medium"),
|
||||
question_en: str = Form(None),
|
||||
answer_en: str = Form(None),
|
||||
hint_en: str = Form(None),
|
||||
options_raw: str = Form(None),
|
||||
status_val: str = Form("active"),
|
||||
sort_order: int = Form(0),
|
||||
@@ -418,6 +474,11 @@ async def admin_edit_question(
|
||||
q.canonical_answer = canonical_answer.strip()
|
||||
q.hint = hint.strip() if hint else None
|
||||
q.question_type = question_type.strip()
|
||||
if difficulty and difficulty.strip() in ["easy", "medium", "hard"]:
|
||||
q.difficulty = difficulty.strip()
|
||||
q.question_en = question_en.strip() if question_en else None
|
||||
q.answer_en = answer_en.strip() if answer_en else None
|
||||
q.hint_en = hint_en.strip() if hint_en else None
|
||||
|
||||
if options_raw and options_raw.strip():
|
||||
val = options_raw.strip()
|
||||
|
||||
Reference in New Issue
Block a user