"""
=============================================================================
第 16 讲 · 代码 3：全课回顾——同一个格子世界上的"算法大比拼"
=============================================================================

对应讲义：lectures/lecture16_frontiers_review.md 第 6 节

-----------------------------------------------------------------------------
【这个文件要做什么？】
-----------------------------------------------------------------------------
把这门课中最核心的表格型算法，放到**同一个**问题上，用**同一把尺子**衡量：
    "与环境交互了 N 个回合之后，学到的策略的真实价值是多少？"

问题：一个 5×5 的格子世界，从左下角出发，右上角是终点（+1），中间有一个陷阱（-1），
每走一步 -0.01，每一步有 10% 的概率滑向随机方向，γ = 0.95。

参赛的算法（括号里是讲次）：
    价值迭代（2）           ：模型已知，直接算出最优策略——所有学习算法的"天花板"
    MC 控制（4）            ：回合结束后，用完整回报更新 Q
    SARSA（4）              ：同策略的 TD 控制
    Q-learning（4）         ：异策略的 TD 控制
    Double Q-learning（4）  ：用两个 Q 表消除最大化偏差
    REINFORCE + 基线（6）   ：直接优化 softmax 策略
    Actor-Critic（7）       ：策略 + 学到的价值函数，每一步更新
    Dyna-Q（15）            ：学模型、在模型中规划
    Q-learning + 计数奖励（13）：乐观初始化 + 探索奖励

每个学习算法都用 10 个随机种子；这里不追求"谁是冠军"——超参数没有仔细调，
不同的问题上排名会变。重要的是看清它们的**共性和差别**。

-----------------------------------------------------------------------------
【运行方式】
-----------------------------------------------------------------------------
    cd code/lecture16
    python 03_course_review.py      （约 20 秒）
"""

import numpy as np

from toy_rlvr import pad

ROWS = COLS = 5
N_S, N_A = ROWS * COLS, 4
START, GOAL, TRAP = (4, 0), (0, 4), (2, 2)
GAMMA, SLIP, STEP_COST = 0.95, 0.1, -0.01
MOVES = [(-1, 0), (1, 0), (0, -1), (0, 1)]


# =============================================================================
# 第 1 部分：环境（已知的模型，只给价值迭代和"评估"用；学习算法只能通过 step() 交互）
# =============================================================================
def build_model():
    """真实的转移 P[s, a, s'] 和期望奖励 R[s, a]。终点和陷阱是吸收状态。"""
    P = np.zeros((N_S, N_A, N_S))
    R = np.zeros((N_S, N_A))
    for s in range(N_S):
        r0, c0 = divmod(s, COLS)
        if (r0, c0) in (GOAL, TRAP):
            P[s, :, s] = 1.0
            continue
        for a in range(N_A):
            for b in range(N_A):
                p = (1 - SLIP) + SLIP / 4 if a == b else SLIP / 4
                r1, c1 = r0 + MOVES[b][0], c0 + MOVES[b][1]
                if not (0 <= r1 < ROWS and 0 <= c1 < COLS):
                    r1, c1 = r0, c0
                P[s, a, r1 * COLS + c1] += p
                R[s, a] += p * (1.0 if (r1, c1) == GOAL else (-1.0 if (r1, c1) == TRAP else STEP_COST))
    return P, R


P_TRUE, R_TRUE = build_model()
CUM_P = np.cumsum(P_TRUE, axis=2)
S0 = START[0] * COLS + START[1]
TERMINAL = {GOAL[0] * COLS + GOAL[1], TRAP[0] * COLS + TRAP[1]}


def step(s, a, rng):
    """与环境交互一步，返回 (s', r, done)。"""
    s2 = min(int(np.searchsorted(CUM_P[s, a], rng.random())), N_S - 1)
    rc = divmod(s2, COLS)
    r = 1.0 if rc == GOAL else (-1.0 if rc == TRAP else STEP_COST)
    return s2, r, s2 in TERMINAL


def true_value(policy_probs):
    """精确计算一个（随机）策略从起点出发的真实价值：解线性方程组。policy_probs[s, a]。"""
    P_pi = np.einsum("sa,sat->st", policy_probs, P_TRUE)
    R_pi = (policy_probs * R_TRUE).sum(axis=1)
    return float(np.linalg.solve(np.eye(N_S) - GAMMA * P_pi, R_pi)[S0])


