"""
=============================================================================
第 1 讲 · 代码 1：智能体-环境交互循环（Agent-Environment Loop）
=============================================================================

对应讲义：lectures/lecture01_intro_to_rl.md 第 3 节（交互循环）、第 4 节（火星车）

-----------------------------------------------------------------------------
【这个文件要解决什么问题？】
-----------------------------------------------------------------------------
强化学习的一切都建立在一个非常简单的循环上：

    智能体看到状态 s_t  →  选择动作 a_t  →  环境返回奖励 r_t 和新状态 s_{t+1}  →  重复

这个文件把这个循环"翻译"成代码，让你亲手摸到下面这些概念：
    - 环境（environment）：MarsRoverEnv 类
    - 智能体（agent）    ：RandomAgent / AlwaysLeftAgent / AlwaysRightAgent 类
    - 回合（episode）    ：run_episode 函数跑完的一整轮交互
    - 历史（history）    ：run_episode 返回的记录列表
    - 评估（evaluation） ：evaluate_agent 函数——"这个智能体平均能拿多少分？"

-----------------------------------------------------------------------------
【整体思路】
-----------------------------------------------------------------------------
1. 先写"世界"：火星车在 7 个格子的直线上，可以尝试向左或向右走，
   动作有一定概率失败（原地不动）。最左边格子奖励 +1，最右边奖励 +10。
2. 再写"智能体"：它们只有一个方法 act(state)，输入状态，输出动作。
   本文件的智能体都**不学习**，只是固定的规则——学习是后面几讲的事。
   我们先把"交互"这个骨架搭好。
3. 用 run_episode 把两者连起来，跑一个完整回合。
4. 用 evaluate_agent 跑很多回合，取平均，比较不同智能体的好坏。

-----------------------------------------------------------------------------
【运行方式】
-----------------------------------------------------------------------------
在终端中进入本文件所在目录，然后运行：

    python 01_agent_environment_loop.py

需要安装 numpy（见 code/lecture01/README.md）。

-----------------------------------------------------------------------------
【注释约定】（全课代码通用）
-----------------------------------------------------------------------------
    【思路】        这一段代码想做什么、为什么这么做
    【参数】        函数的输入，每个参数的类型和合法取值范围
    【返回】        函数的输出
    【边界】        特殊情况、会报错的情况、容易踩的坑
    【Python 小知识】语法讲解，写给编程初学者
    【RL 联系】     这段代码和讲义里哪个概念对应

-----------------------------------------------------------------------------
【下标约定】
-----------------------------------------------------------------------------
讲义里状态写作 s1, s2, ..., s7（从 1 开始数）；
代码里状态是 0, 1, ..., 6（Python 从 0 开始数）。
所以：代码中的状态 i  <==>  讲义中的 s_{i+1}。
"""

# =============================================================================
# 第 0 部分：导入需要的库
# =============================================================================
# 【Python 小知识】
#   import 语句用来"借用"别人写好的代码。
#   `import numpy as np` 的意思是：导入 numpy 这个库，并给它起个短名字 np，
#   之后写 np.xxx 就等于 numpy.xxx。这是整个 Python 科学计算圈的惯例写法。
#
#   numpy 是 Python 里做数值计算（数组、矩阵、随机数）的标准库。
#   本文件主要用它来生成随机数。
import numpy as np


# =============================================================================
# 第 1 部分：动作的常量定义
# =============================================================================
# 【思路】
#   动作在代码里用整数表示：0 表示"尝试向左"，1 表示"尝试向右"。
#   但到处写 0 和 1 很容易混淆（"1 是左还是右来着？"），
#   所以我们给它们起个有意义的名字。全大写的变量名是 Python 的惯例，
#   表示"这是一个常量，请不要修改它"。
TRY_LEFT = 0
TRY_RIGHT = 1

# 一个字典，把动作编号翻译成人类可读的名字，打印时用。
# 【Python 小知识】
#   字典（dict）是"键 → 值"的映射，用花括号 {键: 值, ...} 创建。
#   ACTION_NAMES[0] 会得到 "TryLeft"。
ACTION_NAMES = {TRY_LEFT: "TryLeft", TRY_RIGHT: "TryRight"}


