#!/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))