"""
=============================================================================
第 6 讲 · 代码 3：为什么需要随机策略——短走廊问题
=============================================================================

对应讲义：lectures/lecture06_policy_gradient.md 第 1、6 节

-----------------------------------------------------------------------------
【这个文件要解决什么问题？】
-----------------------------------------------------------------------------
基于价值的方法（Q-learning 等）最终给出的是**确定性**策略 argmax_a Q(s,a)，
训练中最多再加上 ε 的随机探索。但有些问题的最优策略**本身就是随机的**。

Sutton & Barto 例 13.1 "带转换动作的短走廊"：

    [S] ⇄ [ 1 ] ⇄ [ 2 ] → [终点]
     0    （反转）   2

    - 3 个非终止状态 + 终点，每一步奖励 -1，γ = 1
    - 在状态 0 和 2，"右"就是向右，"左"就是向左（状态 0 向左撞墙原地不动）
    - 在状态 1，动作是**反的**："右"会向左走，"左"会向右走
    - 关键：智能体**分辨不出**自己在哪个状态（三个状态的特征完全相同）——
      这是一种部分可观测（第 1 讲 3.6 节），所有状态只能用同一个策略：以概率 p 选"右"。

    - 如果 p = 1（总是选右）：在状态 1 会被送回状态 0，然后又向右……永远到不了终点。
    - 如果 p = 0（总是选左）：在状态 0 撞墙原地不动，永远到不了终点。
    - 最优的 p 在中间！只有**随机**策略才能到达终点。

本文件：
    1. 精确计算每个 p 对应的期望回报，找到最优 p（约 0.59）
    2. 看看 ε-贪心能做到多好（只能在 p = ε/2 或 1 - ε/2 之间选）
    3. 用 REINFORCE（有/无基线）直接学习 p

-----------------------------------------------------------------------------
【运行方式】
-----------------------------------------------------------------------------
    python 03_short_corridor.py      （约 40 秒）
"""

import numpy as np

LEFT, RIGHT = 0, 1
GOAL = 3


def corridor_step(s, a):
    """
    短走廊的一步。返回 (下一状态, 奖励, 是否结束)。

    【规则】状态 1 的动作是反的。每一步奖励都是 -1。
    """
    if s == 0:
        s_next = 1 if a == RIGHT else 0
    elif s == 1:
        s_next = 0 if a == RIGHT else 2          # 反转！
    else:  # s == 2
        s_next = GOAL if a == RIGHT else 1
    return s_next, -1.0, s_next == GOAL


def exact_value(p):
    """
    策略"以概率 p 选右"从起点出发的精确期望回报。

    【思路】这是一个 3 个状态的 MRP（第 1 讲），直接写出贝尔曼方程并求解：
        V0 = -1 + p·V1 + (1-p)·V0
        V1 = -1 + p·V0 + (1-p)·V2
        V2 = -1 + p·0  + (1-p)·V1        （终点价值为 0）
    整理成 (I - P) V = R 的形式，用 np.linalg.solve 求解。

    【边界】p = 0 或 p = 1 时永远到不了终点，价值为 -∞，方程组奇异。
    """
    if p <= 0.0 or p >= 1.0:
        return -np.inf
    P = np.array([[1 - p, p, 0.0],
                  [p, 0.0, 1 - p],
                  [0.0, 1 - p, 0.0]])
    R = -np.ones(3)
    V = np.linalg.solve(np.eye(3) - P, R)
    return float(V[0])


def softmax_prob_right(theta):
    """
    参数 θ = [θ_左, θ_右]（两个动作的偏好），π(右) = exp(θ_右) / (exp(θ_左) + exp(θ_右))。
    因为所有状态特征相同，这就是整个策略。
    """
    h = theta - theta.max()
    e = np.exp(h)
    return e[RIGHT] / e.sum()


def reinforce_corridor(n_episodes, alpha_theta, alpha_w=None, init_p_right=0.05, rng=None, max_steps=1000):
    """
    在短走廊上运行 REINFORCE。

    【算法】（讲义 6.1 节）每个回合结束后，对每个时刻 t：
        G_t = 从 t 开始的回报
        θ ← θ + α_θ · (G_t - b) · ∇_θ log π(a_t)

        - alpha_w = None：不用基线，b = 0
        - alpha_w 给定：用一个可学习的基线 b = w（所有状态共享一个数，因为状态分辨不出），
          同时用 MC 更新它：w ← w + α_w (G_t - w)

    【得分函数】θ = [θ_左, θ_右]，∇ log π(a) = e_a - [π(左), π(右)]。

    【参数】
        init_p_right : 初始的 π(右)。默认 0.05，模拟"从一个接近确定性的差策略开始"。
        max_steps    : 每回合最多步数（p 很小或很大时回合可能非常长）。

    【返回】
        (每回合总奖励数组, 每回合结束时的 π(右))
    """
    # 让初始 π(右) = init_p_right：θ_右 - θ_左 = log(p / (1-p))
    d = np.log(init_p_right / (1 - init_p_right))
    theta = np.array([-d / 2, d / 2])
    w = 0.0
    totals = np.zeros(n_episodes)
    p_hist = np.zeros(n_episodes)
    for k in range(n_episodes):
        s, actions, rewards = 0, [], []
        for _ in range(max_steps):
            p = softmax_prob_right(theta)
            a = RIGHT if rng.random() < p else LEFT
            s, r, done = corridor_step(s, a)
            actions.append(a)
            rewards.append(r)
            if done:
                break
        # 从后往前算回报（γ = 1），并逐步更新
        G = 0.0
        returns = np.zeros(len(rewards))
        for t in range(len(rewards) - 1, -1, -1):
            G += rewards[t]
            returns[t] = G
        for t, a in enumerate(actions):
            p = softmax_prob_right(theta)
            grad = -np.array([1 - p, p])           # -π
            grad[a] += 1.0                          # e_a - π
            b = w if alpha_w is not None else 0.0
            theta += alpha_theta * (returns[t] - b) * grad
            if alpha_w is not None:
                w += alpha_w * (returns[t] - w)
        totals[k] = sum(rewards)
        p_hist[k] = softmax_prob_right(theta)
    return totals, p_hist


