9.1.3 编程案例:乒乓球比赛模拟

众所周知,中国乒乓球项目的技术水平世界第一,以至于所有比赛的冠军几乎都由中国球员包办。为了增强乒乓球运动的吸引力,提高其他国家的人对这项运动的兴趣,国际乒联 想了很多办法来削弱中国球员的绝对优势,例如扩大乒乓球的直径、禁用某些种类的球拍、 改变赛制等等。在本节中,我们将编写程序来模拟乒乓球比赛,以便研究一项针对中国球员 的规则改革是否真的有效。这项改革是:从 2001 年 9 月 1 日起,乒乓球比赛的每一局比分从 21 分改为 11 分。

球员技术水平的表示 乒乓球是两个球员之间的比赛,比赛开始后由一个球员发球,另一个球员将球接回来,然后两人交替击球,直至一方没能将球回到对方台上,这时另一方就得一分。一个球员有几 次发球机会,用完这些发球机会后将换发球。

比赛胜负由球员的技术水平决定,我们用两个球员对阵时各自的得分概率来表示他们的 技术水平。如果球员 A 与 B 水平相当,则 A 拿下 1 分的概率是 50%,B 拿下 1 分的概率也 是 50%;如果 A 水平较高,拿下 1 分的概率是 55%,则 B 拿下 1 分的概率就只有 45%了。 顺便指出,球员技术水平的表示方法并无一定之规,是由编程者自己主观确定的,关键 是表示方法要比较符合实际。我们这里采用的得分概率表示方法很简单,但没有考虑球员作 为发球方和接发球方的区别。读者可以考虑其他的表示方法,如:将球员的世界排名换算成 获胜概率,或者用球员作为发球方时的得分概率,或者用综合考虑发球得分概率和接发球得分概率的某个概率计算公式,等等。

模拟一回合比赛与得分

设 A、B 两球员比赛时,各自得分的概率为 prob 和 1-prob。利用蒙特卡洛方法,下面 的代码即模拟了得到 1 分的一回合比赛,这是整个模拟程序的核心功能。

if random() < prob: 
    pointA = pointA + 1
else:
    pointB = pointB + 1

我们可以立刻来测试这个核心功能。假设 A 的得分概率是 0.55,让 A、B 进行 10000分的较量,看看各自得分情况如何。测试代码如下:

>>> from random import random
>>> pointA = pointB = 0
>>> for i in range(10000): if random() &lt; 0.55:
pointA = pointA + 1 else:
pointB = pointB + 1
>>> print pointA,pointB 5430 4570

最后得分差不多就是 55%比 45%,可见模拟比赛的结果确实反映了 A、B 双方的实力。

模拟一局比赛

乒乓球比赛不是按比赛回合来判定胜负的,而是采用将若干回合组成一局的方式,以局 为单位来判定胜负。老规则采用每局 21 分制,新规则采用每局 11 分制。我们利用上述模拟一回合比赛及得分的代码,改成以局为单位进行比赛(假设采用 21 分制)。

>>> def oneGame():
pointA = pointB = 0
while pointA != 21 and pointB != 21: if random() &lt; 0.55:
pointA = pointA + 1 else:
pointB = pointB + 1 return pointA, pointB

函数 oneGame 模拟了 21 分制的一局比赛:只要还没人达到 21 分,就继续进行回合较量;

否则退出循环,并返回本句中 A 和 B 各自的得分 pointA 和 pointB。调用 oneGame 的人可以 比较这两个返回值的大小,以判断是谁赢了这一局。

下面我们来测试 oneGame 函数,让两个球员进行 1000 局较量,看看胜负如何。

>>> gameA = gameB = 0
>>> for i in range(1000):
        pointA, pointB = oneGame() 
        if pointA > pointB:
            gameA = gameA + 1 
        else:
            gameB = gameB + 1
>>> print gameA,gameB 
751 249

出人意料的是,虽然 A、B 在每一回合的获胜概率相差不大(55%比 45%),但如果按每局21 分制进行比赛的话,A 的胜局数遥遥领先于 B(75%比 25%)!这里面的道理是显然的, 每回合的胜负偶然性对 21 分一局的胜负来说影响减小了,得分能力稍强的人更加可能赢得一局。可以想象,如果将一局的得分减少,就像国际乒联所做得那样改成每局 11 分,那么 每回合的胜负偶然性对一局的胜负就会有较大影响。稍后我们将在程序中验证这一点。

模拟一场比赛

