社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  Python

用Python写一个可视化的点球大战游戏

Crossin的编程教室 • 1 周前 • 58 次点击  

大家好,欢迎来到 Crossin 的编程教室。

之前我用 Python 写过一个罚点球小游戏:

Python写个“点球大战”小游戏

这个游戏是在终端里通过输入数字来选择射门/扑救的方向(可选左/中/右3个方向),玩家和程序轮换进行射门,如果射门和扑救方向不同就算进球,仿照世界杯点球大战的规则进行多轮。

程序代码比较简单,可以看作是对 Python 中列表、循环、条件判断和 random 模块的练习。

今天我们再进一步,把它做成一个可视化的窗口游戏:通过点击选择方向,并通过图形化的展示让射门和扑救的方向一目了然。

1. tkinter 创建窗口应用

要实现我们想要的可视化效果,需要用到绘制界面的 GUI 库。

在 Python 中做简单的带界面程序,最省心的就是用内置的 Tkinter 库,无需安装,开箱即用。创建一个基础窗口只需要三步:

import tkinter as tk
root = tk.Tk()  # 1. 创建主窗口root.title("点球大战")  # 2. 设置标题root.geometry("300x200")  # 设置窗口大小
root.mainloop()  # 3. 让窗口持续运行并监听事件

2. Canvas 绘制游戏元素

有了窗口还不够,我们需要能“画”出球场、球门、足球、守门员等游戏元素。这时候就要用到 tkinter 的 Canvas(画布)组件。

通过指定坐标,可以在 Canvas 上面画出各种几何图形:

import tkinter as tk
root = tk.Tk()# 创建一块 500x220 的绿草地画布canvas = tk.Canvas(root, width=500, height=220, bg="#228B22")canvas.pack()
# 1. 画球门:用 create_rectangle(左, 上, 右, 下) 画白色矩形框canvas.create_rectangle(10030400180, outline="white", width=5)# 2. 画守门员:用蓝色矩形代表门将,放在球门中间gk = canvas.create_rectangle(235100265150, fill="#1E90FF", outline="white")# 3. 画足球:用 create_oval(...) 画一个黄色圆圈,放在罚球点ball = canvas.create_oval(240190260210, fill="#FFD700", outline="black")
root.mainloop()

3. 组件布局管理

游戏窗口上除了绘制游戏元素,还需要有选择方向的按钮和提示文本。

我打算把提示文字放在上方,把6个方向按钮放在下方排成2行3列。

Tkinter 提供了 pack()(上下堆叠)和 grid()(网格矩阵)两种经典的布局管理器来控制界面上的组件摆放:

import tkinter as tk
root = tk.Tk()root.geometry()
# 用 pack() 把状态标签放在最上方status_label = tk.Label(root, text="请选择你的射门方向:", font=("Arial"12))status_label.pack(pady=10)
# 创建一块 500x220 的绿草地画布canvas = tk.Canvas(root, width=500, height=220, bg="#228B22")canvas.pack()
canvas.create_rectangle(10030400180, outline="white", width=5)gk = canvas.create_rectangle(235100265150, fill="#1E90FF", outline="white")ball  = canvas.create_oval(240190260210, fill="#FFD700", outline="black")
# 创建一个容器专门放按钮btn_frame = tk.Frame(root)btn_frame.pack()
# 用 grid(row, column) 把6个按钮排成 2行 * 3列directions = ["左上", "上", "右上", "左", "中", "右"]for i, name in enumerate(directions):    btn = tk.Button(btn_frame, text=name, width=8, height=2)    btn.grid(row=i // 3, column=i % 3, padx=5, pady=5)
root.mainloop()

4. 按钮交互:让画面动起来

按钮排好后,还要增加点击响应。我们可以通过 command 参数为按钮绑定点击事件,并利用 canvas.coords() 动态改变画布组件的坐标,让守门员向指定方向“扑”过去:

import tkinter as tk
# 6个方向的守门员扑球坐标位置GK_POS = {    "左上": (15050),    "上": (25050),    "右上": (35050),    "左": (150130),    "中": (250130),    "右": (350130),}

def move_gk(direction):    # 点击按钮后,获取对应方向的坐标    gx, gy = GK_POS[direction]    # 更新画布上守门员矩形的位置    canvas.coords(gk_marker, gx - 15, gy - 25, gx + 15, gy + 25)

root = tk.Tk()canvas = tk.Canvas(root, width=500, height=200, bg="#228B22")canvas.pack()canvas.create_rectangle(10030400180, outline="white", width=4)# 创建守门员gk_marker = canvas.create_rectangle(    235100265150, fill="#FFD700", outline="white")
# 创建按钮,并通过 lambda 传递各自的方向参数btn_frame = tk.Frame(root)btn_frame.pack()for i, d in enumerate(GK_POS.keys()):    # 核心:点击时触发 move_gk 函数并带上方向参数    btn = tk.Button(        btn_frame, text=d, command=lambda direction=d: move_gk(direction)    )    btn.grid(row=i // 3, column=i % 3, padx=5, pady=5)
root.mainloop()

