From 48a57e875f3a3e123aa7e2e2059a2beab702216e Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Tue, 7 Oct 2025 17:48:22 -0700 Subject: [PATCH] QuizMaster let me know if this is cool --- README.md | 2 + config.template | 1 + data/quiz_questions.json | 16 +++++ mesh_bot.py | 45 ++++++++++++- modules/games/quiz.py | 141 +++++++++++++++++++++++++++++++++++++++ modules/settings.py | 1 + modules/system.py | 5 ++ 7 files changed, 210 insertions(+), 1 deletion(-) create mode 100644 data/quiz_questions.json create mode 100644 modules/games/quiz.py diff --git a/README.md b/README.md index 1a1a47b..1aa1c1c 100644 --- a/README.md +++ b/README.md @@ -162,6 +162,8 @@ git clone https://github.com/spudgunman/meshing-around | `joke` | Tells a joke | | | `lemonstand` | Plays the classic Lemonade Stand finance game | ✅ | | `mastermind` | Plays the classic code-breaking game | ✅ | +| `quiz` | QuizMaster Bot `q: ?` for more | ✅ | +| `tic-tac-toe`| Plays the game classic game | ✅ | | `videopoker` | Plays basic 5-card hold Video Poker | ✅ | ## Other Install Options diff --git a/config.template b/config.template index 6505193..8a4df72 100644 --- a/config.template +++ b/config.template @@ -334,6 +334,7 @@ golfsim = True hangman = True hamtest = True tictactoe = True +quiz = True [messagingSettings] # delay in seconds for response to avoid message collision /throttling diff --git a/data/quiz_questions.json b/data/quiz_questions.json new file mode 100644 index 0000000..67a8be6 --- /dev/null +++ b/data/quiz_questions.json @@ -0,0 +1,16 @@ +[ + { + "question": "Which RFband is commonly used by Meshtastic devices in US regions?", + "answers": ["2.4 GHz", "433 MHz", "900 MHz", "5.8 GHz"], + "correct": 2 + }, + { + "question": "Yogi the bear 🐻 likes what food?", + "answers": ["Picnic baskets", "Fish", "Burgers", "Hot dogs"], + "correct": 0 + }, + { + "question": "What is the password for the Meshtastic MQTT broker?", + "answer": "large4cats" + } +] \ No newline at end of file diff --git a/mesh_bot.py b/mesh_bot.py index 3bde374..4fe6d1a 100755 --- a/mesh_bot.py +++ b/mesh_bot.py @@ -15,7 +15,7 @@ from modules.log import * from modules.system import * # list of commands to remove from the default list for DM only -restrictedCommands = ["blackjack", "videopoker", "dopewars", "lemonstand", "golfsim", "mastermind", "hangman", "hamtest", "tictactoe"] +restrictedCommands = ["blackjack", "videopoker", "dopewars", "lemonstand", "golfsim", "mastermind", "hangman", "hamtest", "tictactoe", "quiz", "q:"] restrictedResponse = "🤖only available in a Direct Message📵" # "" for none cmdHistory = [] # list to hold the command history for lheard and history commands msg_history = [] # list to hold the message history for the messages command @@ -75,6 +75,8 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n "ping": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number), "pinging": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number), "pong": lambda: "🏓PING!!🛜", + "q:": lambda: quizHandler(message, message_from_id, deviceID), + "quiz": lambda: quizHandler(message, message_from_id, deviceID), "readnews": lambda: read_news(), "riverflow": lambda: handle_riverFlow(message, message_from_id, deviceID), "rlist": lambda: handle_repeaterQuery(message_from_id, deviceID, channel_number), @@ -846,6 +848,46 @@ def handleTicTacToe(message, nodeID, deviceID): time.sleep(responseDelay + 1) return msg +def quizHandler(message, nodeID, deviceID): + user_name = get_name_from_number(nodeID) + user_id = nodeID + msg = "" + user_answer = message.lower().replace("quiz","").replace("q:","").replace("quiz ","").replace("q: ","").strip() + if message.startswith("quiz") or message.lower().startswith("q:"): + if user_answer.startswith("start"): + msg = quizGamePlayer.start_game(user_id) + elif user_answer.startswith("stop"): + msg = quizGamePlayer.stop_game(user_id) + elif user_answer.startswith("join"): + msg = quizGamePlayer.join(user_id) + elif user_answer.startswith("leave"): + msg = quizGamePlayer.leave(user_id) + elif user_answer.startswith("next"): + msg = quizGamePlayer.next_question(user_id) + elif user_answer.startswith("score"): + if user_id in quizGamePlayer.players: + score = quizGamePlayer.players[user_id]['score'] + msg = f"Your score: {score}" + else: + msg = "You are not in the quiz." + elif user_answer.startswith("top"): + msg = quizGamePlayer.top_three() + elif user_answer.startswith("broadcast"): + broadcast_msg = user_answer.replace("broadcast", "", 1).strip() + msg = quizGamePlayer.broadcast(user_id, broadcast_msg) + elif user_answer.startswith("?"): + msg = ("Quiz Commands:\n" + "q: join - Join the current quiz\n" + "q: leave - Leave the current quiz\n" + "q: next - Get the next question\n" + "q: - Answer the current question\n" + "q: score - Show your current score\n" + "q: top - Show top 3 players\n") + else: + msg = quizGamePlayer.answer(user_id, user_answer) + + return msg + def handle_riverFlow(message, message_from_id, deviceID): location = get_node_location(message_from_id, deviceID) @@ -1188,6 +1230,7 @@ def checkPlayingGame(message_from_id, message_string, rxNode, channel_number): (hangmanTracker, "Hangman", handleHangman) if 'hangmanTracker' in globals() else None, (hamtestTracker, "HamTest", handleHamtest) if 'hamtestTracker' in globals() else None, (tictactoeTracker, "TicTacToe", handleTicTacToe) if 'tictactoeTracker' in globals() else None, + #quiz does not use a tracker (quizGamePlayer) always active ] trackers = [tracker for tracker in trackers if tracker is not None] diff --git a/modules/games/quiz.py b/modules/games/quiz.py new file mode 100644 index 0000000..b63c1a4 --- /dev/null +++ b/modules/games/quiz.py @@ -0,0 +1,141 @@ +import json +import os +import random +from modules.log import * + +QUIZ_JSON = os.path.join(os.path.dirname(__file__), '../', '../', 'data', 'quiz_questions.json') +QUIZMASTER_ID = bbs_admin_list + +trap_list_quiz = ("quiz", "q:") +help_text_quiz = "quiz", + +class QuizGame: + def __init__(self): + self.quizmaster = QUIZMASTER_ID + self.active = False + self.players = {} # user_id: {'score': int, 'current_q': int, 'answered': set()} + self.questions = [] # Loaded from JSON + self.load_questions() + + def start_game(self, quizmaster_id): + if str(quizmaster_id) not in self.quizmaster: + return "Only the quizmaster can start the quiz." + if self.active: + return "Quiz already running." + self.active = True + self.players = {} + self.load_questions() + return "Quiz started! Players can now join." + + def load_questions(self): + try: + with open(QUIZ_JSON, 'r') as f: + self.questions = json.load(f) + random.shuffle(self.questions) + except Exception as e: + logger.error(f"Failed to load quiz questions: {e}") + self.questions = [] + + def stop_game(self, quizmaster_id): + if not self.active or str(quizmaster_id) not in self.quizmaster: + return "Only the quizmaster can stop the quiz." + return_msg = "Quiz stopped! Final scores:\n" + self.top_three() + self.active = False + self.players = {} + return return_msg + + def join(self, user_id): + if not self.active: + return "No quiz running. Wait for the quizmaster to start." + if user_id in self.players: + return "You are already in the quiz." + self.players[user_id] = {'score': 0, 'current_q': 0, 'answered': set()} + reminder = f"Joined!\n'Q: ' to answer, 'Q: ?' for more.\n" + return reminder + self.next_question(user_id) + + def leave(self, user_id): + if user_id in self.players: + del self.players[user_id] + return "You left the quiz." + return "You are not in the quiz." + + def next_question(self, user_id): + if user_id not in self.players: + return "Join the quiz first." + player = self.players[user_id] + while player['current_q'] < len(self.questions) and player['current_q'] in player['answered']: + player['current_q'] += 1 + if player['current_q'] >= len(self.questions): + return f"No more questions. Your final score: {player['score']}." + q = self.questions[player['current_q']] + msg = f"Q{player['current_q']+1}: {q['question']}\n" + if "answers" in q: + for i, opt in enumerate(q['answers']): + msg += f"{chr(65+i)}. {opt}\n" + return msg + + def answer(self, user_id, answer): + if user_id not in self.players: + return "Join the quiz first." + player = self.players[user_id] + q_idx = player['current_q'] + if q_idx >= len(self.questions): + return "No more questions." + if q_idx in player['answered']: + return "Already answered. Type 'next' for another question." + q = self.questions[q_idx] + # Check if it's multiple choice or free-text + if "answers" in q and "correct" in q: + # Multiple choice + try: + ans_idx = ord(answer.upper()) - 65 + if ans_idx == q['correct']: + player['score'] += 1 + result = "Correct! 🎉" + else: + result = f"Wrong. Correct answer: {chr(65+q['correct'])}" + player['answered'].add(q_idx) + player['current_q'] += 1 + return f"{result}\n" + self.next_question(user_id) + except Exception: + return "Invalid answer. Use A, B, C, etc." + elif "answer" in q: + # Free-text answer + user_ans = answer.strip().lower() + correct_ans = str(q['answer']).strip().lower() + if user_ans == correct_ans: + player['score'] += 1 + result = "Correct! 🎉" + else: + result = f"Wrong. Correct answer: {q['answer']}" + player['answered'].add(q_idx) + player['current_q'] += 1 + return f"{result}\n" + self.next_question(user_id) + else: + return "Invalid question format." + + def top_three(self): + if not self.players: + return "No players in the quiz." + ranking = sorted(self.players.items(), key=lambda x: x[1]['score'], reverse=True) + msg = "🏆 Top 3 Players:\n" + for i, (uid, pdata) in enumerate(ranking[:3]): + msg += f"{i+1}. {uid}: {pdata['score']}\n" + return msg + + def broadcast(self, quizmaster_id, message): + msgToAll = {} + if quizmaster_id and str(quizmaster_id) not in self.quizmaster: + return "Only the quizmaster can broadcast." + if not self.players: + return "No players to broadcast to." + # set up message + message_to_send = f"📢 From Quizmaster: {message}" + msgToAll['message'] = message_to_send + # setup players + for uid in self.players.keys(): + msgToAll.setdefault('players', []).append(uid) + return msgToAll + +# Initialize the quiz game +quizGamePlayer = QuizGame() diff --git a/modules/settings.py b/modules/settings.py index 13015b8..f0b2f6f 100644 --- a/modules/settings.py +++ b/modules/settings.py @@ -373,6 +373,7 @@ try: hangman_enabled = config['games'].getboolean('hangman', True) hamtest_enabled = config['games'].getboolean('hamtest', True) tictactoe_enabled = config['games'].getboolean('tictactoe', True) + quiz_enabled = config['games'].getboolean('quiz', True) # messaging settings responseDelay = config['messagingSettings'].getfloat('responseDelay', 0.7) # default 0.7 diff --git a/modules/system.py b/modules/system.py index a07a0ea..8926698 100644 --- a/modules/system.py +++ b/modules/system.py @@ -263,6 +263,11 @@ if hamtest_enabled: if tictactoe_enabled: from modules.games.tictactoe import * # from the spudgunman/meshing-around repo trap_list = trap_list + ("tictactoe","tic-tac-toe",) + +if quiz_enabled: + from modules.games.quiz import * # from the spudgunman/meshing-around repo + trap_list = trap_list + trap_list_quiz # items quiz, q: + help_message = help_message + ", quiz" games_enabled = True # Games Configuration