"""
=============================================================================
第 7 讲 · 代码 1：Actor-Critic——用 TD 误差代替蒙特卡洛回报
=============================================================================

对应讲义：lectures/lecture07_actor_critic_ppo.md 第 1、2 节

-----------------------------------------------------------------------------
【这个文件要解决什么问题？】
-----------------------------------------------------------------------------
第 6 讲的 REINFORCE（带基线）用蒙特卡洛回报 G_t 来给动作"打分"：

    θ ← θ + α γ^t (G_t - V(s_t)) ∇log π(a_t|s_t)

G_t 包含了整条剩余轨迹的随机性，方差大，而且必须等回合结束才能更新。

Actor-Critic 用第 3 讲 TD 的思想：把 G_t 换成"一步奖励 + 下一状态的估计价值"：

    δ_t = r_t + γ V(s_{t+1}) - V(s_t)          （TD 误差）
    θ ← θ + α_θ γ^t δ_t ∇log π(a_t|s_t)        （Actor：策略，负责"做"）
    w ← w + α_w δ_t ∇V(s_t)                     （Critic：价值，负责"评"）

    - 为什么 δ_t 可以代替优势 A(s_t, a_t)？因为如果 V = V^π，
      E[δ_t | s_t, a_t] = E[r_t + γ V^π(s_{t+1}) | s_t, a_t] - V^π(s_t) = Q^π(s_t,a_t) - V^π(s_t) = A^π(s_t,a_t)。
    - 方差小得多（只含一步的随机性），可以每走一步就更新；
    - 代价：V 不准时 δ_t 是有偏的（第 3 讲 TD 的偏差）。

本文件在 chain_mdp 上比较 REINFORCE（带基线）与一步 Actor-Critic，
每个回合结束后都用**精确的** J(θ) 来衡量策略的真实水平。

-----------------------------------------------------------------------------
【运行方式】
-----------------------------------------------------------------------------
    cd code/lecture07
    python 01_actor_critic.py      （约 15 秒）
"""

import numpy as np

from chain_mdp import N_STATES, GAMMA, J, grad_log_pi, sample_episode, exact, policy_probs

np.set_printoptions(precision=3, suppress=True)


# =============================================================================
# 第 1 部分：两种算法
# =============================================================================
def reinforce_with_baseline(n_episodes, alpha_theta, alpha_w, seed):
    """
    REINFORCE + 表格型价值基线（第 6 讲 7 节）。每个回合结束后才更新。

        对 t = 0..T-1：
            G_t = r_t + γ r_{t+1} + ...
            w[s_t] ← w[s_t] + α_w (G_t - w[s_t])                  （基线：MC 回归）
            θ      ← θ + α_θ γ^t (G_t - w[s_t]) ∇log π(a_t|s_t)

    【返回】每个回合结束后的精确 J(θ)（长度 n_episodes 的数组）。
    """
    rng = np.random.default_rng(seed)
    theta = np.zeros((N_STATES, 2))
    w = np.zeros(N_STATES)
    curve = np.zeros(n_episodes)
    for k in range(n_episodes):
        episode = sample_episode(theta, rng)
        G, returns = 0.0, np.zeros(len(episode))
        for t in range(len(episode) - 1, -1, -1):
            G = episode[t][2] + GAMMA * G
            returns[t] = G
        for t, (s, a, _, _) in enumerate(episode):
            adv = returns[t] - w[s]
            w[s] += alpha_w * adv
            theta += alpha_theta * GAMMA ** t * adv * grad_log_pi(theta, s, a)
        curve[k] = J(theta)
    return curve


def one_step_actor_critic(n_episodes, alpha_theta, alpha_w, seed):
    """
    一步 Actor-Critic（Sutton & Barto 13.5 节）。每走一步就更新。

        I ← 1
        每一步：
            δ = r + γ V(s') - V(s)            （s' 为终止状态时 V(s') = 0）
            w[s] ← w[s] + α_w δ               （Critic：TD(0)，第 3 讲）
            θ    ← θ + α_θ I δ ∇log π(a|s)    （Actor：用 δ 当作优势的估计）
            I    ← γ I                         （这就是 γ^t 因子）

    【注意更新顺序】这里先算 δ，再用同一个 δ 分别更新 Critic 和 Actor。

    【返回】每个回合结束后的精确 J(θ)。
    """
    rng = np.random.default_rng(seed)
    theta = np.zeros((N_STATES, 2))
    w = np.zeros(N_STATES)
    curve = np.zeros(n_episodes)
    for k in range(n_episodes):
        discount = 1.0
        for s, a, r, s_next in sample_episode(theta, rng):
            v_next = 0.0 if s_next is None else w[s_next]
            delta = r + GAMMA * v_next - w[s]
            w[s] += alpha_w * delta
            theta += alpha_theta * discount * delta * grad_log_pi(theta, s, a)
            discount *= GAMMA
        curve[k] = J(theta)
    return curve


# 注意：上面的 one_step_actor_critic 每一步都重新采样回合中的动作——
# 严格地说，sample_episode 是用回合开始时的 θ 采出整条轨迹的，而 θ 在回合中途已经更新了。
# 当步长较小时这点差别可以忽略；为了代码简单、与 REINFORCE 使用同样的采样函数，这里接受这个近似。


