Files
reversi/game/reversi.py
2025-07-26 21:23:45 +08:00

221 lines
7.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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