"""
=============================================================================
第 14 讲 · 代码 2：蒙特卡洛树搜索（UCT）——用模拟评估局面，用 UCB 决定往哪里搜
=============================================================================

对应讲义：lectures/lecture14_mcts_alphazero.md 第 3–4 节

-----------------------------------------------------------------------------
【这个文件要解决什么问题？】
-----------------------------------------------------------------------------
上一个文件的有限深度搜索平均地展开每一个分支，而且需要人来设计评估函数。
MCTS 换了两个思路：
    - 评估：不需要评估函数——从局面随机下到底，看胜负（蒙特卡洛）；
    - 搜索：不平均地展开，用 UCB（第 11 讲）把模拟集中在有希望的着法上（UCT）。
算法见 mcts.py。

本文件：
    1. 一个例子：平坦蒙特卡洛会选错，MCTS 能选对——"树"到底带来了什么？
    2. MCTS 的价值估计如何随模拟次数收敛到极小极大的结果
    3. 下得对不对：在关键局面上，MCTS 与平坦蒙特卡洛选到最优着法的比例 vs 模拟次数
    4. 实战：MCTS 与完美对手、随机对手对局
    5. 探索常数 c 的影响

-----------------------------------------------------------------------------
【运行方式】
-----------------------------------------------------------------------------
    cd code/lecture14
    python 02_mcts_uct.py      （约 25 秒）
"""

import numpy as np

from mcts import best_move, flat_monte_carlo, mcts
from tictactoe import (EMPTY, critical_positions, legal_moves, optimal_moves, play, render, solve,
                       to_move, winner)

EXAMPLE = (1, 0, 0,
           0, -1, 0,
           0, 0, 1)        # 轮到 O；O 必须走边（1、3、5、7），走角（2、6）会输


def play_game(x_player, o_player, rng):
    """
    下一整盘棋。x_player、o_player 是函数 f(board, rng) → 着法。
    【返回】赢家：1（X 赢）、-1（O 赢）、0（平局）
    """
    board = EMPTY
    while winner(board) is None:
        mover = x_player if to_move(board) == 1 else o_player
        board = play(board, mover(board, rng))
    return winner(board)


def perfect_player(board, rng):
    """完美的对手：在所有最优着法中随机选一个。"""
    opts = sorted(optimal_moves(board))
    return opts[rng.integers(len(opts))]


def random_player(board, rng):
    moves = legal_moves(board)
    return moves[rng.integers(len(moves))]


def mcts_player(n_sims, c=1.4):
    return lambda board, rng: best_move(mcts(board, n_sims, c, rng), rng)


