# quiz_app.py
from flask import (
Flask, render_template_string, request, redirect, url_for, session, jsonify
)
import secrets
import threading
from functools import wraps
app = Flask(__name__)
app.secret_key = secrets.token_hex(16)
# In-memory lobbies store (pin -> lobby data)
LOBBIES = {}
LOBBIES_LOCK = threading.Lock()
# Default questions (same as yours)
DEFAULT_QUESTIONS = [
{"q": "What's 2 + 2?", "choices": ["2", "3", "4", "5"], "answer": "4"},
{"q": "Which planet is known as the Red Planet?", "choices": ["Earth", "Mars", "Jupiter", "Venus"], "answer": "Mars"},
]
# --- Templates (kept simple and inline for single-file app) ---
T_INDEX = """
Mini Quiz
Create a lobby or join one:
"""
T_LOBBY_HOST = """
Lobby {{ pin }} (Host)
Share this PIN with players to join.
Players joined:
- {{ p }}
{% for p in players %}
{% endfor %}
"""
T_LOBBY_PLAYER = """
Lobby {{ pin }}
Waiting for host to start the game...
Players:
- {{ p }}
{% for p in players %}
{% endfor %}
"""
T_PLAY = """
{{ question['q'] }}
"""
T_WAIT = """
Answer submitted — waiting for others...
You will be redirected when the quiz finishes.
"""
T_SCOREBOARD = """
Scoreboard for Lobby {{ pin }}
| Player | Score |
|---|---|
| {{ p }} | {{ s }} |
"""
# --- Helpers ---
def require_in_lobby(f):
@wraps(f)
def wrapper(*args, **kwargs):
pin = kwargs.get("pin") or request.args.get("pin")
if not pin or 'name' not in session or session.get('pin') != pin:
return redirect(url_for('index'))
return f(*args, **kwargs)
return wrapper
def make_pin():
return secrets.choice('23456789ABCDEFGHJKMNPQRSTUVWXYZ') + secrets.token_hex(2).upper()
# --- Routes ---
@app.route("/", methods=["GET"])
def index():
return render_template_string(T_INDEX)
@app.route("/create", methods=["POST"])
def create():
name = request.form.get("name", "Host")
pin = make_pin()[:6]
with LOBBIES_LOCK:
LOBBIES[pin] = {
"host": name,
"players": [name],
"questions": DEFAULT_QUESTIONS.copy(),
"started": False,
"current_idx": 0,
"answers": {}, # player -> list of their given answers
"finished": False
}
session['name'] = name
session['pin'] = pin
return redirect(url_for("lobby", pin=pin))
@app.route("/join", methods=["POST"])
def join():
name = request.form.get("name", "Player")
pin = request.form.get("pin", "").strip().upper()
with LOBBIES_LOCK:
lobby = LOBBIES.get(pin)
if not lobby:
return f"Lobby {pin} not found. Back", 404
if name in lobby["players"]:
pass
else:
lobby["players"].append(name)
lobby["answers"][name] = []
session['name'] = name
session['pin'] = pin
return redirect(url_for("lobby", pin=pin))
@app.route("/lobby/
@require_in_lobby
def lobby(pin):
with LOBBIES_LOCK:
lobby = LOBBIES.get(pin)
if not lobby:
return redirect(url_for('index'))
players = lobby["players"]
if session.get('name') == lobby["host"]:
return render_template_string(T_LOBBY_HOST, pin=pin, players=players)
else:
return render_template_string(T_LOBBY_PLAYER, pin=pin, players=players)
@app.route("/lobby/
def lobby_status(pin):
with LOBBIES_LOCK:
lobby = LOBBIES.get(pin)
if not lobby:
return jsonify({"error":"no lobby"}), 404
return jsonify({"started": lobby["started"]})
@app.route("/lobby/
@require_in_lobby
def start_lobby(pin):
name = session.get('name')
with LOBBIES_LOCK:
lobby = LOBBIES.get(pin)
if not lobby or lobby['host'] != name:
return "Forbidden", 403
lobby['started'] = True
# initialize answers dict for all players
for p in lobby['players']:
lobby['answers'].setdefault(p, [])
return redirect(url_for('play', pin=pin))
@app.route("/play/
@require_in_lobby
def play(pin):
name = session['name']
with LOBBIES_LOCK:
lobby = LOBBIES.get(pin)
if not lobby or not lobby['started']:
return redirect(url_for('lobby', pin=pin))
idx = lobby['current_idx']
if request.method == "POST":
choice = request.form.get("choice", "")
lobby['answers'][name].append(choice)
# advance only when *all* players have answered this question
qcount = len(lobby['questions'])
# check if all players have answers length > idx
all_answered = all(len(lobby['answers'].get(p, [])) > idx for p in lobby['players'])
if all_answered:
# move to next question or finish
if idx + 1 >= qcount:
lobby['finished'] = True
return redirect(url_for('scoreboard', pin=pin))
else:
lobby['current_idx'] += 1
# let players fetch next question
return redirect(url_for('play', pin=pin))
else:
# wait page for this player
return render_template_string(T_WAIT, pin=pin)
# GET: show current question to player
if lobby['finished']:
return redirect(url_for('scoreboard', pin=pin))
idx = lobby['current_idx']
question = lobby['questions'][idx]
return render_template_string(T_PLAY, question=question, idx=idx)
@app.route("/player_status/
@require_in_lobby
def player_status(pin):
with LOBBIES_LOCK:
lobby = LOBBIES.get(pin)
if not lobby:
return jsonify({"error":"no lobby"}), 404
name = session['name']
finished = lobby.get('finished', False)
# detect if next question available for this player
next_available = len(lobby['answers'].get(name, [])) < (lobby['current_idx'] if not finished else len(lobby['questions']))
return jsonify({"finished": finished, "next": next_available})
@app.route("/scoreboard/
@require_in_lobby
def scoreboard(pin):
with LOBBIES_LOCK:
lobby = LOBBIES.get(pin)
if not lobby:
return redirect(url_for('index'))
# compute scores
scores = []
for p in lobby['players']:
given = lobby['answers'].get(p, [])
score = 0
for i, q in enumerate(lobby['questions']):
if i < len(given) and given[i] == q['answer']:
score += 1
scores.append((p, score))
# sort descending
scores.sort(key=lambda x: x[1], reverse=True)
return render_template_string(T_SCOREBOARD, scoreboard=scores, pin=pin)
# Easy route to list active lobbies (dev only)
@app.route("/_debug/lobbies")
def list_lobbies():
with LOBBIES_LOCK:
return jsonify({k: {"host": v["host"], "players": v["players"], "started": v["started"]} for k,v in LOBBIES.items()})
if __name__ == "__main__":
app.run(debug=True)