# =============================================================================
# 第 2 部分：环境 —— 火星探测车
# =============================================================================
class MarsRoverEnv:
    """
    火星探测车环境：一条有 7 个格子的直线。

        格子（代码编号）:   0     1     2     3     4     5     6
        格子（讲义编号）:  s1    s2    s3    s4    s5    s6    s7
        奖励           :  +1     0     0     0     0     0   +10
                                          ▲
                                    默认起点（s4）

    【思路】
        这个类扮演"世界"的角色。它对外只提供两个方法（这也是 Gymnasium 等
        主流 RL 库的标准接口，学会了这个你就能看懂大部分 RL 代码）：

            state = env.reset()                       # 开始一个新回合，返回初始状态
            next_state, reward, done = env.step(a)    # 执行动作 a，返回结果

        智能体**看不到**这个类的内部（比如 success_prob 是多少），
        只能通过 reset/step 与之交互——这正是"模型未知"的含义。

    【动力学（世界如何运转）】
        - 选择 TRY_RIGHT：以 success_prob 的概率向右移动一格，
                          以 1 - success_prob 的概率原地不动（轮子打滑）。
        - 选择 TRY_LEFT ：同理，向左。
        - 撞墙：在最左格（0）尝试向左、或在最右格（6）尝试向右，都会原地不动。

    【奖励约定（与讲义 8.1 节 CS234 约定一致）】
        在时刻 t 处于状态 s_t 时执行动作，获得的奖励 r_t = R(s_t)，
        即"奖励取决于你执行动作时所在的格子"。
        例：在格子 6（s7）执行任何动作，都获得 +10。

    【回合何时结束】
        本环境没有"终止状态"，火星车可以一直走下去。
        我们用视界（horizon）max_steps 来限制：走满 max_steps 步回合就结束。
    """

    # -------------------------------------------------------------------------
    # 【Python 小知识】类（class）与 __init__
    #   类是"一类东西的模板"。MarsRoverEnv 是模板，
    #   env = MarsRoverEnv() 会根据模板造出一个具体的环境对象（叫"实例"）。
    #
    #   __init__ 是"构造函数"：创建实例时自动调用，用来设置初始属性。
    #   self 指"这个实例自己"。self.n_states = 7 表示"给这个实例存一个属性"，
    #   之后在类的其他方法里可以通过 self.n_states 读取。
    #
    #   参数里的 success_prob=0.8 叫"默认参数"：调用时不写就用 0.8。
    # -------------------------------------------------------------------------
    def __init__(self, success_prob=0.8, max_steps=10, start_state=3):
        """
        创建一个火星车环境。

        【参数】
            success_prob : float，动作成功的概率，必须在 [0, 1] 之间。
                           1.0 表示完全确定性的世界（动作一定成功）；
                           0.0 表示火星车永远动不了（一个有趣的边界情况）。
            max_steps    : int，每个回合的步数（视界 H），必须是正整数。
            start_state  : int，起始格子，必须在 0 到 6 之间。默认 3（即讲义的 s4）。

        【边界】
            任何参数不合法都会立刻抛出 ValueError，而不是等到运行中才出现奇怪结果。
            "尽早报错"（fail fast）是写可靠代码的好习惯：
            错误离它的源头越近，越容易排查。
        """
        # ---- 固定的环境结构 ----
        self.n_states = 7      # 格子数量
        self.n_actions = 2     # 动作数量（左、右）

        # 每个格子的奖励 R(s)。用 numpy 数组存储，下标就是格子编号。
        # 【Python 小知识】np.array([...]) 把一个 Python 列表变成 numpy 数组。
        #   写成 1.0、10.0（带小数点）是为了让数组是浮点数类型，
        #   避免以后做除法或乘小数时出现整数截断的问题。
        self.rewards = np.array([1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 10.0])

        # ---- 检查参数是否合法 ----
        # 【Python 小知识】
        #   `if not (0.0 <= x <= 1.0):` 是 Python 的"链式比较"，
        #   等价于 `if not (0.0 <= x and x <= 1.0):`。
        #   raise ValueError("...") 会抛出一个错误，程序在这里停止（除非被 try 捕获），
        #   引号里的文字会显示给用户，告诉他哪里错了。
        if not (0.0 <= success_prob <= 1.0):
            raise ValueError(f"success_prob 必须在 [0, 1] 之间，但收到了 {success_prob}")
        # isinstance(x, int) 检查 x 是否为整数类型。
        if not isinstance(max_steps, int) or max_steps <= 0:
            raise ValueError(f"max_steps 必须是正整数，但收到了 {max_steps}")
        if not isinstance(start_state, int) or not (0 <= start_state < self.n_states):
            raise ValueError(
                f"start_state 必须是 0 到 {self.n_states - 1} 之间的整数，但收到了 {start_state}"
            )
        # 【Python 小知识】f"...{变量}..." 叫 f-string（格式化字符串），
        #   花括号里的变量会被替换成它的值。比如 success_prob=1.5 时，
        #   上面的错误信息会显示 "success_prob 必须在 [0, 1] 之间，但收到了 1.5"。

        self.success_prob = success_prob
        self.max_steps = max_steps
        self.start_state = start_state

        # ---- 运行时状态（每个回合会变化的量）----
        # 这些量在 reset() 里才会被真正设置。这里先设为 None（"还没有值"），
        # 这样如果有人忘了调用 reset() 就直接 step()，我们能检测出来。
        self.state = None      # 当前所在格子
        self.t = None          # 当前是第几步
        self.done = True       # 当前回合是否已经结束（初始时视为"已结束"，必须先 reset）

        # 随机数生成器。reset(seed=...) 时会重新创建。
        # 【Python 小知识】np.random.default_rng(seed) 创建一个随机数生成器。
        #   seed（种子）相同，生成的随机数序列就完全相同——这让实验"可复现"。
        #   seed=None 表示用系统时间等不可预测的来源，每次运行结果都不同。
        self.rng = np.random.default_rng()

    def reset(self, seed=None):
        """
        开始一个新回合：把火星车放回起点，计步器清零。

        【参数】
            seed : int 或 None。随机种子。
                   - 传入整数（如 42）：之后的随机结果可复现，便于调试和对比实验。
                   - 不传（None）：沿用当前的随机数生成器，不重新播种。

        【返回】
            int，初始状态（即 start_state）。

        【RL 联系】
            对应讲义中"一个回合（episode）的开始"。
            每个回合都从 reset 开始，到 done=True 结束。
        """
        if seed is not None:
            self.rng = np.random.default_rng(seed)
        self.state = self.start_state
        self.t = 0
        self.done = False
        return self.state

    def step(self, action):
        """
        执行一个动作，让世界前进一步。

        【思路】
            1. 检查动作是否合法、回合是否还在进行中；
            2. 根据"当前"状态给出奖励 r_t = R(s_t)；
            3. 掷骰子决定动作是否成功，算出下一个状态 s_{t+1}；
            4. 计步器加一，检查是否走满 max_steps 步。

        【参数】
            action : int，必须是 TRY_LEFT(0) 或 TRY_RIGHT(1)。

        【返回】
            一个包含三个元素的元组 (next_state, reward, done)：
            - next_state : int，执行动作后所在的格子 s_{t+1}
            - reward     : float，这一步获得的奖励 r_t
            - done       : bool，回合是否结束（True 表示结束）

        【边界】
            - 动作不是 0 或 1 → ValueError
            - 还没 reset 就调用，或回合已结束还继续调用 → RuntimeError
              （真实的 RL 库中，在回合结束后继续 step 是一个常见 bug，
               这里我们明确地报错来防止它。）
        """
        # ---- 第 1 步：合法性检查 ----
        if self.done:
            raise RuntimeError("回合已经结束（或尚未开始），请先调用 reset() 开始新回合")
        # 【Python 小知识】`x not in (a, b)` 检查 x 是否不等于 a 且不等于 b。
        if action not in (TRY_LEFT, TRY_RIGHT):
            raise ValueError(f"动作必须是 {TRY_LEFT}(TryLeft) 或 {TRY_RIGHT}(TryRight)，但收到了 {action}")

        # ---- 第 2 步：奖励取决于当前所在格子 ----
        # 【Python 小知识】self.rewards[self.state] 取出数组中下标为 self.state 的元素。
        #   float(...) 把 numpy 的数字类型转成普通 Python 小数，打印时更干净。
        reward = float(self.rewards[self.state])

        # ---- 第 3 步：掷骰子决定动作是否成功 ----
        # 【Python 小知识】self.rng.random() 返回一个 [0, 1) 之间均匀分布的随机小数。
        #   "它小于 success_prob" 这件事发生的概率恰好是 success_prob。
        #   这是用均匀随机数模拟"以概率 p 发生的事件"的标准技巧。
        move_succeeds = self.rng.random() < self.success_prob

        if move_succeeds:
            # 向左就减 1，向右就加 1
            direction = -1 if action == TRY_LEFT else +1
            # 【Python 小知识】`a if 条件 else b` 叫条件表达式（三元表达式）：
            #   条件为真时取 a，否则取 b。
            next_state = self.state + direction
            # 撞墙处理：不能走出 [0, n_states-1] 的范围。
            # 【Python 小知识】min(max(x, 下限), 上限) 是"把 x 夹在区间内"的常用写法。
            #   例：x=-1 → max(-1,0)=0 → min(0,6)=0；x=7 → max(7,0)=7 → min(7,6)=6。
            next_state = min(max(next_state, 0), self.n_states - 1)
        else:
            # 打滑，原地不动
            next_state = self.state

        # ---- 第 4 步：更新内部状态 ----
        self.state = next_state
        self.t += 1                       # 等价于 self.t = self.t + 1
        self.done = self.t >= self.max_steps

        return next_state, reward, self.done

    def render(self):
        """
        用文字把当前状态"画"出来，方便观察。

        【返回】
            str，一行文字，例如：
                "  .    .    .    R    .    .    .  "
            R 表示火星车（Rover）所在的位置。

        【边界】
            如果还没 reset（self.state 为 None），返回提示文字而不是报错，
            因为"画图"只是辅助功能，不应该让程序崩溃。
        """
        if self.state is None:
            return "(环境尚未 reset)"
        # 【Python 小知识】列表推导式：[表达式 for 变量 in 可迭代对象]
        #   下面这行等价于：
        #       cells = []
        #       for i in range(7):
        #           if i == self.state: cells.append("R")
        #           else:               cells.append(".")
        #   range(n) 生成 0, 1, ..., n-1。
        cells = ["R" if i == self.state else "." for i in range(self.n_states)]
        # "    ".join(列表) 用 4 个空格把列表中的字符串连接成一个字符串。
        return "  " + "    ".join(cells) + "  "


