"""评估与统计:从自身动作日志离线复算虚拟时刻与统计量。 "演练统计"验收要求:总时间与分项耗时可独立复算 [题面/计划 §6]。 recompute_virtual_time 是回放测试与本模块共用的唯一复算实现, 客户端日志格式为 SessionLog 中 accepted 事件的 {path, request, response}。 """ from __future__ import annotations from ..config import CostConfig from ..protocol.rules import CLEAR_SUCCESS from ..protocol.timing import clear_cost, measure_cost def recompute_virtual_time(events: list[dict], cost: CostConfig = CostConfig()) -> dict: """从动作日志复算逐步虚拟时刻。 events 每项形如 {path, request, response}(accepted=true)。 返回逐步表与最终时刻;任何一步与服务器响应偏差超过容差记入 mismatches。 """ position = (0.0, 0.0) channel = 1 virtual = 0.0 steps, mismatches = [], [] for index, event in enumerate(events): path, request = event["path"], event["request"] response = event["response"] if response.get("accepted") is not True: mismatches.append({"index": index, "issue": "响应未被接受"}) continue if path == "/enter": position, channel, virtual = (0.0, 0.0), 1, response["virtual_time_s"] elif path == "/measure": pos = (request["position"]["x"], request["position"]["y"]) predicted = measure_cost(position, pos, channel, request["channel"], cost) position, channel = pos, request["channel"] virtual += predicted.total_s elif path == "/clear": pos = (request["position"]["x"], request["position"]["y"]) success = response.get("clear_result") == CLEAR_SUCCESS predicted = clear_cost(position, pos, success, cost) position = pos virtual += predicted.total_s elif path == "/exit": pass # /exit 不推进虚拟时钟 [题面] else: mismatches.append({"index": index, "issue": f"未知路径 {path}"}) continue delta = response["virtual_time_s"] - virtual if abs(delta) > 1e-6: mismatches.append({"index": index, "path": path, "recomputed_s": virtual, "recorded_s": response["virtual_time_s"], "gap_s": delta}) steps.append({"index": index, "path": path, "predicted_s": round(virtual, 9), "recorded_s": response["virtual_time_s"]}) return {"steps": steps, "final_virtual_time_s": virtual, "mismatches": mismatches} def summarize(events: list[dict]) -> dict: """动作计数摘要:检测/清除/移动距离等基础统计。""" position = (0.0, 0.0) measures, clears, cleared, distance_m = 0, 0, 0, 0.0 for event in events: request = event["request"] if event["path"] in ("/measure", "/clear"): pos = (request["position"]["x"], request["position"]["y"]) distance_m += ((pos[0] - position[0]) ** 2 + (pos[1] - position[1]) ** 2) ** 0.5 position = pos if event["path"] == "/measure": measures += 1 elif event["path"] == "/clear": clears += 1 if event["response"].get("clear_result") == CLEAR_SUCCESS: cleared += 1 return {"measures": measures, "clears": clears, "cleared": cleared, "distance_m": distance_m}