def greedy_probs(Q):
    """Q 表对应的贪心策略（写成概率的形式）。"""
    pr = np.zeros((N_S, N_A))
    pr[np.arange(N_S), Q.argmax(axis=1)] = 1.0
    return pr


def eps_greedy(Q, s, eps, rng):
    if rng.random() < eps:
        return int(rng.integers(N_A))
    q = Q[s]
    best = np.flatnonzero(q == q.max())
    return int(best[rng.integers(len(best))])


def softmax(x):
    e = np.exp(x - x.max(axis=-1, keepdims=True))
    return e / e.sum(axis=-1, keepdims=True)


# =============================================================================
# 第 2 部分：各个算法。每个都是一个"生成器"：每跑完一个回合，交出当前策略（概率形式）
# 【Python 小知识】函数里用 yield 就变成了生成器：每次 yield 暂停并交出一个值，下次从暂停处继续。
# =============================================================================
def value_iteration():
    """第 2 讲：模型已知时的价值迭代。"""
    V = np.zeros(N_S)
    for _ in range(1000):
        V = (R_TRUE + GAMMA * P_TRUE @ V).max(axis=1)
    return greedy_probs(R_TRUE + GAMMA * P_TRUE @ V), V[S0]


def mc_control(rng, eps=0.1, max_len=200):
    """第 4 讲：首次访问 MC 控制（ε-贪心，增量平均）。"""
    Q, N = np.zeros((N_S, N_A)), np.zeros((N_S, N_A))
    while True:
        s, traj = S0, []
        for _ in range(max_len):
            a = eps_greedy(Q, s, eps, rng)
            s2, r, done = step(s, a, rng)
            traj.append((s, a, r))
            s = s2
            if done:
                break
        G, first = 0.0, {}
        for t, (s_, a_, _) in enumerate(traj):
            first.setdefault((s_, a_), t)
        for t in range(len(traj) - 1, -1, -1):
            s_, a_, r_ = traj[t]
            G = r_ + GAMMA * G
            if first[(s_, a_)] == t:
                N[s_, a_] += 1
                Q[s_, a_] += (G - Q[s_, a_]) / N[s_, a_]
        yield greedy_probs(Q)


def td_control(rng, kind, alpha=0.2, eps=0.1, max_len=200):
    """第 4 讲：SARSA（kind = "sarsa"）、Q-learning（"q"）、Double Q-learning（"double"）。"""
    Q1, Q2 = np.zeros((N_S, N_A)), np.zeros((N_S, N_A))
    while True:
        s = S0
        Qb = Q1 + Q2 if kind == "double" else Q1
        a = eps_greedy(Qb, s, eps, rng)
        for _ in range(max_len):
            s2, r, done = step(s, a, rng)
            Qb = Q1 + Q2 if kind == "double" else Q1
            a2 = eps_greedy(Qb, s2, eps, rng)
            if kind == "sarsa":
                target = r + (0 if done else GAMMA * Q1[s2, a2])
                Q1[s, a] += alpha * (target - Q1[s, a])
            elif kind == "q":
                target = r + (0 if done else GAMMA * Q1[s2].max())
                Q1[s, a] += alpha * (target - Q1[s, a])
            else:
                A_, B_ = (Q1, Q2) if rng.random() < 0.5 else (Q2, Q1)
                target = r + (0 if done else GAMMA * B_[s2, A_[s2].argmax()])
                A_[s, a] += alpha * (target - A_[s, a])
            s, a = s2, a2
            if done:
                break
        yield greedy_probs(Q1 + Q2 if kind == "double" else Q1)


def reinforce(rng, lr=0.5, max_len=200):
    """第 6 讲：REINFORCE + 学到的状态价值基线（表格 softmax 策略）。交出的是随机策略本身。"""
    theta, V = np.zeros((N_S, N_A)), np.zeros(N_S)
    while True:
        s, traj = S0, []
        for _ in range(max_len):
            a = int(rng.choice(N_A, p=softmax(theta[s])))
            s2, r, done = step(s, a, rng)
            traj.append((s, a, r))
            s = s2
            if done:
                break
        G = 0.0
        for t in range(len(traj) - 1, -1, -1):
            s_, a_, r_ = traj[t]
            G = r_ + GAMMA * G
            adv = G - V[s_]
            V[s_] += 0.1 * adv
            grad = -softmax(theta[s_])
            grad[a_] += 1.0
            theta[s_] += lr * (GAMMA ** t) * adv * grad
        yield softmax(theta)