# =============================================================================
# 第 3 部分：智能体
# =============================================================================
# 【思路】
#   所有智能体都遵循同一个"接口"：有一个 act(state) 方法，输入状态，返回动作。
#   这样 run_episode 函数不需要关心具体是哪种智能体，换一个智能体只需要换一个对象。
#   这种"统一接口、可替换实现"的设计在 RL 代码中随处可见。
#
# 【RL 联系】
#   act 方法就是讲义 5.2 节的**策略（policy）** π：
#   - AlwaysLeftAgent / AlwaysRightAgent 是**确定性策略** π(s) = a；
#   - RandomAgent 是**随机策略** π(a|s) = 0.5（对每个动作）。
#   注意：这些智能体都没有"学习"功能——它们的策略是写死的。
#   从第 3 讲开始，我们会让智能体根据经验改进策略。

class RandomAgent:
    """
    随机智能体：每一步都以相同概率随机选择向左或向右。

    【用途】
        随机智能体是 RL 实验中最常用的"基线（baseline）"：
        如果你的算法连随机智能体都比不过，那肯定有 bug。
    """

    def __init__(self, n_actions=2, seed=None):
        """
        【参数】
            n_actions : int，可选动作的数量，必须 >= 1。
            seed      : int 或 None，随机种子，用于复现结果。
        """
        if n_actions < 1:
            raise ValueError(f"n_actions 必须 >= 1，但收到了 {n_actions}")
        self.n_actions = n_actions
        self.rng = np.random.default_rng(seed)

    def act(self, state):
        """
        根据状态选择动作（这里完全忽略状态，纯随机）。

        【参数】
            state : int，当前状态。随机智能体不使用它，但为了和其他智能体
                    保持同样的接口，仍然要接收这个参数。
        【返回】
            int，0 到 n_actions-1 之间的随机整数。
        """
        # 【Python 小知识】rng.integers(low, high) 返回 [low, high) 之间的随机整数，
        #   注意**不包含** high。所以 integers(0, 2) 只会返回 0 或 1。
        return int(self.rng.integers(0, self.n_actions))


