"""
=============================================================================
CS486 · 第 9 讲 · 代码 03：学出来的世界模型——误差的累积、搜索"钻空子"、闭环的补救
=============================================================================

【这个脚本要回答的问题】（讲义 §6）
    代码 01、02 都用了**真实的模拟器**：每一步合法与否、执行后的状态都由规则算出。很多实际问题没有这样的模拟器——
    RAP（Hao et al. 2023）让 LLM 自己预测"做了这个动作之后世界变成什么样"，再在这个"脑内模拟"里做 MCTS；
    MuZero 学一个潜在空间里的世界模型。Wang et al.（2024，"Can Language Models Serve as Text-Based World Simulators?"）
    测了 GPT-4 预测文本游戏状态变化的准确率：给出人写的规则时，完整状态预测（动态转移）也只有 59.9%。

    我们训练一个 2 层的 TinyReasoner 当世界模型（search_lab.train_world_model）：输入"状态 + 动作"，输出下一个状态。
    它和提议者、价值函数一样只在 3–5 块积木上训练。

    演示 1：一步预测的准确率（按积木数）。
    演示 2：误差的累积——沿着真实的最短计划，在模型里一步步推演，第 k 步时模型以为的状态还对吗？
    演示 3：在模型里规划——A* 在模型里"找到"的计划，在真实世界里有多少能执行？搜索越费力，找到的计划越不可信吗？
    演示 4：在模型里找到、在现实里失败的计划长什么样？
    演示 5：闭环（MPC）——在模型里规划、只执行第一步、观察真实状态、重新规划。

【运行】
    python3 03_world_models.py           # 约 1.5 分钟（第一次另加训练世界模型约 25 秒）
    python3 03_world_models.py --quick   # 冒烟测试

【依赖】PyTorch + 本目录的 search_lab.py。
"""

import argparse
import os
import random
import sys
import time

HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(HERE, "..", "lecture01"))

import search_lab as sl                                                  # noqa: E402
from console import pct, print_header, print_observations, print_table  # noqa: E402

pl = sl.pl


def blocks_of(s):
    """一个状态里的积木（排好序的字符串）。【边界】s 为 None 时返回空字符串。"""
    return "".join(sorted(b for t in s for b in t)) if s else ""