# =============================================================================
# 第 2 部分：演示
# =============================================================================
def main():
    methods = {"REINFORCE+基线": reinforce_with_baseline, "一步 Actor-Critic": one_step_actor_critic}
    J0 = J(np.zeros((N_STATES, 2)))
    J_opt = J(np.tile([-10.0, 10.0], (N_STATES, 1)))
    J_left = J(np.tile([10.0, -10.0], (N_STATES, 1)))
    print(f"初始策略（均匀随机）J = {J0:.3f}；最优策略（一直向右）J ≈ {J_opt:.3f}；"
          f"局部最优\"一直向左\" J ≈ {J_left:.3f}\n")

    # ---------------------------------------------------------------------
    print("=" * 78)
    print("演示 1：不同步长下的最终表现（300 个回合，30 个种子）")
    print("=" * 78)
    print("  每格显示：30 个种子最终 J 的平均值 /（其中陷入\"一直向左\"局部最优的种子数）")
    alphas_theta = [0.05, 0.1, 0.2, 0.4, 0.8]
    alphas_w = [0.1, 0.3]
    best = {}
    for name, fn in methods.items():
        print(f"\n  【{name}】")
        print("      α_w \\ α_θ " + "".join(f"{a:>12}" for a in alphas_theta))
        scores = {}
        for aw in alphas_w:
            cells = []
            for at in alphas_theta:
                finals = np.array([fn(300, at, aw, seed)[-1] for seed in range(30)])
                scores[(at, aw)] = finals.mean()
                cells.append(f"{finals.mean():6.2f}/({(finals < 3).sum():d})")
            print(f"      {aw:<10}" + "".join(f"{c:>12s}" for c in cells))
        best[name] = max(scores, key=scores.get)
        print(f"      平均值最好的组合：α_θ = {best[name][0]}，α_w = {best[name][1]}")
    print("\n  观察：")
    print("  - REINFORCE 的步长稍大，就有越来越多的种子陷入\"一直向左\"的局部最优（J ≈ -1.43）：")
    print("    梯度估计的方差大，早期几次\"向左碰巧不错\"的回合就可能把策略推向左边；")
    print("    一旦几乎总是向左，就再也看不到右边的 +10，永远困在那里。")
    print("  - Actor-Critic 在 α_θ ≤ 0.4 时几乎从不失败，能承受大得多的步长——方差小带来的直接好处。")
    print("  - 注意：如果只用少数几个种子来调步长，很可能碰巧一个失败都没遇到，从而选出一个危险的步长。")

    # ---------------------------------------------------------------------
    print("\n" + "=" * 78)
    print("演示 2：各自最佳步长下的学习曲线（精确 J，30 个新种子）")
    print("=" * 78)
    checkpoints = [10, 25, 50, 100, 200, 300]
    print("  方法                  " + "".join(f"{f'第{c}回合':>9s}" for c in checkpoints) + "   失败的种子")
    for name, fn in methods.items():
        at, aw = best[name]
        curves = np.array([fn(300, at, aw, seed) for seed in range(100, 130)])
        fails = int((curves[:, -1] < 3).sum())
        print(f"  {name:<18s}" + "".join(f"{curves[:, c - 1].mean():9.2f}" for c in checkpoints) + f"   {fails}/30")
    print("\n  观察：")
    print("  - 两者都从 J = -2.9 出发，在几十个回合内接近最优（5.9）。")
    print("  - 前 10 个回合 REINFORCE 反而更快：Critic 从 0 开始，一开始很不准，")
    print("    TD 误差 δ = r + γV(s') - V(s) 带有很大的偏差——这就是 Actor-Critic 的代价。")
    print("  - Critic 变准之后，Actor-Critic 在第 50 回合左右反超，最终也更接近最优；")
    print("    而且（演示 1）它能用大得多的步长而不陷入局部最优。")
    print("  - 这就是\"用偏差换方差\"：MC 回报无偏但噪声大，TD 误差有偏但噪声小。")

    # ---------------------------------------------------------------------
    print("\n" + "=" * 78)
    print("演示 3：学到了什么？（一步 Actor-Critic，最佳步长，300 个回合）")
    print("=" * 78)
    at, aw = best["一步 Actor-Critic"]
    # 重新跑一次，这次把最终的 θ 和 w 拿出来看（与 one_step_actor_critic 相同的逻辑）
    rng = np.random.default_rng(100)
    theta = np.zeros((N_STATES, 2))
    w = np.zeros(N_STATES)
    for _ in range(300):
        discount = 1.0
        for s, a, r, s_next in sample_episode(theta, rng):
            delta = r + GAMMA * (0.0 if s_next is None else w[s_next]) - w[s]
            w[s] += aw * delta
            theta += at * discount * delta * grad_log_pi(theta, s, a)
            discount *= GAMMA
    pi = policy_probs(theta)
    ex = exact(pi)
    print(f"  π(右|s)       = {pi[:, 1]}")
    print(f"  Critic 的 V   = {w}")
    print(f"  真实的 V^π    = {ex['V']}")
    print(f"  访问量 η(s)   = {ex['eta']}")
    print("  → 起点及其右边（常访问的状态）Critic 估计得较准，策略也几乎确定地向右；")
    print("    起点左边的状态现在几乎不会被访问（η ≈ 0），那里的 Critic 和策略都学得不充分——")
    print("    但这些状态对 J 几乎没有影响。策略梯度只在\"会去的地方\"优化。")


if __name__ == "__main__":
    main()
