QuizMaster

let me know if this is cool
This commit is contained in:
SpudGunMan
2025-10-07 17:48:22 -07:00
parent ce317d8bbe
commit 48a57e875f
7 changed files with 210 additions and 1 deletions

View File

@@ -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

View File

@@ -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

16
data/quiz_questions.json Normal file
View File

@@ -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"
}
]

View File

@@ -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: <your answer> - 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]

141
modules/games/quiz.py Normal file
View File

@@ -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: <Answer>' 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()

View File

@@ -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

View File

@@ -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