"""quiz.py: a quiz master, one bot per room, in Python. The host writes "!quiz", the bot asks the questions in quiz.json one by one, members answer with a client event, and the bot scores each answer by its own clock: right and fast gets most. After each question it shows the right answer, and at the end the leaderboard. Python 3.10+. pip install -r requirements.txt, then python quiz.py (reads .env). Kept simple: an answer is a client event, which every member of the room receives. To hide the answers, send them to the bot alone (send_to) or seal them to a key only the bot holds, as Pling on quiz.pikoman.se does. Its key (dashboard, New API key): rights subscribe and client_events, kind presence, channels: prefix presence-quiz-. """ import asyncio import json import time from pathlib import Path from pikoman_bot import Bot, env ROOM = env("ROOM", "presence-quiz-1") QUESTIONS = json.loads((Path(__file__).parent / env("QUIZ", "quiz.json")).read_text(encoding="utf-8")) PAUSE = float(env("PAUSE_SECONDS", "5")) # between the answer and the next question bot = Bot("Quiz master", access=3) room = bot.join(ROOM) state = {"running": False, "open": None, "task": None} # open: the question taking answers scores = {} # user -> {name, points} def user_of(sender): # Members are told apart by what your backend signed into their join token, never by what they # send: {"user_id": "42", "name": "Ada"}, and {"host": true} for the one who may start the quiz. return room.member(sender).get("user_id", sender) @room.on("message") async def message(sender, data, info): if str((data or {}).get("text", "")).strip() != "!quiz" or room.is_bot(sender): return if room.member(sender).get("host") is not True: return await room.send("message", {"text": "Only the host can start the quiz."}) if not state["running"]: state["task"] = asyncio.create_task(play()) # kept, so it is not collected while it plays # An answer: room.send("answer", { n: 1, option: 2 }) on the member's page. The first one counts. @room.on("answer") def answer(sender, data, info): question = state["open"] if not question or not isinstance(data, dict) or data.get("n") != question["n"] or room.is_bot(sender): return option, user = data.get("option"), user_of(sender) if type(option) is not int or user in question["answers"]: return # The time is the bot's own, when the answer reached it: nothing a player writes counts. question["answers"][user] = {"option": option, "ms": (time.monotonic() - question["asked"]) * 1000, "name": room.member(sender).get("name", "Someone")} def leaderboard(): return sorted(scores.values(), key=lambda s: -s["points"])[:10] async def play(): state["running"] = True scores.clear() await room.send("message", {"text": f"The quiz starts: {len(QUESTIONS)} questions."}) for n, q in enumerate(QUESTIONS, start=1): seconds = q.get("seconds", 15) # The question, without the right answer: that stays with the bot until the reveal. await room.send("question", {"n": n, "total": len(QUESTIONS), "question": q["question"], "options": q["options"], "seconds": seconds}) state["open"] = {"n": n, "asked": time.monotonic(), "answers": {}} await asyncio.sleep(seconds) answers, state["open"] = state["open"]["answers"], None for user, a in answers.items(): score = scores.setdefault(user, {"name": a["name"], "points": 0}) # Right: 500 points, and up to 500 more for speed. Wrong: nothing. if a["option"] == q["answer"]: score["points"] += 500 + round(500 * max(0, 1 - a["ms"] / (seconds * 1000))) right = sum(1 for a in answers.values() if a["option"] == q["answer"]) await room.send("reveal", {"n": n, "answer": q["answer"], "right": right, "answered": len(answers), "leaderboard": leaderboard()}) await room.send("message", {"text": f"{q['options'][q['answer']]}! {right} of {len(answers)} got it right."}) if n < len(QUESTIONS): await asyncio.sleep(PAUSE) top = leaderboard() await room.send("leaderboard", {"final": True, "leaderboard": top}) await room.send("message", {"text": f"The winner: {top[0]['name']}, {top[0]['points']} points." if top else "Nobody answered."}) state["running"] = False print(f"Quiz master in {ROOM}: {len(QUESTIONS)} questions") bot.start()