From a59a4bbe1ab45adb29d1367995230d3f09847d9b Mon Sep 17 00:00:00 2001 From: "Somhairle H. Marisol" Date: Tue, 29 Sep 2026 00:24:08 +0800 Subject: p55: 当日任务经济闭环——设计口径物品报酬入账(夜捕鱼×2/送粮食物×1, 其余不发) + 连续完成天数(streak 跨日保留/漏完成清零) + v3 streak 尾段与 save(None) 兼容 + LV_P55_SHOT 证据 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/analyze-p55.py | 142 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 scripts/analyze-p55.py (limited to 'scripts') diff --git a/scripts/analyze-p55.py b/scripts/analyze-p55.py new file mode 100644 index 0000000..2664a68 --- /dev/null +++ b/scripts/analyze-p55.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +"""P55 profession daily-task reward + streak analysis. + +Reads the `LV_P55_SHOT=1` frame sequence + /log.txt and writes +docs/evidence/p55-analysis.txt with per-frame measured behaviour: + + 1. sequence from the real game log: offered -> active -> reward -> done, for + the day-3 farmer task 送粮 (DeliverGrain); + 2. the reward is the design-doc completion item only ("+食物×1"), the task + reaches Done only after the real sell signal, and the streak goes 0 -> 1; + 3. the backpack Food stays whole across the flow (sold 1, rewarded 1 back), + and the reward is surfaced in the short Chinese HUD feedback. + +Usage: + python3 scripts/analyze-p55.py + +Exit 0 when checks pass, 1 when they fail, 2 on usage/read error. +""" + +import re +import sys +from pathlib import Path + +import numpy as np +from PIL import Image + +SHOT_RE = re.compile( + r"p55-shot=(\S+) template=(\S+) state=(\S+) streak=(\d+) panel=(\S+) status=(.*?) bag=\[([^\]]*)\]" +) +FRAMES = ["p55-task-offered", "p55-task-active", "p55-reward", "p55-task-done"] +PANEL_DIFF_MIN = 1.0 + + +def fail(lines, out_txt, message): + lines.append(f"FAIL: {message}") + lines.append("") + lines.append("verdict: FAIL") + text = "\n".join(lines) + "\n" + if out_txt is not None: + out_txt.parent.mkdir(parents=True, exist_ok=True) + out_txt.write_text(text, encoding="utf-8") + print(text) + return 1 + + +def load(path): + return np.asarray(Image.open(path).convert("RGB"), dtype=np.float64) + + +def bag_food(bag): + for part in bag.split(";"): + if part.startswith("Food="): + return int(part.split("=")[1]) + return 0 + + +def main(argv): + if len(argv) != 3: + sys.stderr.write(__doc__) + return 2 + record_dir = Path(argv[1]) + out_txt = Path(argv[2]) + log_path = record_dir / "log.txt" + if not log_path.exists(): + sys.stderr.write(f"missing {log_path}\n") + return 2 + + lines = ["P55 profession daily-task reward + streak analysis (LV_P55_SHOT=1)", ""] + frames = {} + for line in log_path.read_text(encoding="utf-8", errors="replace").splitlines(): + match = SHOT_RE.search(line) + if match: + frames[match.group(1)] = { + "template": match.group(2), + "state": match.group(3), + "streak": int(match.group(4)), + "panel": match.group(5), + "status": match.group(6).strip(), + "bag": match.group(7), + } + + for name in FRAMES: + if name not in frames: + return fail(lines, out_txt, f"missing log frame {name}") + record = frames[name] + lines.append( + f"{name}: template={record['template']} state={record['state']} streak={record['streak']} " + f"panel={record['panel']} status={record['status']} bag=[{record['bag']}]" + ) + + lines.append("") + if frames["p55-task-offered"]["state"] != "offered": + return fail(lines, out_txt, "the daily task must start Offered (visible in the task panel)") + if frames["p55-task-offered"]["template"] != "送粮": + return fail(lines, out_txt, "seed 42 / farmer / day 3 must draw the 送粮 template") + if frames["p55-task-active"]["state"] != "active" or frames["p55-task-active"]["streak"] != 0: + return fail(lines, out_txt, "accepting must move the task Offered -> Active without touching streak") + reward = frames["p55-reward"] + if reward["state"] != "done": + return fail(lines, out_txt, "the real sell signal must complete the task") + if "任务完成:送粮 +食物×1" not in reward["status"]: + return fail(lines, out_txt, "the design-doc item reward must appear in the short Chinese feedback") + if reward["streak"] != 1: + return fail(lines, out_txt, "completing the day must bump the streak to 1") + if frames["p55-task-done"]["state"] != "done" or frames["p55-task-done"]["streak"] != 1: + return fail(lines, out_txt, "the task and streak must stay Done/1 in the panel") + + images = {} + for name in FRAMES: + path = record_dir / f"{name}.png" + if not path.exists(): + return fail(lines, out_txt, f"missing frame {path}") + images[name] = load(path) + std = float(images[name].std()) + lines.append(f"frame {name}: {images[name].shape[1]}x{images[name].shape[0]} std={std:.1f}") + if std < 5.0: + return fail(lines, out_txt, f"frame {name} looks blank (std {std:.1f})") + + panel_delta = float(np.abs(images["p55-task-offered"] - images["p55-reward"]).mean()) + food_reward = bag_food(reward["bag"]) + food_start = bag_food(frames["p55-task-offered"]["bag"]) + lines += [ + "", + f"task panel vs world delta = {panel_delta:.2f} (>= {PANEL_DIFF_MIN})", + f"backpack Food start = {food_start}, after reward = {food_reward}", + "", + ] + if panel_delta < PANEL_DIFF_MIN: + return fail(lines, out_txt, "task panel frame does not differ from the world frame") + if food_reward != food_start: + return fail(lines, out_txt, "Food must stay whole (sold 1, rewarded 1 back) across the reward flow") + + lines.append("verdict: PASS") + text = "\n".join(lines) + "\n" + out_txt.parent.mkdir(parents=True, exist_ok=True) + out_txt.write_text(text, encoding="utf-8") + print(text) + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) -- cgit v1.2.3