def main():
    rng = np.random.default_rng(0)

    # ---------------------------------------------------------------------
    print("=" * 78)
    print("演示 1：\"树\"带来了什么？平坦蒙特卡洛 vs MCTS")
    print("=" * 78)
    print("  局面（轮到 O）：")
    print("    " + render(EXAMPLE).replace("\n", "\n    "))
    truth = {m: -solve(play(EXAMPLE, m)) for m in legal_moves(EXAMPLE)}
    print(f"  真实价值（极小极大，从 O 的角度）：{truth}   → 走边（1、3、5、7）和棋，走角（2、6）输")
    for n in [100, 1000, 10000]:
        m, scores = flat_monte_carlo(EXAMPLE, n, rng)
        print(f"  平坦蒙特卡洛（{n:5d} 次模拟）：平均得分 " + "  ".join(f"{k}:{v:+.2f}" for k, v in scores.items())
              + f"   → 选 {m}{'（输棋！）' if truth[m] < 0 else ''}")
    for n in [100, 1000, 10000]:
        root = mcts(EXAMPLE, n, 1.4, rng)
        stats = "  ".join(f"{k}:{ch.N}次/{ch.W / ch.N:+.2f}" for k, ch in sorted(root.children.items()))
        m = best_move(root, rng)
        print(f"  MCTS（{n:5d} 次模拟）：访问次数/平均得分 {stats}   → 选 {m}{'（输棋！）' if truth[m] < 0 else ''}")
    print("  观察：")
    print("  - 平坦蒙特卡洛评估的是\"O 走这步之后，**双方都随机下**\"的胜率。在随机对局中，占角看起来很好——")
    print("    因为随机的 X 很少能找到那步\"双杀\"。所以它系统性地偏爱会输的角，模拟再多也纠正不了。")
    print("  - MCTS 在树里面也用 UCB 为 X 挑选着法：X 那一层很快发现\"双杀\"的应对，")
    print("    O 走角的分支的平均得分随之大幅下降，MCTS 于是把模拟集中到走边的分支上。")
    print("    **树让对手也\"认真下棋\"**——这就是 MCTS 比平坦蒙特卡洛强的根本原因。")

    # ---------------------------------------------------------------------
    print("\n" + "=" * 78)
    print("演示 2：MCTS 的价值估计如何收敛（空棋盘，X 的每个第一步的平均得分；真实值全是 0 = 和棋）")
    print("=" * 78)
    print("   模拟次数    中心(4)    角(0)     边(1)     根节点的平均得分")
    for n in [100, 1000, 10000, 100000]:
        root = mcts(EMPTY, n, 1.4, rng)
        q = {m: ch.W / ch.N for m, ch in root.children.items()}
        root_q = sum(ch.W for ch in root.children.values()) / root.N
        print(f"   {n:8d}    {q[4]:+.3f}    {q[0]:+.3f}    {q[1]:+.3f}        {root_q:+.3f}")
    print("  观察：")
    print("  - 模拟少时，估计值反映的是\"接近随机对局\"的胜率——先手在随机对局中占很大优势（得分明显为正）。")
    print("  - 模拟越多，树越深，双方在树里都越来越\"认真\"，估计值逐渐向极小极大的真实值 0（和棋）靠拢。")
    print("  - 收敛是**慢**的：平均得分是整棵子树所有模拟的平均，早期那些\"随便下\"的模拟一直留在平均值里。")
    print("    （但选着法看的是访问次数，而访问次数会很快集中到好棋上——下得对，比估得准来得早。）")

    # ---------------------------------------------------------------------
    print("\n" + "=" * 78)
    print("演示 3：下得对不对？在 1000 个关键局面上选到最优着法的比例")
    print("=" * 78)
    crit = critical_positions()
    idx = np.random.default_rng(1).choice(len(crit), 1000, replace=False)
    positions = [crit[i] for i in idx]
    print("   模拟次数    MCTS（UCT，c = 1.4）    平坦蒙特卡洛")
    for n in [10, 30, 100, 300, 1000, 3000]:
        ok_uct = np.mean([best_move(mcts(b, n, 1.4, rng), rng) in optimal_moves(b) for b in positions])
        ok_flat = np.mean([flat_monte_carlo(b, n, rng)[0] in optimal_moves(b) for b in positions])
        print(f"   {n:8d}         {ok_uct * 100:5.1f}%                {ok_flat * 100:5.1f}%")
    print("  观察：")
    print("  - 模拟很少时，两者差不多（MCTS 的树还没长起来，本质上也在做平坦的随机评估）。")
    print("  - 模拟多了，MCTS 趋近 100%，平坦蒙特卡洛停在 100% 以下：它的错误是**系统性的偏差**")
    print("    （假设对手随机下），而不是随机误差，模拟再多也消除不了——演示 1 就是一个例子。")

    # ---------------------------------------------------------------------
    print("\n" + "=" * 78)
    print("演示 4：实战（MCTS 执 X 和执 O 各下 100 盘）")
    print("=" * 78)
    print("   MCTS 模拟次数    对手      MCTS 胜   和棋   MCTS 负")
    for n in [10, 100, 1000]:
        for opp_name, opp in [("完美对手", perfect_player), ("随机对手", random_player)]:
            w = d = l = 0
            for g in range(200):
                if g % 2 == 0:
                    r = play_game(mcts_player(n), opp, rng)
                    me = 1
                else:
                    r = play_game(opp, mcts_player(n), rng)
                    me = -1
                w += r == me
                d += r == 0
                l += r == -me
            print(f"   {n:8d}        {opp_name}    {w:4d}    {d:4d}    {l:4d}")
    print("  观察：")
    print("  - 对完美对手：井字棋双方完美下棋是和棋，所以\"不输\"就是满分。模拟次数越多，输得越少。")
    print("  - 对随机对手：MCTS 能抓住对手的失误赢棋；模拟越多，赢得越多、输得越少。")

    # ---------------------------------------------------------------------
    print("\n" + "=" * 78)
    print("演示 5：探索常数 c 的影响（每步 100 次模拟，1000 个关键局面）")
    print("=" * 78)
    print("       c      选到最优着法的比例")
    for c in [0.0, 0.25, 0.5, 1.0, 1.4, 4.0, 10.0, 30.0, 100.0]:
        ok = np.mean([best_move(mcts(b, 100, c, rng), rng) in optimal_moves(b) for b in positions])
        print(f"   {c:6.2f}          {ok * 100:5.1f}%")
    print("  观察：")
    print("  - c = 0：树策略是纯贪心，某个着法早期的一两次随机走子运气好，就会被一直选下去——和第 11 讲贪心\"卡住\"一样。")
    print("  - c 在 0.5～4 之间效果都差不多：在井字棋上，UCT 对 c 并不敏感（差异在 1～2 个百分点内，接近随机波动）。")
    print("  - c 很大（30、100）时明显变差：探索项压倒了平均得分，模拟被几乎平均地分给所有着法，树长不深；")
    print("    而且最后按\"访问次数最多\"选着法——访问次数几乎一样时，这个选择就接近随机了。")
    print("  - 理论分析（Kocsis & Szepesvári, 2006）给出的 UCT 保证需要足够的探索，实际中 c 常常需要针对问题调整。")


if __name__ == "__main__":
    main()
