#!/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()