Python 实战开发:用 Tkinter 制作一个完整的围棋对弈平台

本文将带你一步步使用 Python 和 Tkinter 开发一个图形化围棋游戏。适合初学者入门 GUI 编程与游戏逻辑设计。


项目背景

围棋是一项历史悠久的策略类棋类游戏,规则虽简单,但变化无穷。在学习编程的过程中,我们常常通过制作小游戏来锻炼逻辑思维和算法能力。本次项目中,我使用 Python 的 Tkinter 模块实现了一个完整的 图形化围棋对弈平台,支持两人对战、实时计时、悔棋功能等,是一个非常有趣且实用的小型项目。


技术选型

  • 语言:Python 3.x
  • GUI 库:Tkinter(标准库,无需额外安装)
  • 数据结构:numpy 数组用于棋盘状态管理
  • 功能模块
    • 棋盘绘制
    • 鼠标交互处理
    • 围棋基本规则(提子、劫、气)
    • 时间计算
    • 悔棋系统
    • 界面布局与美化

功能展示图解

在这里插入图片描述


核心功能模块详解

1. GoBoard 类 —— 游戏核心逻辑

该类负责维护棋盘状态、落子判断、胜负判定等核心逻辑。

class GoBoard:
    def __init__(self, size=19):
        self.size = size
        self.board = np.zeros((size, size), dtype=int)
        self.history = []
        self.current_player = 1
        self.ko = None
        self.pass_count = 0
        self.time_used = {1: 0, 2: 0}
        self.last_move_time = time.time()
        self.move_stack = []
        self.captured = {1: 0, 2: 0}
关键方法包括:
  • play(x, y):执行落子操作,检查是否合法
  • get_group(x, y):获取同色相连的棋子群
  • get_liberties(group):计算一组棋子的“气”
  • remove_group(group):移除一组被提走的棋子
  • undo():通过栈式回退实现悔棋功能

2. GoGUI 类 —— 图形用户界面

使用 Tkinter 构建了完整的图形界面,包括棋盘绘制、时间显示、按钮响应等。

class GoGUI:
    def __init__(self, root, size=19):
        self.root = root
        self.size = size
        self.board_size = 600
        self.margin = 30
        self.cell = (self.board_size - 2 * self.margin) // (self.size - 1)
        self.info_width = 200
        self.canvas = tk.Canvas(root, width=self.board_size+self.info_width, height=self.board_size, bg="#DEB887", highlightthickness=0)
        self.canvas.pack()
        self.board = GoBoard(size)
        self.draw_board()
        self.canvas.bind("<Button-1>", self.click)
UI 组件包括:
  • 棋盘绘制(含星位)
  • 当前玩家提示
  • 实时计时器(每秒刷新)
  • 吃子数量统计
  • 悔棋 / 重开按钮

总结

本项目通过 Python + Tkinter 实现了一个完整的图形化围棋对弈平台,不仅考验对 GUI 编程的理解,也需要对围棋规则逻辑进行了解。整个项目结构清晰、模块分明,非常适合初学者学习和拓展。


附录:完整源码

import numpy as np
import time
import tkinter as tk
from tkinter import messagebox

