哨兵边界实现翻转棋Board
This commit is contained in:
220
game/reversi.py
Normal file
220
game/reversi.py
Normal file
@@ -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
|
||||
241
gameGUI.py
Normal file
241
gameGUI.py
Normal file
@@ -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()
|
||||
Reference in New Issue
Block a user