class AlwaysLeftAgent:
    """确定性智能体：无论在哪里，永远尝试向左。"""

    def act(self, state):
        return TRY_LEFT


class AlwaysRightAgent:
    """确定性智能体：无论在哪里，永远尝试向右。"""

    def act(self, state):
        return TRY_RIGHT


# =============================================================================
# 第 4 部分：交互循环 —— 把智能体和环境连起来
# =============================================================================
def run_episode(env, agent, seed=None, verbose=False):
    """
    让智能体在环境中跑完一个完整的回合，并记录下全部历史。

    【思路】
        这就是讲义 3.1 节那张交互循环图的代码版：

            state = env.reset()
            while 回合没结束:
                action = agent.act(state)                      # 智能体决策
                next_state, reward, done = env.step(action)    # 世界响应
                记录 (t, state, action, reward, next_state)
                state = next_state

    【参数】
        env     : 环境对象，必须有 reset(seed) 和 step(action) 方法。
        agent   : 智能体对象，必须有 act(state) 方法。
        seed    : int 或 None，传给 env.reset，用来复现这个回合的随机性。
        verbose : bool，为 True 时每一步都打印出来（用于观察），默认 False。

    【返回】
        (history, total_reward) 二元组：
        - history      : list，每个元素是一个元组 (t, state, action, reward, next_state)。
                         这就是讲义 3.4 节的"历史" h_t。
        - total_reward : float，本回合所有奖励之和（未打折）。

    【边界】
        - 如果 env.max_steps 很大且 verbose=True，会打印很多行，注意别刷屏。
        - 本函数假设回合一定会结束（我们的环境有 max_steps 保证这一点）。
          如果你以后写了一个"可能永不结束"的环境，这里的 while 循环会变成死循环，
          那时应当额外加一个最大步数限制作为保险。
    """
    history = []          # 空列表，用来逐步记录每一步发生了什么
    total_reward = 0.0

    state = env.reset(seed=seed)
    done = False

    if verbose:
        print(f"  t={0:2d}  {env.render()}   (起点)")

    # 【Python 小知识】while 条件: 会一直重复执行下面缩进的代码，直到条件为假。
    #   `not done` 在 done 为 False 时为真，所以回合没结束就继续循环。
    while not done:
        action = agent.act(state)
        next_state, reward, done = env.step(action)
        # 【Python 小知识】上面这行叫"元组解包"：step 返回 3 个值，
        #   按顺序分别赋给 next_state、reward、done 三个变量。

        # env.t 在 step 里已经加过 1 了，所以当前这步的时刻是 env.t - 1
        t = env.t - 1
        history.append((t, state, action, reward, next_state))
        total_reward += reward

        if verbose:
            print(
                f"  t={env.t:2d}  {env.render()}   "
                f"在 s{state + 1} 执行 {ACTION_NAMES[action]:<8s} → 奖励 {reward:+5.1f}，到达 s{next_state + 1}"
            )
            # 【Python 小知识】格式说明符：
            #   {x:2d}   整数，占 2 个字符宽
            #   {s:<8s}  字符串，左对齐，占 8 个字符宽
            #   {r:+5.1f} 小数，显示正负号，总宽 5，保留 1 位小数

        state = next_state

    return history, total_reward


