ssctopper/generators/base.py

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