class GoBoard:
    def __init__(self, size=19):
        self.size = size
        self.board = np.zeros((size, size), dtype=int)
        self.history = []
        self.current_player = 1
        self.ko = None
        self.pass_count = 0
        self.time_used = {1: 0, 2: 0}
        self.last_move_time = time.time()
        self.move_stack = []
        self.captured = {1: 0, 2: 0}  # 记录黑白双方被吃掉的子数

    def reset(self):
        self.board = np.zeros((self.size, self.size), dtype=int)
        self.history = []
        self.current_player = 1
        self.ko = None
        self.pass_count = 0
        self.time_used = {1: 0, 2: 0}
        self.last_move_time = time.time()
        self.move_stack = []

    def switch_player(self):
        self.current_player = 3 - self.current_player

    def is_on_board(self, x, y):
        return 0 <= x < self.size and 0 <= y < self.size

    def neighbors(self, x, y):
        for dx, dy in [(-1,0),(1,0),(0,-1),(0,1)]:
            nx, ny = x+dx, y+dy
            if self.is_on_board(nx, ny):
                yield nx, ny

    def get_group(self, x, y):
        color = self.board[x, y]
        group = set()
        queue = [(x, y)]
        while queue:
            cx, cy = queue.pop()
            if (cx, cy) not in group:
                group.add((cx, cy))
                for nx, ny in self.neighbors(cx, cy):
                    if self.board[nx, ny] == color:
                        queue.append((nx, ny))
        return group

    def get_liberties(self, group):
        liberties = set()
        for x, y in group:
            for nx, ny in self.neighbors(x, y):
                if self.board[nx, ny] == 0:
                    liberties.add((nx, ny))
        return liberties

    def remove_group(self, group):
        for x, y in group:
            self.board[x, y] = 0

    def play(self, x, y):
        if not self.is_on_board(x, y) or self.board[x, y] != 0:
            return False
        now = time.time()
        self.time_used[self.current_player] += int(now - self.last_move_time)
        self.last_move_time = now
        self.move_stack.append((np.copy(self.board), self.current_player, list(self.history), dict(self.time_used), dict(self.captured)))
        self.board[x, y] = self.current_player
        group = self.get_group(x, y)
        liberties = self.get_liberties(group)
        captured = []
        for nx, ny in self.neighbors(x, y):
            if self.board[nx, ny] == 3 - self.current_player:
                opp_group = self.get_group(nx, ny)
                opp_liberties = self.get_liberties(opp_group)
                if len(opp_liberties) == 0:
                    captured.extend(opp_group)
        if len(liberties) == 0 and not captured:
            self.board[x, y] = 0
            return False
        for cx, cy in captured:
            self.captured[3 - self.current_player] += 1  # 统计被吃子数
            self.board[cx, cy] = 0
        board_tuple = tuple(map(tuple, self.board))
        if board_tuple in self.history:
            self.board[x, y] = 0
            for cx, cy in captured:
                self.board[cx, cy] = 3 - self.current_player
                self.captured[3 - self.current_player] -= 1  # 撤销统计
            return False
        self.history.append(board_tuple)
        self.switch_player()
        self.pass_count = 0
        return True

    def pass_move(self):
        self.pass_count += 1
        self.switch_player()
        if self.pass_count >= 2:
            return True
        return False

    def is_game_over(self):
        return self.pass_count >= 2

    def count_score(self):
        black_score = np.sum(self.board == 1)
        white_score = np.sum(self.board == 2)
        return black_score, white_score

    def undo(self):
        if self.move_stack:
            board_state, player, history, time_used, captured = self.move_stack.pop()
            self.board = board_state
            self.current_player = player
            self.history = history
            self.time_used = time_used
            self.captured = captured
            self.last_move_time = time.time()
            return True
        return False

