feat: 添加Python围棋游戏

- 支持9x9、13x13、19x19三种棋盘大小
- 实现完整围棋规则:落子、提子、禁入点、打劫
- 添加悔棋和PASS功能
- 使用Tkinter实现GUI界面
This commit is contained in:
kendrike
2026-03-14 16:08:14 +08:00
parent 4f9c49a665
commit d178be518c
2 changed files with 561 additions and 1 deletions

517
go_game.py Normal file
View File

@@ -0,0 +1,517 @@
#!/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()

View File

@@ -1 +1,44 @@
hello world!
# DataWhisper 项目
## 围棋游戏 (Go Game)
一个使用 Python Tkinter 实现的简易围棋游戏。
### 功能特性
- 支持三种棋盘大小9x9、13x13、19x19
- 完整的围棋规则实现:
- 黑白交替落子
- 气的计算
- 提子(吃子)功能
- 禁入点规则(禁止自杀)
- 打劫规则(禁止全局同形)
- 悔棋功能
- PASS 功能
- 悬停预览
- 坐标显示
- 星位标记
- 提子计数
### 运行方法
```bash
python go_game.py
```
### 操作说明
- **落子**:鼠标左键点击棋盘交叉点
- **新游戏**:点击"新游戏"按钮重新开始
- **悔棋**:点击"悔棋"撤销上一步
- **PASS**:点击"PASS"跳过当前回合
- **切换棋盘大小**:从下拉菜单选择棋盘尺寸
### 系统要求
- Python 3.6+
- TkinterPython 内置)
### 截图
运行后会显示一个可视化的围棋棋盘界面,支持完整的围棋对弈功能。