完整示例代码

把以上四个核心部分组合起来,再加上比赛计分、轮流攻防切换、10%踢飞概率以及配套的文字提示,就完成了这个可视化的点球大战游戏。

你可以直接复制到编辑器中运行体验:

import randomimport tkinter as tk


    
from tkinter import messagebox
# 6个方向在宽画布中的坐标定义DIRECTIONS = {    "左上": (15050),    "上": (25050),    "右上": (35050),    "左": (150130),    "中": (250130),    "右": (350130),}

class PenaltyGameSmooth:
    def __init__(self, root):        self.root = root        self.root.title("点球大战(内嵌提示版) ⚽")        self.root.geometry("550x530")  # 稍微加高一点点,给结果文字留出空间
        # 游戏状态        self.is_player_shooting = True  # True: 玩家射门, False: 电脑射门        self.round_num = 1        self.is_waiting_next = (            False  # 锁状态:防止玩家在动画展示的1.5秒内重复连续点击按钮        )
        # 双方固定颜色(球衣颜色)        self.PLAYER_COLOR = "#1E90FF"  # 道奇蓝        self.COM_COLOR = "#FFD700"  # 金黄色
        # 记录每轮结果 (⚽ 或 ❌)        self.player_history = []        self.com_history = []
        # --- GUI 界面布局 ---
        # 1. 顶部的状态与攻防提示        self.status_label = tk.Label(            root,            text="【 第 1 轮:轮到你射门了! 】",            font=("Microsoft YaHei"13"bold"),            fg="#228B22",        )        self.status_label.pack(pady=(100))
        # 【新增】专门用来显示每脚球结果的醒目文字标签(初始为空)        self.result_label = tk.Label(            root, text="", font=("Microsoft YaHei"14"bold"), fg="#FF4500"        )        self.result_label.pack(pady=(55))
        # 2. 实时记分牌        score_frame = tk.Frame(root, bg="#F0F0F0", bd=2, relief="groove")        score_frame.pack(fill="x", padx=40, pady=5)
        # 玩家记分排 (固定蓝色)        self.p_color_lbl = tk.Label(            score_frame,            text="■",            fg=self.PLAYER_COLOR,            bg="#F0F0F0",            font=("Arial"14),        )        self.p_color_lbl.grid(row=0, column=0, padx=(102), sticky="w")        self.p_score_lbl = tk.Label(            score_frame,            text="玩家 (得分: 0) : 开始游戏...",            font=("Microsoft YaHei"11),            bg="#F0F0F0",        )        self.p_score_lbl.grid(row=0, column=1, sticky="w")
        # 电脑记分排 (固定黄色)        self.c_color_lbl = tk.Label(            score_frame,            text="■",            fg=self.COM_COLOR,            bg="#F0F0F0",            font=("Arial"14),        )        self.c_color_lbl.grid(row=1, column=0, padx=(102), sticky="w")        self.c_score_lbl = tk.Label(            score_frame,             text="电脑 (得分: 0) : 开始游戏...",            font=("Microsoft YaHei"11),            bg="#F0F0F0",        )        self.c_score_lbl.grid(row=1, column=1, sticky="w")
        # 3. 中间足球门画布        self.canvas = tk.Canvas(root, width=500, height=220, bg="#228B22")        self.canvas.pack(pady=10)
        # 绘制球门        self.canvas.create_rectangle(            10030400180, outline="white", width=5        )        self.canvas.create_line(0180500180, fill="white", width=3)
        # 初始化球和守门员物体        self.gk_marker = self.canvas.create_rectangle(            0000, outline="white", width=2        )        self.ball_marker = self.canvas.create_oval(            0000, outline="black", width=2        )
        # 4. 底部操作按钮        btn_frame = tk.Frame(root)        btn_frame.pack(pady=5)
        for i, direction in enumerate(DIRECTIONS.keys()):            btn = tk.Button(                btn_frame,                text=direction,                width=8,                height=2,                font=("Microsoft YaHei"10),                command=lambda d=direction: self.play_turn(d),            )            btn.grid(row=i // 3, column=i % 3, padx=8, pady=5)
        # 首次刷新        self.update_score_board()        self.reset_visuals()
    def reset_visuals(self):        """重置画布,清空单发文字提示"""        if self.is_player_shooting:            ball_color = self.PLAYER_COLOR            gk_color = self.COM_COLOR        else:            ball_color = self.COM_COLOR            gk_color = self.PLAYER_COLOR
        self.canvas.itemconfig(self.ball_marker, fill=ball_color)        self.canvas.itemconfig(self.gk_marker, fill=gk_color)
        self.canvas.coords(self.ball_marker, 240190260210)        self.canvas.coords(self.gk_marker, 235100265150)
        # 清空上一次的射门文字结果,并允许下一次点击        self.result_label.config(text="")        self.is_waiting_next = False
    def update_score_board(self):        """更新记分牌文字和顶部状态提示"""        p_total = self.player_history.count("⚽")        c_total = self.com_history.count("⚽")        p_bar = " ".join(self.player_history) if self.player_history else "无"        c_bar = " ".join(self.com_history) if self.com_history else "无"
        if self.is_player_shooting:            self.status_label.config(                text=f"【 第 {self.round_num}  轮:轮到你射门了! 】", fg="#228B22"            )        else:            self.status_label.config(                text=f"【 第 {self.round_num} 轮:请注意防守! 】", fg="#D2691E"            )
        self.p_score_lbl.config(text=f"玩家 (总分: {p_total}) 进度: {p_bar}")        self.c_score_lbl.config(text=f"电脑 (总分: {c_total}) 进度: {c_bar}")
    def play_turn(self, player_choice):        # 如果当前正处于进球动画展示的冷却中,禁止再次触发点击        if self.is_waiting_next:            return        self.is_waiting_next = True
        com_choice = random.choice(list(DIRECTIONS.keys()))
        shoot_dir = player_choice if self.is_player_shooting else com_choice        save_dir = com_choice if self.is_player_shooting else player_choice
        # 1. 移动动画        bx, by = DIRECTIONS[shoot_dir]        gx, gy = DIRECTIONS[save_dir]        self.canvas.coords(self.ball_marker, bx - 10, by - 10, bx + 10, by + 10)        self.canvas.coords(self.gk_marker, gx - 15, gy - 25, gx + 15, gy + 25)
        # 2. 判定逻辑        is_missed = random.random() 0.10        goal = False
        if is_missed:            result_text = f"你选【{player_choice}】· 电脑选【{com_choice}】 ❌ 踢飞了!高出横梁!"            text_color = "#FF0000"  # 红色            self.canvas.coords(                self.ball_marker, bx - 10, by - 45, bx + 10, by - 25            )        elif shoot_dir == save_dir:            result_text = f"你选【{player_choice}】· 电脑选【{com_choice}】 🧤 被扑住了!"            text_color = "#FF4500"  # 橙红色        else:            result_text = f"你选【{player_choice}】· 电脑选【{com_choice}】 ⚽ 球进了!!"            text_color = "#008000"  # 绿色            goal = True
        # 将结果直接呈现在界面上,取代弹窗        self.result_label.config(text=result_text, fg=text_color)
        # 3. 记录历史        mark = "⚽" if goal else "❌"        if self.is_player_shooting:            self.player_history.append(mark)        else:            self.com_history.append(mark)
        # 4. 推进回合与状态切换        if not self.is_player_shooting:            self.round_num += 1
        self.is_player_shooting = not self.is_player_shooting
        # 延迟1.5秒后更新记分牌并重置场地(给玩家留出时间看结果字样和动画)        self.root.after(1500self.next_turn_check)
    def next_turn_check(self):        """1.5秒延迟后执行:刷新记分牌,并检查是否进入结局"""        self.update_score_board()
        if self.round_num > 5 and  self.is_player_shooting:            self.end_game()        else:            self.reset_visuals()
    def end_game(self):        p_total = self.player_history.count("⚽")        c_total = self.com_history.count("⚽")
        if p_total > c_total:            final_msg = "🏆 最终胜利属于你!太棒了!"        elif p_total < c_total:            final_msg = "💀 电脑赢得了比赛,洗洗手再来一把吧!"        else:            final_msg = "🤝 双方难解难分,最终战平!"
        # 只有在游戏5轮完全结束时,才弹出一个最终结算大框        messagebox.showinfo(            "比赛结束",            f"点球大战结束!\n\n最终比分 -> 玩家 {p_total} : {c_total} 电脑\n{final_msg}",        )
        # 全局重置        self.player_history.clear()        self.com_history.clear()        self.round_num = 1        self.is_player_shooting = True        self.update_score_board()        self.reset_visuals()

if __name__ == "__main__":    root = tk.Tk()    game = PenaltyGameSmooth(root)    root.mainloop()

利用 Tkinter 制作基础窗口交互非常方便,但如果你想追求更高级的效果——比如增加足球的运行轨迹、守门员的扑救动画、射门的音效等,Tkinter 就会显得有些吃力了。

这时候,你可以去了解 Python 的另一个游戏开发利器:Pygame。

Pygame 是专为 2D 游戏开发的第三方库,拥有完整的逐帧渲染机制、碰撞检测和音频混合器,能做出更加商业化的游戏作品。之前我们也做过不少 Pygame 的开发案例:

手把手教你用Python写一个俄罗斯方块

手把手带你用 Python 写《2048》

用Python写一个FlappyBird游戏

用python写一个简单的贪吃蛇游戏

如何开发一个打飞机游戏

感兴趣的同学,不妨试着把今天的逻辑用 Pygame 的框架改写一下吧。

如果本文对你有帮助,欢迎点赞、评论、转发。你们的支持是我更新的动力~


添加微信 crossin123 ,加入编程教室共同学习~
感谢转发点赞的各位~

Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/198544