mirror of
https://github.com/SpudGunMan/meshing-around.git
synced 2026-03-28 17:32:36 +01:00
QuizMaster
let me know if this is cool
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
16
data/quiz_questions.json
Normal 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"
|
||||
}
|
||||
]
|
||||
45
mesh_bot.py
45
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: <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
141
modules/games/quiz.py
Normal 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()
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user