"""
=============================================================================
第 13 讲 · 代码 2：对决老虎机算法 —— 随机配对、Interleaved Filter、RUCB、Sparring
=============================================================================

对应讲义：lectures/lecture13_dueling_pbo.md 第 3 节

-----------------------------------------------------------------------------
【这个文件要解决什么问题？】
-----------------------------------------------------------------------------
每一步选两个选项让用户比较（例如给用户同时看两个回答，问哪个好），只观察谁赢。
目标：尽快找到最好的选项，同时少让用户看到差的选项（遗憾小）。

    演示 1：BT 模型（8 个选项，有孔多塞赢家）：四种算法的强遗憾、弱遗憾，以及每个选项出现的比例。
    演示 2：孔多塞赢家不是 Borda 赢家（代码 01 的矩阵 2）：各算法最后在玩谁？
    演示 3：没有孔多塞赢家（A、B、C 成环，D 最弱）：各算法的出场分布，和冯·诺依曼混合策略比较。

-----------------------------------------------------------------------------
【运行方式】
-----------------------------------------------------------------------------
    python3 02_dueling_bandits.py        # 约 20 秒

只需要 numpy。依赖本目录的 duel_tools.py。
"""

import numpy as np

from duel_tools import ALGORITHMS, bt_matrix, pad, print_header, run_dueling, von_neumann


def upper_matrix(K, upper):
    """由上三角构造完整的偏好矩阵（和 01 号文件相同）。"""
    P = np.full((K, K), 0.5)
    for (i, j), p in upper.items():
        P[i, j] = p
        P[j, i] = 1 - p
    return P


def worst_case_win_rate(q, P):
    """混合策略 q 对"最难对付的单个选项"的胜率：min_j Σ_i q_i P[i, j]。冯·诺依曼混合策略的这个值 ≥ 0.5。"""
    return float(np.min(q @ P))


def demo_bt(rng, T=20000, runs=10):
    print_header("演示 1：BT 模型，8 个选项")
    P = bt_matrix(np.linspace(1.5, 0.0, 8))
    print(f"真实分数从 1.5 均匀降到 0（相邻两个选项的胜率约 0.55）。选项 0 是孔多塞赢家。每种算法 {runs} 次、每次 {T} 步。\n")
    checks = [1000, 5000, 20000]
    print(f"  {pad('算法', 20)}" + "".join(pad(f"强遗憾 T={c}", 15, "right") for c in checks)
          + pad("弱遗憾 T=2万", 14, "right") + pad("选项 0 出场比例", 16, "right"))
    for algo, name in ALGORITHMS.items():
        out = run_dueling(algo, P, T, runs, rng)
        print(f"  {pad(name, 20)}" + "".join(f"{out['strong'][c - 1]:>15.1f}" for c in checks)
              + f"{out['weak'][-1]:>14.1f}{out['play_counts'][0] / T:>16.3f}")
    print("""
（强遗憾：每一步 (ε(最好, a) + ε(最好, b))/2 的累加 —— 两个出场的选项都要好；
 弱遗憾：每一步 min(ε(最好, a), ε(最好, b)) 的累加 —— 只要有一个好就行。ε(i, j) = P(i 胜 j) − 0.5。）

观察：
  · 随机配对：遗憾线性增长 —— 它在“学”，但从不“用”。这正是传统的离线评测（把所有模型两两比较）的代价。
  · RUCB 的遗憾最小：先挑一个“乐观地看可能是孔多塞赢家”的候选，再挑“最可能打败它”的对手 ——
    把比较集中在“谁才是最好的”这个问题上。到最后，它大部分时间让选项 0 和自己“对决”（遗憾为 0）。
  · Sparring（两个 UCB1 互相陪练）实现最简单，前 1000 步的遗憾最小，后期强遗憾增长得比 RUCB 快：
    两个“选手”都要收敛到最好的选项，总有一部分时间在“陪练”。
  · Interleaved Filter 要把每个对手都以很高的置信度淘汰掉（相邻选项的胜率只有 0.55），前期很慢，后期稳定。
  · 强遗憾和弱遗憾对应不同的应用：同时给用户看两个回答（两个都要好）用强遗憾；
    推荐列表里只要有一个好的就行（用户会选好的那个）用弱遗憾。""")


