本文将带你一步步使用 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()
版权声明
本文为原创首发文章,未经允许请勿转载至其他平台。感谢您的理解与支持!