def evaluate_agent(env, agent, n_episodes=1000, base_seed=0):
    """
    让智能体跑很多个回合，估计它的"平均表现"。

    【思路】
        单个回合的结果是随机的（运气好坏）。要公平地比较两个智能体，
        必须跑很多回合取平均。这其实就是**蒙特卡洛估计**：
        用样本平均来近似期望值 E[总奖励]。讲义 8.7 节会正式讨论这个方法。

        我们同时计算**标准误差**（standard error）= 标准差 / sqrt(回合数)，
        它衡量"平均值本身的不确定性"：回合数越多，标准误差越小，估计越可靠。
        粗略地说，真实期望大约有 95% 的可能落在 平均值 ± 2×标准误差 之内。

    【参数】
        env        : 环境对象。
        agent      : 智能体对象。
        n_episodes : int，回合数，必须 >= 1。越大估计越准，但越慢。
                     注意：n_episodes = 1 时无法计算标准差（会返回 0）。
        base_seed  : int，第 i 个回合使用种子 base_seed + i，使整个评估可复现，
                     而且不同智能体用同一组种子，比较更公平（"相同的运气"）。

    【返回】
        (mean, std_error) 二元组：
        - mean      : float，平均总奖励
        - std_error : float，平均值的标准误差

    【边界】
        - n_episodes < 1 → ValueError
    """
    if n_episodes < 1:
        raise ValueError(f"n_episodes 必须 >= 1，但收到了 {n_episodes}")

    totals = []
    for i in range(n_episodes):
        # 【Python 小知识】_ 是一个惯用的变量名，表示"这个返回值我不需要"。
        _, total_reward = run_episode(env, agent, seed=base_seed + i)
        totals.append(total_reward)

    totals = np.array(totals)
    mean = float(totals.mean())
    # ddof=1 表示用"样本标准差"（除以 n-1 而不是 n），这是估计总体标准差的无偏做法。
    # 当只有 1 个样本时，n-1=0 无法计算，我们直接返回 0。
    std = float(totals.std(ddof=1)) if n_episodes > 1 else 0.0
    std_error = std / np.sqrt(n_episodes)
    return mean, std_error


