#!/usr/bin/env python3 """Тренажёр «числа и факты» — карточки под промахи диагностики. Уровни карточек: base — база, с этого начинать (объяснение есть в поле why) core — то, что почти наверняка спросят deep — нишевое, в базовый прогон не попадает Использование (из каталога diag): python3 facts_drill.py # 20 карточек уровня base+core, интерактивно python3 facts_drill.py --level base # только база (первый заход — отсюда) python3 facts_drill.py --level all --all # все 101 python3 facts_drill.py --domain algo --level base python3 facts_drill.py --learn # после каждого ответа показать пояснение python3 facts_drill.py --list --level base # вывести вопросы с ответами и пояснениями python3 facts_drill.py --answers "20|62|ttl|..." # проверить строку ответов python3 facts_drill.py --answers "..." --level all --all --seed 42 python3 facts_drill.py --selftest Ответы сравниваются нестрого: регистр, «ё», тире и лишние пробелы не важны. Результат пишется в results_facts.json. """ import argparse import json import os import random import re import sys HERE = os.path.dirname(os.path.abspath(__file__)) FACTS_PATH = os.path.join(HERE, "facts.json") LEVELS = ("base", "core", "deep") def norm(s: str) -> str: s = (s or "").lower().replace("ё", "е") s = s.replace("–", "-").replace("—", "-").replace("−", "-") s = re.sub(r"[^0-9a-zа-я\-\s]", " ", s) return re.sub(r"\s+", " ", s).strip() def load(): with open(FACTS_PATH, encoding="utf-8") as f: return json.load(f)["facts"] def check(fact, answer: str) -> bool: a = norm(answer) if not a: return False return any(a == norm(x) for x in fact.get("accept", [])) def hint(fact) -> str: """Пояснение и ссылка, куда смотреть.""" parts = [] if fact.get("why"): parts.append(fact["why"]) if fact.get("ref"): parts.append("см. " + fact["ref"]) return " · ".join(parts) def selftest(facts) -> int: problems = [] for i, f in enumerate(facts, 1): for key in ("domain", "q", "a", "accept", "level"): if not f.get(key): problems.append(f"карточка {i}: пустое поле {key}") if f.get("level") not in LEVELS: problems.append(f"карточка {i}: неизвестный уровень {f.get('level')!r}") if f.get("accept") and not any(norm(x) for x in f["accept"]): problems.append(f"карточка {i}: варианты ответа нормализуются в пустоту") if not check(f, f["accept"][0]): problems.append(f"карточка {i}: канонический ответ не проходит проверку") if not (f.get("why") or f.get("ref")): problems.append(f"карточка {i}: нет ни пояснения, ни ссылки — карточка не учит") doms, lvls = {}, {} for f in facts: doms[f["domain"]] = doms.get(f["domain"], 0) + 1 lvls[f["level"]] = lvls.get(f["level"], 0) + 1 print(f"карточек: {len(facts)}; по доменам: {doms}; по уровням: {lvls}") if problems: print("ПРОБЛЕМЫ:") for p in problems: print(" -", p) return 1 print("SELFTEST OK") return 0 def main(): ap = argparse.ArgumentParser() ap.add_argument("--answers", default=None, help="ответы через | (тот же порядок, что в интерактиве)") ap.add_argument("--count", type=int, default=20) ap.add_argument("--all", action="store_true", help="взять все карточки после фильтров") ap.add_argument("--domain", default=None, choices=["algo", "linux", "net", "c_cpp"]) ap.add_argument("--level", default="base,core", help="base,core,deep или all (по умолчанию base,core)") ap.add_argument("--seed", type=int, default=42) ap.add_argument("--learn", action="store_true", help="показывать пояснение после каждого ответа") ap.add_argument("--list", action="store_true") ap.add_argument("--selftest", action="store_true") args = ap.parse_args() facts = load() if args.selftest: sys.exit(selftest(facts)) if args.level.strip().lower() == "all": want = set(LEVELS) else: want = {x.strip() for x in args.level.split(",") if x.strip()} bad = want - set(LEVELS) if bad: print(f"неизвестный уровень: {', '.join(sorted(bad))} (есть base, core, deep, all)") sys.exit(2) facts = [f for f in facts if f["level"] in want] if args.domain: facts = [f for f in facts if f["domain"] == args.domain] if not facts: print("нет карточек под фильтр") sys.exit(1) if args.list: for i, f in enumerate(facts, 1): print(f"{i:3d}. [{f['domain']}/{f['level']}] {f['q']}") print(f" -> {f['a']}") h = hint(f) if h: print(f" {h}") print(f"\nвсего: {len(facts)} (полный набор: {FACTS_PATH})") sys.exit(0) rng = random.Random(args.seed) pool = facts[:] rng.shuffle(pool) picked = pool if args.all else pool[:min(args.count, len(pool))] results = [] if args.answers is not None: given = [x.strip() for x in args.answers.split("|")] if len(given) < len(picked): print(f"ответов меньше, чем карточек: {len(given)} < {len(picked)} (проверяю сколько есть)") for i, f in enumerate(picked): ans = given[i] if i < len(given) else "" results.append({"n": i + 1, "domain": f["domain"], "level": f["level"], "q": f["q"], "answer": ans, "ok": check(f, ans), "correct": f["a"], "hint": hint(f)}) if args.learn: for r in results: if not r["ok"]: print(f"#{r['n']} [{r['domain']}] {r['q']}") print(f" твой ответ: {r['answer'] or '(пусто)'}") print(f" верно: {r['correct']}") if r["hint"]: print(f" {r['hint']}") print() else: print(f"Карточек в прогоне: {len(picked)} (уровни: {', '.join(sorted(want))}).") print("Пустой ввод = не знаю, 'q' = выход.\n") for i, f in enumerate(picked, 1): print(f"[{i}/{len(picked)}] ({f['domain']}/{f['level']}) {f['q']}") try: ans = input(" > ").strip() except (EOFError, KeyboardInterrupt): print() break if ans.lower() in ("q", "quit", "выход"): break ok = check(f, ans) if ok: print(" ok" + (" — " + hint(f) if args.learn and hint(f) else "")) else: print(" нет. верно: " + f["a"]) h = hint(f) if h: print(" " + h) results.append({"n": len(results) + 1, "domain": f["domain"], "level": f["level"], "q": f["q"], "answer": ans, "ok": ok, "correct": f["a"], "hint": hint(f)}) ok = sum(1 for r in results if r["ok"]) total = len(results) per_dom, per_lvl = {}, {} for r in results: for bucket, key in ((per_dom, r["domain"]), (per_lvl, r["level"])): v = bucket.setdefault(key, {"total": 0, "ok": 0}) v["total"] += 1 v["ok"] += 1 if r["ok"] else 0 with open(os.path.join(HERE, "results_facts.json"), "w", encoding="utf-8") as fh: json.dump({"seed": args.seed, "level": args.level, "correct": ok, "total": total, "per_domain": per_dom, "per_level": per_lvl, "details": results}, fh, ensure_ascii=False, indent=2) print(f"\n=== ИТОГ КАРТОЧЕК: {ok}/{total} ===") for title, bucket in (("по доменам", per_dom), ("по уровням", per_lvl)): print(f" {title}:") for d, v in sorted(bucket.items()): share = 100 * v["ok"] / v["total"] if v["total"] else 0 mark = "ок" if share >= 80 else ("добор" if share >= 50 else "дыра") print(f" {d:8s} {v['ok']}/{v['total']} ({share:.0f}%) — {mark}") print("Подробности: results_facts.json") sys.exit(0 if total and ok == total else 1) if __name__ == "__main__": main()