Files
datawhisper/go_game.py
kendrike d178be518c feat: 添加Python围棋游戏
- 支持9x9、13x13、19x19三种棋盘大小
- 实现完整围棋规则:落子、提子、禁入点、打劫
- 添加悔棋和PASS功能
- 使用Tkinter实现GUI界面
2026-03-14 16:08:14 +08:00

517 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
简易围棋游戏 - Python Tkinter实现
支持9x9, 13x13, 19x19棋盘
包含基本的围棋规则:落子、提子、禁入点、打劫
"""
import tkinter as tk
from tkinter import messagebox, ttk
from typing import Optional, Set, Tuple
from copy import deepcopy
class GoGame:
"""围棋游戏逻辑类"""
EMPTY = 0
BLACK = 1
WHITE = 2
def __init__(self, size: int = 19):
self.size = size
self.board = [[self.EMPTY] * size for _ in range(size)]
self.current_player = self.BLACK
self.previous_board = None # 用于打劫判断
self.captured = {self.BLACK: 0, self.WHITE: 0} # 提子计数
self.history = [] # 历史记录
self.game_over = False
self.pass_count = 0 # 连续pass计数
def get_neighbors(self, x: int, y: int) -> list:
"""获取相邻位置"""
neighbors = []
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
nx, ny = x + dx, y + dy
if 0 <= nx < self.size and 0 <= ny < self.size:
neighbors.append((nx, ny))
return neighbors
def get_group(self, x: int, y: int) -> Set[Tuple[int, int]]:
"""获取连通棋子群"""
color = self.board[y][x]
if color == self.EMPTY:
return set()
group = set()
stack = [(x, y)]
while stack:
cx, cy = stack.pop()
if (cx, cy) in group:
continue
group.add((cx, cy))
for nx, ny in self.get_neighbors(cx, cy):
if self.board[ny][nx] == color and (nx, ny) not in group:
stack.append((nx, ny))
return group
def get_liberties(self, group: Set[Tuple[int, int]]) -> Set[Tuple[int, int]]:
"""获取棋子群的气"""
liberties = set()
for x, y in group:
for nx, ny in self.get_neighbors(x, y):
if self.board[ny][nx] == self.EMPTY:
liberties.add((nx, ny))
return liberties
def capture_group(self, group: Set[Tuple[int, int]]) -> int:
"""提掉一个棋子群,返回提子数"""
color = self.board[list(group)[0][1]][list(group)[0][0]]
for x, y in group:
self.board[y][x] = self.EMPTY
self.captured[color] += len(group)
return len(group)
def is_suicide(self, x: int, y: int) -> bool:
"""检查是否是自杀"""
# 放置棋子
self.board[y][x] = self.current_player
# 检查是否能提对方的子
opponent = self.WHITE if self.current_player == self.BLACK else self.BLACK
can_capture = False
for nx, ny in self.get_neighbors(x, y):
if self.board[ny][nx] == opponent:
group = self.get_group(nx, ny)
if len(self.get_liberties(group)) == 0:
can_capture = True
break
# 检查自己的气
my_group = self.get_group(x, y)
my_liberties = len(self.get_liberties(my_group))
# 恢复棋盘
self.board[y][x] = self.EMPTY
return not can_capture and my_liberties == 0
def is_ko(self, x: int, y: int) -> bool:
"""检查是否违反打劫规则"""
if self.previous_board is None:
return False
# 模拟落子后的棋盘
test_board = deepcopy(self.board)
test_board[y][x] = self.current_player
# 提子
opponent = self.WHITE if self.current_player == self.BLACK else self.BLACK
for nx, ny in self.get_neighbors(x, y):
if test_board[ny][nx] == opponent:
# 检查这个棋子群
stack = [(nx, ny)]
group = set()
while stack:
cx, cy = stack.pop()
if (cx, cy) in group:
continue
if test_board[cy][cx] != opponent:
continue
group.add((cx, cy))
for nnx, nny in self.get_neighbors(cx, cy):
if test_board[nny][nnx] == opponent and (nnx, nny) not in group:
stack.append((nnx, nny))
# 检查气
has_liberty = False
for gx, gy in group:
for nnx, nny in self.get_neighbors(gx, gy):
if test_board[nny][nnx] == self.EMPTY:
has_liberty = True
break
if has_liberty:
break
if not has_liberty:
for gx, gy in group:
test_board[gy][gx] = self.EMPTY
# 比较是否和上一步棋盘相同
return test_board == self.previous_board
def play(self, x: int, y: int) -> bool:
"""落子"""
if self.game_over:
return False
if not (0 <= x < self.size and 0 <= y < self.size):
return False
if self.board[y][x] != self.EMPTY:
return False
if self.is_suicide(x, y):
return False
if self.is_ko(x, y):
return False
# 保存历史
self.history.append({
'board': deepcopy(self.board),
'player': self.current_player,
'captured': deepcopy(self.captured)
})
# 保存当前棋盘(用于打劫判断)
self.previous_board = deepcopy(self.board)
# 落子
self.board[y][x] = self.current_player
# 提子
opponent = self.WHITE if self.current_player == self.BLACK else self.BLACK
for nx, ny in self.get_neighbors(x, y):
if self.board[ny][nx] == opponent:
group = self.get_group(nx, ny)
if len(self.get_liberties(group)) == 0:
self.capture_group(group)
# 切换玩家
self.current_player = self.WHITE if self.current_player == self.BLACK else self.BLACK
self.pass_count = 0
return True
def pass_turn(self):
"""跳过"""
if self.game_over:
return
self.history.append({
'board': deepcopy(self.board),
'player': self.current_player,
'captured': deepcopy(self.captured)
})
self.pass_count += 1
self.current_player = self.WHITE if self.current_player == self.BLACK else self.BLACK
# 双方连续pass游戏结束
if self.pass_count >= 2:
self.game_over = True
def undo(self) -> bool:
"""悔棋"""
if not self.history:
return False
state = self.history.pop()
self.board = state['board']
self.current_player = state['player']
self.captured = state['captured']
self.previous_board = self.history[-1]['board'] if self.history else None
self.game_over = False
self.pass_count = 0
return True
def get_state(self, x: int, y: int) -> int:
"""获取指定位置的棋子状态"""
return self.board[y][x]
class GoBoard(tk.Canvas):
"""围棋棋盘组件"""
def __init__(self, master, game: GoGame, cell_size: int = 30, **kwargs):
self.cell_size = cell_size
self.game = game
self.board_size = (game.size + 1) * cell_size
self.stone_radius = int(cell_size * 0.45)
super().__init__(master, width=self.board_size, height=self.board_size,
bg='#DCB35C', **kwargs)
self.callback = None
self.hover_pos = None
self.bind('<Motion>', self.on_motion)
self.bind('<Leave>', self.on_leave)
self.bind('<Button-1>', self.on_click)
self.draw_board()
def get_board_position(self, event_x: int, event_y: int) -> Optional[Tuple[int, int]]:
"""将像素坐标转换为棋盘坐标"""
x = round((event_x - self.cell_size) / self.cell_size)
y = round((event_y - self.cell_size) / self.cell_size)
if 0 <= x < self.game.size and 0 <= y < self.game.size:
return (x, y)
return None
def draw_board(self):
"""绘制棋盘"""
self.delete('all')
# 绘制网格线
for i in range(self.game.size):
x = self.cell_size + i * self.cell_size
y = self.cell_size + i * self.cell_size
# 横线
self.create_line(self.cell_size, y,
self.cell_size + (self.game.size - 1) * self.cell_size, y,
fill='black')
# 竖线
self.create_line(x, self.cell_size,
x, self.cell_size + (self.game.size - 1) * self.cell_size,
fill='black')
# 绘制星位
star_points = self.get_star_points()
for x, y in star_points:
px = self.cell_size + x * self.cell_size
py = self.cell_size + y * self.cell_size
r = 4
self.create_oval(px - r, py - r, px + r, py + r, fill='black')
# 绘制坐标
for i in range(self.game.size):
# 列标A-T跳过I
col = chr(ord('A') + i) if i < 8 else chr(ord('A') + i + 1)
x = self.cell_size + i * self.cell_size
self.create_text(x, self.board_size - 10, text=col, font=('Arial', 10))
self.create_text(x, 10, text=col, font=('Arial', 10))
# 行标1-19
row = self.game.size - i
y = self.cell_size + i * self.cell_size
self.create_text(10, y, text=str(row), font=('Arial', 10))
self.create_text(self.board_size - 10, y, text=str(row), font=('Arial', 10))
# 绘制棋子
self.draw_stones()
def get_star_points(self) -> list:
"""获取星位坐标"""
if self.game.size == 19:
return [(3, 3), (9, 3), (15, 3),
(3, 9), (9, 9), (15, 9),
(3, 15), (9, 15), (15, 15)]
elif self.game.size == 13:
return [(3, 3), (9, 3), (6, 6),
(3, 9), (9, 9)]
elif self.game.size == 9:
return [(2, 2), (6, 2), (4, 4),
(2, 6), (6, 6)]
return []
def draw_stones(self):
"""绘制所有棋子"""
for y in range(self.game.size):
for x in range(self.game.size):
state = self.game.get_state(x, y)
if state != GoGame.EMPTY:
self.draw_stone(x, y, state)
def draw_stone(self, x: int, y: int, color: int):
"""绘制单个棋子"""
px = self.cell_size + x * self.cell_size
py = self.cell_size + y * self.cell_size
r = self.stone_radius
if color == GoGame.BLACK:
# 黑子带渐变效果
self.create_oval(px - r, py - r, px + r, py + r,
fill='#1a1a1a', outline='black', width=1)
# 高光
self.create_oval(px - r + 3, py - r + 3, px - r + 8, py - r + 8,
fill='#4a4a4a', outline='')
else:
# 白子带渐变效果
self.create_oval(px - r, py - r, px + r, py + r,
fill='white', outline='#888888', width=1)
# 高光
self.create_oval(px - r + 3, py - r + 3, px - r + 8, py - r + 8,
fill='#f0f0f0', outline='')
def on_motion(self, event):
"""鼠标移动"""
pos = self.get_board_position(event.x, event.y)
if pos != self.hover_pos:
self.hover_pos = pos
self.draw_board()
if pos and self.game.get_state(pos[0], pos[1]) == GoGame.EMPTY:
self.draw_hover_stone(pos[0], pos[1])
def on_leave(self, event):
"""鼠标离开"""
self.hover_pos = None
self.draw_board()
def draw_hover_stone(self, x: int, y: int):
"""绘制悬停预览"""
if self.game.game_over:
return
px = self.cell_size + x * self.cell_size
py = self.cell_size + y * self.cell_size
r = self.stone_radius
color = '#1a1a1a' if self.game.current_player == GoGame.BLACK else 'white'
self.create_oval(px - r, py - r, px + r, py + r,
fill=color, outline='gray', width=1, stipple='gray50')
def on_click(self, event):
"""点击落子"""
pos = self.get_board_position(event.x, event.y)
if pos and self.callback:
self.callback(pos[0], pos[1])
def set_callback(self, callback):
"""设置落子回调"""
self.callback = callback
def update_display(self):
"""更新显示"""
self.draw_board()
class GoApp:
"""围棋应用主类"""
def __init__(self, root: tk.Tk):
self.root = root
self.root.title("围棋 - Go Game")
self.root.resizable(False, False)
self.game = GoGame(19)
self.setup_ui()
def setup_ui(self):
"""设置界面"""
# 主框架
main_frame = ttk.Frame(self.root, padding="10")
main_frame.pack()
# 控制面板
control_frame = ttk.Frame(main_frame)
control_frame.pack(fill='x', pady=(0, 10))
# 棋盘大小选择
ttk.Label(control_frame, text="棋盘大小:").pack(side='left', padx=5)
self.size_var = tk.StringVar(value="19x19")
size_combo = ttk.Combobox(control_frame, textvariable=self.size_var,
values=["9x9", "13x13", "19x19"], width=8,
state='readonly')
size_combo.pack(side='left', padx=5)
size_combo.bind('<<ComboboxSelected>>', self.on_size_change)
# 按钮
ttk.Button(control_frame, text="新游戏", command=self.new_game).pack(side='left', padx=5)
ttk.Button(control_frame, text="悔棋", command=self.undo).pack(side='left', padx=5)
ttk.Button(control_frame, text="PASS", command=self.pass_turn).pack(side='left', padx=5)
ttk.Button(control_frame, text="退出", command=self.root.quit).pack(side='right', padx=5)
# 信息面板
info_frame = ttk.Frame(main_frame)
info_frame.pack(fill='x', pady=(0, 10))
self.current_player_label = ttk.Label(info_frame, text="当前: 黑方",
font=('Arial', 12, 'bold'))
self.current_player_label.pack(side='left', padx=10)
self.captured_label = ttk.Label(info_frame, text="提子 - 黑: 0 | 白: 0",
font=('Arial', 10))
self.captured_label.pack(side='left', padx=20)
self.status_label = ttk.Label(info_frame, text="", font=('Arial', 10))
self.status_label.pack(side='left', padx=20)
# 棋盘
self.board = GoBoard(main_frame, self.game, cell_size=30)
self.board.pack()
self.board.set_callback(self.on_move)
def on_size_change(self, event=None):
"""改变棋盘大小"""
size_str = self.size_var.get()
size = int(size_str.split('x')[0])
self.game = GoGame(size)
# 更新棋盘
self.board.destroy()
self.board = GoBoard(self.root.winfo_children()[0].winfo_children()[0], self.game, cell_size=30)
self.board.pack()
self.board.set_callback(self.on_move)
self.update_status()
def new_game(self):
"""开始新游戏"""
size_str = self.size_var.get()
size = int(size_str.split('x')[0])
self.game = GoGame(size)
# 更新棋盘
self.board.destroy()
self.board = GoBoard(self.root.winfo_children()[0].winfo_children()[0], self.game, cell_size=30)
self.board.pack()
self.board.set_callback(self.on_move)
self.update_status()
self.status_label.config(text="")
def on_move(self, x: int, y: int):
"""处理落子"""
col = chr(ord('A') + x) if x < 8 else chr(ord('A') + x + 1)
row = self.game.size - y
if self.game.play(x, y):
self.board.update_display()
self.update_status()
self.status_label.config(text=f"落子: {col}{row}")
if self.game.game_over:
messagebox.showinfo("游戏结束", "双方连续PASS游戏结束\n请自行计算胜负。")
else:
self.status_label.config(text="无效落子(位置已被占用/自杀/打劫)")
def undo(self):
"""悔棋"""
if self.game.undo():
self.board.update_display()
self.update_status()
self.status_label.config(text="已悔棋")
else:
self.status_label.config(text="无法悔棋")
def pass_turn(self):
"""跳过"""
self.game.pass_turn()
self.update_status()
if self.game.game_over:
messagebox.showinfo("游戏结束", "双方连续PASS游戏结束\n请自行计算胜负。")
def update_status(self):
"""更新状态显示"""
player = "黑方" if self.game.current_player == GoGame.BLACK else "白方"
self.current_player_label.config(text=f"当前: {player}")
black_captured = self.game.captured[GoGame.WHITE] # 黑方提的白子
white_captured = self.game.captured[GoGame.BLACK] # 白方提的黑子
self.captured_label.config(text=f"提子 - 黑提: {black_captured} | 白提: {white_captured}")
def main():
"""主函数"""
root = tk.Tk()
app = GoApp(root)
root.mainloop()
if __name__ == "__main__":
main()