90 lines
2.7 KiB
Python
90 lines
2.7 KiB
Python
|
|
|
|
#!/usr/bin/env python3
|
|
"""Base utilities for question generation."""
|
|
import random
|
|
import sys
|
|
import os
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
|
from db.init import get_db, get_question_type_id, insert_questions_batch
|
|
|
|
|
|
def shuffle_options(correct, wrongs, explanation=""):
|
|
"""Create options with correct answer randomly placed."""
|
|
options = [correct] + wrongs[:3]
|
|
# Map original correct to its letter
|
|
indices = list(range(4))
|
|
random.shuffle(indices)
|
|
shuffled = [options[i] for i in indices]
|
|
correct_letter = chr(65 + indices.index(0)) # A, B, C, D
|
|
return {
|
|
'option_a': str(shuffled[0]),
|
|
'option_b': str(shuffled[1]),
|
|
'option_c': str(shuffled[2]),
|
|
'option_d': str(shuffled[3]),
|
|
'correct_option': correct_letter,
|
|
'explanation': explanation
|
|
}
|
|
|
|
|
|
def make_question(qtype_id, text, correct, wrongs, explanation="", difficulty=1):
|
|
"""Build a question dict ready for DB insertion."""
|
|
q = shuffle_options(correct, wrongs, explanation)
|
|
q['question_type_id'] = qtype_id
|
|
q['question_text'] = text
|
|
q['difficulty'] = difficulty
|
|
return q
|
|
|
|
|
|
def get_qtid(conn, subject, subtopic, topic, qtype):
|
|
"""Shorthand for get_question_type_id."""
|
|
return get_question_type_id(conn, subject, subtopic, topic, qtype)
|
|
|
|
|
|
def nearby_wrong(val, spread=None):
|
|
"""Generate plausible wrong answers near the correct value."""
|
|
if spread is None:
|
|
spread = max(1, abs(val) // 5) if val != 0 else 5
|
|
wrongs = set()
|
|
attempts = 0
|
|
while len(wrongs) < 3 and attempts < 50:
|
|
offset = random.randint(1, max(1, spread))
|
|
sign = random.choice([-1, 1])
|
|
w = val + sign * offset
|
|
if w != val and w not in wrongs:
|
|
wrongs.add(w)
|
|
attempts += 1
|
|
# Fallback
|
|
while len(wrongs) < 3:
|
|
wrongs.add(val + len(wrongs) + 1)
|
|
return [str(w) for w in wrongs]
|
|
|
|
|
|
def nearby_wrong_float(val, spread=None, decimals=2):
|
|
"""Generate plausible wrong answers for float values."""
|
|
if spread is None:
|
|
spread = max(0.5, abs(val) * 0.2)
|
|
wrongs = set()
|
|
attempts = 0
|
|
while len(wrongs) < 3 and attempts < 50:
|
|
offset = round(random.uniform(0.1, spread), decimals)
|
|
sign = random.choice([-1, 1])
|
|
w = round(val + sign * offset, decimals)
|
|
if w != round(val, decimals) and w not in wrongs and w > 0:
|
|
wrongs.add(w)
|
|
attempts += 1
|
|
while len(wrongs) < 3:
|
|
wrongs.add(round(val + (len(wrongs) + 1) * 0.5, decimals))
|
|
return [str(w) for w in wrongs]
|
|
|
|
|
|
def frac_str(num, den):
|
|
"""Format a fraction as string."""
|
|
from math import gcd
|
|
g = gcd(abs(num), abs(den))
|
|
n, d = num // g, den // g
|
|
if d == 1:
|
|
return str(n)
|
|
return f"{n}/{d}"
|