summaryrefslogtreecommitdiff
path: root/scripts/analyze-p55.py
blob: 2664a682b86b802f313e3b2ccade3a791aa060d0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
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))