一场乒乓球比赛也不是无限制地打很多局才能定胜负,一般都是采取 3 局 2 胜、5 局 3 胜或 7 局 4 胜的方式来完成比赛。下面我们采用 21 分制、3 局 2 胜的赛制,来编写模拟一 场比赛的程序 oneMatch,并通过模拟 100 场比赛来测试 oneMatch。

>>> def oneMatch():
        gameOver = [(3,0),(0,3),(3,1),(1,3),(3,2),(2,3)]
        gameA = gameB = 0
        while not (gameA,gameB) in gameOver: 
            pointA, pointB = oneGame()
            if pointA > pointB: 
                gameA = gameA + 1
            else:
                gameB = gameB + 1 
        return gameA, gameB
>>> matchA = matchB = 0
>>> for i in range(100):
        gameA, gameB = oneMatch() 
        if gameA > gameB:
            matchA = matchA + 1
        else:
            matchB = matchB + 1
>>> print matchA,matchB 
89 11

可见按 3 局 2 胜来计算胜负,导致 A 和 B 的胜负更加悬殊了(89%比 11%),这是因为 3 局 2 胜的规则将每一局胜负偶然性的影响削弱了。

完整程序

通过以上设计过程,我们的模拟乒乓球比赛的程序越来越完善了。接下去我们可以进一 步改善程序的功能,例如将球员的技术水平改成由用户输入而不是固定的 0.55,允许采取不 同的比赛规则(21 分或 11 分),增加对比赛结果的分析,等等。这些新增特性就不详细解 释了,请读者自行阅读下面的完整程序代码。

【程序 9.2】pingpong.py

from random import random
def getInputs():
    p = input("Player A's winning prob: ")
    n = input("How many matches to simulate? ") 
    return p, n
def simNMatches(n,prob,rule): matchA = matchB = 0
    for i in range(n):
    gameA, gameB = oneMatch(prob,rule) 
    if gameA &gt; gameB:
        matchA = matchA + 1 
    else:
        matchB = matchB + 1 
    return matchA, matchB
def oneMatch(prob,rule):
    gameOver = [(3,0),(0,3),(3,1),(1,3),(3,2),(2,3)]
    gameA = gameB = 0
    while not (gameA,gameB) in gameOver: 
        pointA, pointB = oneGame(prob,rule) 
        if pointA > pointB:
            gameA = gameA + 1 
        else:
            gameB = gameB + 1 
        return gameA, gameB
def oneGame(prob,rule): 
    pointA = pointB = 0
    while not gameOver(pointA,pointB,rule): 
        if random() < prob:
            pointA = pointA + 1 
        else:
            pointB = pointB + 1 
    return pointA, pointB
def gameOver(a,b,rule):
    return (a&gt;=rule or b&gt;=rule) and abs(a-b)&gt;=2
def printSummary(a,b): n = float(a + b)
    print "Wins for A: %d (%0.1f%%)" % (a, a/n*100) 
    print "Wins for B: %d (%0.1f%%)" % (b, b/n*100)
def main():
    p, n = getInputs()
    matchA, matchB = simNMatches(n,p,21)
    print "\nRule: 21 points, best of 3 games." printSummary(matchA,matchB)
    matchA, matchB = simNMatches(n,p,11)
    print "\nRule: 11 points, best of 3 games." printSummary(matchA,matchB)
main()

本程序的核心代码在前面介绍了,其他如 getInputs 和 printSummary 的功能都是显然的, 只有判断一局比赛结束的 gameOver 中有个条件 abs(a-b)>=2 需要说明一下。乒乓球比赛规 则规定,赢得一局比赛的球员至少要比对手多得 2 分,即 20:20(或 10:10)之后,一定要 连得 2 分才能赢得此局。

下面是本程序的一次运行结果:

Player A's winning prob: 0.52
How many matches to simulate? 100
Rule: 21 points, best of 3 games. Wins for A: 74 (74.0%)
Wins for B: 26 (26.0%)
Rule: 11 points, best of 3 games. Wins for A: 68 (68.0%)
Wins for B: 32 (32.0%)

结果表明,假设球员 A 对球员 B 的得分概率为 52%,当采用每局 21 分、3 局 2 胜的规 则时,A 有 74%的机会赢得比赛;当采用每局 11 分、3 局 2 胜的规则时,A 的获胜概率降到了 68%。这说明,国际乒联对规则的修改确实能削弱强手的优势程度。不过即便如此, 强手获胜的概率还是相当高,何况中国球员的得分概率恐怕远不止 52%。或许要将每局分 数再减少点,或者用其他方法,才能增加外国选手获胜机会吧:-)。

读者可以试着修改程序 9.2,比如采取发球方得分概率作为技术水平的表示,并且将发 球、换发球等因素添加到模拟程序中。