def main():
    ap = argparse.ArgumentParser(description="学出来的世界模型")
    ap.add_argument("--quick", action="store_true", help="冒烟测试")
    args = ap.parse_args()
    t0 = time.perf_counter()
    count = 8 if args.quick else 100
    max_expand = 100 if args.quick else 500

    print_header("准备：世界模型（2 层，3–5 块积木：状态 + 动作 → 下一个状态）；测试题和代码 01 相同")
    wm = sl.WorldModel(sl.train_world_model(quick=args.quick))
    proposer = pl.train_proposer(quick=args.quick)
    sets = {n: pl.test_problems(n, count, seed=1) for n in (4, 5)}

    # ------------------------------------------------------------------
    print_header("演示 1：一步预测的准确率（每个 n 300 个随机的 (状态, 合法动作)；训练只见过 3–5 块）")
    rng = random.Random(5)
    rows, one = [], {}
    for n in (3, 4, 5, 6, 7):
        pairs = []
        for _ in range(30 if args.quick else 300):
            s = pl.random_state(rng, "".join(sorted(rng.sample(pl.LETTERS, n))))
            pairs.append((s, rng.choice(pl.legal_moves(s))))
        pred = wm(pairs)
        acc = sum(t == pl.apply_move(s, m)[0] for (s, m), t in zip(pairs, pred)) / len(pairs)
        keep = sum(blocks_of(t) == blocks_of(s) for (s, m), t in zip(pairs, pred)) / len(pairs)
        none = sum(t is None for t in pred) / len(pairs)
        one[n] = acc
        rows.append([n, pct(acc), pct(keep), pct(none)])
    print_table(["积木数 n", "下一个状态完全正确", "积木一块不少也不多", "说不出像样的状态"], rows, [10, 20, 20, 18])

    # ------------------------------------------------------------------
    print_header("演示 2：误差的累积——沿真实的最短计划在模型里推演，第 k 步时模型以为的状态 = 真实状态的比例")
    roll = {}
    for n in (4, 5):
        for a, g, best in sets[n]:
            believed, real = a, a
            for k, m in enumerate(best, 1):
                real = pl.apply_move(real, m)[0]
                believed = sl.rollout_in_model(wm, believed, [m]) if believed is not None else None
                roll.setdefault((n, k), []).append(believed == real)
    rows = []
    for n in (4, 5):
        ks = [k for k in range(1, 9) if len(roll.get((n, k), [])) >= 10]
        rows.append([f"n={n} 模型推演", *[pct(sum(roll[(n, k)]) / len(roll[(n, k)]), 0) for k in ks]])
        rows.append([f"n={n} 一步准确率的 k 次方", *[pct(one[n] ** k, 0) for k in ks]])
        rows.append([f"n={n} 题数", *[str(len(roll[(n, k)])) for k in ks]])
    print_table(["", *[f"k={k}" for k in range(1, 9)]], rows, [26] + [7] * 8)
    print("  （只统计计划长度 ≥ k 的题；推演到某一步动作在模型以为的状态里不合法，也算错）")

    # ------------------------------------------------------------------
    print_header("演示 3：在模型里规划——A* 在模型里找到的计划，在真实世界里能执行吗？")
    rows, d3, false_plans = [], {}, []
    for n in (4, 5):
        found = true_ok = 0
        by_effort = {}
        for a, g, best in sets[n]:
            plan, exp = sl.astar_in_model(wm, a, g, max_expand)
            if plan is None:
                continue
            found += 1
            v = pl.validate(a, g, pl.plan_to_str(plan))
            true_ok += v.ok
            key = "≤ 10" if exp <= 10 else ("11–50" if exp <= 50 else "> 50")
            by_effort.setdefault(key, []).append(v.ok)
            if not v.ok:
                false_plans.append((n, a, g, plan, v))
        # 对照：提议者独立写出的计划，交给世界模型"检查"（在模型里推演，看是否到达目标）
        plans = pl.propose(proposer, [pl.prompt(a, g) for a, g, _ in sets[n]], 2 * n + 2)
        acc = acc_true = real_ok = 0
        for (a, g, _), p in zip(sets[n], plans):
            moves = [(p[i], p[i + 1]) for i in range(0, len(p) - 1, 2)]
            ok = pl.validate(a, g, p).ok
            real_ok += ok
            if sl.rollout_in_model(wm, a, moves) == g:
                acc += 1
                acc_true += ok
        d3[n] = (found, true_ok, by_effort, acc, acc_true, real_ok)
        rows.append([n, "A* 在模型里搜索", f"{found}", f"{true_ok}", pct(true_ok / max(1, found), 0)])
        rows.append([n, "提议者的计划，模型说能到", f"{acc}", f"{acc_true}", pct(acc_true / max(1, acc), 0)])
    print_table(["n", "计划从哪来", "模型说能到目标", "真实世界里合法并到达", "模型说'能'时的可信度"],
                rows, [4, 26, 16, 22, 22])
    print(f"\n  按搜索的费力程度分组（A* 在模型里展开了多少个状态才找到计划）：真实合法 / 找到")
    print_table(["n", "≤ 10", "11–50", "> 50"],
                [[n, *[f"{sum(d3[n][2].get(k, []))}/{len(d3[n][2].get(k, []))}" for k in ("≤ 10", "11–50", "> 50")]]
                 for n in (4, 5)], [4, 10, 10, 10])

    # ------------------------------------------------------------------
    print_header("演示 4：在模型里找到、在现实里失败的计划——模型在哪一步想错了？（举 3 个例子）")
    for n, a, g, plan, v in false_plans[:3]:
        believed, real = a, a
        for i, m in enumerate(plan):
            nb = wm([(believed, m)])[0]
            nr, err = pl.apply_move(real, m)
            if nb != nr:
                what = f"真实世界：{err}" if err else f"真实是 {pl.state_str(nr)!r}"
                print(f"  n={n}  {pl.state_str(a)!r} → {pl.state_str(g)!r}，计划 {pl.plan_to_str(plan)!r}："
                      f"第 {i} 步 {m[0] + m[1]}，模型以为变成 {pl.state_str(nb) if nb else None!r}，{what}")
                break
            believed, real = nb, nr

    # ------------------------------------------------------------------
    print_header("演示 5：闭环（MPC）——在模型里规划 → 只执行第一步 → 观察真实状态 → 重新规划")
    rows, d5 = [], {}
    for n in (4, 5):
        res = [sl.mpc(wm, a, g, 4 * n, max_expand) for a, g, _ in sets[n]]
        ok = sum(r[0] for r in res) / count
        d5[n] = (ok, sum(r[2] for r in res) / count)
        rows.append([n, pct(d3[n][1] / count, 0), pct(ok, 0), f"{d5[n][1]:.1f}", "100%"])
    print_table(["n", "开环：模型里规划一次就执行", "闭环 MPC", "平均规划次数", "A*（真实模拟器）"], rows, [4, 28, 10, 14, 18])

    if args.quick:
        print("\n（--quick 冒烟测试：数字没有意义，不打印观察。）")
        print(f"\n⏱ 总用时 {time.perf_counter() - t0:.0f} 秒")
        return

    f5, t5, eff5, acc5, acct5, real5 = d3[5]
    f4, t4, _, acc4, acct4, real4 = d3[4]
    k5 = [k for k in range(1, 9) if len(roll.get((5, k), [])) >= 10]
    lastk = k5[-1]
    hard = eff5.get("> 50", [])
    easy = eff5.get("≤ 10", [])
    print_observations([
        f"世界模型的一步准确率随积木数下降，而且出了训练分布就崩溃（演示 1）：3 / 4 / 5 块 {pct(one[3])} / {pct(one[4])} / {pct(one[5])}，"
        f"6 / 7 块 {pct(one[6])} / {pct(one[7])}。'预测下一个状态'看起来只是抄写加一处修改，但对小模型来说，"
        "状态越长越难（第 6 讲的长度问题在世界模型里同样存在）。",
        f"误差会累积（演示 2）：n = 5 时沿最短计划推演，第 1 步 {pct(sum(roll[(5, 1)]) / len(roll[(5, 1)]), 0)} 正确，第 {lastk} 步只剩 "
        f"{pct(sum(roll[(5, lastk)]) / len(roll[(5, lastk)]), 0)}。一步 {pct(one[5], 0)} 的准确率听起来不错，"
        "但规划要连续推演很多步——这就是为什么 Wang et al. 测出的 59.9%（一步！）意味着 GPT-4 还不能当可靠的模拟器。",
        f"在模型里搜索找到的计划不可信（演示 3）：n = 5 时 A* 在模型里'找到'了 {f5} 道题的计划，真实世界里只有 {t5} 个能执行"
        f"（{pct(t5 / max(1, f5), 0)}）。对照：提议者独立写出的计划，模型说'能到目标'的 {acc5} 个里有 {acct5} 个真的能（"
        f"{pct(acct5 / max(1, acc5), 0)}）。同一个模型、同样的'能到'，当计划是**通过在模型里搜索挑出来的**时，可信度低得多——"
        "搜索专门找'模型认为能走通'的路，模型的错误正好提供了这样的路。",
        f"搜索越费力，找到的计划越不可信（演示 3）：n = 5 时展开 ≤ 10 个状态就找到的计划 {sum(easy)}/{len(easy)} 合法，"
        f"展开 50 个以上才找到的 {sum(hard)}/{len(hard)}。容易的题，真实的路很近；难的题，搜索在模型里找了很久，"
        "最后找到的常常是穿过某个错误转移的'捷径'。这和第 4 讲 best-of-N 的过度优化、第 5 讲的奖励黑客是同一个现象："
        "**优化压力越大，越会找到评估者（这里是世界模型）的错误**。",
        f"另一面：世界模型当'检查者'时，漏判多于误判（演示 3）：提议者在 n = 5 上真正合法的计划有 {real5} 个，模型只认可了其中 {acct5} 个——"
        "模型的错误让很多正确的计划'走不通'。不可靠的检查者两头出错，只是方向取决于它被怎样使用。",
        f"闭环把模型的错误从'致命'变成'可恢复'（演示 5）：n = 5 时开环（在模型里规划一次就执行）{pct(t5 / count, 0)}，"
        f"闭环 MPC {pct(d5[5][0], 0)}（平均规划 {d5[5][1]:.1f} 次）；n = 4：{pct(t4 / count, 0)} → {pct(d5[4][0], 0)}。"
        "每一步都用真实观察纠正'模型以为的状态'，错误只影响眼前一步。这就是智能体在环境里行动的基本形式（第 11 讲），"
        "也是运行时重规划（第 10 讲）的最简单版本。当然，有真实模拟器时 A* 就是 100%——学世界模型，是因为很多问题没有模拟器。",
    ])
    print(f"\n⏱ 总用时 {time.perf_counter() - t0:.0f} 秒")


if __name__ == "__main__":
    main()
