План v2 по диагностике (квиз 14/32) + 4 новые задачи (hash, heap, dijkstra, subnet) + тренажёр 60 карточек
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Тренажёр «числа и факты» — добор по промахам квиза (22.09: algo 1/8, linux 3/8, net 3/8).
|
||||
|
||||
Использование (из каталога diag):
|
||||
python3 facts_drill.py # интерактивно, 20 случайных карточек
|
||||
python3 facts_drill.py --count 60 --all # все 60
|
||||
python3 facts_drill.py --domain algo # только алгоритмы
|
||||
python3 facts_drill.py --list # вывести все вопросы с ответами (читать)
|
||||
python3 facts_drill.py --answers "20|62|ttl|..." # проверить строку ответов
|
||||
python3 facts_drill.py --answers "..." --count 60 --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")
|
||||
|
||||
|
||||
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 selftest(facts) -> int:
|
||||
problems = []
|
||||
for i, f in enumerate(facts, 1):
|
||||
for key in ("domain", "q", "a", "accept"):
|
||||
if not f.get(key):
|
||||
problems.append(f"карточка {i}: пустое поле {key}")
|
||||
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}: канонический ответ не проходит проверку")
|
||||
doms = {}
|
||||
for f in facts:
|
||||
doms[f["domain"]] = doms.get(f["domain"], 0) + 1
|
||||
print(f"карточек: {len(facts)}; по доменам: {doms}")
|
||||
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("--seed", type=int, default=42)
|
||||
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.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:2d}. [{f['domain']}] {f['q']}")
|
||||
print(f" -> {f['a']}")
|
||||
print(f"\nвсего: {len(facts)} (полный ответ в facts.json: {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"], "q": f["q"], "answer": ans,
|
||||
"ok": check(f, ans), "correct": f["a"]})
|
||||
else:
|
||||
print(f"Карточек в прогоне: {len(picked)}. Пустой ввод = не знаю, 'q' = выход.\n")
|
||||
for i, f in enumerate(picked, 1):
|
||||
print(f"[{i}/{len(picked)}] ({f['domain']}) {f['q']}")
|
||||
try:
|
||||
ans = input(" > ").strip()
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
print()
|
||||
break
|
||||
if ans.lower() in ("q", "quit", "выход"):
|
||||
break
|
||||
ok = check(f, ans)
|
||||
print(" ok" if ok else " нет. верно: " + f["a"])
|
||||
results.append({"n": len(results) + 1, "domain": f["domain"], "q": f["q"],
|
||||
"answer": ans, "ok": ok, "correct": f["a"]})
|
||||
|
||||
ok = sum(1 for r in results if r["ok"])
|
||||
total = len(results)
|
||||
per = {}
|
||||
for r in results:
|
||||
d = per.setdefault(r["domain"], {"total": 0, "ok": 0})
|
||||
d["total"] += 1
|
||||
d["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, "correct": ok, "total": total,
|
||||
"per_domain": per, "details": results}, fh, ensure_ascii=False, indent=2)
|
||||
|
||||
print(f"\n=== ИТОГ КАРТОЧЕК: {ok}/{total} ===")
|
||||
for d, v in sorted(per.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()
|
||||
Reference in New Issue
Block a user