class GoGUI:
    def __init__(self, root, size=19):
        self.root = root
        self.size = size
        self.board_size = 600
        self.margin = 30
        self.cell = (self.board_size - 2 * self.margin) // (self.size - 1)
        self.info_width = 200
        self.canvas = tk.Canvas(root, width=self.board_size+self.info_width, height=self.board_size, bg="#DEB887", highlightthickness=0)
        self.canvas.pack()
        self.board = GoBoard(size)
        self.draw_board()
        self.canvas.bind("<Button-1>", self.click)

        # 信息区背景
        self.info_frame = tk.Frame(root, width=self.info_width, height=self.board_size, bg="#F5F5F5")
        self.info_frame.place(x=self.board_size, y=0)

        # 当前执棋方
        self.status_label = tk.Label(self.info_frame, text="", font=("微软雅黑", 18, "bold"), fg="#222", bg="#F5F5F5", anchor="w", justify="left")
        self.status_label.place(x=20, y=30)

        # 分隔线
        self.sep1 = tk.Frame(self.info_frame, bg="#CCCCCC", height=2, width=self.info_width-40)
        self.sep1.place(x=20, y=105)

        # 计时
        self.time_label = tk.Label(self.info_frame, text="", font=("微软雅黑", 15), fg="#444", bg="#F5F5F5", justify="left")
        self.time_label.place(x=20, y=115)

         # 分隔线
        self.sep2 = tk.Frame(self.info_frame, bg="#CCCCCC", height=2, width=self.info_width-40)
        self.sep2.place(x=20, y=175)

        # 吃子统计
        self.capture_label = tk.Label(self.info_frame, text="", font=("微软雅黑", 13), fg="#444", bg="#F5F5F5", justify="left")
        self.capture_label.place(x=20, y=180)

        # 分隔线
        self.sep2 = tk.Frame(self.info_frame, bg="#CCCCCC", height=2, width=self.info_width-40)
        self.sep2.place(x=20, y=230)

        # 按钮区
        self.btn_undo = tk.Button(self.info_frame, text="悔棋", font=("微软雅黑", 14), width=10, bg="#E0E0E0", relief="ridge", command=self.undo)
        self.btn_undo.place(x=40, y=240)
        self.btn_reset = tk.Button(self.info_frame, text="重开", font=("微软雅黑", 14), width=10, bg="#E0E0E0", relief="ridge", command=self.reset)
        self.btn_reset.place(x=40, y=290)

        self.update_status()
        self.update_time()
        self.root.after(100, self.update_time)  # 100ms刷新

    def draw_board(self):
        self.canvas.delete("all")
        for i in range(self.size):
            x0 = self.margin
            y = self.margin + i * self.cell
            x1 = self.margin + (self.size - 1) * self.cell
            self.canvas.create_line(x0, y, x1, y)
            self.canvas.create_line(y, x0, y, x1)
        # 画星位
        star_points = [3, 9, 15] if self.size == 19 else []
        for i in star_points:
            for j in star_points:
                x = self.margin + i * self.cell
                y = self.margin + j * self.cell
                self.canvas.create_oval(x-4, y-4, x+4, y+4, fill="black")
        # 画棋子
        for x in range(self.size):
            for y in range(self.size):
                if self.board.board[x, y] == 1:
                    self.draw_stone(x, y, "black")
                elif self.board.board[x, y] == 2:
                    self.draw_stone(x, y, "white")

    def draw_stone(self, x, y, color):
        px = self.margin + y * self.cell
        py = self.margin + x * self.cell
        self.canvas.create_oval(px-13, py-13, px+13, py+13, fill=color, outline="black")

    def click(self, event):
        x = round((event.y - self.margin) / self.cell)
        y = round((event.x - self.margin) / self.cell)
        if 0 <= x < self.size and 0 <= y < self.size:
            if self.board.play(x, y):
                self.draw_board()
                self.update_status()
            else:
                pass

    def update_status(self):
        player_str = "● 黑棋" if self.board.current_player == 1 else "○ 白棋"
        self.status_label.config(text=f"当前执棋方:\n{player_str}")

    def update_time(self):
        # 实时读秒,显示为 mm:ss
        t1 = int(self.board.time_used[1])
        t2 = int(self.board.time_used[2])
        if self.board.current_player == 1:
            t1 += int(time.time() - self.board.last_move_time)
        else:
            t2 += int(time.time() - self.board.last_move_time)
        def fmt(sec):
            return f"{sec//60:02d}:{sec%60:02d}"
        self.time_label.config(text=f"黑棋用时: {fmt(t1)}\n白棋用时: {fmt(t2)}")
        # 更新吃子数
        self.capture_label.config(text=f"黑棋吃子: {self.board.captured[2]}\n白棋吃子: {self.board.captured[1]}")
        self.root.after(100, self.update_time)

    def undo(self):
        if self.board.undo():
            self.draw_board()
            self.update_status()
        else:
            messagebox.showinfo("提示", "无法悔棋!")

    def reset(self):
        self.board.reset()
        self.draw_board()
        self.update_status()

if __name__ == "__main__":
    root = tk.Tk()
    root.title("围棋")
    gui = GoGUI(root)
    root.mainloop()

版权声明

本文为原创首发文章,未经允许请勿转载至其他平台。感谢您的理解与支持!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值