3.2 混合策略与纳什直觉

纯策略没有鞍点时,双方按概率混着出招——混合策略把 maximin 与 minimax 之间的间隙补上。本节用整数行列式精确解出 2×2 子博弈,再在完整 4×5 矩阵上验证:重击/格挡按 1/3 与 2/3 混、强攻/反击按 1/4 与 3/4 混,博弈值恰为 −4,且任一纯策略单方面偏离都不会更好。

站内《数值平衡与商业化运营》 关心的是数值该调到多少、赛季怎么膨胀、卖多少钱,是一套商业化与运营口径的调参方法;本节关心的是对手也在选时,双方各按什么概率出招才谁都不亏。前者调一个静态的数,后者求一个动态的均衡分布——这是博弈最优口径,与数值/付费无关。混合策略算出来的是概率分布,不是数值曲线。

3.1 minimax 与极小极大 算出这张 4×5 零和矩阵 maximin=−6、minimax=−3、间隙 3、鞍点 0 个。间隙不是误差,是信息:它正是「确定性出招」够不着、只能靠随机化去抢的那段空间。本节把这段空间填上,并证明填完之后双方都没有动机再动。

3.2 混合策略与纳什直觉

混合策略(mixed strategy)指玩家不再固定选一招,而是按一个概率分布随机选。混合的意义在于让对方无法针对:只要你的概率分布里有两招都被对方盯着,对方无论锁定哪一招都会漏掉另一招。

均衡的判据叫无差异(indifference):在均衡里,玩家对自己用到的(概率 > 0)那几招,期望收益必须完全相等——否则他会把概率全压到更高的那一招上;同理,对手对用到的架势也必须让玩家的期望收益相等。加上「没被用到的一招不能比均衡值更好」(否则玩家会切过去),这套条件合起来就是纳什均衡。

极小极大定理(von Neumann)保证:任何有限零和博弈,混合策略下一定存在均衡,而且此时 maximin = minimax——间隙被填平。3.1 里 3 的间隙,混合后归零。

支撑枚举:2×2 的精确解

小矩阵上,混合均衡可以直接解。对一个 2×2 子博弈

玩家 \ 敌人列 0列 1
行 0ab
行 1cd

记 det = a − b − c + d,则玩家选行 0 的概率、敌人选列 0 的概率、博弈值都是有理数,可以只用整数行列式算出来:

量公式含义
玩家行 0 概率 p(d − c) / det让敌人在两列之间无差异
敌人列 0 概率 q(d − b) / det让玩家在两行之间无差异
博弈值 v(a·d − b·c) / det双方都无差异时的期望收益

支撑枚举(support enumeration)就是把这个解法用到每一个 2×2 子博弈上:4 行取 2、5 列取 2,共 C(4,2)·C(5,2) = 6·10 = 60 个子博弈。解出候选后,还要在完整矩阵上验证——候选的玩家混合必须让每一列的期望收益都不低于 v,敌人混合必须让每一行都不高于 v。全部通过,这个 2×2 解才是整局博弈的均衡;否则只是子博弈的局部解,会被别的招戳穿。

S 层:枚举、验证、验纳什

脚本做三件事:枚举 60 个 2×2 子博弈并用整数行列式求解;在完整 4×5 矩阵上验证支撑有效性;对通过的解做纯策略偏离检验(任一单招都不能更好)。熵用整数定点 log2 计算,全程无浮点。

#!/usr/bin/env python3
# file: sim/gd_play_03_nash.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/gd_play_03_nash.py
"""卷二 3.2 混合策略与纳什直觉(S 层):当纯策略没有鞍点时,用混合策略补齐间隙。

3.1 算出 maximin=−6 < minimax=−3(间隙 3),矩阵没有纯策略鞍点。本节的算法是
**支撑枚举(support enumeration)**:枚举每一对行 × 每一对列的 2×2 子博弈,
用整数行列式精确解出混合概率,再**在完整矩阵上验证**——玩家混合必须让每一列都
不低于 v,敌人混合必须让每一行都不高于 v。通过验证的 2×2 解就是整局博弈的
纳什均衡(含零概率的纯策略不受益偏离)。

全整数:概率用分数 p = pn/pd 表示,比较用交叉相乘,禁止浮点。
熵用整数定点 log2(milli-bit):H_milli = L(1000) − (Σ cᵢ·L(cᵢ)) // 1000,
其中 cᵢ 是千分比整数、L(x)=floor(1000·log2 x)。
产出 out/play03_nash.json + out/play03_nash.svg。
"""
from __future__ import annotations

