"""安全找茬 (Spot the Risk) — find every security hazard in an office scene. A scene is a list of described elements (some are hazards, some are harmless). The player selects ALL the hazards; the server scores by how many hazards were found vs. missed and how many safe items were wrongly flagged (precision + recall). Which elements are hazards + why stay server-side; the player sees only labels. One scene per round (short, ~60s). Score rewards finding all hazards without over-flagging. """ from __future__ import annotations import random from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from app.core.exceptions import AppException from app.domains.arcade.catalog import BuildResult, GradeResult from app.domains.arcade.content_models import STATUS_ACTIVE, ArcadeSpotScene from app.identity.person_models import IdentityPerson TIME_LIMIT_MS = 60_000 POINTS_PER_HAZARD = 150 # per correctly-found hazard WRONG_FLAG_PENALTY = 60 # per safe element wrongly flagged ALL_FOUND_BONUS = 200 # found every hazard, no false flags async def build(db: AsyncSession, person: IdentityPerson, variant: str | None = None) -> BuildResult: # noqa: ARG001 rng = random.SystemRandom() scene = (await db.execute( select(ArcadeSpotScene) .where(ArcadeSpotScene.status == STATUS_ACTIVE) .order_by(func.rand()) .limit(1) )).scalars().first() if scene is None or not isinstance(scene.elements, list) or len(scene.elements) < 2: raise AppException( code="ARCADE_GAME_UNAVAILABLE", message="No spot-the-risk scene is available.", status_code=422, ) elements = list(scene.elements) rng.shuffle(elements) # present in random order public_elements = [{"key": e["key"], "label": e["label"]} for e in elements] return BuildResult( payload={"title": scene.title, "elements": elements}, public={"title": scene.title, "elements": public_elements}, ) def grade(payload: dict, submission: dict, elapsed_ms: int) -> GradeResult: """``submission``: ``{"selected": ["a","c",...]}`` — the element keys the player flagged as hazards.""" elements = payload.get("elements", []) hazards = {e["key"] for e in elements if e.get("hazard")} valid_keys = {e["key"] for e in elements} raw = submission.get("selected") if isinstance(submission, dict) else None selected = set() if isinstance(raw, list): for k in raw: if isinstance(k, str) and k in valid_keys: selected.add(k) found = selected & hazards # correctly flagged hazards missed = hazards - selected # hazards not flagged wrong = selected - hazards # safe items wrongly flagged score = len(found) * POINTS_PER_HAZARD - len(wrong) * WRONG_FLAG_PENALTY all_found = not missed and not wrong and hazards if all_found: score += ALL_FOUND_BONUS score = max(0, score) breakdown = [ { "key": e["key"], "label": e["label"], "hazard": bool(e.get("hazard")), "selected": e["key"] in selected, "reason": e.get("reason"), } for e in elements ] return GradeResult( score=score, detail={ "title": payload.get("title"), "hazard_count": len(hazards), "found": len(found), "missed": len(missed), "wrong_flags": len(wrong), "all_found": bool(all_found), "elapsed_ms": elapsed_ms, "breakdown": breakdown, }, ) __all__ = ["build", "grade", "TIME_LIMIT_MS"]