diff --git a/app.py b/app.py index cbd9575..de39ff5 100644 --- a/app.py +++ b/app.py @@ -5,6 +5,7 @@ import base64 import pandas as pd import qrcode import uuid +import time, math from flask import Flask, render_template, request, session, redirect, url_for, abort from flask_socketio import SocketIO, emit, join_room from dotenv import load_dotenv @@ -30,12 +31,20 @@ game = { @app.route('/', methods=['GET']) def host(): + # If this browser is already a player, don't let it hit the host view. + if session.get('role') == 'player': + secret = session.get('secret') + if secret: + return redirect(url_for('player', secret=secret)) + return redirect(url_for('player')) + session['role'] = 'host' # — manage per-game secret in session — secret = session.get('secret') if not secret: secret = uuid.uuid4().hex[:8] session['secret'] = secret + # ensure game exists (with questions placeholder) if secret not in games: games[secret] = { @@ -198,9 +207,13 @@ def player(): @app.route('/player/join', methods=['POST']) def do_join(): - name = request.form.get('name') or 'Anonymous' - session['role'] = 'player' - session['joined'] = True + name = (request.form.get('name') or 'Anonymous').strip() + if not name: + name = 'Anonymous' + + session['role'] = 'player' + session['joined'] = True + # make sure the player is still talking to a live game secret = session.get('secret') if not secret or secret not in games: @@ -213,11 +226,25 @@ def do_join(): error='Spiel nicht gefunden' ), 400 + # ─── enforce unique name per game (case-insensitive) ─── + existing_lower = {v['name'].strip().lower() for v in games[secret]['players'].values()} + if name.lower() in existing_lower: + # do NOT mark session as joined; force them back to join view with error + session['joined'] = False + return render_template( + 'player.html', + joined=False, + state='join', + options=[], + answered=False, + error='Dieser Name ist bereits vergeben. Bitte wähle einen anderen.' + ), 400 + + # proceed with normal join session['player_name'] = name session['player_id'] = str(uuid.uuid4()) - return redirect( - url_for('player', secret=session.get('secret')) - ) + return redirect(url_for('player', secret=session.get('secret'))) + @socketio.on('connect') @@ -290,7 +317,7 @@ def on_start_game(): 'finished': False }) for p in game['players'].values(): - p['score'] = 0 + p['score'] = 0.0 game['started'] = True socketio.emit('game_started', room='player') @@ -308,7 +335,6 @@ def on_get_roster(): emit('update_roster', {'players': roster}) - @socketio.on('submit_answer') def on_submit_answer(data): secret = session.get('secret') @@ -318,10 +344,16 @@ def on_submit_answer(data): game = games[secret] if game.get('revealed'): return - if any(pid == p for p,_ in game['answered']): + if any(pid == t[0] for t in game['answered']): return - game['answered'].append((pid, data.get('answer'))) + # capture reaction time + now = time.perf_counter() + q_started = game.get('q_started', now) + rt = max(0.0, now - q_started) + + # store (pid, answer, rt) + game['answered'].append((pid, data.get('answer'), rt)) _score_and_emit(secret) @@ -403,7 +435,7 @@ def _send_sync_state(pid, game): else: idx = game['current_q'] q = game['questions'][idx] - answered = any(pid == p for p,_ in game['answered']) + answered = any(pid == t[0] for t in game['answered']) emit('sync_state', { 'state': 'question', 'question': q['question'], @@ -416,62 +448,58 @@ def _send_question(secret): game = games[secret] game['revealed'] = False q = game['questions'][game['current_q']] - # clear previous answers game['answered'].clear() - # broadcast - socketio.emit( - 'new_question', - {'question': q['question'], 'options': q['options']}, - room='player' - ) - socketio.emit( - 'new_question', - {'question': q['question']}, - room='host' - ) + + # NEW: start monotonic timer for this question + game['q_started'] = time.perf_counter() + game['q_deadline_s'] = 20 # configurable per question if you want + + socketio.emit('new_question', {'question': q['question'], 'options': q['options']}, room='player') + socketio.emit('new_question', {'question': q['question']}, room='host') + + +def _calc_points(rt_s: float, deadline_s: float, correct: bool) -> float: + if not correct or deadline_s <= 0: + return 0.0 + if rt_s < 0: + rt_s = 0.0 + if rt_s >= deadline_s: + return 0.0 + + BASE = 10 + MIN = 1 + frac = 1.0 - (rt_s / float(deadline_s)) + pts = MIN + (BASE - MIN) * frac + return round(pts, 1) # ← keep a decimal, not an int def _score_and_emit(secret): - game = games[secret] - # Punkteverteilung: - # 1. richtige Antwort → 5 Punkte - # 2. richtige Antwort → 4 Punkte - # 3. richtige Antwort → 3 Punkte - # alle weiteren richtigen Antworten → 1 Punkt - score_map = {1: 5, 2: 4, 3: 3} - per_q = [] + game = games[secret] + per_q = [] - # grab the current question’s correct answer correct = game['questions'][game['current_q']]['correct'] - # take only the most‐recent submission - pid, ans = game['answered'][-1] + pid, ans, rt = game['answered'][-1] # NEW: includes rt - # count how many *earlier* answers were correct - prev_correct = sum(1 for p, a in game['answered'][:-1] if a == correct) - rank = prev_correct + 1 - - # assign points only if korrekt - if ans == correct: - # für Platz 1–3 nach Mapping, ab Platz 4 immer 1 Punkt - pts = score_map.get(rank, 1) + # NEW: time-based points for correct answers + pts = _calc_points(rt, game.get('q_deadline_s', 20), ans == correct) + if pts > 0: game['players'][pid]['score'] += pts per_q.append({ 'name': game['players'][pid]['name'], 'points': pts }) - # rebuild overall leaderboard board = sorted( [{'name': v['name'], 'score': v['score']} for v in game['players'].values()], key=lambda x: -x['score'] ) - # emit just the new‐answer results, plus the overall board socketio.emit('question_leader', {'results': per_q}, room='host') socketio.emit('question_leader', {'results': per_q}, room='player') socketio.emit('overall_leader', {'board': board}, room='host') socketio.emit('overall_leader', {'board': board}, room='player') + # if that was the last question, finish up if game['current_q'] >= len(game['questions']): game['finished'] = True diff --git a/templates/host.html b/templates/host.html index 515f1fa..864e3aa 100644 --- a/templates/host.html +++ b/templates/host.html @@ -1,19 +1,91 @@ {% extends 'base.html' %} {% block content %} + +

Public Quiz

+
- +

Scanne den QR-Code, um dem Spiel beizutreten!

-
+ @@ -23,46 +95,117 @@

Spieler Beigetreten:

- + {% endblock %} diff --git a/templates/player.html b/templates/player.html index 73c0951..c722d04 100644 --- a/templates/player.html +++ b/templates/player.html @@ -2,38 +2,61 @@ {% extends 'base.html' %} {% block content %} +

Dem Quiz Beitreten

- {% if not error %} - -
- - -
- - {% else %} - + + {% if error %}
- +
{% endif %} + + {# Show the form unless the error is exactly "Spiel nicht gefunden" #} + {% if not (error and error == 'Spiel nicht gefunden') %} +
+
+ + +
+
+ {% endif %}
+ +
-
+ -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/upload.html b/templates/upload.html index e2f1bab..18c5a54 100644 --- a/templates/upload.html +++ b/templates/upload.html @@ -4,41 +4,38 @@

- Willkommen zum interaktiven Gruppenquiz! Lade deinen eigenen Fragebogen hoch und starte ein spannendes Live-Quiz. - Deine Teilnehmer beantworten die Fragen schnell und wettbewerbsorientiert direkt über ihre Smartphones. + Willkommen zum interaktiven Gruppenquiz! Lade deinen Fragebogen hoch und starte ein spannendes Live-Quiz. + Die Teilnehmenden antworten direkt über ihre Smartphones – schnell, fair und mit Live-Rangliste.

- + +

Wie werden Punkte vergeben?

- Schnelle und richtige Antworten werden mit mehr Punkten belohnt. Jeder Spieler hat pro Frage nur einen Versuch. - Ein guter Mix aus Schnelligkeit und Genauigkeit zahlt sich aus! -

-

- Hinweis: Die ersten drei richtigen Antworten erhalten höhere Punktwerte, alle weiteren richtigen Antworten bringen jeweils 1 Punkt. + Pro Frage zählt genau ein Antwortversuch pro Spieler. Punkte gibt es ausschließlich für + richtige Antworten und sie hängen davon ab, wie schnell geantwortet wird. + Das Zeitlimit pro Frage beträgt 20 Sekunden.

- -

Sobald die richtige Antwort für alle anzeigt wird, werden keine weiteren Punkte verteilt.

+

+ Die Punkte werden zwischen 10 (schnellste richtige Antwort) und 1 (direkt vor Fristende) berechnet + und auf eine Dezimalstelle gerundet (z. B. 7,3 Punkte). +

+

+ Sobald die richtige Lösung eingeblendet wurde, werden keine weiteren Antworten mehr gewertet. + Die Rangliste aktualisiert sich nach jeder gewerteten Antwort und zeigt am Ende die finale Platzierung. +

Fragebogen erstellen

- Nutze die Beispieldatei als Vorlage und trage deine Fragen sowie die Antwortoptionen ein. - Deine Excel-Datei sollte folgende Spalten enthalten: + Nutze die Beispieldatei als Vorlage und trage deine Fragen sowie die Antwortoptionen ein.

-

- Unterstützte Formate: .xlsx, .xls - Maximal 100 Fragen! + Unterstützte Formate: .xlsx, .xls — maximal 100 Fragen.