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