import json
import os
import sys
import xml.etree.ElementTree as ET

import ashlib

HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(HERE)
OUT = os.path.join(ROOT, "out")
CONFIG = os.path.join(ROOT, "config/gd_play_03_matrix.json")


def log2_milli(n: int) -> int:
    """floor(1000·log2(n)),n>=1,纯整数定点(20 位二进制小数),无浮点。"""
    k = n.bit_length() - 1
    p = 20
    x = (n << p) >> k                     # x ≈ (n / 2^k)·2^P ∈ [2^P, 2^(P+1))
    frac = 0
    for i in range(20):
        x = (x * x) >> p                  # 平方 → 逐位取二进制小数位
        if x >= (1 << (p + 1)):
            x >>= 1
            frac |= 1 << (19 - i)
    return k * 1000 + (frac * 1000 >> 20)


def entropy_milli(counts: list[int]) -> int:
    """千分比整数分布(Σ=1000)的 Shannon 熵,单位 milli-bit,纯整数。"""
    pos = [c for c in counts if c > 0]
    return log2_milli(1000) - sum(c * log2_milli(c) for c in pos) // 1000


def payoff_matrix(spec: dict, cfg: dict) -> list[list[int]]:
    stances = cfg["enemyStances"]
    return [[max(0, c["damage"] - s["block"]) - max(0, s["attack"] - c["block"])
             for s in stances] for c in spec["cards"]]


def solve_2x2(a: int, b: int, c: int, d: int):
    """2×2 零和子博弈精确解:返回 (pn,pd, qn,qd, vn,vd) 或 None(退化/越界)。"""
    det = a - b - c + d
    if det == 0:
        return None
    vn, vd = a * d - b * c, det
    pn, pd = d - c, det            # 玩家:行 0 的概率
    qn, qd = d - b, det            # 敌人:列 0 的概率
    if pd < 0:
        pn, pd = -pn, -pd
    if qd < 0:
        qn, qd = -qn, -qd
    if vd < 0:
        vn, vd = -vn, -vd
    if not (0 <= pn <= pd and 0 <= qn <= qd):
        return None
    return pn, pd, qn, qd, vn, vd


def valid_full(m, i1, i2, j1, j2, sol) -> bool:
    """完整矩阵验证支撑有效性:玩家混合处处 ≥ v、敌人混合处处 ≤ v(交叉相乘)。"""
    pn, pd, qn, qd, vn, vd = sol
    for j in range(len(m[0])):                          # 玩家混合 vs 每一列
        if (pn * m[i1][j] + (pd - pn) * m[i2][j]) * vd < vn * pd:
            return False
    for i in range(len(m)):                             # 敌人混合 vs 每一行
        if (qn * m[i][j1] + (qd - qn) * m[i][j2]) * vd > vn * qd:
            return False
    return True


