diff --git a/game/reversi.py b/game/reversi.py new file mode 100644 index 0000000..fa97f09 --- /dev/null +++ b/game/reversi.py @@ -0,0 +1,220 @@ +import numpy as np +from typing import Tuple, List, Optional + +class Board: + """ + 翻转棋 (Reversi/Othello) + 为深度学习神经网络提供多通道输入。 + + 核心算法基于从落子点向8个方向扫描,寻找并翻转被“夹住”的对方棋子。 + """ + def __init__(self, h: int, w: int): + """ + 初始化棋盘。 + + 参数: + h, w: 棋盘的高度和宽度,建议为大于4的偶数。 + """ + if h < 4 or w < 4 or h % 2 != 0 or w % 2 != 0: + raise ValueError("高度和宽度必须是大于等于4的偶数。") + self.h = h + self.w = w + + # 定义8个方向的偏移量 (dr, dc) + self._DIRECTIONS = np.array([[-1, -1], [-1, 0], [-1, 1], + [0, -1], [0, 1], + [1, -1], [1, 0], [1, 1]], dtype=np.int8) + + # 存储棋盘状态的变量 + self.board: np.ndarray = None # 主棋盘: 1=黑, -1=白, 0=空 + self.player: int = None # 当前玩家: 1=黑, -1=白 + + # 为神经网络准备的通道 + self.board_b: np.ndarray = None # 通道2: 黑棋位置 (0/1) + self.board_w: np.ndarray = None # 通道3: 白棋位置 (0/1) + self.board_move: np.ndarray = None # 通道4: 当前玩家的合法走法 (0/1) + self.player_channel: np.ndarray = None # 通道5: 当前玩家指示 (全1或全-1) + + self.reset() + + def reset(self): + """ + 重置棋盘到初始状态,开始新游戏。 + """ + board = np.zeros((self.h, self.w), dtype=np.int8) + # 初始中心棋子 + mid_h, mid_w = self.h // 2, self.w // 2 + board[mid_h - 1, mid_w - 1] = -1 # 白 + board[mid_h - 1, mid_w] = 1 # 黑 + board[mid_h, mid_w - 1] = 1 # 黑 + board[mid_h, mid_w] = -1 # 白 + + # 黑棋先手 + self.load(board, 1) + + def load(self, board: np.ndarray, player: int): + """ + 加载指定的棋盘状态和当前玩家。 + + 参数: + board: 一个 (h, w) 的 numpy 数组,1=黑, -1=白, 0=空。 + player: 当前玩家, 1=黑, -1=白。 + """ + if board.shape != (self.h, self.w): + raise ValueError("加载的棋盘尺寸与初始化尺寸不符。") + self.board = board.copy().astype(np.int8) + self.player = player + self._update_channels() + + def _update_channels(self): + """ + 【核心解析器】 + 根据 self.board 和 self.player,更新所有输入通道。 + 这个函数是所有状态变更后必须调用的,以确保数据一致性。 + """ + # 通道2 & 3: 使用布尔索引高效生成黑棋和白棋位置通道 + self.board_b = (self.board == 1).astype(np.float32) + self.board_w = (self.board == -1).astype(np.float32) + + # 通道4: 生成合法移动位置通道 + self.board_move = np.zeros_like(self.board, dtype=np.float32) + # 遍历所有空位 + empty_cells = np.argwhere(self.board == 0) + for r, c in empty_cells: + if len(self._get_flips_for_move(r, c)) > 0: + self.board_move[r, c] = 1.0 + + # 通道5: 生成玩家指示通道 + self.player_channel = np.full((self.h, self.w), float(self.player), dtype=np.float32) + + def _get_flips_for_move(self, r: int, c: int) -> List[Tuple[int, int]]: + """ + 【核心算法】 + 计算在 (r, c) 位置落子后,能够翻转的所有对方棋子的坐标列表。 + 这也是判断 (r, c) 是否为合法走法的基础。 + + 返回: + 一个包含所有可被翻转棋子坐标 `(row, col)` 的列表。如果列表为空,则该走法不合法。 + """ + opponent = -self.player + pieces_to_flip = [] + + # 扫描8个方向 + for dr, dc in self._DIRECTIONS: + line_flips = [] + curr_r, curr_c = r + dr, c + dc + + # 持续沿该方向探索 + while 0 <= curr_r < self.h and 0 <= curr_c < self.w: + if self.board[curr_r, curr_c] == opponent: + line_flips.append((curr_r, curr_c)) + elif self.board[curr_r, curr_c] == self.player: + # 找到了己方棋子,形成"夹击",该方向上的翻转有效 + pieces_to_flip.extend(line_flips) + break + else: + # 遇到空位或边界,中断该方向的扫描 + break + curr_r, curr_c = curr_r + dr, curr_c + dc + + return pieces_to_flip + + def play(self, r: int, c: int): + """ + 在 (r, c) 位置执行走子操作。 + + 参数: + r, c: 落子位置的行和列。 + """ + if not (0 <= r < self.h and 0 <= c < self.w and self.board_move[r, c] == 1): + raise ValueError(f"位置 ({r}, {c}) 不是一个合法的走法。") + + # 1. 获取要翻转的棋子 + flips = self._get_flips_for_move(r, c) + + # 2. 在棋盘上执行落子和翻转 + self.board[r, c] = self.player + for fr, fc in flips: + self.board[fr, fc] = self.player + + # 3. 交换玩家 + self.player *= -1 + + # 4. 更新所有通道以反映新状态 + self._update_channels() + + # 5. 如果新玩家无棋可走,则跳过其回合 + if np.sum(self.board_move) == 0 and not self.is_game_over(): + self.player *= -1 + self._update_channels() + + def get_state(self) -> np.ndarray: + """ + 获取为神经网络准备的5通道输入状态。 + + 返回: + 一个 (5, h, w) 的 numpy 数组。 + """ + return np.stack([ + self.board.astype(np.float32), # 通道1: 主棋盘 (1, -1, 0) + self.board_b, + self.board_w, + self.board_move, + self.player_channel + ]) + + def is_game_over(self) -> bool: + """检查游戏是否结束。""" + # 如果棋盘已满 + if np.all(self.board != 0): + return True + # 如果双方都无棋可走 + if np.sum(self.board_move) == 0: + # 临时切换到对手,检查对手是否也无棋可走 + original_player = self.player + self.player *= -1 + + opponent_has_move = False + empty_cells = np.argwhere(self.board == 0) + for r, c in empty_cells: + if len(self._get_flips_for_move(r, c)) > 0: + opponent_has_move = True + break + + self.player = original_player # 恢复玩家 + return not opponent_has_move + return False + + def get_winner(self) -> Optional[int]: + """ + 获取赢家。 + 返回: 1 (黑棋赢), -1 (白棋赢), 0 (平局), None (游戏未结束)。 + """ + if not self.is_game_over(): + return None + + score = np.sum(self.board) + if score > 0: + return 1 + elif score < 0: + return -1 + else: + return 0 + + def __str__(self): + """方便打印和调试棋盘。""" + symbols = {1: 'X', -1: 'O', 0: '.'} + header = ' ' + ' '.join(f'{i:X}' for i in range(self.w)) + '\n' + board_str = header + for r in range(self.h): + board_str += f'{r:X} ' + ' '.join(symbols[self.board[r, c]] for c in range(self.w)) + '\n' + + current_player = 'Black (X)' if self.player == 1 else 'White (O)' + board_str += f"\nCurrent Player: {current_player}\n" + + winner = self.get_winner() + if winner is not None: + winner_str = {1: "Black (X) Wins", -1: "White (O) Wins", 0: "Draw"}[winner] + board_str += f"Game Over! Winner: {winner_str}\n" + + return board_str diff --git a/gameGUI.py b/gameGUI.py new file mode 100644 index 0000000..da49d38 --- /dev/null +++ b/gameGUI.py @@ -0,0 +1,241 @@ +import tkinter as tk +from tkinter import ttk, messagebox +import numpy as np +from game.reversi import Board + +class ReversiGUI: + def __init__(self, master): + self.master = master + self.master.title("黑白棋 (Reversi)") + self.master.geometry("1000x600") + self.master.resizable(True, True) + + # 创建棋盘实例 (8x8) + self.board = Board(8, 8) + + # 棋子颜色和符号 + self.colors = { + 0: "#228B22", # 空位 - 绿色 + 1: "#000000", # 黑棋 - 黑色 + -1: "#FFFFFF" # 白棋 - 白色 + } + self.bg_colors = { + 0: "#228B22", # 空位 - 绿色 + 1: "#000000", # 黑棋 - 黑色 + -1: "#FFFFFF" # 白棋 - 白色 + } + self.symbols = { + 1: "●", + -1: "●", + 0: "" + } + + # 创建主框架 + self.main_frame = ttk.Frame(self.master) + self.main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + # 创建左侧主棋盘框架 + self.board_frame = ttk.LabelFrame(self.main_frame, text="主棋盘") + self.board_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5) + + # 创建右侧通道可视化框架 + self.channels_frame = ttk.LabelFrame(self.main_frame, text="通道可视化") + self.channels_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=5, pady=5) + + # 状态标签 + self.status_var = tk.StringVar() + self.status_label = ttk.Label(self.master, textvariable=self.status_var, font=('Arial', 12)) + self.status_label.pack(pady=5) + + # 初始化棋盘按钮 + self.board_buttons = [] + self.setup_board() + + # 初始化通道可视化 + self.channel_labels = { + "board_b": [], # 黑棋位置 + "board_w": [], # 白棋位置 + "board_move": [], # 合法走法 + "player": [] # 当前玩家 + } + self.setup_channels() + + # 更新显示 + self.update_display() + + # 重置按钮 + self.reset_button = ttk.Button(self.master, text="重置游戏", command=self.reset_game) + self.reset_button.pack(pady=10) + + def setup_board(self): + """设置主棋盘界面""" + # 创建行列标签 + col_frame = ttk.Frame(self.board_frame) + col_frame.pack(fill=tk.X) + + # 空白格用于对齐 + ttk.Label(col_frame, text="", width=2).pack(side=tk.LEFT) + + # 列标签 (A-H) + for c in range(8): + ttk.Label(col_frame, text=chr(65 + c), width=4).pack(side=tk.LEFT) + + # 创建棋盘按钮 + for r in range(8): + row_frame = ttk.Frame(self.board_frame) + row_frame.pack(fill=tk.X) + + # 行标签 (1-8) + ttk.Label(row_frame, text=str(r+1), width=2).pack(side=tk.LEFT) + + row_buttons = [] + for c in range(8): + btn = tk.Button( + row_frame, + width=3, height=1, + font=('Arial', 14, 'bold'), + command=lambda r=r, c=c: self.make_move(r, c) + ) + btn.pack(side=tk.LEFT, padx=1, pady=1) + row_buttons.append(btn) + self.board_buttons.append(row_buttons) + + def setup_channels(self): + """设置通道可视化界面""" + # 创建通道标签和网格 + channels = [ + ("黑棋位置", "board_b"), + ("白棋位置", "board_w"), + ("合法走法", "board_move"), + ("当前玩家", "player") + ] + + for idx, (title, key) in enumerate(channels): + # 为每个通道创建框架 + channel_frame = ttk.LabelFrame(self.channels_frame, text=title) + channel_frame.grid(row=idx//2, column=idx%2, padx=5, pady=5, sticky="nsew") + + # 创建网格 + grid_frame = ttk.Frame(channel_frame) + grid_frame.pack(padx=5, pady=5) + + # 创建小格子 + cell_labels = [] + for r in range(8): + row_labels = [] + for c in range(8): + lbl = tk.Label( + grid_frame, + width=2, height=1, + relief=tk.RIDGE, + borderwidth=1 + ) + lbl.grid(row=r, column=c, sticky="nsew") + row_labels.append(lbl) + cell_labels.append(row_labels) + self.channel_labels[key] = cell_labels + + # 使行列权重相等 + for i in range(2): + self.channels_frame.grid_columnconfigure(i, weight=1) + self.channels_frame.grid_rowconfigure(i, weight=1) + + def update_display(self): + """更新所有显示元素""" + # 更新主棋盘 + for r in range(8): + for c in range(8): + value = self.board.board[r, c] + btn = self.board_buttons[r][c] + + # 设置按钮颜色和文本 + btn.config( + text=self.symbols[value], + fg="#FFFFFF" if value == 1 else "#000000", # 黑棋白字,白棋黑字 + bg=self.bg_colors[value], # 使用对应的背景色 + state=tk.NORMAL if self.board.board_move[r, c] == 1 else tk.DISABLED + ) + + # 更新通道可视化 + # 黑棋位置通道 + self._update_channel_display("board_b", self.board.board_b) + # 白棋位置通道 + self._update_channel_display("board_w", self.board.board_w) + # 合法走法通道 + self._update_channel_display("board_move", self.board.board_move) + # 当前玩家通道 + self._update_channel_display("player", self.board.player_channel) + + # 更新状态信息 + player_name = "黑棋" if self.board.player == 1 else "白棋" + if self.board.is_game_over(): + winner = self.board.get_winner() + if winner == 1: + status = "游戏结束!黑棋获胜!" + elif winner == -1: + status = "游戏结束!白棋获胜!" + else: + status = "游戏结束!平局!" + else: + black_count = np.sum(self.board.board == 1) + white_count = np.sum(self.board.board == -1) + status = f"当前玩家: {player_name} | 黑棋: {black_count} | 白棋: {white_count}" + + self.status_var.set(status) + + def _update_channel_display(self, channel_key, data): + """更新特定通道的显示""" + for r in range(8): + for c in range(8): + value = data[r, c] + label = self.channel_labels[channel_key][r][c] + + # 设置颜色强度 + if channel_key == "board_b": + # 黑棋通道: 黑色 + intensity = int(value * 255) + color = f"#{intensity:02x}{intensity:02x}{intensity:02x}" + elif channel_key == "board_w": + # 白棋通道: 白色 + intensity = int(255 - value * 255) + color = f"#{intensity:02x}{intensity:02x}{intensity:02x}" + elif channel_key == "board_move": + # 合法走法通道: 蓝色 + intensity = int(value * 255) + color = f"#00{intensity:02x}ff" + else: + # 当前玩家通道: 红色(黑棋)/黄色(白棋) + if value > 0: + color = f"#ff0000" # 红色表示黑棋 + else: + color = f"#ffff00" # 黄色表示白棋 + + label.config(bg=color) + + def make_move(self, r, c): + """执行走棋操作""" + try: + self.board.play(r, c) + self.update_display() + + # 检查游戏是否结束 + if self.board.is_game_over(): + winner = self.board.get_winner() + if winner == 1: + messagebox.showinfo("游戏结束", "黑棋获胜!") + elif winner == -1: + messagebox.showinfo("游戏结束", "白棋获胜!") + else: + messagebox.showinfo("游戏结束", "平局!") + except ValueError as e: + messagebox.showerror("错误", str(e)) + + def reset_game(self): + """重置游戏""" + self.board.reset() + self.update_display() + +if __name__ == "__main__": + root = tk.Tk() + app = ReversiGUI(root) + root.mainloop()