首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >用Python写一个可视化的点球大战游戏

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

作者头像
Crossin先生
发布2026-07-08 16:13:49
发布2026-07-08 16:13:49
130
举报

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

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

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

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

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

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

1. tkinter 创建窗口应用

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

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

代码语言:javascript
复制
import tkinter as tk

root = tk.Tk()  # 1. 创建主窗口
root.title("点球大战")  # 2. 设置标题
root.geometry("300x200")  # 设置窗口大小

root.mainloop()  # 3. 让窗口持续运行并监听事件

2. Canvas 绘制游戏元素

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

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

代码语言:javascript
复制
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(100, 30, 400, 180, outline="white", width=5)
# 2. 画守门员:用蓝色矩形代表门将,放在球门中间
gk = canvas.create_rectangle(235, 100, 265, 150, fill="#1E90FF", outline="white")
# 3. 画足球:用 create_oval(...) 画一个黄色圆圈,放在罚球点
ball = canvas.create_oval(240, 190, 260, 210, fill="#FFD700", outline="black")

root.mainloop()

3. 组件布局管理

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

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

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

代码语言:javascript
复制
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(100, 30, 400, 180, outline="white", width=5)
gk = canvas.create_rectangle(235, 100, 265, 150, fill="#1E90FF", outline="white")
ball = canvas.create_oval(240, 190, 260, 210, 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() 动态改变画布组件的坐标,让守门员向指定方向“扑”过去:

代码语言:javascript
复制
import tkinter as tk

# 6个方向的守门员扑球坐标位置
GK_POS = {
    "左上": (150, 50),
    "上": (250, 50),
    "右上": (350, 50),
    "左": (150, 130),
    "中": (250, 130),
    "右": (350, 130),
}


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(100, 30, 400, 180, outline="white", width=4)
# 创建守门员
gk_marker = canvas.create_rectangle(
    235, 100, 265, 150, 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%踢飞概率以及配套的文字提示,就完成了这个可视化的点球大战游戏。

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

代码语言:javascript
复制
import random
import tkinter as tk
from tkinter import messagebox

# 6个方向在宽画布中的坐标定义
DIRECTIONS = {
    "左上": (150, 50),
    "上": (250, 50),
    "右上": (350, 50),
    "左": (150, 130),
    "中": (250, 130),
    "右": (350, 130),
}


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=(10, 0))

        # 【新增】专门用来显示每脚球结果的醒目文字标签(初始为空)
        self.result_label = tk.Label(
            root, text="", font=("Microsoft YaHei", 14, "bold"), fg="#FF4500"
        )
        self.result_label.pack(pady=(5, 5))

        # 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=(10, 2), 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=(10, 2), 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(
            100, 30, 400, 180, outline="white", width=5
        )
        self.canvas.create_line(0, 180, 500, 180, fill="white", width=3)

        # 初始化球和守门员物体
        self.gk_marker = self.canvas.create_rectangle(
            0, 0, 0, 0, outline="white", width=2
        )
        self.ball_marker = self.canvas.create_oval(
            0, 0, 0, 0, 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, 240, 190, 260, 210)
        self.canvas.coords(self.gk_marker, 235, 100, 265, 150)

        # 清空上一次的射门文字结果,并允许下一次点击
        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(1500, self.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 的框架改写一下吧。

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


本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-07-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Crossin的编程教室 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档