summaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-29 00:24:08 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-29 00:24:08 +0800
commita59a4bbe1ab45adb29d1367995230d3f09847d9b (patch)
tree9570b3c7168a6252920138e380e2a9d9cb6f9acf /scripts
parent80c99bc9251e4ca6304761ef9bd88f2724642278 (diff)
downloadliving-village-a59a4bbe1ab45adb29d1367995230d3f09847d9b.tar.gz
p55: 当日任务经济闭环——设计口径物品报酬入账(夜捕鱼×2/送粮食物×1, 其余不发) + 连续完成天数(streak 跨日保留/漏完成清零) + v3 streak 尾段与 save(None) 兼容 + LV_P55_SHOT 证据
Diffstat (limited to 'scripts')
-rw-r--r--scripts/analyze-p55.py142
1 files changed, 142 insertions, 0 deletions
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 + <record-dir>/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 <record-dir> <out-txt>
+
+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))