"""弱密码狙击 (Password Sniper) — rapid binary judgment: weak or strong? Passwords stream one at a time; the player calls each one weak (shoot) or strong (let pass) against the clock. The bank is code-defined (like the badge catalog — the content IS the curriculum), each item carrying its truth + a one-line reason shown in the result reveal, so every round reinforces what actually makes a password weak. Server-side truth: the round payload stores each item's ``is_weak`` + reason; the client submits judgments only. """ from __future__ import annotations import random from typing import Any from sqlalchemy.ext.asyncio import AsyncSession from app.domains.arcade.catalog import BuildResult, GradeResult from app.identity.person_models import IdentityPerson TIME_LIMIT_MS = 45_000 ITEM_COUNT = 16 BASE_POINTS = 80 COMBO_STEP = 15 COMBO_CAP = 5 # (password, is_weak, reason). Reasons are player-facing content (zh, like # the quiz bank's explanations). Strong examples are illustrative shapes, # not real credentials. _BANK: list[tuple[str, bool, str]] = [ ("123456", True, "纯数字顺子,永远在字典攻击的第一页。"), ("password", True, "最常见密码之一,秒破。"), ("qwerty123", True, "键盘顺位 + 数字尾巴,字典攻击标配组合。"), ("pingcap2026", True, "公司名+年份,攻击者最先尝试的企业密码模板。"), ("zhangsan1990", True, "姓名+生日,社工库一查一个准。"), ("admin@123", True, "系统默认风格密码,扫描器内置字典。"), ("iloveyou", True, "高频情感短语,字典攻击常客。"), ("Passw0rd!", True, "看似复杂,实为最著名的『伪强密码』,早已进字典。"), ("abc12345", True, "字母顺子+数字顺子,规则生成器一轮就出。"), ("666888", True, "吉利数字组合在中文字典表里排名极前。"), ("woaini520", True, "拼音短语+数字谐音,中文社工字典高频。"), ("computer", True, "单个常用英文单词,纯字典词。"), ("11111111", True, "重复字符,熵接近于零。"), ("dragon2024", True, "常用词+年份,规则变形字典必中。"), ("P@ssword2026", True, "常用词做字符替换(@=a, 0=o)是字典规则的一部分,并不加分。"), ("shanghai123", True, "城市名+123,地域字典组合。"), ("qazwsx", True, "键盘纵列顺位,和 qwerty 同级别。"), ("letmein", True, "经典弱密码榜单常年前列。"), ("Xk7#mQ2$vLp9!wRz", False, "16 位全随机大小写+数字+符号,无规律可循。"), ("correct-horse-battery-staple", False, "四个随机单词的长口令,长度带来的熵远胜复杂字符。"), ("蓝鲸吃了三明治9!Tq", False, "无关联词组+混合字符,长且不可预测。"), ("rV$8pLm#2xWq", False, "12 位随机混合字符,密码管理器生成的典型形状。"), ("Trombone!Nebula7Cactus", False, "三个无关单词+符号数字,长度和随机性都够。"), ("9f#Kd2!mXz$Q", False, "随机生成、无词典词根,抗字典与规则攻击。"), ("月光-Router-42-Pineapple", False, "跨语言随机词组,长度熵双达标。"), ("uH3$nB8@kF5^", False, "全随机 12 位,四类字符齐备。"), ("purple-tiger-eats-11-clouds", False, "随机词组长口令,好记又难猜。"), ("Zq!7Xw#4Vr$9Ty", False, "14 位随机混合,无任何个人信息。"), ] async def build(db: AsyncSession, person: IdentityPerson, variant: str | None = None) -> BuildResult: # noqa: ARG001 rng = random.SystemRandom() # Balanced-ish draw: at least 1/3 of each class so the round never # degenerates into "shoot everything". weak = [it for it in _BANK if it[1]] strong = [it for it in _BANK if not it[1]] n_strong = max(ITEM_COUNT // 3, 4) n_weak = ITEM_COUNT - n_strong picked = rng.sample(weak, min(n_weak, len(weak))) + rng.sample(strong, min(n_strong, len(strong))) rng.shuffle(picked) items_secret = [ {"index": i, "password": pw, "is_weak": is_weak, "reason": reason} for i, (pw, is_weak, reason) in enumerate(picked) ] items_public = [{"index": i, "password": pw} for i, (pw, _w, _r) in enumerate(picked)] return BuildResult( payload={"items": items_secret}, public={"items": items_public, "item_count": len(items_public)}, ) def grade(payload: dict, submission: dict, elapsed_ms: int) -> GradeResult: """``submission``: ``{"judgments": [{"index": 0, "weak": true}, …]}``. Unanswered items score 0 and break the combo.""" raw = submission.get("judgments") if isinstance(submission, dict) else None by_index: dict[int, bool] = {} if isinstance(raw, list): for j in raw: if not isinstance(j, dict): continue idx_raw = j.get("index") if idx_raw is None: continue try: idx = int(idx_raw) except (TypeError, ValueError): continue if idx not in by_index: by_index[idx] = bool(j.get("weak")) score = 0 combo = 0 best_combo = 0 correct_count = 0 answered_count = 0 breakdown: list[dict[str, Any]] = [] for item in payload.get("items", []): idx = item["index"] answered = idx in by_index is_correct = answered and by_index[idx] == item["is_weak"] gained = 0 if answered: answered_count += 1 if is_correct: correct_count += 1 combo += 1 best_combo = max(best_combo, combo) gained = BASE_POINTS + COMBO_STEP * min(combo - 1, COMBO_CAP) score += gained else: combo = 0 breakdown.append({ "index": idx, "password": item["password"], "is_weak": item["is_weak"], "your_call_weak": by_index.get(idx), "is_correct": is_correct, "reason": item["reason"], "gained": gained, }) return GradeResult( score=score, detail={ "correct_count": correct_count, "answered_count": answered_count, "item_count": len(payload.get("items", [])), "best_combo": best_combo, "elapsed_ms": elapsed_ms, "breakdown": breakdown, }, ) __all__ = ["build", "grade", "TIME_LIMIT_MS", "ITEM_COUNT"]