Files
Kelly fde22f75ea Patches like its 1999 (#318)
* new-pool-time

studyUp!

* patch-NONE

* fix loop
2026-06-06 13:35:42 -07:00

178 lines
6.5 KiB
Python

# hamradio test module for meshbot DE K7MHI 2025
# depends on the JSON question data files from https://github.com/russolsen/ham_radio_question_pool
# data files which are expected to be in ../../data/hamradio/ similar to the following:
# https://raw.githubusercontent.com/russolsen/ham_radio_question_pool/refs/heads/master/technician-2022-2026/technician.json
# https://raw.githubusercontent.com/russolsen/ham_radio_question_pool/refs/heads/master/general-2023-2027/general.json
# https://raw.githubusercontent.com/russolsen/ham_radio_question_pool/refs/heads/master/extra-2024-2028/extra.json
import json
import random
import os
from modules.log import logger
from modules.settings import hamtestTracker
class HamTest:
def __init__(self):
self.questions = {}
self.load_questions()
self.game = {}
def load_questions(self):
for level in ['technician', 'general', 'extra']:
try:
with open(f'{os.path.dirname(__file__)}/../../data/hamradio/{level}.json', encoding='utf-8') as f:
self.questions[level] = json.load(f)
except FileNotFoundError:
logger.error(f"File not found: ../../data/hamradio/{level}.json")
self.questions[level] = []
except json.JSONDecodeError:
logger.error(f"Error decoding JSON from file: ../../data/hamradio/{level}.json")
self.questions[level] = []
def newGame(self, id, level='technician'):
msg = f"📻New {level} quiz started, 'end' to exit."
if id in self.game:
level = self.game[id]['level']
self.game[id] = {
'level': level,
'score': 0,
'total': 0,
'errors': [],
'qId': None,
'question': None,
'answers': None,
'correct': None
}
# set the pool needed for the game
if self.game[id]['level'] == 'extra':
self.game[id]['total'] = 50
else:
self.game[id]['total'] = 35
# randomize the questions
random.shuffle(self.questions[level])
msg += f"\n{self.nextQuestion(id)}"
return msg
def nextQuestion(self, id):
level = self.game[id]['level']
# if question has the word figure in it, skip it
question = random.choice(self.questions[level])
while 'figure' in question['question'].lower():
question = random.choice(self.questions[level])
self.game[id]['question'] = question['question']
self.game[id]['answers'] = question['answers']
self.game[id]['correct'] = self.get_correct_index(question)
self.game[id]['qId'] = question['id']
self.game[id]['total'] -= 1
if self.game[id]['total'] == 0:
return self.endGame(id)
# ask the question and return answers in A, B, C, D format
msg = f"{self.game[id]['question']}\n"
for i, answer in enumerate(self.game[id]['answers']):
msg += f"{chr(65+i)}. {answer}\n"
return msg
def get_correct_index(self, question):
if question.get('correct_letter'):
letter = str(question['correct_letter']).strip().upper()
if len(letter) == 1 and 'A' <= letter <= 'D':
return ord(letter) - 65
if question.get('correct') is not None:
try:
return int(question['correct'])
except (ValueError, TypeError):
pass
return 0
def parse_answer(self, answer):
if not isinstance(answer, str):
return None
normalized = answer.strip().upper()
if normalized in ['A', 'B', 'C', 'D']:
return ord(normalized) - 65
if normalized.isdigit():
idx = int(normalized) - 1
if 0 <= idx < 4:
return idx
return None
def answer(self, id, answer):
if id not in self.game:
return "No game in progress"
answer_index = self.parse_answer(answer)
if answer_index is None:
return "Please answer with A, B, C, or D."
if self.game[id]['correct'] == answer_index:
self.game[id]['score'] += 1
return f"Correct👍\n" + self.nextQuestion(id)
else:
# record the section of the question for study aid
section = self.game[id]['qId'][:3]
self.game[id]['errors'].append(section)
# provide the correct answer
answer = [self.game[id]['correct']]
return f"Wrong.⛔️ Correct is {chr(65+self.game[id]['correct'])}\n" + self.nextQuestion(id)
def getScore(self, id):
if id not in self.game:
return "No game in progress"
score = self.game[id]['score']
total = self.game[id]['total']
level = self.game[id]['level']
if self.game[id]['errors']:
areaofstudy = max(set(self.game[id]['errors']), key = self.game[id]['errors'].count)
else:
areaofstudy = "None"
if level == 'extra':
pool = 50
else:
pool = 35
return f"Score: {score}/{pool}\nQuestions left: {total}\nArea of study: {areaofstudy}"
def endGame(self, id):
if id not in self.game:
return "No game in progress"
score = self.game[id]['score']
level = self.game[id]['level']
if level == 'extra':
# passing score for extra is 37 out of 50
passing = 37
else:
# passing score for technician and general is 26 out of 35
passing = 26
if score >= passing:
msg = f"Game over. Score: {score} 73! 🎉You passed the {level} exam."
else:
# find the most common section of the questions missed
if self.game[id]['errors']:
areaofstudy = max(set(self.game[id]['errors']), key = self.game[id]['errors'].count)
else:
areaofstudy = "None"
msg = f"Game over. Score: {score} 73! 😿You did not pass the {level} exam. \nYou may want to study {areaofstudy}."
# remove the game[id] from the list
del self.game[id]
# hamtestTracker stores dicts like {"nodeID": nodeID, ...}
for i in range(len(hamtestTracker)):
try:
if hamtestTracker[i].get('nodeID') == id:
hamtestTracker.pop(i)
break
except Exception:
continue
return msg
hamtest = HamTest()