"""
=============================================================================
CS486 · 第 8 讲 · 代码 01：经典规划——状态空间、搜索与验证
=============================================================================

【这个脚本要回答的问题】（讲义 §2）
    在问"LLM 能不能规划"之前，先看看"规划"在经典 AI 里是怎么定义、怎么解决的：
    一个规划问题 = 初始状态 + 目标 + 一组动作（每个动作有前提和效果）；解 = 一个动作序列。
    这类问题可以用**搜索**精确求解，而且一个计划对不对可以**机械地验证**。

    演示 1：状态空间有多大？——n 块积木的状态数（Lah 数之和），和最短计划的长度。
    演示 2：盲目搜索 vs 启发式搜索——BFS 和 A* 各要展开多少个状态？
    演示 3：STRIPS 表示——把"移动"领域落地成事实和动作；部分目标（只要求某几块的位置）。
    演示 4：验证器——它能精确地说出一个计划错在哪一步、为什么错（这就是后面 LLM-Modulo 用的反馈）。

【运行】
    python3 01_classical_planning.py      # 约 10 秒（只用标准库）

【依赖】本目录的 plan_lab.py（这个脚本只用到其中的标准库部分）。
"""

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 plan_lab as pl                                                    # noqa: E402
from console import print_header, print_observations, print_table       # noqa: E402