# =============================================================================
# 第 5 部分：演示
# =============================================================================
def demo_one_episode():
    """演示 1：观察一个随机智能体的完整回合，理解"交互循环"和"历史"。"""
    print("=" * 78)
    print("演示 1：随机智能体跑一个回合（看清交互循环的每一步）")
    print("=" * 78)
    print("  格子:     s1   s2   s3   s4   s5   s6   s7")
    print("  奖励:     +1    0    0    0    0    0  +10\n")

    env = MarsRoverEnv(success_prob=0.8, max_steps=10, start_state=3)
    agent = RandomAgent(seed=1)
    history, total = run_episode(env, agent, seed=1, verbose=True)

    print(f"\n  本回合总奖励（未打折）= {total}")
    print(
        "  注意奖励约定：r_t = R(s_t)，奖励取决于\"执行动作时所在的格子\"。\n"
        "  所以如果最后一步才到达 s1 或 s7，这次到达本身不会带来奖励（回合已经结束了）。"
    )
    print("\n  记录下来的历史 h（前 3 条），格式为 (t, 状态, 动作, 奖励, 下一状态)：")
    # 【Python 小知识】history[:3] 叫"切片"，取列表的前 3 个元素。
    for (t, s, a, r, s_next) in history[:3]:
        print(f"    ({t}, s{s + 1}, {ACTION_NAMES[a]}, {r}, s{s_next + 1})")
    print("    ...")