def demo_borda(rng, T=20000, runs=10):
    print_header("演示 2：孔多塞赢家不是 Borda 赢家")
    P = upper_matrix(5, {(0, 1): 0.55, (0, 2): 0.55, (0, 3): 0.55, (0, 4): 0.55,
                         (1, 2): 0.90, (1, 3): 0.90, (1, 4): 0.90,
                         (2, 3): 0.50, (2, 4): 0.50, (3, 4): 0.50})
    print("代码 01 的矩阵 2：A 对每个人都略胜（55%），B 略输给 A、却大胜其余三个（90%）。")
    print("孔多塞赢家是 A，Borda 赢家是 B。各算法在 2 万步里给每个选项的出场比例：\n")
    names = ["A", "B", "C", "D", "E"]
    print(f"  {pad('算法', 20)}" + "".join(pad(n, 8, "right") for n in names))
    for algo, name in ALGORITHMS.items():
        out = run_dueling(algo, P, T, runs, rng, reference=0)
        print(f"  {pad(name, 20)}" + "".join(f"{c / T:>8.3f}" for c in out["play_counts"]))
    print("""
观察：
  · RUCB、Interleaved Filter 的目标是孔多塞赢家：它们最终集中到 A。
  · Sparring 背后的 UCB1 老虎机，奖励是“对手随机时的平均胜率” —— 这接近 Borda 的定义，
    所以它在 B 上花的时间明显更多。
  · 算法隐含着对“最好”的定义。选算法之前，先想清楚你要找的是哪一种“最好”（代码 01）。""")


def demo_cycle(rng, T=20000, runs=10):
    print_header("演示 3：没有孔多塞赢家（A、B、C 成环，D 最弱）")
    P = upper_matrix(4, {(0, 1): 0.7, (1, 2): 0.7, (0, 2): 0.3, (0, 3): 0.8, (1, 3): 0.8, (2, 3): 0.8})
    names = ["A", "B", "C", "D"]
    vn = von_neumann(P)
    print("代码 01 的矩阵 3。冯·诺依曼混合策略 ≈ " + "、".join(f"{n} {v:.2f}" for n, v in zip(names, vn))
          + f"，它对任何选项的胜率 ≥ {worst_case_win_rate(vn, P):.3f}。")
    print(f"各算法的出场比例（2 万步，{runs} 次平均），以及把出场比例当作一个混合策略时的“最差情况胜率”：\n")
    print(f"  {pad('算法', 20)}" + "".join(pad(n, 8, "right") for n in names) + pad("最差情况胜率", 14, "right"))
    for algo, name in ALGORITHMS.items():
        out = run_dueling(algo, P, T, runs, rng, reference=0)
        q = out["play_counts"] / T
        print(f"  {pad(name, 20)}" + "".join(f"{c:>8.3f}" for c in q) + f"{worst_case_win_rate(q, P):>14.3f}")
    print("""
观察：
  · RUCB 假设存在孔多塞赢家。这里不存在：“乐观地可能是孔多塞赢家”的候选集合常常是空的，它就随机挑，
    于是有约八分之一的出场浪费在明显最弱的 D 上。
  · Interleaved Filter 假设有全序：擂主在 A、B、C 之间换来换去，最后停在谁身上取决于运气。
    10 次平均下来，出场分布仍然不均匀（C 只有约 0.20），偏多的那个就更容易被它的“克星”打败。
  · Sparring 把 A、B、C 各放约三分之一、几乎不放 D，接近冯·诺依曼混合策略：两个互相陪练的选手，
    就像在玩“石头剪刀布”，最后都学会了混合。（严格的保证需要用对抗性的老虎机算法，例如 EXP3，Ailon 等 2014。）
  · 偏好成环时，“找到最好的那一个”这个目标本身就不成立。Copeland 赢家（Zoghi 等 2015 的 CCB 算法）、
    冯·诺依曼赢家（Dudík 等 2015）是两种推广。""")


def main():
    rng = np.random.default_rng(13)
    demo_bt(rng)
    demo_borda(rng)
    demo_cycle(rng)


if __name__ == "__main__":
    main()