def main():
    # ---------------------------------------------------------------------
    print("=" * 78)
    print("演示 1：每个 p 的精确期望回报（p = 选\"右\"的概率）")
    print("=" * 78)
    ps = [0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 0.55, 0.6, 0.65, 0.7, 0.8, 0.9, 0.95]
    for p in ps:
        v = exact_value(p)
        bar = "█" * int(max(0, 60 + v) / 1.5)        # 简易条形图（越长越好）
        print(f"  p = {p:4.2f}：V(起点) = {v:8.2f}  {bar}")
    grid = np.linspace(0.01, 0.99, 9801)
    values = np.array([exact_value(p) for p in grid])
    p_star = grid[np.argmax(values)]
    print(f"\n  最优 p* ≈ {p_star:.3f}，V(起点) = {values.max():.2f}")
    print(f"  p = 0 或 1（确定性策略）：永远到不了终点，V = -∞")

    # ---------------------------------------------------------------------
    print("\n" + "=" * 78)
    print("演示 2：基于价值的 ε-贪心能做到多好？")
    print("=" * 78)
    eps = 0.1
    print(f"  ε-贪心（ε = {eps}）只能选择\"主要向右\"（p = 1 - ε/2 = {1 - eps / 2}）或\"主要向左\"（p = ε/2 = {eps / 2}）：")
    print(f"    主要向右：V = {exact_value(1 - eps / 2):.2f}")
    print(f"    主要向左：V = {exact_value(eps / 2):.2f}")
    print(f"  最优随机策略：V = {values.max():.2f}")
    print("  → ε-贪心的\"随机性\"只是为了探索，大小由人手工设定，而不是学出来的；")
    print("    在需要随机性的问题上，它远不如直接学习一个随机策略。")

    # ---------------------------------------------------------------------
    print("\n" + "=" * 78)
    print("演示 3：REINFORCE 直接学习 p（从 π(右) = 0.05 出发；1000 个回合；30 次运行平均）")
    print("=" * 78)
    n_runs, n_episodes = 30, 1000
    settings = [
        ("A 无基线 α=2^-13", dict(alpha_theta=2 ** -13), n_runs),
        ("B 无基线 α=2^-12", dict(alpha_theta=2 ** -12), n_runs),
        ("C 无基线 α=2^-9", dict(alpha_theta=2 ** -9), 10),
        ("D 有基线 α=2^-9", dict(alpha_theta=2 ** -9, alpha_w=2 ** -6), n_runs),
    ]
    windows = [(0, 10), (10, 50), (50, 100), (100, 300), (300, 600), (600, 1000)]
    print("  （D 的基线学习率 α_w = 2^-6。每回合最多 1000 步，到不了终点的回合总奖励为 -1000。")
    print("    C 的回合大多是跑满 1000 步的\"失败回合\"，非常耗时，所以只运行 10 次。）")
    print("  每回合平均总奖励  " + "".join(f"{f'{a + 1}-{b}':>9s}" for a, b in windows) + "  最终p中位数  塌缩到0/1")
    for label, kw, runs_k in settings:
        rng = np.random.default_rng(0)
        runs = [reinforce_corridor(n_episodes, rng=rng, **kw) for _ in range(runs_k)]
        totals = np.mean([r[0] for r in runs], axis=0)
        final_p = np.array([r[1][-1] for r in runs])
        collapsed = np.mean((final_p < 0.01) | (final_p > 0.99))
        print(f"  {label:<18s}" + "".join(f"{totals[a:b].mean():9.1f}" for a, b in windows)
              + f"   {np.median(final_p):8.3f}   {collapsed * 100:6.0f}%")
    print(f"\n  （最优随机策略的期望回报 {values.max():.2f}，对应 p* ≈ {p_star:.2f}）")
    print("  观察：")
    print("  - REINFORCE 从一个很差的策略（p = 0.05，期望回报约 -82）出发，逐渐把 p 推向最优值附近。")
    print("  - 不用基线时，回报的数值很大（几十），梯度估计方差大，学习率必须很小（A、B）才稳定，学得慢。")
    print("  - 不用基线、学习率调大到 2^-9（C）：灾难！一次很长的回合（回报 -几百）产生巨大的梯度，")
    print("    把 p 一下子推到 0 或 1——策略变成确定性的，再也到不了终点，也就再也学不回来。")
    print("  - 同样的学习率 2^-9，加上基线（D）：学得最快，也最稳定。")
    print("    基线不改变梯度的期望，只减小方差——这让我们可以放心地使用更大的学习率。")


if __name__ == "__main__":
    main()