def main():
    t0 = time.perf_counter()
    print_header("演示 1：n 块积木的状态数，以及随机题目的最短计划长度（每个 n 200 道题）")
    rows, lens = [], {}
    for n in range(2, 9):
        probs = pl.test_problems(n, 200) if n <= 7 else []
        L = [len(p[2]) for p in probs]
        lens[n] = L
        rows.append([n, f"{pl.count_states(n):,}", f"{sum(L) / len(L):.2f}" if L else "-", max(L) if L else "-"])
    print_table(["积木数 n", "状态数", "最短计划平均长度", "最长"], rows, [10, 12, 18, 6])
    print(f"  （n = 10 时状态数 {pl.count_states(10):,}，n = 15 时 {pl.count_states(15):,}）")

    print_header("演示 2：BFS（盲目）vs A*（启发：不在最终位置的积木数）——平均展开的状态数（每个 n 30 道题）")
    rows, nodes = [], {}
    for n in range(3, 9):
        rng = random.Random(500 + n)
        probs = []
        for _ in range(30):
            blocks = "".join(sorted(rng.sample(pl.LETTERS, n)))
            probs.append((pl.random_state(rng, blocks), pl.random_state(rng, blocks)))
        ta = time.perf_counter()
        res = [pl.astar(a, b) for a, b in probs]
        tast = time.perf_counter() - ta
        a_nodes = [x[1] for x in res]
        if n <= 7:                                   # n = 8 时 BFS 每道题要展开上万到几十万个状态，太慢，只跑 A*
            tb = time.perf_counter()
            b_res = [pl.bfs(a, b) for a, b in probs]
            tbfs = time.perf_counter() - tb
            assert [len(x[0]) for x in b_res] == [len(x[0]) for x in res]     # 两者找到的计划一样短
            b_mean = sum(x[1] for x in b_res) / 30
        nodes[n] = (b_mean if n <= 7 else None, sum(a_nodes) / 30)
        rows.append([n, f"{b_mean:,.0f}" if n <= 7 else "-", f"{nodes[n][1]:,.1f}",
                     f"{b_mean / nodes[n][1]:.0f}×" if n <= 7 else "-",
                     f"⏱ {tbfs:.2f} / {tast:.2f} 秒" if n <= 7 else f"⏱ - / {tast:.2f} 秒"])
    print_table(["n", "BFS 展开", "A* 展开", "倍数", "用时 BFS / A*"], rows, [4, 12, 10, 8, 22])

    print_header("演示 3：STRIPS 表示——事实、动作、部分目标")
    init, goal = pl.parse_state("ac;b"), pl.parse_state("abc")
    acts = pl.strips_move_actions("abc")
    print(f"  初始状态 {pl.state_str(init)!r} 的事实：{sorted(pl.state_facts(init))}")
    print(f"  3 块积木落地出 {len(acts)} 个 STRIPS 动作，例如：")
    a = next(x for x in acts if x.name == "move_to_table(c,a)")
    print(f"    {a.name}：前提 {sorted(a.pre)}，添加 {sorted(a.add)}，删除 {sorted(a.delete)}")
    full, n_full = pl.strips_bfs(pl.state_facts(init), pl.state_facts(goal), acts)
    part, n_part = pl.strips_bfs(pl.state_facts(init), frozenset({"on(b,a)"}), acts)
    print(f"  完整目标 {sorted(pl.state_facts(goal))}：计划 {full}（展开 {n_full} 个状态）")
    print(f"  部分目标 ['on(b,a)']：计划 {part}（展开 {n_part} 个状态）")
    rows = []
    for n in range(3, 7):
        blocks = pl.LETTERS[:n]
        rng = random.Random(900 + n)
        a_, b_ = pl.random_state(rng, blocks), pl.random_state(rng, blocks)
        ts = time.perf_counter()
        sp, sn = pl.strips_bfs(pl.state_facts(a_), pl.state_facts(b_), pl.strips_move_actions(blocks))
        t_s = time.perf_counter() - ts
        bp, bn = pl.bfs(a_, b_)
        rows.append([n, len(pl.strips_move_actions(blocks)), len(sp), len(bp), sn, bn, f"⏱ {t_s:.2f} 秒"])
    print_table(["n", "落地动作数", "STRIPS 计划长度", "专用 BFS 长度", "STRIPS 展开", "专用展开", "STRIPS 用时"],
                rows, [4, 10, 16, 14, 12, 10, 14])

    print_header("演示 4：验证器的反馈（初始 'ac;b'，目标 'abc'）")
    cases = [("c_bacb", "最短计划"), ("bacb", "忘了先把 c 搬开"), ("c_cbba", "顺序反了"), ("c_ba", "少了最后一步"),
             ("c_bacbc_cb", "多余的来回"), ("c_bdcb", "不存在的积木")]
    rows = []
    for plan_s, note in cases:
        v = pl.validate(init, goal, plan_s)
        rows.append([plan_s, note, "合法、到达目标" if v.ok else v.kind, v.step if not v.ok else "-", v.reason])
    print_table(["计划", "说明", "判决", "第几步", "原因"], rows, [12, 16, 16, 8, 28])

    print_observations([
        f"状态空间随积木数超指数增长（演示 1）：n = 5 时 {pl.count_states(5):,} 个状态，n = 8 时 {pl.count_states(8):,}，"
        f"n = 15 时超过 10^{len(str(pl.count_states(15))) - 1}。但随机题的最短计划并不长（n = 7 平均 "
        f"{sum(lens[7]) / len(lens[7]):.2f} 步）——难点不在于计划长，而在于在巨大的空间里找到它、并且每一步都合法。",
        f"启发式搜索大幅减少展开的状态（演示 2）：n = 7 时 BFS 平均展开 {nodes[7][0]:,.0f} 个状态，"
        f"A* 只要 {nodes[7][1]:,.1f} 个（约 {nodes[7][0] / nodes[7][1]:.0f} 倍）；两者找到的计划一样短（启发可采纳）。"
        "经典规划器（例如 Fast Downward）用更强的、自动从领域描述里推导出的启发函数，能处理大得多的问题。",
        "STRIPS（演示 3）把领域写成'事实 + 动作的前提 / 添加 / 删除'，规划器不需要知道任何领域知识就能搜索——"
        "同一个搜索算法可以用于积木、物流、调度……这正是 PDDL 和规划竞赛的基础。部分目标（只要 on(b,a)）的计划更短；"
        "PlanBench 的目标通常也是部分的。",
        "验证器（演示 4）能精确地说出计划错在哪一步、为什么错：'a 不在最上面'、'没有积木 d'、'每一步都合法但没到目标'。"
        "检查一个计划只要沿着它模拟一遍（和计划长度成正比），找一个计划却要搜索——这就是规划里的生成–验证不对称"
        "（第 4、7 讲），也是 LLM-Modulo 框架让 LLM 提议、让验证器把关的理由（代码 03）。",
    ])
    print(f"\n⏱ 总用时 {time.perf_counter() - t0:.0f} 秒")


if __name__ == "__main__":
    main()