def actor_critic(rng, lr_pi=0.5, lr_v=0.2, max_len=200):
    """第 7 讲：一步 Actor-Critic（用 TD 误差作为优势）。"""
    theta, V = np.zeros((N_S, N_A)), np.zeros(N_S)
    while True:
        s, I = S0, 1.0
        for _ in range(max_len):
            a = int(rng.choice(N_A, p=softmax(theta[s])))
            s2, r, done = step(s, a, rng)
            delta = r + (0 if done else GAMMA * V[s2]) - V[s]
            V[s] += lr_v * delta
            grad = -softmax(theta[s])
            grad[a] += 1.0
            theta[s] += lr_pi * I * delta * grad
            I *= GAMMA
            s = s2
            if done:
                break
        yield softmax(theta)


def dyna_q(rng, n_plan=20, alpha=0.2, eps=0.1, max_len=200):
    """第 15 讲：Dyna-Q。随机环境里用计数估计转移概率（而不是只记最近一次的结果）。"""
    Q = np.zeros((N_S, N_A))
    counts = np.zeros((N_S, N_A, N_S))
    r_sum = np.zeros((N_S, N_A, N_S))
    seen = []
    while True:
        s = S0
        for _ in range(max_len):
            a = eps_greedy(Q, s, eps, rng)
            s2, r, done = step(s, a, rng)
            Q[s, a] += alpha * (r + (0 if done else GAMMA * Q[s2].max()) - Q[s, a])
            if counts[s, a].sum() == 0:
                seen.append((s, a))
            counts[s, a, s2] += 1
            r_sum[s, a, s2] += r
            for _ in range(n_plan):
                ps, pa = seen[rng.integers(len(seen))]
                c = counts[ps, pa]
                ps2 = int(rng.choice(N_S, p=c / c.sum()))
                pr = r_sum[ps, pa, ps2] / c[ps2]
                pdone = ps2 in TERMINAL
                Q[ps, pa] += alpha * (pr + (0 if pdone else GAMMA * Q[ps2].max()) - Q[ps, pa])
            s = s2
            if done:
                break
        yield greedy_probs(Q)


def q_bonus(rng, c=0.1, alpha=0.2, max_len=200):
    """第 13 讲：乐观初始化（Q = 1，即可能的最大回报）+ 计数探索奖励 c / sqrt(N)，贪心地行动。"""
    Q = np.ones((N_S, N_A))
    N = np.zeros((N_S, N_A))
    while True:
        s = S0
        for _ in range(max_len):
            a = eps_greedy(Q, s, 0.0, rng)
            s2, r, done = step(s, a, rng)
            N[s, a] += 1
            target = r + c / np.sqrt(N[s, a]) + (0 if done else GAMMA * Q[s2].max())
            Q[s, a] += alpha * (target - Q[s, a])
            s = s2
            if done:
                break
        yield greedy_probs(Q)