def main() -> int:
    spec = ashlib.load_spec()
    with open(CONFIG, encoding="utf-8") as f:
        cfg = json.load(f)
    stances, actions = cfg["enemyStances"], cfg["playerActions"]
    m = payoff_matrix(spec, cfg)
    nrow, ncol = len(m), len(m[0])
    maximin = max(min(r) for r in m)
    minimax = min(max(m[i][j] for i in range(nrow)) for j in range(ncol))
    print(f"maximin={maximin}  minimax={minimax}  间隙={minimax - maximin}(纯策略无鞍点)")

    print("\n== 支撑枚举:所有 2×2 子博弈的精确解 ==")
    found, checked = [], 0
    for i1 in range(nrow):
        for i2 in range(i1 + 1, nrow):
            for j1 in range(ncol):
                for j2 in range(j1 + 1, ncol):
                    checked += 1
                    sol = solve_2x2(m[i1][j1], m[i1][j2], m[i2][j1], m[i2][j2])
                    if sol and valid_full(m, i1, i2, j1, j2, sol):
                        found.append((i1, i2, j1, j2, sol))
    for i1, i2, j1, j2, sol in found:
        pn, pd, qn, qd, vn, vd = sol
        print(f"行{{{actions[i1]['name']},{actions[i2]['name']}}} × "
              f"列{{{stances[j1]['name']},{stances[j2]['name']}}}: "
              f"玩家={pn}/{pd} 敌人={qn}/{qd} 值={vn}/{vd}")
    print(f"子博弈 {checked} 个,通过全矩阵验证 {len(found)} 个")
    assert found, "至少应有一个 2×2 混合均衡"

    i1, i2, j1, j2, (pn, pd, qn, qd, vn, vd) = found[0]
    assert maximin * vd <= vn <= minimax * vd, "博弈值应落在 [maximin, minimax]"

    pmix = [0] * nrow                                   # 玩家混合千分比(地板 + 余数修正)
    pmix[i1], pmix[i2] = pn * 1000 // pd, (pd - pn) * 1000 // pd
    pmix[i1] += 1000 - sum(pmix)
    qmix = [0] * ncol
    qmix[j1], qmix[j2] = qn * 1000 // qd, (qd - qn) * 1000 // qd
    qmix[j1] += 1000 - sum(qmix)
    vpermille = vn * 1000 // vd

    # 纳什检验(交叉相乘,精确):任一纯策略单方面偏离都不能改善
    p_ok = all((qn * m[i][j1] + (qd - qn) * m[i][j2]) * vd <= vn * qd for i in range(nrow))
    q_ok = all((pn * m[i1][j] + (pd - pn) * m[i2][j]) * vd >= vn * pd for j in range(ncol))

    h_player = entropy_milli(pmix)
    h_enemy = entropy_milli(qmix)
    h_uniform_p = entropy_milli([250, 250, 250, 250])
    h_uniform_q = entropy_milli([200, 200, 200, 200, 200])
    support_p = sum(1 for x in pmix if x > 0)

    plist = ", ".join(f"{actions[i]['name']}={pmix[i]}‰" for i in range(nrow) if pmix[i])
    qlist = ", ".join(f"{stances[j]['name']}={qmix[j]}‰" for j in range(ncol) if qmix[j])
    print("\n== 混合均衡 ==")
    print(f"玩家:{plist}")
    print(f"敌人:{qlist}")
    print(f"博弈值 v = {vn}/{vd} = {vpermille}‰")
    print(f"玩家混合熵={h_player} milli-bit  敌人混合熵={h_enemy} milli-bit")
    print(f"均匀参照:4 牌={h_uniform_p}  5 架势={h_uniform_q}")
    print(f"支撑大小={support_p} / {nrow}  纯策略偏离检验:玩家 {p_ok}、敌人 {q_ok}")
    assert p_ok and q_ok, "混合解必须是纳什均衡"

    W, H = 640, 320
    root = ashlib.svg_begin(W, H)
    ashlib.svg_text(root, 16, 28, "混合均衡:两侧的均衡概率分布(‰)", 15)
    ashlib.svg_text(root, 16, 48,
                    f"博弈值 v={vpermille}‰  支撑={support_p}/{nrow}  玩家熵={h_player} milli-bit",
                    11, "#94a3b8")
    y = 80
    for i in range(nrow):
        ashlib.svg_text(root, 16, y + 16, f"玩家·{actions[i]['name']}", 12)
        ashlib.svg_bar(root, 120, y, 380, 18, pmix[i], 1000, "#38bdf8", f"{pmix[i]}‰")
        y += 30
    y += 16
    for j in range(ncol):
        ashlib.svg_text(root, 16, y + 16, f"敌人·{stances[j]['name']}", 12)
        ashlib.svg_bar(root, 120, y, 380, 18, qmix[j], 1000, "#f472b6", f"{qmix[j]}‰")
        y += 30
    size = ashlib.svg_save(root, os.path.join(OUT, "play03_nash.svg"))

    report = {"id": "gd_play_03_nash", "maximin": maximin, "minimax": minimax,
              "subgamesChecked": checked, "equilibria": len(found),
              "playerMix": pmix, "enemyMix": qmix, "valueNum": vn, "valueDen": vd,
              "valuePermille": vpermille, "playerEntropyMilliBit": h_player,
              "enemyEntropyMilliBit": h_enemy, "uniformPlayer": h_uniform_p,
              "uniformEnemy": h_uniform_q, "support": support_p,
              "nashPlayer": p_ok, "nashEnemy": q_ok}
    os.makedirs(OUT, exist_ok=True)
    with open(os.path.join(OUT, "play03_nash.json"), "w", encoding="utf-8") as f:
        json.dump(report, f, ensure_ascii=False, indent=2, sort_keys=True)
        f.write("\n")
    print(f"svg={size} bytes")
    print("wrote out/play03_nash.json out/play03_nash.svg")
    ET.parse(os.path.join(OUT, "play03_nash.svg"))
    return 0


