"""
=============================================================================
第 13 讲 · 代码 1："最好的选项"是谁？—— 孔多塞、Borda、Copeland、冯·诺依曼
=============================================================================

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

-----------------------------------------------------------------------------
【这个文件要解决什么问题？】
-----------------------------------------------------------------------------
只有两两比较时，"最好"不是一个显然的概念。这个文件在四个偏好矩阵上比较四种定义：

    矩阵 1：BT 模型（有一个分数）—— 四种定义一致。
    矩阵 2：有孔多塞赢家，但它不是 Borda 赢家 —— "人人都略输给 A，但 B 大胜其他所有人"。
    矩阵 3：A、B、C 成环，D 最弱 —— 没有孔多塞赢家，Copeland 赢家并列，冯·诺依曼赢家是混合策略。
    矩阵 4：第 9 讲的不对称环 —— Borda 赢家会被"克星"打败。

-----------------------------------------------------------------------------
【运行方式】
-----------------------------------------------------------------------------
    python3 01_winners.py        # 约 1 秒

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

import numpy as np

from duel_tools import (borda_scores, bt_matrix, condorcet_winner, copeland_scores, pad, print_header,
                        von_neumann)


def matrix_from_upper(K, upper):
    """由上三角 P[i, j]（i < j）构造完整的偏好矩阵：P[j, i] = 1 − P[i, j]，对角线 0.5。"""
    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 show(P, title, names):
    print_header(title)
    K = len(P)
    print("  偏好矩阵 P[行 胜 列]：")
    print("        " + "".join(f"{n:>7s}" for n in names))
    for i in range(K):
        print(f"    {names[i]:>3s} " + "".join(f"{P[i, j]:>7.2f}" if i != j else f"{'—':>7s}" for j in range(K)))
    cw = condorcet_winner(P)
    bd = borda_scores(P)
    cp = copeland_scores(P)
    vn = von_neumann(P)
    print(f"\n  {pad('选项', 6)} {pad('Borda 得分', 11, 'right')} {pad('Copeland 得分', 14, 'right')} "
          f"{pad('冯·诺依曼混合策略', 18, 'right')} {pad('它对冯·诺依曼策略的胜率', 24, 'right')}")
    for i in range(K):
        print(f"  {pad(names[i], 6)} {bd[i]:>11.3f} {cp[i]:>14.0f} {vn[i]:>18.3f} {P[i] @ vn:>24.3f}")
    copeland = [names[i] for i in np.flatnonzero(cp == cp.max())]
    borda = [names[i] for i in np.flatnonzero(bd > bd.max() - 1e-9)]          # 并列的都列出来
    print(f"\n  孔多塞赢家：{names[cw] if cw is not None else '不存在'}；Borda 赢家：{'、'.join(borda)}；"
          f"Copeland 赢家：{'、'.join(copeland)}")
    print("  （冯·诺依曼混合策略用乘法权重迭代近似求出，不在支撑上的选项分到的零点几的概率是近似误差。）")


def main():
    names5 = ["A", "B", "C", "D", "E"]
    show(bt_matrix([1.2, 0.8, 0.4, 0.0, -0.4]), "矩阵 1：BT 模型", names5)
    print("""
观察：BT 模型下所有选项能用一个分数排序，四种定义一致：A 是孔多塞赢家、Borda 赢家、Copeland 赢家，
冯·诺依曼混合策略几乎全部放在 A 上。第 5~9 讲的 BT 世界里，“最好”没有歧义。""")

    P2 = matrix_from_upper(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})
    show(P2, "矩阵 2：孔多塞赢家 ≠ Borda 赢家", names5)
    print("""
观察：A 对每个人都只是略胜（55%），B 略输给 A，却大胜其他所有人（90%）。
  · 孔多塞、Copeland、冯·诺依曼：A —— 一对一谁也赢不了它。
  · Borda：B —— 平均胜率最高。
  · 哪个“对”？如果用户每次只看一个回答、和“随机的另一个回答”比，Borda 更合适；如果关心“和任何对手正面交锋都不输”，
    孔多塞更合适。第 9 讲：BT / DPO 在混合人群的数据上学到的排序是 Borda 排序；
    这里 A 和 B 都不是“明显错误”的答案，选择哪种定义是价值判断（第 16 讲）。""")

    names4 = ["A", "B", "C", "D"]
    P3 = matrix_from_upper(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})
    show(P3, "矩阵 3：A、B、C 成环（A 胜 B、B 胜 C、C 胜 A），D 最弱", names4)
    print("""
观察：
  · 没有孔多塞赢家；A、B、C 各赢两个对手，Copeland 并列；Borda 也并列。
  · 冯·诺依曼混合策略：A、B、C 各约 1/3，D 不放。它对任何一个选项的胜率都 ≥ 0.5 —— 这就是“最好”在成环时的含义。
  · 对决老虎机算法如果假设存在孔多塞赢家（RUCB、Interleaved Filter），在这里会一直在 A、B、C 之间打转（代码 02 演示 3）。""")

    P4 = matrix_from_upper(3, {(0, 1): 0.9, (1, 2): 0.6, (0, 2): 0.4})
    show(P4, "矩阵 4：第 9 讲的不对称环", ["A", "B", "C"])
    print("""
观察：
  · Borda 赢家是 A（它大胜 B），可 A 对 C 只有 40% 的胜率 —— 一个总是选 A 的策略会被“总是选 C”打败。
  · 冯·诺依曼混合策略约为 A 1/6、B 1/6、C 2/3（第 9 讲推导过的封闭解），对任何选项的胜率都是 0.5。
  · 现实中，人群的偏好经常出现这种“相互克制”：不同的用户群体看重不同的东西（第 9 讲隐藏的情境）。""")


if __name__ == "__main__":
    main()