def demo_compare_agents():
    """演示 2：比较三种固定策略的平均表现。"""
    print("\n" + "=" * 78)
    print("演示 2：比较三种策略（各跑 2000 个回合取平均）")
    print("=" * 78)

    env = MarsRoverEnv(success_prob=0.8, max_steps=10, start_state=3)
    agents = {
        "随机 (Random)": RandomAgent(seed=0),
        "一直向左 (AlwaysLeft)": AlwaysLeftAgent(),
        "一直向右 (AlwaysRight)": AlwaysRightAgent(),
    }

    # 【Python 小知识】dict.items() 同时遍历字典的键和值。
    #   名字放在行尾打印，是因为中文字符在终端里占两个英文字符的宽度，
    #   放在行首用 {:<26s} 对齐会歪掉；放在行尾就不需要对齐了。
    for name, agent in agents.items():
        mean, se = evaluate_agent(env, agent, n_episodes=2000)
        print(f"  平均总奖励 = {mean:6.2f}  ± {2 * se:.2f}（约 95% 置信区间）  ← {name}")

    print(
        "\n  解读：\n"
        "  - 一直向右最好：虽然前几步奖励都是 0，但到达 s7 后每步都拿 +10。\n"
        "    这就是\"延迟后果\"——前几步的 0 奖励是在为后面的 +10 铺路。\n"
        "  - 一直向左能稳定拿到 s1 的 +1，但总分反而略低于随机策略！\n"
        "    因为随机策略偶尔会游荡到 s7 拿一次 +10，平均下来超过了\"稳定的 +1\"。\n"
        "  - 再看误差范围：随机策略的 ± 明显更大，说明它的结果很不稳定（全靠运气），\n"
        "    而一直向左每次的结果几乎一样。平均值相近的两个策略，风险可以非常不同。\n"
        "  - 注意：智能体事先并不知道 s7 有 +10！如果一个会学习的智能体碰巧先发现了\n"
        "    s1 的 +1 并从此只往左走，它就永远不会发现 s7——这就是\"探索\"问题。"
    )


def demo_boundaries():
    """演示 3：边界情况——故意犯错，看看代码如何保护我们。"""
    print("\n" + "=" * 78)
    print("演示 3：边界情况（故意传入非法输入，观察报错信息）")
    print("=" * 78)

    # 【Python 小知识】try ... except 错误类型 as e:
    #   先尝试执行 try 下面的代码；如果抛出了指定类型的错误，
    #   就跳到 except 下面执行，而不是让整个程序崩溃。e 是错误对象，
    #   打印它可以看到错误信息。这里我们用它来"安全地"展示报错。

    # 情况 1：非法动作
    env = MarsRoverEnv()
    env.reset(seed=0)
    try:
        env.step(2)            # 只有 0 和 1 是合法动作
    except ValueError as e:
        print(f"  [非法动作]       ValueError: {e}")

    # 情况 2：忘记 reset
    env = MarsRoverEnv()
    try:
        env.step(TRY_RIGHT)    # 没有先调用 reset()
    except RuntimeError as e:
        print(f"  [忘记 reset]     RuntimeError: {e}")

    # 情况 3：回合结束后继续 step
    env = MarsRoverEnv(max_steps=1)
    env.reset(seed=0)
    env.step(TRY_RIGHT)        # 走完 1 步，回合结束
    try:
        env.step(TRY_RIGHT)
    except RuntimeError as e:
        print(f"  [回合已结束]     RuntimeError: {e}")

    # 情况 4：非法参数
    try:
        MarsRoverEnv(success_prob=1.5)
    except ValueError as e:
        print(f"  [概率越界]       ValueError: {e}")

    # 情况 5：极端但合法的参数——success_prob=0，火星车永远动不了
    env = MarsRoverEnv(success_prob=0.0, max_steps=5)
    _, total = run_episode(env, AlwaysRightAgent(), seed=0)
    print(f"  [success_prob=0] 合法但极端：火星车寸步难行，5 步总奖励 = {total}（一直停在 s4）")


# 【Python 小知识】if __name__ == "__main__":
#   这行代码的意思是："只有当这个文件被直接运行时（python 01_xxx.py），才执行下面的代码"。
#   如果这个文件被别的文件 import，下面的代码不会执行。
#   这是 Python 脚本的标准写法，让文件既可以当脚本运行，也可以当模块被导入。
if __name__ == "__main__":
    demo_one_episode()
    demo_compare_agents()
    demo_boundaries()
