517 lines
17 KiB
Python
517 lines
17 KiB
Python
#!/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() |