Формат v3: уроки D1 (теория+разбор), задачи ступенями, карточки 101 с уровнями и пояснениями

This commit is contained in:
Kodlo-chan
2026-09-23 19:58:23 +07:00
parent 75a10f9999
commit 387f29d376
10 changed files with 1623 additions and 160 deletions
+90 -32
View File
@@ -1,14 +1,21 @@
#!/usr/bin/env python3
"""Тренажёр «числа и факты» — добор по промахам квиза (22.09: algo 1/8, linux 3/8, net 3/8).
"""Тренажёр «числа и факты» — карточки под промахи диагностики.
Уровни карточек:
base — база, с этого начинать (объяснение есть в поле why)
core — то, что почти наверняка спросят
deep — нишевое, в базовый прогон не попадает
Использование (из каталога 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 # проверить сам набор карточек
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.
@@ -22,6 +29,7 @@ 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:
@@ -43,20 +51,35 @@ def check(fact, answer: str) -> bool:
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"):
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}: канонический ответ не проходит проверку")
doms = {}
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
print(f"карточек: {len(facts)}; по доменам: {doms}")
lvls[f["level"]] = lvls.get(f["level"], 0) + 1
print(f"карточек: {len(facts)}; по доменам: {doms}; по уровням: {lvls}")
if problems:
print("ПРОБЛЕМЫ:")
for p in problems:
@@ -70,9 +93,11 @@ 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("--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()
@@ -80,6 +105,16 @@ def main():
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:
@@ -88,9 +123,12 @@ def main():
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})")
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)
@@ -105,12 +143,22 @@ def main():
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"]})
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)}. Пустой ввод = не знаю, 'q' = выход.\n")
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['q']}")
print(f"[{i}/{len(picked)}] ({f['domain']}/{f['level']}) {f['q']}")
try:
ans = input(" > ").strip()
except (EOFError, KeyboardInterrupt):
@@ -119,26 +167,36 @@ def main():
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"]})
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 = {}
per_dom, per_lvl = {}, {}
for r in results:
d = per.setdefault(r["domain"], {"total": 0, "ok": 0})
d["total"] += 1
d["ok"] += 1 if r["ok"] else 0
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, "correct": ok, "total": total,
"per_domain": per, "details": results}, fh, ensure_ascii=False, indent=2)
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 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}")
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)