介绍
蒙提霍尔问题(Monty Hall problem)是一个基于游戏节目场景的著名概率谜题。在游戏中,一名参赛者面对三扇门。其中一扇门后面是奖品(比如一辆汽车),而另外两扇门后面藏着山羊。参赛者选择一扇门。主持人知道奖品在哪里,然后打开另外两扇门中的一扇,露出一只山羊。然后参赛者可以选择是坚持原来的选择还是切换到另一扇未打开的门。问题是:最佳策略是什么,切换还是坚持?本项目将指导你使用 Python 中的 Tkinter 库构建一个 GUI 应用程序来模拟蒙提霍尔问题。
👀 预览

🎯 任务
在本项目中,你将学习:
- 如何使用 Tkinter 设计和开发图形用户界面(GUI)。
- 如何模拟蒙提霍尔问题以理解其概率结果。
- 如何在 Python 中实现游戏逻辑以处理用户选择并揭示结果。
- 如何使用 Python 的随机库在其中一扇门后面随机分配奖品。
- 如何重置游戏状态以允许进行多轮游戏而无需重新启动应用程序。
🏆 成果
完成本项目后,你将能够:
- 应用 GUI 设计原则并使用 Tkinter 在 Python 中实现它们。
- 理解概率和统计在游戏模拟中的实际应用。
- 在 GUI 应用程序中实现事件驱动编程并处理用户交互。
- 利用高级 Python 编程技术,如 lambda 函数和列表推导式。
- 认识到用户体验(UX)在游戏设计中的重要性,并使用消息框提供反馈机制。
创建项目文件
首先,在 ~/project 中创建一个名为 monty_hall_gui.py 的 Python 文件。然后在文本编辑器或集成开发环境(IDE)中打开它。
cd ~/project
touch monty_hall_gui.py
导入必要的库
首先,我们将导入所需的模块。
import tkinter as tk
from tkinter import messagebox
import random
from typing import List, Optional
在上述代码中,我们正在导入所需的 Python 库。tkinter 用于创建图形用户界面(GUI),messagebox 用于弹出警报,而 random 用于实现随机化的游戏逻辑。
初始化 MontyHallSimulation 类
现在,我们将创建主类来管理我们的模拟。
class MontyHallSimulation:
def __init__(self, root: tk.Tk) -> None:
"""
初始化蒙提霍尔模拟。
:param root: 主 tkinter 窗口
"""
## 窗口配置
self.root = root
self.root.title("蒙提霍尔问题模拟")
self.root.geometry("1200x800")
在上述说明中,我们开始定义用于模拟的主类。
设置变量和用户界面组件
在类中,初始化必要的变量和用户界面元素。
## 用于跟踪用户选择的门、展示的门和获胜门的变量
self.selected_door: Optional[int] = None
self.show_door: Optional[int] = None
self.winning_door: int = random.randint(1, 3)
## 用于根据改变或坚持的决定跟踪获胜次数的变量
self.wins_change: int = 0
self.wins_stay: int = 0
## 在顶部显示的说明
self.label = tk.Label(root, text="选择一扇门!", font=("Arial", 16))
self.label.pack(pady=5)
## 创建门按钮的框架
self.doors_frame = tk.Frame(root)
self.doors_frame.pack(pady=5)
上述说明指导你初始化各种游戏变量,并为我们的模拟设置主要的用户界面元素。
设置门的图像和按钮
加载门的图像并配置门按钮,以允许用户进行交互。
## 加载门的图像
self.door_imgs: List[tk.PhotoImage] = [
tk.PhotoImage(file="door_closed.png"),
tk.PhotoImage(file="door_opened_empty.png"),
tk.PhotoImage(file="door_opened_prize.png")
]
## 创建带有图像的门按钮并附加选择门的函数
self.door_buttons: List[tk.Button] = [
tk.Button(self.doors_frame, image=self.door_imgs[0], command=lambda: self.select_door(1)),
tk.Button(self.doors_frame, image=self.door_imgs[0], command=lambda: self.select_door(2)),
tk.Button(self.doors_frame, image=self.door_imgs[0], command=lambda: self.select_door(3))
]
for button in self.door_buttons:
button.pack(side=tk.LEFT, padx=5)
## 用于选择是否换门的按钮
self.yes_button = tk.Button(self.root, text="是", command=self.switch_door)
self.yes_button.pack_forget()
self.no_button = tk.Button(self.root, text="否", command=self.stay_door)
self.no_button.pack_forget()
## 显示结果
self.results_label = tk.Label(root, text="", font=("Arial", 14))
self.results_label.pack(pady=5)
在上述代码中,我们正在加载代表不同门状态的门图像。然后,我们创建并配置三个代表门的按钮,允许用户做出选择。
实现门的选择逻辑
首先,我们来处理选择一扇门的逻辑。
def select_door(self, door: int) -> None:
"""
处理选择一扇门的事件。
:param door: 所选门的编号
"""
## 确保不会重新选择门
if self.selected_door:
return
self.selected_door = door
## 确定哪些门没有被选中
non_selected_doors = [d for d in [1, 2, 3] if d!= self.selected_door]
## 决定展示哪扇门
if self.selected_door == self.winning_door:
show_door = random.choice(non_selected_doors)
else:
non_selected_doors.remove(self.winning_door)
show_door = non_selected_doors[0]
self.show_door = show_door
self.door_buttons[show_door - 1].config(image=self.door_imgs[1])
## 提示用户决定是否换门
self.label.config(text="你想换门吗?")
## 显示“是”和“否”按钮
self.yes_button.pack(side=tk.LEFT, padx=10, pady=20)
self.no_button.pack(side=tk.RIGHT, padx=10, pady=20)
在上述代码中,我们定义了select_door方法,当玩家选择一扇门时将使用该方法。该方法会记住玩家的选择,并展示一扇没有奖品的门。
处理门的切换逻辑
接下来,我们深入研究交换门选择的逻辑。
def switch_door(self) -> None:
"""处理用户决定换门的事件。"""
## 找到未被选中且未被展示的门
remaining_doors = [d for d in [1, 2, 3] if d!= self.selected_door and d!= self.show_door]
new_door = remaining_doors[0]
## 检查是否获胜
if new_door == self.winning_door:
self.wins_change += 1
self.show_win(True)
else:
self.show_win(False)
在这里,switch_door函数管理着玩家在主持人展示了一扇未选中门后的山羊后选择换门时所发生的事情。
解决门的保留逻辑
现在,让我们来处理保留初始门选择的逻辑。
def stay_door(self) -> None:
"""处理用户决定保留初始门选择的事件。"""
## 检查是否获胜
if self.selected_door == self.winning_door:
self.wins_stay += 1
self.show_win(True)
else:
self.show_win(False)
在这段代码中,stay_door函数监督玩家决定保留其初始门选择时的结果。
说明输赢情况
随后,展示玩家是否赢得了游戏。
def show_win(self, did_win: bool) -> None:
"""
在展示所有门之后显示结果。
:param did_win: 布尔值,表示用户是否获胜
"""
## 根据结果更新门的图像
for i, button in enumerate(self.door_buttons):
if i + 1 == self.winning_door:
button.config(image=self.door_imgs[2])
else:
button.config(image=self.door_imgs[1])
## 显示输赢消息
if did_win:
messagebox.showinfo("恭喜!", "你赢了!")
else:
messagebox.showinfo("抱歉!", "下次好运!")
## 更新获胜统计信息
self.results_label.config(text=f"换门获胜次数:{self.wins_change} 不换门获胜次数:{self.wins_stay}")
## 为下一轮游戏做准备
self.reset_game()
这个show_win方法展示了游戏结果。如果玩家最终选择的门后藏有奖品,他们将被宣布为获胜者;否则,就祝他们下次好运。
重置游戏机制
最后,确保你能够为新的一轮重新初始化游戏。
def reset_game(self) -> None:
"""为新的一轮重置游戏。"""
self.selected_door = None
self.winning_door = random.randint(1, 3)
for button in self.door_buttons:
button.config(image=self.door_imgs[0])
self.label.config(text="选择一扇门!")
self.yes_button.pack_forget()
self.no_button.pack_forget()
这个名为reset_game的函数会重新启动游戏状态,让玩家能够尝试新的一轮。
运行应用程序
最后,让我们初始化并运行我们的应用程序。
if __name__ == "__main__":
root = tk.Tk()
sim = MontyHallSimulation(root)
root.mainloop()
现在我们已经完成了所有步骤,可以使用以下命令运行代码:
cd ~/project
python monty_hall_gui.py

总结
恭喜你!你刚刚使用 Tkinter 创建了一个基于图形用户界面(GUI)的蒙提霍尔问题模拟程序。要运行该模拟程序,请执行monty_hall_gui.py脚本并与图形界面进行交互。请记住,这个游戏展示了概率的反直觉性质,而最佳策略是始终换门!



