From d178be518c0fe45ed52b85006ae71ea290543957 Mon Sep 17 00:00:00 2001 From: kendrike Date: Sat, 14 Mar 2026 16:08:14 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0Python=E5=9B=B4?= =?UTF-8?q?=E6=A3=8B=E6=B8=B8=E6=88=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 支持9x9、13x13、19x19三种棋盘大小 - 实现完整围棋规则:落子、提子、禁入点、打劫 - 添加悔棋和PASS功能 - 使用Tkinter实现GUI界面 --- go_game.py | 517 +++++++++++++++++++++++++++++++++++++++++++++++++++++ readme.md | 45 ++++- 2 files changed, 561 insertions(+), 1 deletion(-) create mode 100644 go_game.py diff --git a/go_game.py b/go_game.py new file mode 100644 index 0000000..8123174 --- /dev/null +++ b/go_game.py @@ -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('', self.on_motion) + self.bind('', self.on_leave) + self.bind('', 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('<>', 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() \ No newline at end of file diff --git a/readme.md b/readme.md index a042389..6971c2f 100644 --- a/readme.md +++ b/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 内置) + +### 截图 + +运行后会显示一个可视化的围棋棋盘界面,支持完整的围棋对弈功能。 \ No newline at end of file