if __name__ == "__main__":
    sys.exit(main())

本机实跑 /opt/homebrew/bin/python3 sim/gd_play_03_nash.py:

maximin=-6  minimax=-3  间隙=3(纯策略无鞍点)

== 支撑枚举:所有 2×2 子博弈的精确解 ==
行{重击,格挡} × 列{强攻,反击}: 玩家=4/12 敌人=3/12 值=-48/12
子博弈 60 个,通过全矩阵验证 1 个

== 混合均衡 ==
玩家:重击=334‰, 格挡=666‰
敌人:强攻=250‰, 反击=750‰
博弈值 v = -48/12 = -4000‰
玩家混合熵=919 milli-bit  敌人混合熵=812 milli-bit
均匀参照:4 牌=2000  5 架势=2322
支撑大小=2 / 4  纯策略偏离检验:玩家 True、敌人 True
svg=4137 bytes
wrote out/play03_nash.json out/play03_nash.svg

判读:60 个候选里只有 1 个活下来

60 个 2×2 子博弈里,只有 {重击,格挡} × {强攻,反击} 这一个通过了完整矩阵验证。这不是巧合,而是「混合必须同时压得住所有没被用到的招」这条硬约束筛出来的:很多 2×2 在局部有解,但一把别的架势放进来就被戳穿。

解出来的均衡是:

侧用到的一招概率期望收益
玩家重击4/12(≈334‰)对强攻 2×4/12 + (−7)×8/12 = −4
玩家格挡8/12(≈666‰)对反击 (−6)×4/12 + (−3)×8/12 = −4
敌人强攻3/12(=250‰)对重击 2×3/12 + (−6)×9/12 = −4
敌人反击9/12(=750‰)对格挡 (−7)×3/12 + (−3)×9/12 = −4

四行期望收益全是 −4,这正是无差异条件:玩家对重击与格挡无差异,敌人在强攻与反击之间也无差异。博弈值 v = −48/12 = −4,落在 3.1 的间隙 [−6, −3] 正中偏上——混合把安全线从 −6 抬到了 −4,抬升 2 点。

验证这一步是关键,脚本给出两个布尔量:

检验条件结果
玩家不偏离敌人按 (250‰, 750‰) 混时,玩家每一招(含未用的打击、刺击)的期望收益都 ≤ −4True
敌人不偏离玩家按 (334‰, 666‰) 混时,敌人每一式(含未用的稳守、佯攻、蓄力)的期望收益都 ≥ −4True

两个 True 合起来就是「谁单方面动一招都不会更好」——纳什均衡的定义,被整数交叉相乘精确验证,不靠浮点容差。

熵:均衡把策略空间收窄到什么程度

均衡分布有多「分散」可以用 Shannon 熵度量(单位 milli-bit,整数定点计算)。它在本章是代理指标(proxy):熵不是「好玩」,只用于相对比较不同均衡的分散度,不能绝对化。

分布熵(milli-bit)读法
玩家均衡 (334, 666)919两招近似「三七开」,比均匀低
玩家均匀 (250×4)2000四招等概率的上限
敌人均衡 (250, 750)812两式「二八开」
敌人均匀 (200×5)2322五式等概率的上限

均衡熵 919 只有均匀熵 2000 的 459‰:均衡并不鼓励「什么都试」,它把概率压在少数几招上。但注意它没有归零——支撑是 2 而不是 1,意味着均衡本身就是一段「有意义的随机」,这正是下一节要算的账:最优解的代价,一半花在把它算出来,一半花在它把可玩空间压窄。

自检表

检查项合格标准
解为精确分数概率写成 pn/pd 分数,比较用交叉相乘,无浮点
全矩阵验证候选解必须过「每一列 ≥ v、每一行 ≤ v」,不只做局部 2×2
无差异成立用到的招期望收益全相等,表里逐格列出
纳什检验两个布尔量都为 True,未用到的招也不受益偏离
代理指标显式熵标注为代理指标,只做相对比较、禁绝对化

下一节把「代价」这笔账算清:算出最优要搜索多大的树,以及最优一旦落地,策略空间会被压掉多少。

阅读导航:上一节:3.1 minimax 与极小极大 · 下一节:3.3 最优解的代价 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「gamedesign」更多文章

  1. 附录 A 品类参数表、机制距离矩阵与数据集索引
  2. 9.3 自走棋的涌现
  3. 9.2 自走棋的经济与平衡