1. 场景描述
孙颖莎和伊藤美诚是世界女子乒坛的顶尖运动员。根据国际乒联2022年11月15日公布的世界排名,孙颖莎位列第一,伊藤美诚排名第五。孙颖莎是中国女子乒乓球队的三大主力,曾获得2020年东京奥运会女乒团体赛金牌,2019年亚洲乒乓球锦标赛女单冠军,蝉联2021和2022年WTT世界杯女单冠军;伊藤美诚则是日本女子乒乓球队的绝对主力和王牌,她是2020年东京奥运会乒乓球混合双打冠军,女单季军的获得者。伊藤美诚最辉煌的战绩是在2018年国际乒联瑞典公开赛中,一路过关斩将,连续击败3名中国的世界冠军刘诗雯、丁宁和朱雨玲,一举夺得瑞典公开赛冠军,这个冠军含金量十足。
截止目前孙颖莎和伊藤美诚在国际赛事中共有8次交手,孙颖莎获胜6次,伊藤美诚获胜2次。在孙颖莎的获胜记录中有2次是通过抢赛点获胜,这种获胜方式多少有一些偶然性。当然,乒乓球比赛不仅是技术水平的比拼,而且也是心理层面的较量。总体而言,孙颖莎和伊藤美诚水平和实力极为接近。她们两人同为00后运动员,注定也是一生的对手!
现在我们要通过计算机仿真模拟100场乒乓球比赛,以此分析孙颖莎和伊藤美诚的比赛胜负率,我相信计算机模拟仿真结果对于许多的乒乓球爱好者来说有着浓厚的兴趣。
2. 编程思路
计算机模拟技术广泛应用于航天航空、科学研究、工程技术、军事、教育、体育和娱乐等领域。例如:可以使用计算机仿真模拟技术进行飞机设计、气候预测,为电影创建特效,开发游戏场景,预测体育赛事等,就连当前炙手可热、大名鼎鼎的“元宇宙”也与计算机模拟技术有着千丝万缕的关联。现在还是让我们言归正传,聚焦乒乓球赛事的模拟预测。
2.1 乒乓球的基本规则
理解业务需求是编写程序的第一步,因此我们必须了解乒乓球比赛的基本规则。一场乒乓球比赛可以采用3局2胜制、5局3胜制或者7局4胜制。在一局比赛中,先得11分的一方为胜方,10平后,先多得2分的一方为胜方。一场比赛是由抽签决定选择权。当一方运动员率先选择了先发球,则另一方是接发球,反之亦然。在获得每2分之后,接发球方即成为发球方,依此类推,直至该局比赛结束,或者直至双方比分都达到10分或实行轮换发球法,这时发球和接发次序仍然不变,但每人只轮发一分球。一局中首先发球的一方,在该场比赛的下一局则应首先接发球。
2.2 编程的基本思路
1. 基础数据采集
我们需要一些基础数据,方可进行计算机模拟:
match_nums : 模拟比赛场次数
win_prob1 : 1号选手发球回合的得分概率;
win_prob2 : 2号选手发球回合的得分概率;
2. 判断发球回合胜负
这里我们要用到Python的随机函数random(),这个函数没有输入参数,调用它可以随机产生一个介于(0,1)区间的浮点数,但不包括0和1两个区间端点。从长时间调用random()来看,它产生均匀的分布,这就意味着所有的值都会出现大约相等的次数,理解这一点非常重要。
假设1号选手的发球回合得分概率是0.68,其含义是这个选手应该赢下68%的发球回合。现在我们调用random()生成一个0~1之间的随机数。区间 (0,1) 的68%正好在0.68的左边。所以有68%的时间产生的随机数将小于0.68,而其他32%的时间产生的随机数将大于等于0.68。一般说来,如果变量win_prob代表选手发球回合的得分概率,那么random()<win_prob就成功地表示了正确的概率。可以使用以下语句:
if random() < win_prob1:
score1 += 1 # 选手1得分
else:
score2 += 1 # 选手2得分
3. 厘清比赛、局和回合的关系
在乒乓球运动中,赛制决定了一场比赛包含的局数,每一局包含多个回合,每一个回合的输赢就是1分。因此,我们可以归纳为以下关系:比赛 - 局 - 回合。
在程序编码方面,设计函数game()完成每一局的模拟计分,核心语句是要判断发球回合输赢的计分;函数match()完成一场比赛的模拟和计分,可以循环调用game()函数实现;函数matches()用于模拟多场比赛,最重要的语句是循环调用match()函数即可实现。
4. 判断一局比赛结束
据乒乓球计分规则,在一局比赛中,先得11分的一方为胜方,10平后,先多得2分的一方为胜方。我们通过以下表达式求值,来判断一局比赛是否结束。假设score1代表选手1的得分,score2代表选手2的得分:
def game_end(score1, score2):
"""
功能:判断1局比赛是否结束?
参数:
score1 : 选手1的得分,score2 : 选手2的得分
"""
return (score1 == 11 and score2 < 10) or \
(score2 == 11 and score1 < 10) or \
(score1 >= 10 and score2 >= 10 and abs(score1-score2) >= 2)
如果上述表达式求值为真,则这一局比赛到此结束。
5. 判断一场比赛结束
根据乒乓球比赛的赛制,决定了不同的赛制有不同的结束条件。这里要使用到输入参数:match_type
match_type = 1, 赛制为3局2胜制
match_type = 2, 赛制为5局3胜制
match_type = 3, 赛制为7局4胜制
假设win_game1代表1号选手获胜的局数,win_game2代表2号选手获胜的局数,我们可以使用以下方式判断本场比赛结束条件:
def match_end(match_type, win_game1, win_game2):
"""
功能:判断一场比赛是否结束
参数:
match_type : 乒乓球赛制
win_game1 : 1号选手获胜局数
win_game2 : 2号选手获胜局数
"""
status = False
if match_type == '1': # 3局2胜制
if (win_game1 == 2) or (win_game2 == 2):
status = True
elif match_type == '2': # 5局3胜制
if (win_game1 == 3) or (win_game2 == 3):
status = True
elif match_type == '3': # 7局4胜制
if (win_game1 == 4) or (win_game2 == 4):
status = True
return status
3. 代码实现
本程序由两个模块构成。模块match.py 是模拟乒乓球比赛的公共基础函数,而table_tennis.py是主程序模块。
"""
table_tennis.py : 模拟孙颖莎与伊藤美诚乒乓球比赛
"""
from match import *
def main():
win_prob1, win_prob2, match_nums, match_type = get_data() # ①
win_match1, win_match2, scoreboard = matches(match_nums, match_type, win_prob1, win_prob2) # ②
print_result(win_match1, win_match2) # ③
print_scoreboard(scoreboard) # ④
def get_data():
"""
功能 :读入模拟数据
"""
while True:
try:
win_prob1 = float(input('孙颖莎的发球回合得分概率 (0.00-1.00):'))
win_prob2 = float(input('伊藤美诚发球回合得分概率 (0.00-1.00):'))
match_nums = int(input('模拟乒乓球比赛场次:'))
match_type = input('赛制:1-(3局2胜), 2-(5局3胜), 3-(7局4胜) :')
if (0 < win_prob1 < 1) and (0 < win_prob2 < 1) \
and match_nums > 0 and (match_type in ['1', '2', '3']): # ⑤
break
else:
print('超出取值范围,重新输入!')
except ValueError:
print("数据有误,重试一次!")
return win_prob1, win_prob2, match_nums, match_type # ⑥
def print_result(win_match1, win_match2):
"""
功能 : 打印比赛成绩
参数 :
win_match1 : 选手1赢得的比赛场次数
win_match2 : 选手2赢得的比赛场次数
"""
match_num = win_match1 + win_match2
print('-' * 50)
print('孙颖莎 vs 伊藤美诚')
print('乒乓球模拟比赛:\t'+str(match_num)+' 场')
print('孙颖莎 获胜:\t{0} 场,获胜率:{1:0.1%}'.format(win_match1, win_match1/match_num))
print('伊藤美诚 获胜:\t{0} 场,获胜率:{1:0.1%}'.format(win_match2, win_match2/match_num))
def print_scoreboard(scoreboard):
"""
功能:统计比赛数据
参数:scoreboard 记分牌数据,每场比赛的具体比分
"""
scoreboard.sort(reverse=True)
board_dict = {}
for score in scoreboard: # ⑦
if score in board_dict:
board_dict[score] += 1
else:
board_dict[score] = 1
print('-'*50)
print('每场比分统计数据:')
for score, count in board_dict.items(): # ⑧
print(score + ' ... ' + str(count) + ' 场')
主要代码功能说明如下:
语句①从键盘输入主要的模拟数据,包括两位选手在发球轮回合得分的概率、模拟多少场比赛、以及比赛采用的赛制。
语句②调用函数matches()模拟多场比赛,具体实现功能随后还会详细介绍。
语句③和语句④将打印比赛的模拟数据。
语句⑤在函数get_data()数据输入中,对输入数据进行有效性验证。
语句⑥是get_data()函数是以元组形式返回多个参数。
语句⑦是使用循环方式对计分牌列表的内容进行分类统计,结果是使用字典变量board_dict进行存放。
语句⑧循环打印比赛的统计结果数据,输出数据参见“执行效果”一节的内容。
"""
match.py : 模拟乒乓球比赛的基础函数模块
"""
from random import random
def matches(match_num, match_type, win_prob1, win_prob2):
"""
功能: 模拟多场乒乓球比赛
参数:
match_num : 模拟比赛场次数
match_type : 乒乓球比赛的赛制
win_prob1 : 选手1发球回合得分概率
win_prob2 : 选手2发球回合得分概率
返回值:
win_match1 : 选手1的获胜场数
win_match2 : 选手2的获胜场数
scoreboard : 计分牌列表,用于记录每场比赛具体比分
"""
win_match1 = win_match2 = 0
scoreboard = []
for i in range(match_num):
win_game1, win_game2 = match(match_type, win_prob1, win_prob2) # ①
# 一场比赛结束开始计分
if win_game1 > win_game2: # ②
win_match1 += 1 # 选手1计分
else:
win_match2 += 1 # 选手2计分
scoreboard += [str(win_game1)+':'+str(win_game2)] # ③
return win_match1, win_match2, scoreboard
def match(match_type, win_prob1, win_prob2):
"""
功能: 模拟一场乒乓球比赛
参数:
match_type : 乒乓球比赛的赛制
win_prob1 : 选手1发球回合得分概率
win_prob2 : 选手2发球回合得分概率
返回值:
win_match1 : 选手1的获胜场数
win_match2 : 选手2的获胜场数
"""
win_game1 = win_game2 = 0
game_num = 0
while not match_end(match_type, win_game1, win_game2):
# 每一局轮流率先发球
if game_num % 2 == 0:
serving = '1' # 选手1率先发球
else:
serving = '2' # 选手2率先发球
score1, score2 = game(serving, win_prob1, win_prob2) # ④
if score1 > score2: # ⑤
win_game1 += 1
else:
win_game2 += 1
game_num += 1
return win_game1, win_game2
def game(serving, win_prob1, win_prob2):
"""
功能:模拟一局比赛
参数:
serving : 1-选手1发球,2-选手2发球
win_prob1 : 选手1发球回合得分概率
win_prob2 : 选手2发球回合得分概率
返回值:
score1 : 选手1累计得分
score2 : 选手2累计得分
"""
score1 = score2 = 0
while not game_end(score1, score2):
if serving == '1':
# 选手1发球,选手2接发球
if random() < win_prob1: # ⑥
score1 = score1 + 1
else:
score2 = score2 + 1
# 判断是否交换发球权
if (score1 >= 10) and (score2 >= 10): # ⑦
# 比分10平后,每人轮流发1个球
serving = '2'
else:
# 每人轮流发2个球
if (score1 + score2) % 2 == 0:
serving = '2'
else:
# 选手2发球,选手1接发球
if random() < win_prob2:
score2 = score2 + 1
else:
score1 = score1 + 1
# 判断是否交换发球权
if (score1 >= 10) and (score2 >= 10):
# 比分10平后,每人轮流发1个球
serving = '1'
else:
# 每人轮流发2个球
if (score1 + score2) % 2 == 0:
serving = '1'
return score1, score2
def game_end(score1, score2):
"""
功能 :判断一局比赛是否结束?
参数 :
score1 : 选手1的得分,score2 : 选手2的得分
"""
return (score1 == 11 and score2 < 10) or \
(score2 == 11 and score1 < 10) or \
(score1 >= 10 and score2 >= 10 and abs(score1-score2) >= 2) # ⑧
def match_end(match_type, win_game1, win_game2):
"""
功能:判断一场比赛是否结束
参数:
match_type : 乒乓球赛制
win_game1 : 1号选手获胜局数
win_game2 : 2号选手获胜局数
"""
status = False
if match_type == '1': # 3局2胜制 # ⑨
if (win_game1 == 2) or (win_game2 == 2):
status = True
elif match_type == '2': # 5局3胜制
if (win_game1 == 3) or (win_game2 == 3):
status = True
elif match_type == '3': # 7局4胜制
if (win_game1 == 4) or (win_game2 == 4):
status = True
return status
在模块match.py中,主要由5个函数构成。它们是:
matches() 模拟多场乒乓球比赛,函数内部循环调用函数match()。
match() 模拟一场乒乓球比赛,函数内容循环调用game()。
game() 模拟一局乒乓球比赛。
match_end() 判断一场比赛是否结束。
game_end() 判断一局比赛是否结束。
下面是模块中的主要代码说明:
语句①调用match()模拟一场比赛,返回值是以元组形式的两位选手在一场比赛中获胜的局数。其中元组的第1个元素,代表选手1的获胜局数,元组第二个元素则代表选手2的获胜局数。
语句②根据比较两位选手在一场比赛中的获胜局数,确定谁是本场比赛的获胜者,然后对应累加其比赛获胜的场次数。
语句③将本场比赛的比分添加到计分牌列表当中。
语句④调用game()模拟一局比赛,返回值是以元组形式表示的两位选手在一局比赛中的得分。其中元组的第1个元素,代表选手1得分,元组的第二个元素则代表选手的得分。
语句⑤根据比较两位选手在一局比赛中的得分,确定谁是本局比赛的获胜者,然后对应累加其获胜的局数。
语句⑥根据选手发球回合得分情况,相应累加其本局比赛的得分。
语句⑦对选手轮流发球进行判断。
语句⑧判断本局比赛是否结束。
语句⑨根据不同赛制,判断本场比赛是否结束。
4. 执行效果
D:\cases\乒坛争锋:孙颖莎vs伊藤美诚>python table_tennis.py
孙颖莎的发球回合得分概率 (0.00-1.00):0.75
伊藤美诚发球回合得分概率 (0.00-1.00):0.70
模拟乒乓球比赛场次:100
孙颖莎 vs 伊藤美诚
乒乓球模拟比赛: 100 场
孙颖莎 获胜: 67 场,获胜率:67.0%
伊藤美诚 获胜: 33 场,获胜率:33.0%
每场比分统计数据:
4:3 … 12 场
4:2 … 17 场
4:1 … 27 场
4:0 … 11 场
3:4 … 12 场
2:4 … 11 场
1:4 … 7 场
0:4 … 3 场
D:\cases\乒坛争锋:孙颖莎vs伊藤美诚>
通过计算机模拟100场孙颖莎vs伊藤美诚的乒乓球比赛,结果表明孙颖莎获胜67场,获胜率67%;伊藤美诚获胜33场,获胜率33%。我们可以得出这样的结论:由于发球回合得分概率的小差异(两人相差0.05),致使比赛结果的大差距。需要说明的是,这里的计算机模拟没有考虑比赛经验和心理抗压能力对比赛结果的影响。当然你完全可以把这两个因素纳入到发球回合得分概率综合考虑和体现,如果这样的话,可以使计算机模拟更加真实地反映运动员的竞技水平。
5. 场景扩展
通过对以上案例的学习,你应该对使用计算机模拟乒乓球比赛的设计思路以及编程方法有了深入了解,现在你可以利用计算机模拟网球和羽毛球比赛,分析和预测比赛的结果。当你完成了这项具有一定挑战性的工作时,你完全有资格向你的小伙伴们炫耀你的作品,因为你使用Python解决现实问题的能力又精进了一大步。