def main():
    pi_star, v_star = value_iteration()
    random_value = true_value(np.full((N_S, N_A), 1 / N_A))
    print("=" * 78)
    print("演示 1：问题与\"天花板\"")
    print("=" * 78)
    print("  5×5 格子世界：S = 起点，G = 终点（+1），X = 陷阱（-1），每步 -0.01，10% 概率打滑，γ = 0.95")
    arrows = "↑↓←→"
    for r in range(ROWS):
        row = []
        for c in range(COLS):
            if (r, c) == GOAL:
                row.append("G")
            elif (r, c) == TRAP:
                row.append("X")
            else:
                row.append(arrows[int(pi_star[r * COLS + c].argmax())])
        print("    " + " ".join(row))
    print(f"  上图是价值迭代（第 2 讲，模型已知）算出的最优策略。最优价值 V*(起点) = {v_star:.3f}；")
    print(f"  完全随机的策略价值 = {random_value:.3f}。")

    print("\n" + "=" * 78)
    print("演示 2：交互 N 个回合后，学到的策略的真实价值（10 个随机种子的平均）")
    print("=" * 78)
    algos = [("MC 控制（第 4 讲）", lambda rng: mc_control(rng)),
             ("SARSA（第 4 讲）", lambda rng: td_control(rng, "sarsa")),
             ("Q-learning（第 4 讲）", lambda rng: td_control(rng, "q")),
             ("Double Q-learning（第 4 讲）", lambda rng: td_control(rng, "double")),
             ("REINFORCE + 基线（第 6 讲）", lambda rng: reinforce(rng)),
             ("Actor-Critic（第 7 讲）", lambda rng: actor_critic(rng)),
             ("Q-learning + 计数奖励（第 13 讲）", lambda rng: q_bonus(rng)),
             ("Dyna-Q，n = 20（第 15 讲）", lambda rng: dyna_q(rng))]
    checkpoints = [10, 30, 100, 300, 1000]
    print("   " + pad("算法", 34) + "".join(f"  N={n:<5d}" for n in checkpoints) + "  达到 V* 的 95% 所需回合（中位数）")
    for name, make in algos:
        vals = np.zeros((10, len(checkpoints)))
        reach = []
        for sd in range(10):
            gen = make(np.random.default_rng(sd))
            hit = None
            for ep in range(1, checkpoints[-1] + 1):
                pol = next(gen)
                need = ep in checkpoints or (hit is None and ep % 5 == 0)
                if need:
                    v = true_value(pol)
                    if ep in checkpoints:
                        vals[sd, checkpoints.index(ep)] = v
                    if hit is None and v >= 0.95 * v_star:
                        hit = ep
            reach.append(hit if hit is not None else np.inf)
        med = np.median(reach)
        med_s = f"{med:6.0f}" if np.isfinite(med) else "  >1000"
        print("   " + pad(name, 34) + "".join(f"{x:8.3f}" for x in vals.mean(axis=0)) + f"          {med_s}")
    print(f"   （最优 V* = {v_star:.3f}，95% 即 {0.95 * v_star:.3f}；\"达到\"按每 5 个回合检查一次）")
    print("  观察（超参数都没有仔细调，只有 10 个种子——在别的问题上、换一组超参数，排名可能完全不同）：")
    print("  - Dyna-Q 学得最快：只交互了 10 个回合，策略的价值就已经接近最优。模型把每一条经验反复利用，")
    print("    **样本效率**最高（第 15 讲）。代价是每一步都要做 20 次规划更新——用计算换数据。")
    print("    之后它的价值在 0.50～0.57 之间波动：ε-贪心下 Q 值的噪声让贪心策略时好时坏（与下面 Q-learning 的情况类似）。")
    print("  - TD 方法（SARSA、Q-learning、Double Q）居中。Q-learning 的曲线不单调（第 300 回合反而变差）：")
    print("    检查第 300 回合时的策略发现，10 个种子中有 2 个的贪心策略出现了\"转圈\"——两个格子互相指向对方，")
    print("    永远到不了终点，价值因此变成负的。原因是固定学习率下 Q 值的估计一直带着噪声，")
    print("    个别格子的贪心动作指错了方向。Double Q-learning 取两张 Q 表的平均，估计更平稳（第 4 讲），")
    print("    10 个种子都没有出现这种情况，这里最快达到 95%。")
    print("  - 策略梯度方法（REINFORCE、Actor-Critic）在这个小的表格问题上最慢：它们每次只把策略推动一点点，")
    print("    需要很多回合。它们的优势在别处——连续动作、随机策略、与神经网络和大模型的结合（第 6、7、9、16 讲）。")
    print("    Actor-Critic 用学到的价值做每一步的更新，达到 95% 的回合数中位数比 REINFORCE 少（645 vs 802），")
    print("    但在第 100 回合时反而更低——在这个问题上两者的差别不大。")
    print("  - 乐观 + 计数奖励（第 13 讲）前期表现差：它先系统地把每个 (状态, 动作) 都试一遍，然后才开始利用。")
    print("    在这个容易探索的小问题上，这种系统的探索不是必需的；而在 RiverSwim 那样的问题上，没有它就学不会。")
    print("  - 所有学习算法都在逼近价值迭代算出的那个\"天花板\"——它们的区别在于：需要知道什么（模型？），")
    print("    利用数据的效率，以及每一步花多少计算。**没有在所有问题上都最好的算法。**")


if __name__ == "__main__":
    main()
