Files
eduplan-cpp-eltex/diag/facts_drill.py
T

206 lines
9.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()